Compare commits

..

39 Commits

Author SHA1 Message Date
wang-liang0615
f194d26450 env 2024-11-07 11:52:19 +08:00
wang-liang0615
093098bbec feat:add employee work_region 2024-11-07 11:46:29 +08:00
Zhuohao Li
28dca7f086 fix permission bug (#632)
不同的appid下可能有相同的resource type name.
2024-10-27 14:04:34 +08:00
pycook
4a3c21eec4 feat(api): add builtin attributes (#631) 2024-10-22 18:21:07 +08:00
Leo Song
5d28c28023 Merge pull request #630 from veops/dev_ui_241022
fix(ui): update userPanel style
2024-10-22 14:12:14 +08:00
songlh
ba6edb3abe fix(ui): update userPanel style 2024-10-22 14:11:36 +08:00
Leo Song
2f1d57cee1 Merge pull request #629 from veops/dev_ui_241022
feat(ui): add userPanel component
2024-10-22 14:01:02 +08:00
songlh
4111c634d9 feat(ui): add userPanel component 2024-10-22 13:59:38 +08:00
pycook
f1fababa3d feat(api): save relation search option 2024-10-18 11:03:31 +08:00
pycook
fe6373422e chore: release v2.4.13 2024-10-18 09:52:26 +08:00
pycook
b3ea776886 Merge pull request #628 from veops/dev_api_relation_path_search
feat(api): relation path search
2024-10-17 19:47:34 +08:00
pycook
c4d2ce313d feat(api): relation path search 2024-10-17 19:46:39 +08:00
Leo Song
20103a0fe6 Merge pull request #627 from veops/dev_ui_241017
feat(ui): add relation search
2024-10-17 17:56:13 +08:00
songlh
394e2aeac6 feat(ui): add relation search 2024-10-17 17:55:36 +08:00
pycook
8f7d78c26c Merge pull request #623 from veops/dev_api_relation_path_search
Dev api relation path search
2024-09-30 17:33:45 +08:00
pycook
7eecf3cec3 feat(api): add api /ci_type_relations/path 2024-09-26 20:32:21 +08:00
pycook
f6e9c443f7 Merge pull request #622 from novohool/master
Update cache support for environment variables in settings.example.py
2024-09-26 18:09:54 +08:00
pycook
857cbd82fd feat(api): add relation path search 2024-09-26 17:59:08 +08:00
novohool
9a14296e02 Update settings.example.py 2024-09-26 17:00:51 +08:00
pycook
f638b52759 fix(api): change records of attribute values for date and datetime 2024-09-25 19:37:08 +08:00
pycook
78da728105 fix(api): search for multiple CIType 2024-09-24 17:46:27 +08:00
pycook
eb69029a51 fix(api): ci relations search 2024-09-23 19:46:43 +08:00
Leo Song
07a097eba2 Merge pull request #619 from veops/dev_ui_240920
feat: update computed attr tip
2024-09-20 15:36:55 +08:00
songlh
e843e3eac9 feat: update computed attr tip 2024-09-20 15:36:19 +08:00
Leo Song
7308cfa6c2 Merge pull request #617 from veops/dev_ui_240914
dev_ui_240914
2024-09-14 17:28:42 +08:00
songlh
64ea4fb21f fix(ui): operation history search expand error 2024-09-14 17:27:57 +08:00
songlh
e15cefaa38 fix(ui): employeeTreeSelect display error 2024-09-14 17:26:33 +08:00
pycook
f32339b969 Merge pull request #616 from thexqn/optimize_history
feat: Add show_attr value column to operation history table
2024-09-14 11:55:01 +08:00
thexqn
131d213a73 优化CITypeCache的调用方式 2024-09-14 11:30:45 +08:00
thexqn
ff98777689 feat(cmdb): 添加操作历史表的唯一值列 (Add unique value column to operation history table) 2024-09-14 01:13:07 +08:00
thexqn
383d4c88ed feat: Add unique value column to operation history table 2024-09-13 23:44:40 +08:00
Leo Song
bb7157e292 Merge pull request #615 from veops/dev_ui_240913
feat(ui): add employeeTreeSelect otherOptions prop
2024-09-13 18:36:48 +08:00
songlh
b1a82f1a67 feat(ui): add employeeTreeSelect otherOptions prop 2024-09-13 18:36:24 +08:00
pycook
de86ea3852 fix(api): remote ip for login log 2024-09-10 11:41:35 +08:00
pycook
bf05ea240e feat(api): acl supports channel 2024-09-09 15:28:20 +08:00
Leo Song
8ec0d619d7 Merge pull request #613 from veops/dev_ui_240909
feat(ui): add SplitPane calcBasedParent prop
2024-09-09 10:45:27 +08:00
songlh
61f8c463bc feat(ui): add SplitPane calcBasedParent prop 2024-09-09 10:44:58 +08:00
Leo Song
9b4dc3e43b Merge pull request #611 from veops/dev_ui_240903
feat: update icon select
2024-09-03 16:41:18 +08:00
songlh
9e69be8256 feat: update icon select 2024-09-03 16:40:46 +08:00
85 changed files with 8665 additions and 1593 deletions

1
.gitignore vendored
View File

@@ -78,3 +78,4 @@ cmdb-ui/npm-debug.log*
cmdb-ui/yarn-debug.log*
cmdb-ui/yarn-error.log*
cmdb-ui/package-lock.json
start.sh

View File

@@ -68,6 +68,7 @@ pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2"
python-magic = "==0.4.27"
jsonpath = "==0.82.2"
networkx = ">=3.1"
[dev-packages]
# Testing

View File

@@ -1,14 +1,13 @@
# -*- coding:utf-8 -*-
import click
import copy
import datetime
import json
import requests
import time
import uuid
import click
import requests
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
@@ -37,11 +36,14 @@ from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import App
from api.models.acl import ResourceType
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CIType
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import OperationRecord
from api.models.cmdb import PreferenceRelationView
from api.tasks.cmdb import batch_ci_cache
@click.command()
@@ -557,5 +559,20 @@ def cmdb_patch(version):
existed.update(option=option, commit=False)
db.session.commit()
if version >= "2.4.14": # update ci columns: updated_at and updated_by
ci_ids = []
for i in CI.get_by(only_query=True).filter(CI.updated_at.is_(None)):
hist = AttributeHistory.get_by(ci_id=i.id, only_query=True).order_by(AttributeHistory.id.desc()).first()
if hist is not None:
record = OperationRecord.get_by_id(hist.record_id)
if record is not None:
u = UserCache.get(record.uid)
i.update(updated_at=record.created_at, updated_by=u and u.nickname, flush=True)
ci_ids.append(i.id)
db.session.commit()
batch_ci_cache.apply_async(args=(ci_ids,))
except Exception as e:
print("cmdb patch failed: {}".format(e))

View File

@@ -45,6 +45,7 @@ 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.perm.acl.cache import UserCache
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import handle_arg_list
@@ -206,6 +207,8 @@ class CIManager(object):
res['_type'] = ci_type.id
res['ci_type_alias'] = ci_type.alias
res['_id'] = ci_id
res['_updated_at'] = str(ci.updated_at)
res['_updated_by'] = ci.updated_by
return res
@@ -581,6 +584,9 @@ class CIManager(object):
else:
ci_relation_add(ref_ci_dict, ci.id)
u = UserCache.get(current_user.uid)
ci.update(updated_at=now, updated_by=u and u.nickname)
@staticmethod
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)))
@@ -709,13 +715,18 @@ class CIManager(object):
elif fields:
_res = []
for d in res:
if isinstance(fields, dict) and d.get("_type") not in fields:
_res.append(d)
continue
_d = dict()
_d["_id"], _d["_type"] = d.get("_id"), d.get("_type")
_d["ci_type"] = d.get("ci_type")
if unique_required:
_d[d.get('unique')] = d.get(d.get('unique'))
for field in fields + ['ci_type_alias', 'unique', 'unique_alias']:
_fields = list(fields.get(_d['_type']) or [] if isinstance(fields, dict) else fields)
for field in _fields + ['ci_type_alias', 'unique', 'unique_alias']:
_d[field] = d.get(field)
_res.append(_d)
return _res
@@ -732,9 +743,8 @@ class CIManager(object):
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_IDS
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_VALUE_TABLE
if not fields:
filter_fields_sql = ""
else:
filter_fields_sql = ""
if fields and isinstance(fields, list):
_fields = list()
for field in fields:
attr = AttributeCache.get(field)
@@ -776,6 +786,10 @@ class CIManager(object):
ci_set.add(ci_id)
res[ci2pos[ci_id]] = ci_dict
if isinstance(fields, dict) and fields.get(type_id):
if attr_name not in fields[type_id]:
continue
if ret_key == RetKey.NAME:
attr_key = attr_name
elif ret_key == RetKey.ALIAS:
@@ -813,7 +827,7 @@ class CIManager(object):
if not ci_ids:
return []
fields = [] if fields is None or not isinstance(fields, list) else fields
fields = [] if not fields else fields
ci_id_tuple = tuple(map(int, ci_ids))
res = cls._get_cis_from_cache(ci_id_tuple, ret_key, fields, unique_required, excludes=excludes)

View File

@@ -3,6 +3,7 @@
from collections import defaultdict
import copy
import networkx as nx
import toposort
from flask import abort
from flask import current_app
@@ -845,6 +846,29 @@ class CITypeRelationManager(object):
return ids
@staticmethod
def find_path(source_type_id, target_type_ids):
source_type_id = int(source_type_id)
target_type_ids = map(int, target_type_ids)
graph = nx.DiGraph()
def get_children(_id):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
for i in children:
if i.child_id != _id:
graph.add_edge(i.parent_id, i.child_id)
get_children(i.child_id)
get_children(source_type_id)
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))
del graph
return paths
@staticmethod
def _wrap_relation_type_dict(type_id, relation_inst):
ci_type_dict = CITypeCache.get(type_id).to_dict()

View File

@@ -1,6 +1,8 @@
# -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.utils import BaseEnum
@@ -110,17 +112,23 @@ class ExecuteStatusEnum(BaseEnum):
FAILED = '1'
RUNNING = '2'
class RelationSourceEnum(BaseEnum):
ATTRIBUTE_VALUES = "0"
AUTO_DISCOVERY = "1"
BUILTIN_ATTRIBUTES = {
"_updated_at": _l("Update Time"),
"_updated_by": _l("Updated By"),
}
CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id'}
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id', *BUILTIN_ATTRIBUTES.keys()}
L_TYPE = None
L_CI = None

View File

@@ -10,6 +10,7 @@ from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import RelationTypeCache
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.cache import UserCache
@@ -22,6 +23,7 @@ from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import OperationRecord
from api.lib.cmdb.utils import TableMap
class AttributeHistoryManger(object):
@@ -59,8 +61,23 @@ class AttributeHistoryManger(object):
total = len(records)
res = {}
show_attr_set = {}
show_attr_cache = {}
for record in records:
record_id = record.OperationRecord.id
type_id = record.OperationRecord.type_id
ci_id = record.AttributeHistory.ci_id
show_attr_set[ci_id] = None
show_attr = show_attr_cache.setdefault(
type_id,
AttributeCache.get(
CITypeCache.get(type_id).show_id or CITypeCache.get(type_id).unique_id) if CITypeCache.get(type_id) else None
)
if show_attr:
attr_table = TableMap(attr=show_attr).table
attr_record = attr_table.get_by(attr_id=show_attr.id, ci_id=ci_id, first=True, to_dict=False)
show_attr_set[ci_id] = attr_record.value if attr_record else None
attr_hist = record.AttributeHistory.to_dict()
attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id'])
if attr_hist['attr']:
@@ -76,6 +93,7 @@ class AttributeHistoryManger(object):
if record_id not in res:
record_dict = record.OperationRecord.to_dict()
record_dict['show_attr_value'] = show_attr_set.get(ci_id)
record_dict["user"] = UserCache.get(record_dict.get("uid"))
if record_dict["user"]:
record_dict['user'] = record_dict['user'].nickname

View File

@@ -1,6 +1,5 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
import copy
@@ -17,6 +16,7 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
@@ -25,7 +25,6 @@ from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation
@@ -137,17 +136,24 @@ class PreferenceManager(object):
_type = CITypeCache.get(type_id)
type_id = _type and _type.id
attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.type_id == type_id).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
CITypeAttribute.attr_id).all()
# attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
# CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
# PreferenceShowAttributes.uid == current_user.uid).filter(
# PreferenceShowAttributes.type_id == type_id).filter(
# PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
# CITypeAttribute.attr_id).all()
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False)
result = []
for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order):
item = i.PreferenceShowAttributes.attr.to_dict()
item.update(dict(is_fixed=i.PreferenceShowAttributes.is_fixed))
for i in sorted(attrs, key=lambda x: x.order):
if i.attr_id:
item = i.attr.to_dict()
elif i.builtin_attr:
item = dict(name=i.builtin_attr, alias=BUILTIN_ATTRIBUTES[i.builtin_attr])
else:
item = dict(name="", alias="")
item.update(dict(is_fixed=i.is_fixed))
result.append(item)
is_subscribed = True
@@ -156,10 +162,14 @@ class PreferenceManager(object):
choice_web_hook_parse=False,
choice_other_parse=False)
result = [i for i in result if i['default_show']]
for i in BUILTIN_ATTRIBUTES:
result.append(dict(name=i, alias=BUILTIN_ATTRIBUTES[i]))
is_subscribed = False
for i in result:
if i["is_choice"]:
if i.get("is_choice"):
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
@@ -173,24 +183,34 @@ class PreferenceManager(object):
_attr, is_fixed = x
else:
_attr, is_fixed = x, False
attr = AttributeCache.get(_attr) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
if _attr in BUILTIN_ATTRIBUTES:
attr = None
builtin_attr = _attr
else:
attr = AttributeCache.get(_attr) or abort(
404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
builtin_attr = None
existed = PreferenceShowAttributes.get_by(type_id=type_id,
uid=current_user.uid,
attr_id=attr.id,
attr_id=attr and attr.id,
builtin_attr=builtin_attr,
first=True,
to_dict=False)
if existed is None:
PreferenceShowAttributes.create(type_id=type_id,
uid=current_user.uid,
attr_id=attr.id,
attr_id=attr and attr.id,
builtin_attr=builtin_attr,
order=order,
is_fixed=is_fixed)
else:
existed.update(order=order, is_fixed=is_fixed)
attr_dict = {int(i[0]) if isinstance(i, list) else int(i): j for i, j in attr_order}
attr_dict = {(int(i[0]) if i[0].isdigit() else i[0]) if isinstance(i, list) else
(int(i) if i.isdigit() else i): j for i, j in attr_order}
for i in existed_all:
if i.attr_id not in attr_dict:
if (i.attr_id and i.attr_id not in attr_dict) or (i.builtin_attr and i.builtin_attr not in attr_dict):
i.soft_delete()
if not existed_all and attr_order:
@@ -384,7 +404,7 @@ class PreferenceManager(object):
def add_search_option(**kwargs):
kwargs['uid'] = current_user.uid
if kwargs['name'] in ('__recent__', '__favor__'):
if kwargs['name'] in ('__recent__', '__favor__', '__relation_favor__'):
if kwargs['name'] == '__recent__':
for i in PreferenceSearchOption.get_by(
only_query=True, name=kwargs['name'], uid=current_user.uid).order_by(

View File

@@ -154,3 +154,5 @@ class ErrFormat(CommonErrFormat):
topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在
# 因为该分组下定义了拓扑视图,不能删除
topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists")
relation_path_search_src_target_required = _l("Both the source model and the target model must be selected")

View File

@@ -4,8 +4,8 @@
from __future__ import unicode_literals
import copy
import six
import time
from flask import current_app
from flask_login import current_user
from jinja2 import Template
@@ -15,6 +15,7 @@ from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
@@ -66,6 +67,7 @@ class Search(object):
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.only_ids = only_ids
self.multi_type_has_ci_filter = False
self.valid_type_names = []
self.type2filter_perms = dict()
@@ -104,35 +106,56 @@ class Search(object):
else:
raise SearchError(ErrFormat.attribute_not_found.format(key))
def _type_query_handler(self, v, queries):
def _type_query_handler(self, v, queries, is_sub=False):
new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v]
type_num = len(new_v)
type_id_list = []
for _v in new_v:
ci_type = CITypeCache.get(_v)
if len(new_v) == 1 and not self.sort and ci_type and ci_type.default_order_attr:
if type_num == 1 and not self.sort and ci_type and ci_type.default_order_attr:
self.sort = ci_type.default_order_attr
if ci_type is not None:
if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names:
self.type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms:
if not is_sub:
self.type_id_list.append(str(ci_type.id))
type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms and not is_sub:
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter and self.use_ci_filter and not self.use_id_filter:
sub = []
ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','):
if i.startswith("~") and not sub:
queries.append(i)
if type_num == 1:
if i.startswith("~") and not sub:
queries.append(i)
else:
sub.append(i)
else:
sub.append(i)
if sub:
queries.append(dict(operator="&", queries=sub))
if type_num == 1:
queries.append(dict(operator="&", queries=sub))
else:
if str(ci_type.id) in self.type_id_list:
self.type_id_list.remove(str(ci_type.id))
type_id_list.remove(str(ci_type.id))
sub.extend([i for i in queries[1:] if isinstance(i, six.string_types)])
sub.insert(0, "_type:{}".format(ci_type.id))
queries.append(dict(operator="|", queries=sub))
self.multi_type_has_ci_filter = True
if self.type2filter_perms[ci_type.id].get('attr_filter'):
if not self.fl:
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
if type_num == 1:
if not self.fl:
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
self.fl = self.fl or {}
if not self.fl or isinstance(self.fl, dict):
self.fl[ci_type.id] = set(self.type2filter_perms[ci_type.id]['attr_filter'])
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
@@ -146,13 +169,17 @@ class Search(object):
else:
raise SearchError(ErrFormat.ci_type_not_found2.format(_v))
if self.type_id_list:
type_ids = ",".join(self.type_id_list)
if type_num != len(self.type_id_list) and queries and queries[0].startswith('_type') and not is_sub:
queries[0] = "_type:({})".format(";".join(self.type_id_list))
if type_id_list:
type_ids = ",".join(type_id_list)
_query_sql = QUERY_CI_BY_TYPE.format(type_ids)
if self.only_type_query:
if self.only_type_query or self.multi_type_has_ci_filter:
return _query_sql
else:
return ""
elif type_num > 1: # there must be instance-level access control
return "select c_cis.id as ci_id from c_cis where c_cis.id=0"
return ""
@staticmethod
@@ -229,7 +256,7 @@ class Search(object):
return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format(
(self.page - 1) * self.count, sort_type, self.count))
elif self.type_id_list:
elif self.type_id_list and not self.multi_type_has_ci_filter:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@@ -254,7 +281,7 @@ class Search(object):
def __sort_by_type(self, sort_type, query_sql):
ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}"
if self.type_id_list:
if self.type_id_list and not self.multi_type_has_ci_filter:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@@ -278,16 +305,23 @@ class Search(object):
(self.page - 1) * self.count, sort_type, self.count))
def __sort_by_field(self, field, sort_type, query_sql):
attr = AttributeCache.get(field)
attr_id = attr.id
if field not in BUILTIN_ATTRIBUTES:
table_name = TableMap(attr=attr).table_name
_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
attr = AttributeCache.get(field)
attr_id = attr.id
if self.only_type_query or not self.type_id_list:
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT ALIAS.ci_id, {0}.value
FROM ({1}) AS ALIAS INNER JOIN {0} ON {0}.ci_id = ALIAS.ci_id
WHERE {0}.attr_id = {2}""".format(table_name, query_sql, attr_id)
new_table = _v_query_sql
else:
_v_query_sql = """SELECT c_cis.id AS ci_id, c_cis.{0} AS value
FROM c_cis INNER JOIN ({1}) AS ALIAS ON ALIAS.ci_id = c_cis.id""".format(
field[1:], query_sql)
new_table = _v_query_sql
if self.only_type_query or not self.type_id_list or self.multi_type_has_ci_filter:
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
@@ -325,7 +359,9 @@ class Search(object):
INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A")
elif operator == "|" or operator == "|~":
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL SELECT * FROM ({2}) as {3}".format(query_sql, alias,
_query_sql,
alias + "A")
elif operator == "~":
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
@@ -430,14 +466,14 @@ class Search(object):
return result
def __query_by_attr(self, q, queries, alias):
def __query_by_attr(self, q, queries, alias, is_sub=False):
k = q.split(":")[0].strip()
v = "\:".join(q.split(":")[1:]).strip()
v = v.replace("'", "\\'")
v = v.replace('"', '\\"')
field, field_type, operator, attr = self._attr_name_proc(k)
if field == "_type":
_query_sql = self._type_query_handler(v, queries)
_query_sql = self._type_query_handler(v, queries, is_sub)
elif field == "_id":
_query_sql = self._id_query_handler(v)
@@ -484,19 +520,20 @@ class Search(object):
return alias, _query_sql, operator
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&'):
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&',
is_sub=False):
query_sql = ""
for q in queries:
_query_sql = ""
if isinstance(q, dict):
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias)
current_app.logger.info(_query_sql)
current_app.logger.info((operator, is_first, alias))
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True)
# current_app.logger.info(_query_sql)
# current_app.logger.info((operator, is_first, alias))
operator = q['operator']
elif ":" in q and not q.startswith("*"):
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias)
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)
elif q == "*":
continue
elif q:
@@ -547,7 +584,6 @@ class Search(object):
queries = handle_arg_list(self.orig_query)
queries = self._extra_handle_query_expr(queries)
queries = self.__confirm_type_first(queries)
current_app.logger.debug(queries)
_, query_sql, _ = self.__query_build_by_field(queries)
@@ -585,13 +621,16 @@ class Search(object):
return facet_result
def _fl_build(self):
_fl = list()
for f in self.fl:
k, _, _, _ = self._attr_name_proc(f)
if k:
_fl.append(k)
if isinstance(self.fl, list):
_fl = list()
for f in self.fl:
k, _, _, _ = self._attr_name_proc(f)
if k:
_fl.append(k)
return _fl
return _fl
else:
return self.fl
def search(self):
numfound, ci_ids = self._query_build_raw()
@@ -610,6 +649,8 @@ class Search(object):
if ci_ids:
response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes)
for res in response:
if not res:
continue
ci_type = res.get("ci_type")
if ci_type not in counter.keys():
counter[ci_type] = 0

View File

@@ -1,8 +1,11 @@
# -*- coding:utf-8 -*-
import json
import sys
from collections import Counter
from collections import defaultdict
import copy
import json
import networkx as nx
import sys
from flask import abort
from flask import current_app
from flask_login import current_user
@@ -13,6 +16,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
@@ -25,10 +29,12 @@ from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI
from api.models.cmdb import CITypeRelation
from api.models.cmdb import RelationType
class Search(object):
def __init__(self, root_id,
def __init__(self, root_id=None,
level=None,
query=None,
fl=None,
@@ -419,3 +425,169 @@ class Search(object):
level_ids = _level_ids
return result
@staticmethod
def _get_src_ids(src):
q = src.get('q') or ''
if not q.startswith('_type:'):
q = "_type:{},{}".format(src['type_id'], q)
return SearchFromDB(q, use_ci_filter=True, only_ids=True, count=100000).search()
@staticmethod
def _filter_target_ids(target_ids, type_ids, q):
if not q.startswith('_type:'):
q = "_type:({}),{}".format(";".join(map(str, type_ids)), q)
ci_ids = SearchFromDB(q, ci_ids=target_ids, use_ci_filter=True, only_ids=True, count=100000).search()
cis = CI.get_by(fl=['id', 'type_id'], only_query=True).filter(CI.id.in_(ci_ids))
return [(str(i.id), i.type_id) for i in cis]
@staticmethod
def _path2level(src_type_id, target_type_ids, path):
if not src_type_id or not target_type_ids:
return abort(400, ErrFormat.relation_path_search_src_target_required)
graph = nx.DiGraph()
graph.add_edges_from([(n, _path[idx + 1]) for _path in path for idx, n in enumerate(_path[:-1])])
relation_types = defaultdict(dict)
level2type = defaultdict(set)
type2show_key = dict()
for _path in path:
for idx, node in enumerate(_path[1:]):
level2type[idx + 1].add(node)
src = CITypeCache.get(_path[idx])
target = CITypeCache.get(node)
relation_type = RelationType.get_by(only_query=True).join(
CITypeRelation, CITypeRelation.relation_type_id == RelationType.id).filter(
CITypeRelation.parent_id == src.id).filter(CITypeRelation.child_id == target.id).first()
relation_types[src.alias].update({target.alias: relation_type.name})
if src.id not in type2show_key:
type2show_key[src.id] = AttributeCache.get(src.show_id or src.unique_id).name
if target.id not in type2show_key:
type2show_key[target.id] = AttributeCache.get(target.show_id or target.unique_id).name
nodes = graph.nodes()
return level2type, list(nodes), relation_types, type2show_key
def _build_graph(self, source_ids, source_type_id, level2type, target_type_ids, acl):
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
target_type_ids = set(target_type_ids)
graph = nx.DiGraph()
target_ids = []
key = [(str(i), source_type_id) for i in source_ids]
graph.add_nodes_from(key)
for level in level2type:
filter_type_ids = level2type[level]
id_filter_limit = dict()
for _type_id in filter_type_ids:
if type2filter_perms.get(_type_id):
_id_filter_limit, _ = self._get_ci_filter(type2filter_perms[_type_id])
id_filter_limit.update(_id_filter_limit)
has_target = filter_type_ids & target_type_ids
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get([i[0] for i in key],
REDIS_PREFIX_CI_RELATION) or []]]
_key = []
for idx, _id in enumerate(key):
valid_targets = [i for i in res[idx] if i[1] in filter_type_ids and
(not id_filter_limit or int(i[0]) in id_filter_limit)]
_key.extend(valid_targets)
graph.add_edges_from(zip([_id] * len(valid_targets), valid_targets))
if has_target:
target_ids.extend([j[0] for i in res for j in i if j[1] in target_type_ids])
key = copy.deepcopy(_key)
return graph, target_ids
@staticmethod
def _find_paths(graph, source_ids, source_type_id, target_ids, valid_path, max_depth=6):
paths = []
for source_id in source_ids:
_paths = nx.all_simple_paths(graph,
source=(source_id, source_type_id),
target=target_ids,
cutoff=max_depth)
for __path in _paths:
if tuple([i[1] for i in __path]) in valid_path:
paths.append([i[0] for i in __path])
return paths
@staticmethod
def _wrap_path_result(paths, types, valid_path, target_types, type2show_key):
ci_ids = [j for i in paths for j in i]
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, types))),
use_ci_filter=False,
ci_ids=list(map(int, ci_ids)),
count=1000000).search()
id2ci = {str(i.get('_id')): i if i['_type'] in target_types else {
type2show_key[i['_type']]: i[type2show_key[i['_type']]],
"ci_type_alias": i["ci_type_alias"],
"_type": i["_type"],
} for i in response}
result = defaultdict(list)
counter = defaultdict(int)
for path in paths:
key = "-".join([id2ci.get(i, {}).get('ci_type_alias') or '' for i in path])
if tuple([id2ci.get(i, {}).get('_type') for i in path]) in valid_path:
counter[key] += 1
result[key].append(path)
return result, counter, id2ci
def search_by_path(self, source, target, path):
"""
:param source: {type_id: id, q: expr}
:param target: {type_ids: [id], q: expr}
:param path: [source_type_id, ..., target_type_id], use type id
:return:
"""
acl = ACLManager('cmdb')
if not self.is_app_admin:
res = {i['name'] for i in acl.get_resources(ResourceTypeEnum.CI_TYPE)}
for type_id in (source.get('type_id') and [source['type_id']] or []) + (target.get('type_ids') or []):
_type = CITypeCache.get(type_id)
if _type and _type.name not in res:
return abort(403, ErrFormat.no_permission.format(_type.alias, PermEnum.READ))
target['type_ids'] = [i[-1] for i in path]
level2type, types, relation_types, type2show_key = self._path2level(
source.get('type_id'), target.get('type_ids'), path)
if not level2type:
return [], {}, 0, self.page, 0, {}, {}
source_ids = self._get_src_ids(source)
graph, target_ids = self._build_graph(source_ids, source['type_id'], level2type, target['type_ids'], acl)
target_ids = self._filter_target_ids(target_ids, target['type_ids'], target.get('q') or '')
paths = self._find_paths(graph,
source_ids,
source['type_id'],
set(target_ids),
{tuple(i): 1 for i in path})
numfound = len(paths)
paths = paths[(self.page - 1) * self.count:self.page * self.count]
response, counter, id2ci = self._wrap_path_result(paths,
types,
{tuple(i): 1 for i in path},
set(target.get('type_ids') or []),
type2show_key)
return response, counter, len(paths), self.page, numfound, id2ci, relation_types, type2show_key

View File

@@ -97,6 +97,8 @@ class AttributeValueManager(object):
deserialize = ValueTypeMap.deserialize[value_type]
try:
v = deserialize(value)
if value_type in (ValueTypeEnum.DATE, ValueTypeEnum.DATETIME):
return str(v)
return v
except ValueDeserializeError as e:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))

View File

@@ -376,7 +376,7 @@ class AuditCRUD(object):
origin=origin, current=current, extra=extra, source=source.value)
@classmethod
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, ip=None, browser=None):
if _id is not None:
existed = AuditLoginLog.get_by_id(_id)
if existed is not None:
@@ -387,8 +387,9 @@ class AuditCRUD(object):
is_ok=is_ok,
description=description,
logout_at=logout_at,
ip=request.headers.get('X-Real-IP') or request.remote_addr,
browser=request.headers.get('User-Agent'),
ip=(ip or request.headers.get('X-Forwarded-For') or
request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
browser=browser or request.headers.get('User-Agent'),
channel=request.values.get('channel', 'web'),
)

View File

@@ -71,7 +71,7 @@ class PermissionCRUD(object):
@classmethod
def get_all2(cls, resource_name, resource_type_name, app_id):
rt = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
rt = ResourceType.get_by(name=resource_type_name, app_id=app_id, first=True, to_dict=False)
rt or abort(404, ErrFormat.resource_type_not_found.format(resource_type_name))
r = Resource.get_by(name=resource_name, resource_type_id=rt.id, app_id=app_id, first=True, to_dict=False)

View File

@@ -253,6 +253,7 @@ class CI(Model):
status = db.Column(db.Enum(*CIStatusEnum.all(), name="status"))
heartbeat = db.Column(db.DateTime, default=lambda: datetime.datetime.now())
is_auto_discovery = db.Column('a', db.Boolean, default=False)
updated_by = db.Column(db.String(64))
ci_type = db.relationship("CIType", backref="c_cis.type_id")
@@ -534,6 +535,7 @@ class CustomDashboard(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'))
builtin_attr = db.Column(db.String(256), nullable=True)
level = db.Column(db.Integer)
options = db.Column(db.JSON)

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-08-20 13:47+0800\n"
"POT-Creation-Date: 2024-09-26 17:57+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
@@ -492,6 +492,10 @@ msgstr "拓扑视图分组 {} 已经存在"
msgid "The group cannot be deleted because the topology view already exists"
msgstr "因为该分组下定义了拓扑视图,不能删除"
#: api/lib/cmdb/resp_format.py:158
msgid "Both the source model and the target model must be selected"
msgstr "源模型和目标模型不能为空!"
#: api/lib/common_setting/resp_format.py:8
msgid "Company info already existed"
msgstr "公司信息已存在,无法创建!"

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import datetime
import jwt
import six
from flask import abort
@@ -17,10 +16,12 @@ from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import User
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from api.lib.perm.acl.role import RoleRelationCRUD
from api.lib.perm.auth import auth_abandoned
from api.lib.perm.auth import auth_with_app_token
from api.models.acl import Role
@@ -124,10 +125,17 @@ class AuthWithKeyView(APIView):
if not user.get('username'):
user['username'] = user.get('name')
return self.jsonify(user=user,
authenticated=authenticated,
rid=role and role.id,
can_proxy=can_proxy)
result = dict(user=user,
authenticated=authenticated,
rid=role and role.id,
can_proxy=can_proxy)
if request.values.get('need_parentRoles') in current_app.config.get('BOOL_TRUE'):
app_id = AppCache.get(request.values.get('app_id'))
parent_ids = RoleRelationCRUD.recursive_parent_ids(role and role.id, app_id and app_id.id)
result['user']['parentRoles'] = [RoleCache.get(rid).name for rid in set(parent_ids) if RoleCache.get(rid)]
return self.jsonify(result)
class AuthWithTokenView(APIView):
@@ -184,6 +192,8 @@ class LogoutView(APIView):
def post(self):
logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
AuditCRUD.add_login_log(None, None, None,
_id=session.get('LOGIN_ID') or request.values.get('LOGIN_ID'),
logout_at=datetime.datetime.now())
self.jsonify(code=200)

View File

@@ -11,6 +11,7 @@ from flask_login import current_user
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import AuditCRUD
from api.lib.perm.acl.acl import role_required
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
@@ -48,6 +49,13 @@ class GetUserInfoView(APIView):
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar'))
if request.values.get('channel'):
_id = AuditCRUD.add_login_log(name, True, ErrFormat.login_succeed,
ip=request.values.get('ip'),
browser=request.values.get('browser'))
session['LOGIN_ID'] = _id
result['LOGIN_ID'] = _id
current_app.logger.info("get user info for3: {}".format(result))
return self.jsonify(result=result)

View File

@@ -2,7 +2,6 @@
import time
from flask import abort
from flask import current_app
from flask import request
@@ -65,6 +64,42 @@ class CIRelationSearchView(APIView):
result=response)
class CIRelationSearchPathView(APIView):
url_prefix = ("/ci_relations/path/s", "/ci_relations/path/search")
@args_required("source", "target", "path")
def post(self):
"""@params: page: page number
page_size | count: page size
source: source CIType, e.g. {type_id: 1, q: `search expr`}
target: target CIType, e.g. {type_ids: [2], q: `search expr`}
path: Path from the Source CIType to the Target CIType, e.g. [1, ..., 2]
"""
page = get_page(request.values.get("page", 1))
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
source = request.values.get("source")
target = request.values.get("target")
path = request.values.get("path")
s = Search(page=page, count=count)
try:
(response, counter, total, page, numfound, id2ci,
relation_types, type2show_key) = s.search_by_path(source, target, path)
except SearchError as e:
return abort(400, str(e))
return self.jsonify(numfound=numfound,
total=total,
page=page,
counter=counter,
paths=response,
id2ci=id2ci,
relation_types=relation_types,
type2show_key=type2show_key)
class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics"

View File

@@ -8,7 +8,6 @@ from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeRelationManager
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.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.common_setting.decorator import perms_role_required
@@ -17,7 +16,7 @@ from api.lib.decorator import args_required
from api.lib.perm.acl.acl import ACLManager
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 handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
@@ -42,6 +41,19 @@ class GetParentsView(APIView):
return self.jsonify(parents=CITypeRelationManager.get_parents(child_id))
class CITypeRelationPathView(APIView):
url_prefix = ("/ci_type_relations/path",)
@args_required("source_type_id", "target_type_ids")
def get(self):
source_type_id = request.values.get("source_type_id")
target_type_ids = handle_arg_list(request.values.get("target_type_ids"))
paths = CITypeRelationManager.find_path(source_type_id, target_type_ids)
return self.jsonify(paths=paths)
class CITypeRelationView(APIView):
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")

View File

@@ -56,3 +56,4 @@ colorama>=0.4.6
lz4>=4.3.2
python-magic==0.4.27
jsonpath==0.82.2
networkx>=3.1

View File

@@ -39,9 +39,9 @@ SQLALCHEMY_ENGINE_OPTIONS = {
# # cache
CACHE_TYPE = 'redis'
CACHE_REDIS_HOST = '127.0.0.1'
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = ''
CACHE_REDIS_HOST = env.str('CACHE_REDIS_HOST', default='redis')
CACHE_REDIS_PORT = env.str('CACHE_REDIS_PORT', default='6379')
CACHE_REDIS_PASSWORD = env.str('CACHE_REDIS_PASSWORD', default='')
CACHE_KEY_PREFIX = 'CMDB::'
CACHE_DEFAULT_TIMEOUT = 3000

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1725331691589') format('woff2'),
url('iconfont.woff?t=1725331691589') format('woff'),
url('iconfont.ttf?t=1725331691589') format('truetype');
src: url('iconfont.woff2?t=1729157759723') format('woff2'),
url('iconfont.woff?t=1729157759723') format('woff'),
url('iconfont.ttf?t=1729157759723') format('truetype');
}
.iconfont {
@@ -13,6 +13,206 @@
-moz-osx-font-smoothing: grayscale;
}
.oneterm-mysql:before {
content: "\e9e8";
}
.oneterm-redis:before {
content: "\e9e7";
}
.veops-sign_out:before {
content: "\e9e6";
}
.veops-company:before {
content: "\e9e5";
}
.veops-emails:before {
content: "\e9e4";
}
.veops-switch:before {
content: "\e9e3";
}
.qiyeweixin:before {
content: "\e9e2";
}
.veops-progress:before {
content: "\e9e1";
}
.veops-completed:before {
content: "\e9e0";
}
.itsm-ticketTime:before {
content: "\e9df";
}
.veops-notification:before {
content: "\e9dc";
}
.a-veops-account1:before {
content: "\e9dd";
}
.veops-personal:before {
content: "\e9de";
}
.itsm-customer_satisfaction2:before {
content: "\e9da";
}
.itsm-over2:before {
content: "\e9db";
}
.veops-search1:before {
content: "\e9d9";
}
.itsm-customer_satisfaction:before {
content: "\e9d8";
}
.itsm-over:before {
content: "\e9d7";
}
.itsm-request:before {
content: "\e9d6";
}
.itsm-release:before {
content: "\e9d5";
}
.veops-link:before {
content: "\e9d4";
}
.oneterm-command_record:before {
content: "\e9d3";
}
.ai-question:before {
content: "\e9d2";
}
.ai-sending:before {
content: "\e9d1";
}
.ai-dialogue:before {
content: "\e9d0";
}
.ai-report2:before {
content: "\e9cf";
}
.ai-delete:before {
content: "\e9cd";
}
.caise-knowledge:before {
content: "\e9ce";
}
.ai-article:before {
content: "\e9cc";
}
.ai-model_setup1:before {
content: "\e9cb";
}
.ai-report:before {
content: "\e9ca";
}
.ai-customer_service:before {
content: "\e9c9";
}
.oneterm-connect1:before {
content: "\e9c6";
}
.oneterm-session1:before {
content: "\e9c7";
}
.oneterm-assets:before {
content: "\e9c8";
}
.a-oneterm-ssh1:before {
content: "\e9c3";
}
.a-oneterm-ssh2:before {
content: "\e9c4";
}
.oneterm-rdp:before {
content: "\e9c5";
}
.caise-websphere:before {
content: "\e9c2";
}
.caise-vps:before {
content: "\e9c1";
}
.caise-F5:before {
content: "\e9c0";
}
.caise-HAProxy:before {
content: "\e9bf";
}
.caise-JBoss:before {
content: "\e9be";
}
.caise-dongfangtong:before {
content: "\e9bd";
}
.caise-kafka:before {
content: "\e9b7";
}
.caise-weblogic:before {
content: "\e9b8";
}
.caise-TDSQL:before {
content: "\e9b9";
}
.caise-kingbase:before {
content: "\e9ba";
}
.caise-dameng:before {
content: "\e9bb";
}
.caise-TIDB:before {
content: "\e9bc";
}
.veops-expand:before {
content: "\e9b6";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,356 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "42155223",
"name": "oneterm-mysql",
"font_class": "oneterm-mysql",
"unicode": "e9e8",
"unicode_decimal": 59880
},
{
"icon_id": "42155225",
"name": "oneterm-redis",
"font_class": "oneterm-redis",
"unicode": "e9e7",
"unicode_decimal": 59879
},
{
"icon_id": "42154436",
"name": "veops-sign_out",
"font_class": "veops-sign_out",
"unicode": "e9e6",
"unicode_decimal": 59878
},
{
"icon_id": "42154310",
"name": "veops-company",
"font_class": "veops-company",
"unicode": "e9e5",
"unicode_decimal": 59877
},
{
"icon_id": "42154325",
"name": "veops-emails",
"font_class": "veops-emails",
"unicode": "e9e4",
"unicode_decimal": 59876
},
{
"icon_id": "42154350",
"name": "veops-switch",
"font_class": "veops-switch",
"unicode": "e9e3",
"unicode_decimal": 59875
},
{
"icon_id": "42154370",
"name": "veops-qiyeweixin",
"font_class": "qiyeweixin",
"unicode": "e9e2",
"unicode_decimal": 59874
},
{
"icon_id": "42134185",
"name": "veops-progress",
"font_class": "veops-progress",
"unicode": "e9e1",
"unicode_decimal": 59873
},
{
"icon_id": "42134110",
"name": "veops-completed",
"font_class": "veops-completed",
"unicode": "e9e0",
"unicode_decimal": 59872
},
{
"icon_id": "42133882",
"name": "itsm-ticketTime",
"font_class": "itsm-ticketTime",
"unicode": "e9df",
"unicode_decimal": 59871
},
{
"icon_id": "42122869",
"name": "veops-notification",
"font_class": "veops-notification",
"unicode": "e9dc",
"unicode_decimal": 59868
},
{
"icon_id": "42122868",
"name": "veops-account_password",
"font_class": "a-veops-account1",
"unicode": "e9dd",
"unicode_decimal": 59869
},
{
"icon_id": "42122861",
"name": "veops-personal",
"font_class": "veops-personal",
"unicode": "e9de",
"unicode_decimal": 59870
},
{
"icon_id": "42101103",
"name": "itsm-evaluation2",
"font_class": "itsm-customer_satisfaction2",
"unicode": "e9da",
"unicode_decimal": 59866
},
{
"icon_id": "42101098",
"name": "itsm-over2",
"font_class": "itsm-over2",
"unicode": "e9db",
"unicode_decimal": 59867
},
{
"icon_id": "42065574",
"name": "veops-search",
"font_class": "veops-search1",
"unicode": "e9d9",
"unicode_decimal": 59865
},
{
"icon_id": "42063479",
"name": "itsm-evaluation",
"font_class": "itsm-customer_satisfaction",
"unicode": "e9d8",
"unicode_decimal": 59864
},
{
"icon_id": "42062436",
"name": "itsm-over",
"font_class": "itsm-over",
"unicode": "e9d7",
"unicode_decimal": 59863
},
{
"icon_id": "42050642",
"name": "itsm-requirement",
"font_class": "itsm-request",
"unicode": "e9d6",
"unicode_decimal": 59862
},
{
"icon_id": "42050622",
"name": "itsm-release",
"font_class": "itsm-release",
"unicode": "e9d5",
"unicode_decimal": 59861
},
{
"icon_id": "41903314",
"name": "veops-link",
"font_class": "veops-link",
"unicode": "e9d4",
"unicode_decimal": 59860
},
{
"icon_id": "41876664",
"name": "oneterm-command_record",
"font_class": "oneterm-command_record",
"unicode": "e9d3",
"unicode_decimal": 59859
},
{
"icon_id": "41859436",
"name": "ai-question",
"font_class": "ai-question",
"unicode": "e9d2",
"unicode_decimal": 59858
},
{
"icon_id": "41859414",
"name": "ai-sending",
"font_class": "ai-sending",
"unicode": "e9d1",
"unicode_decimal": 59857
},
{
"icon_id": "41859374",
"name": "ai-dialogue",
"font_class": "ai-dialogue",
"unicode": "e9d0",
"unicode_decimal": 59856
},
{
"icon_id": "41859191",
"name": "ai-report2",
"font_class": "ai-report2",
"unicode": "e9cf",
"unicode_decimal": 59855
},
{
"icon_id": "41858720",
"name": "ai-delete",
"font_class": "ai-delete",
"unicode": "e9cd",
"unicode_decimal": 59853
},
{
"icon_id": "41858484",
"name": "caise-knowledge",
"font_class": "caise-knowledge",
"unicode": "e9ce",
"unicode_decimal": 59854
},
{
"icon_id": "41833445",
"name": "ai-article",
"font_class": "ai-article",
"unicode": "e9cc",
"unicode_decimal": 59852
},
{
"icon_id": "41811974",
"name": "ai-model_setup (1)",
"font_class": "ai-model_setup1",
"unicode": "e9cb",
"unicode_decimal": 59851
},
{
"icon_id": "41811980",
"name": "ai-report",
"font_class": "ai-report",
"unicode": "e9ca",
"unicode_decimal": 59850
},
{
"icon_id": "41811915",
"name": "ai-customer_service",
"font_class": "ai-customer_service",
"unicode": "e9c9",
"unicode_decimal": 59849
},
{
"icon_id": "41735717",
"name": "oneterm-connect",
"font_class": "oneterm-connect1",
"unicode": "e9c6",
"unicode_decimal": 59846
},
{
"icon_id": "41735716",
"name": "oneterm-session",
"font_class": "oneterm-session1",
"unicode": "e9c7",
"unicode_decimal": 59847
},
{
"icon_id": "41735703",
"name": "oneterm-assets",
"font_class": "oneterm-assets",
"unicode": "e9c8",
"unicode_decimal": 59848
},
{
"icon_id": "41725683",
"name": "oneterm-RDP",
"font_class": "a-oneterm-ssh1",
"unicode": "e9c3",
"unicode_decimal": 59843
},
{
"icon_id": "41725684",
"name": "oneterm-SSH",
"font_class": "a-oneterm-ssh2",
"unicode": "e9c4",
"unicode_decimal": 59844
},
{
"icon_id": "41725685",
"name": "oneterm-VNC",
"font_class": "oneterm-rdp",
"unicode": "e9c5",
"unicode_decimal": 59845
},
{
"icon_id": "41724497",
"name": "caise-websphere",
"font_class": "caise-websphere",
"unicode": "e9c2",
"unicode_decimal": 59842
},
{
"icon_id": "41724575",
"name": "caise-vps",
"font_class": "caise-vps",
"unicode": "e9c1",
"unicode_decimal": 59841
},
{
"icon_id": "41724631",
"name": "caise-F5",
"font_class": "caise-F5",
"unicode": "e9c0",
"unicode_decimal": 59840
},
{
"icon_id": "41724653",
"name": "caise-HAProxy",
"font_class": "caise-HAProxy",
"unicode": "e9bf",
"unicode_decimal": 59839
},
{
"icon_id": "41722953",
"name": "caise-JBoss",
"font_class": "caise-JBoss",
"unicode": "e9be",
"unicode_decimal": 59838
},
{
"icon_id": "41722960",
"name": "caise-dongfangtong",
"font_class": "caise-dongfangtong",
"unicode": "e9bd",
"unicode_decimal": 59837
},
{
"icon_id": "41722681",
"name": "caise-kafka",
"font_class": "caise-kafka",
"unicode": "e9b7",
"unicode_decimal": 59831
},
{
"icon_id": "41722680",
"name": "caise-weblogic",
"font_class": "caise-weblogic",
"unicode": "e9b8",
"unicode_decimal": 59832
},
{
"icon_id": "41722679",
"name": "caise-TDSQL",
"font_class": "caise-TDSQL",
"unicode": "e9b9",
"unicode_decimal": 59833
},
{
"icon_id": "41722678",
"name": "caise-kingbase",
"font_class": "caise-kingbase",
"unicode": "e9ba",
"unicode_decimal": 59834
},
{
"icon_id": "41722677",
"name": "达梦",
"font_class": "caise-dameng",
"unicode": "e9bb",
"unicode_decimal": 59835
},
{
"icon_id": "41722675",
"name": "caise-TIDB",
"font_class": "caise-TIDB",
"unicode": "e9bc",
"unicode_decimal": 59836
},
{
"icon_id": "41681675",
"name": "veops-expand",

Binary file not shown.

View File

@@ -177,7 +177,7 @@ export const linearIconList = [
}]
}, {
value: 'icon-xianxing-application',
label: '应用',
label: '常用组件',
list: [{
value: 'icon-xianxing-yilianjie',
label: '已连接'
@@ -517,7 +517,7 @@ export const fillIconList = [
}]
}, {
value: 'icon-shidi-application',
label: '应用',
label: '常用组件',
list: [{
value: 'icon-shidi-yilianjie',
label: '已连接'
@@ -729,6 +729,18 @@ export const multicolorIconList = [
value: 'database',
label: '数据库',
list: [{
value: 'caise-TIDB',
label: 'TIDB'
}, {
value: 'caise-dameng',
label: '达梦'
}, {
value: 'caise-kingbase',
label: 'KingBase'
}, {
value: 'caise-TDSQL',
label: 'TDSQL'
}, {
value: 'caise-DB2',
label: 'DB2'
}, {
@@ -809,6 +821,9 @@ export const multicolorIconList = [
value: 'system',
label: '操作系统',
list: [{
value: 'ciase-aix',
label: 'aix'
}, {
value: 'caise-Windows',
label: 'Windows'
}, {
@@ -903,8 +918,38 @@ export const multicolorIconList = [
}]
}, {
value: 'caise-application',
label: '应用',
label: '常用组件',
list: [{
value: 'caise-websphere',
label: 'WebSphere'
}, {
value: 'caise-vps',
label: 'VPS'
}, {
value: 'caise-F5',
label: 'F5'
}, {
value: 'caise-HAProxy',
label: 'HAProxy'
}, {
value: 'caise-kafka',
label: 'kafka'
}, {
value: 'caise-dongfangtong',
label: '东方通'
}, {
value: 'cmdb-vcenter',
label: 'VCenter'
}, {
value: 'ops-KVM',
label: 'KVM'
}, {
value: 'caise-JBoss',
label: 'JBoss'
}, {
value: 'caise-weblogic',
label: 'WebLogic'
}, {
value: 'caise-disk_array',
label: '磁盘阵列'
}, {
@@ -928,9 +973,6 @@ export const multicolorIconList = [
}, {
value: 'caise_pool',
label: 'ip池'
}, {
value: 'ciase-aix',
label: 'aix'
}, {
value: 'caise-storage_volume1',
label: '存储卷'

View File

@@ -0,0 +1,24 @@
.cmdb-side-menu-search {
background-color: #FFFFFF !important;
cursor: auto !important;
:global {
.ant-input-affix-wrapper {
max-width: 170px !important;
width: 170px;
border-radius: 30px;
}
.ant-input {
box-shadow: none;
border: none;
background-color: #F7F8FA;
height: 30px;
line-height: 30px;
}
.ant-input-suffix {
right: 0px !important;
}
}
}

View File

@@ -9,6 +9,8 @@ import {
import { searchResourceType } from '@/modules/acl/api/resource'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
import styles from './index.module.less'
import { mapActions } from 'vuex'
const { Item, SubMenu } = Menu
@@ -40,7 +42,8 @@ export default {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: [],
resource_type: {}
resource_type: {},
currentAppRoute: ''
}
},
computed: {
@@ -64,6 +67,7 @@ export default {
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then(res => {
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
})
this.currentAppRoute = this.$route?.matched?.[0]?.name || ''
this.updateMenu()
},
watch: {
@@ -75,12 +79,14 @@ export default {
this.openKeys = this.cachedOpenKeys
}
},
$route: function () {
$route: function (route) {
this.currentAppRoute = route?.matched?.[0]?.name
this.updateMenu()
},
},
inject: ['reload'],
methods: {
...mapActions(['UpdateCMDBSEarchValue']),
cancelAttributes(e, menu) {
const that = this
e.preventDefault()
@@ -286,6 +292,47 @@ export default {
this.$message.error(this.$t('noPermission'))
}
})
},
jumpCMDBSearch(value) {
this.UpdateCMDBSEarchValue(value)
if (this.$route.name !== 'cmdb_resource_search') {
this.$router.push({
name: 'cmdb_resource_search',
})
}
},
renderCMDBSearch() {
if (this.currentAppRoute !== 'cmdb' || this.collapsed) {
return null
}
return (
<Item class={styles['cmdb-side-menu-search']}>
<a-input
ref="cmdbSideMenuSearchInputRef"
class={styles['cmdb-side-menu-search-input']}
style={{
border: this.$route.name === 'cmdb_resource_search' ? 'solid 1px #B1C9FF' : ''
}}
placeholder={this.$t('cmdbSearch')}
onPressEnter={(e) => {
this.jumpCMDBSearch(e.target.value)
}}
>
<ops-icon
slot="suffix"
type="veops-search1"
onClick={() => {
const value = this.$refs?.cmdbSideMenuSearchInputRef?.$refs?.input?.value || ''
this.jumpCMDBSearch(value)
}}
/>
</a-input>
</Item>
)
}
},
@@ -313,6 +360,7 @@ export default {
// {...{ props, on: on }}
return (
<Menu class="ops-side-bar" selectedKeys={this.selectedKeys} {...{ props, on: on }}>
{this.renderCMDBSearch()}
{menuTree}
</Menu>
)

View File

@@ -1,183 +1,187 @@
<template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot>
</div>
<div class="spliter-wrap">
<a-button
v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'"
class="collapse-btn"
@click="handleExpand"
></a-button>
<div
class="pane-trigger"
@mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div>
</div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SplitPane',
props: {
direction: {
type: String,
default: 'row',
},
min: {
type: Number,
default: 10,
},
max: {
type: Number,
default: 90,
},
paneLengthPixel: {
type: Number,
default: 220,
},
triggerLength: {
type: Number,
default: 8,
},
appName: {
type: String,
default: 'viewer',
},
collapsable: {
type: Boolean,
default: false,
},
triggerColor: {
type: String,
default: '#f7f8fa',
},
},
data() {
return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false,
parentContainer: null,
}
},
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight'
},
paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthValue2() {
const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthPercent() {
const clientRectWidth = this.parentContainer
? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100
},
},
watch: {
isExpanded(newValue) {
if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
},
mounted() {
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (paneLengthPixel) {
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
}
this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
methods: {
// 按下滑动器
handleMouseDown(e) {
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
// 按下滑动器后移动鼠标
handleMouseMove(e) {
this.isExpanded = false
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPixel = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
}
if (paneLengthPixel < this.min) {
paneLengthPixel = this.min
}
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
}
this.$emit('update:paneLengthPixel', paneLengthPixel)
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
},
// 松开滑动器
handleMouseUp() {
document.removeEventListener('mousemove', this.handleMouseMove)
},
handleExpand() {
this.isExpanded = !this.isExpanded
this.$emit('expand', this.isExpanded)
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
},
}
</script>
<style scoped lang="less">
@import './index.less';
</style>
<template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot>
</div>
<div class="spliter-wrap">
<a-button
v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'"
class="collapse-btn"
@click="handleExpand"
></a-button>
<div
class="pane-trigger"
@mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div>
</div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SplitPane',
props: {
direction: {
type: String,
default: 'row',
},
min: {
type: Number,
default: 10,
},
max: {
type: Number,
default: 90,
},
paneLengthPixel: {
type: Number,
default: 220,
},
triggerLength: {
type: Number,
default: 8,
},
appName: {
type: String,
default: 'viewer',
},
collapsable: {
type: Boolean,
default: false,
},
triggerColor: {
type: String,
default: '#f7f8fa',
},
calcBasedParent: {
type: Boolean,
defualt: false
}
},
data() {
return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false,
parentContainer: null,
}
},
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight'
},
paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthValue2() {
const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthPercent() {
const clientRectWidth = this.parentContainer && this.calcBasedParent
? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100
},
},
watch: {
isExpanded(newValue) {
if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
},
mounted() {
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (paneLengthPixel) {
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
}
this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
methods: {
// 按下滑动器
handleMouseDown(e) {
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
// 按下滑动器后移动鼠标
handleMouseMove(e) {
this.isExpanded = false
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPixel = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
}
if (paneLengthPixel < this.min) {
paneLengthPixel = this.min
}
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
}
this.$emit('update:paneLengthPixel', paneLengthPixel)
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
},
// 松开滑动器
handleMouseUp() {
document.removeEventListener('mousemove', this.handleMouseMove)
},
handleExpand() {
this.isExpanded = !this.isExpanded
this.$emit('expand', this.isExpanded)
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
},
}
</script>
<style scoped lang="less">
@import './index.less';
</style>

View File

@@ -6,7 +6,8 @@
:paneLengthPixel.sync="paneLengthPixel"
:appName="appName"
:triggerColor="triggerColor"
:triggerLength="18"
:triggerLength="triggerLength"
:calcBasedParent="calcBasedParent"
>
<template #one>
<div class="two-column-layout-sidebar">
@@ -37,6 +38,14 @@ export default {
type: String,
default: '#f7f8fa',
},
triggerLength: {
type: Number,
default: 18
},
calcBasedParent: {
type: Boolean,
defualt: false
}
},
data() {
return {

View File

@@ -11,38 +11,11 @@
<span class="common-settings-btn-text">{{ $t('settings') }}</span>
</span>
<a-popover
overlayClassName="lang-popover-wrap"
placement="bottomRight"
:getPopupContainer="(trigger) => trigger.parentNode"
>
<span class="locale">{{ languageList.find((lang) => lang.key === locale).title }}</span>
<div class="lang-menu" slot="content">
<a
v-for="(lang) in languageList"
:key="lang.key"
:class="['lang-menu-item', lang.key === locale ? 'lang-menu-item_active' : '']"
@click="changeLang(lang.key)"
>
{{ lang.title }}
</a>
</div>
</a-popover>
<a-popover
:overlayStyle="{ width: '130px' }"
placement="bottomRight"
overlayClassName="custom-user"
>
<template slot="content">
<router-link :to="{ name: 'setting_person' }" :style="{ color: '#000000a6' }">
<div class="custom-user-item">
<a-icon type="user" :style="{ marginRight: '10px' }" />
<span>{{ $t('topMenu.personalCenter') }}</span>
</div>
</router-link>
<div @click="handleLogout" class="custom-user-item">
<a-icon type="logout" :style="{ marginRight: '10px' }" />
<span>{{ $t('topMenu.logout') }}</span>
</div>
<UserPanel />
</template>
<span class="action ant-dropdown-link user-dropdown-menu user-info-wrap">
<a-avatar
@@ -63,11 +36,13 @@
import { mapState, mapActions, mapGetters, mapMutations } from 'vuex'
import DocumentLink from './DocumentLink.vue'
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import UserPanel from './userPanel.vue'
export default {
name: 'UserMenu',
components: {
DocumentLink,
UserPanel
},
data() {
return {

View File

@@ -0,0 +1,487 @@
<template>
<div class="user-panel">
<a-avatar
class="user-panel-avatar"
size="small"
icon="user"
:src="avatarSrc"
/>
<div class="user-panel-nickname">
{{ userInfo.nickname }}
</div>
<div class="user-panel-info">
<ops-icon
type="veops-company"
class="user-panel-info-icon"
/>
<div class="user-panel-info-text">
{{ companyName }}
</div>
</div>
<div class="user-panel-info">
<ops-icon
type="veops-emails"
class="user-panel-info-icon"
/>
<div class="user-panel-info-text">
{{ email }}
</div>
</div>
<div class="user-panel-btn">
<div
v-for="(item) in userBtnGroup"
:key="item.type"
class="user-panel-btn-item"
@click="clickBtnGroup(item.type)"
>
<ops-icon
:type="item.icon"
class="user-panel-btn-icon"
/>
<span class="user-panel-btn-title">
{{ $t(item.title) }}
</span>
</div>
</div>
<div class="user-panel-row">
<div class="user-panel-row-label">
{{ $t('userPanel.switchLanguage') }}
</div>
<div class="user-panel-lang">
<div
v-for="(lang, index) in languageList"
:key="index"
:class="['user-panel-lang-item', lang.key === locale ? 'user-panel-lang-item_active' : '']"
@click="changeLang(lang.key)"
>
{{ lang.title }}
</div>
</div>
</div>
<div class="user-panel-row">
<div class="user-panel-row-label">
{{ $t('userPanel.bindAccount') }}
</div>
<div class="user-panel-bind">
<a-tooltip
v-for="(item) in bindList"
:key="item.type"
:title="$t(item.title)"
>
<ops-icon
class="user-panel-bind-item"
:type="userInfo.notice_info && userInfo.notice_info[item.type] ? item.existedIcon : item.icon"
@click="handleBindInfo(item.type)"
/>
</a-tooltip>
</div>
</div>
<div class="user-panel-account">
<div
v-for="(item, index) in accountActions"
:key="index"
class="user-panel-account-item"
@click="handleLogout"
>
<ops-icon class="user-panel-account-icon" :type="item.icon" />
<span class="user-panel-account-title">
{{ $t(item.title) }}
</span>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapMutations } from 'vuex'
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import {
bindPlatformByUid,
unbindPlatformByUid,
} from '@/api/employee'
import { getCompanyInfo } from '@/api/company'
export default {
name: 'UserPanel',
data() {
return {
userBtnGroup: [
{
icon: 'veops-personal',
title: 'userPanel.myProfile',
type: 'myProfile'
},
{
icon: 'a-veops-account1',
title: 'userPanel.accountPassword',
type: 'accountPassword'
}
],
languageList: [
{
title: '简中',
key: 'zh'
},
{
title: 'EN',
key: 'en'
},
],
bindList: [
{
type: 'wechatApp',
icon: 'qiyeweixin',
existedIcon: 'wechatApp',
title: 'wechat'
},
{
type: 'feishuApp',
icon: 'ops-setting-notice-feishu-selected',
existedIcon: 'feishuApp',
title: 'feishu'
},
{
type: 'dingdingApp',
icon: 'ops-setting-notice-dingding-selected',
existedIcon: 'dingdingApp',
title: 'dingding'
},
],
accountActions: [
{
icon: 'veops-switch',
title: 'userPanel.switchAccount'
},
{
icon: 'veops-sign_out',
title: 'userPanel.logout'
},
],
hoverBindAccountList: []
}
},
computed: {
...mapState({
email: (state) => state.user.email,
locale: (state) => state.locale,
userInfo: (state) => state.user,
companyName: (state) => state.company.name
}),
avatarSrc() {
const avatar = this.userInfo.avatar
if (!avatar) {
return null
}
return avatar.startsWith('https') ? avatar : `/api/common-setting/v1/file/${avatar}`
}
},
mounted() {
if (this.companyName === undefined) {
this.getCompanyInfo()
}
},
methods: {
...mapActions(['Logout', 'GetInfo']),
...mapMutations(['SET_LOCALE', 'SET_COMPANY_NAME']),
async getCompanyInfo() {
const res = await getCompanyInfo()
const name = res?.info?.name || ''
this.SET_COMPANY_NAME(name)
},
changeLang(lang) {
this.SET_LOCALE(lang)
this.$i18n.locale = lang
this.$nextTick(() => {
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
})
},
handleBindInfo(platform) {
const isBind = this?.userInfo?.notice_info?.[platform]
const uid = this?.userInfo?.uid
if (isBind) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('cs.person.confirmUnbind'),
onOk: () => {
unbindPlatformByUid(platform, uid)
.then(() => {
this.$message.success(this.$t('cs.person.unbindSuccess'))
})
.finally(() => {
this.GetInfo()
})
},
})
} else {
bindPlatformByUid(platform, uid)
.then(() => {
this.$message.success(this.$t('cs.person.bindSuccess'))
})
.finally(() => {
this.GetInfo()
})
}
},
handleLogout() {
this.$confirm({
title: this.$t('tip'),
content: this.$t('topMenu.confirmLogout'),
onOk: () => {
this.Logout()
},
onCancel() {},
})
},
clickBtnGroup(type) {
switch (type) {
case 'myProfile':
if (this.$route.name === 'setting_person') {
this.$bus.$emit('changeSettingPersonCurrent', '1')
} else {
this.$router.push({
name: 'setting_person',
query: {
current: '1'
}
})
}
break
case 'accountPassword':
if (this.$route.name === 'setting_person') {
this.$bus.$emit('changeSettingPersonCurrent', '2')
} else {
this.$router.push({
name: 'setting_person',
query: {
current: '2'
}
})
}
break
default:
break
}
},
handleBindAccountMouse(type, isHover) {
const index = this.hoverBindAccountList.findIndex((item) => item === type)
if (isHover && index === -1) {
this.hoverBindAccountList.push(type)
} else if (!isHover && index !== -1) {
this.hoverBindAccountList.splice(index, 1)
}
}
}
}
</script>
<style lang="less" scoped>
.user-panel {
display: flex;
flex-direction: column;
align-items: center;
width: 350px;
padding: 0 20px;
&-avatar {
width: 62px;
height: 62px;
border-radius: 62px;
margin-top: 13px;
display: flex;
align-items: center;
justify-content: center;
color: #000000;
background-color: #FFFFFF;
font-size: 48px !important;
}
&-nickname {
color: #1D2129;
font-size: 15px;
font-weight: 700;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
margin-top: 8px;
}
&-info {
display: flex;
align-items: center;
column-gap: 6px;
margin-top: 6px;
max-width: 100%;
&-icon {
flex-shrink: 0;
font-size: 12px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #4E5969;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
}
&-btn {
width: 100%;
height: 72px;
display: flex;
align-items: center;
margin-top: 11px;
&-icon {
font-size: 22px;
color: #CACDD9;
}
&-title {
font-size: 14px;
font-weight: 400;
color: #1D2129;
margin-top: 8px;
}
&-item {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #F7F8FA;
cursor: pointer;
&:hover {
background-color: #EBEFF8;
.user-panel-btn-icon {
color: #2F54EB;
}
.user-panel-btn-title {
color: #2F54EB;
}
}
}
}
&-row {
width: 100%;
margin-top: 22px;
display: flex;
align-items: center;
justify-content: space-between;
&-label {
font-size: 14px;
font-weight: 400;
color: #4E5969;
}
}
&-lang {
display: flex;
align-items: center;
height: 28px;
width: 108px;
border-radius: 28px;
overflow: hidden;
&-item {
flex: 1;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background-color: #F7F8FA;
cursor: pointer;
&:first-child {
border-right: solid 1px #E4E7ED;
}
&_active {
background-color: #EBEFF8;
color: #2F54EB;
}
&:hover {
color: #2F54EB;
}
}
}
&-bind {
display: flex;
align-items: center;
column-gap: 22px;
&-item {
cursor: pointer;
font-size: 16px;
}
}
&-account {
margin-top: 22px;
padding-top: 13px;
padding-bottom: 20px;
border-top: solid 1px #F0F1F5;
display: flex;
align-items: center;
justify-content: space-evenly;
width: 100%;
&-icon {
font-size: 14px;
color: #CACDD9;
}
&-title {
font-size: 14px;
color: #86909C;
margin-left: 5px;
}
&-item {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
cursor: pointer;
&:hover {
.user-panel-account-icon {
color: #2F54EB;
}
.user-panel-account-title {
color: #2F54EB;
}
}
}
}
}
</style>

View File

@@ -108,6 +108,7 @@ export default {
visual: 'Visual',
default: 'default',
tip: 'Tip',
cmdbSearch: 'Search',
pagination: {
total: '{range0}-{range1} of {total} items'
},
@@ -167,6 +168,15 @@ export default {
monetaryAmount: 'monetary amount',
custom: 'custom',
},
userPanel: {
myProfile: 'My Profile',
accountPassword: 'Password',
notice: 'Notice',
switchLanguage: 'Switch Language',
bindAccount: 'Bind Account',
switchAccount: 'Switch Account',
logout: 'Logout'
},
cmdb: cmdb_en,
cs: cs_en,
acl: acl_en,

View File

@@ -108,6 +108,7 @@ export default {
visual: '虚拟',
default: '默认',
tip: '提示',
cmdbSearch: '搜索一下',
pagination: {
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
},
@@ -167,6 +168,15 @@ export default {
monetaryAmount: '货币金额',
custom: '自定义',
},
userPanel: {
myProfile: '个人中心',
accountPassword: '账号密码',
notice: '通知中心',
switchLanguage: '切换语言',
bindAccount: '绑定账号',
switchAccount: '切换账号',
logout: '退出账号'
},
cmdb: cmdb_zh,
cs: cs_zh,
acl: acl_zh,

View File

@@ -81,3 +81,11 @@ export function searchCIRelationFull(params) {
params,
})
}
export function searchCIRelationPath(data) {
return axios({
url: `/v0.1/ci_relations/path/s`,
method: 'POST',
data,
})
}

View File

@@ -74,3 +74,11 @@ export function getCanEditByParentIdChildId(parent_id, child_id) {
method: 'GET'
})
}
export function getCITypeRelationPath(params) {
return axios({
url: `/v0.1/ci_type_relations/path`,
method: 'GET',
params
})
}

View File

@@ -13,7 +13,10 @@
v-decorator="['filename', { rules: [{ required: true, message: $t('cmdb.components.filenameInputTips') }] }]"
/>
</a-form-item>
<a-form-item :label="$t('cmdb.components.saveType')">
<a-form-item
v-if="showFileTypeSelect"
:label="$t('cmdb.components.saveType')"
>
<a-select
:placeholder="$t('cmdb.components.saveTypeTips')"
v-decorator="[
@@ -83,6 +86,10 @@ export default {
type: String,
default: 'default',
},
showFileTypeSelect: {
type: Boolean,
default: true
}
},
data() {
return {

View File

@@ -286,6 +286,7 @@ const cmdb_en = {
attrCode: 'Attr Code',
computedAttrTip1: 'Reference attributes follow jinja2 syntax',
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: """{{ attr_name | join(',') }}""" where commas are separators`,
computedAttrTip3: `Cannot refer to other computed attributes`,
example: 'Example',
attrFilterTip: `The third column of values allows you to select attributes of this model to cascade attributes`,
rule: 'Rule',
@@ -740,5 +741,22 @@ if __name__ == "__main__":
topoViewSearchPlaceholder: 'Please enter the node name.',
moreBtn: 'Show more({count})'
},
relationSearch: {
relationSearch: 'Relation Search',
sourceCIType: 'Source CIType',
sourceCITypeTip: 'Please input or select',
sourceCITYpeInput: 'Please input keywords',
targetCIType: 'Target CIType',
targetCITypeTip: 'Please input or select, multiple choices available',
pathSelect: 'Path Select',
pathSelectTip: 'Please select source CIType and target CIType first',
saveCondition: 'Save Condition',
conditionFilter: 'Condition Filter',
level: 'Level',
returnPath: 'Return Path',
conditionName: 'Condition Name',
path: 'Path',
expandCondition: 'Expand Condition',
}
}
export default cmdb_en

View File

@@ -286,6 +286,7 @@ const cmdb_zh = {
attrCode: '属性代码',
computedAttrTip1: '引用属性遵循jinja2语法',
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',') }}""" 其中逗号为分隔符`,
computedAttrTip3: `不能引用其他计算属性`,
example: '例如',
attrFilterTip: '第三列值可选择本模型的属性,来实现级联属性的功能',
rule: '规则',
@@ -739,5 +740,22 @@ if __name__ == "__main__":
topoViewSearchPlaceholder: '请输入节点名字',
moreBtn: '展示更多({count})'
},
relationSearch: {
relationSearch: '关系搜索',
sourceCIType: '源模型',
sourceCITypeTip: '请输入或选择',
sourceCITYpeInput: '请输入关键词',
targetCIType: '目标模型',
targetCITypeTip: '请输入或选择,可多选',
pathSelect: '路径选择',
pathSelectTip: '请先选择源模型和目标模型',
saveCondition: '保存条件',
conditionFilter: '条件过滤',
level: '层级',
returnPath: '返回路径',
conditionName: '条件命名',
path: '路径',
expandCondition: '展开条件',
}
}
export default cmdb_zh

View File

@@ -53,6 +53,7 @@ const genCmdbRoutes = async () => {
{
path: '/cmdb/resourcesearch',
name: 'cmdb_resource_search',
hidden: true,
meta: { title: 'cmdb.menu.ciSearch', icon: 'ops-cmdb-search', selectedIcon: 'ops-cmdb-search', keepAlive: false },
component: () => import('../views/resource_search_2/index.vue')
},

View File

@@ -11,7 +11,7 @@ export function sum(arr) {
})
}
const strLength = (fData) => {
export const strLength = (fData) => {
if (!fData) {
return 0

View File

@@ -398,6 +398,7 @@
<div v-show="isShowComputedArea" class="computed-attr-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
<div>3. {{ $t('cmdb.ciType.computedAttrTip3') }}</div>
</div>
<ComputedArea
showCalcComputed

View File

@@ -391,6 +391,7 @@
<div v-show="isShowComputedArea" class="computed-attr-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
<div>3. {{ $t('cmdb.ciType.computedAttrTip3') }}</div>
</div>
<ComputedArea ref="computedArea" v-if="isShowComputedArea" :canDefineComputed="canDefineComputed" />
</a-form-item>

View File

@@ -13,6 +13,7 @@
:paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-ci-types"
:triggerLength="18"
calcBasedParent
>
<template #one>
<div class="ci-types-left">
@@ -1112,7 +1113,7 @@ export default {
}
.ci-types-left-content {
max-height: calc(100% - 45px);
height: calc(100% - 45px);
overflow: hidden;
margin-top: 10px;

View File

@@ -198,6 +198,7 @@
<div class="script-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
<div>3. {{ $t('cmdb.ciType.computedAttrTip3') }}</div>
</div>
<div class="all-attr-btn">

View File

@@ -1,5 +1,5 @@
<template>
<TwoColumnLayout appName="cmdb-adc">
<TwoColumnLayout appName="cmdb-adc" calcBasedParent>
<template #one>
<div class="cmdb-adc-group" v-for="group in ci_types_list" :key="group.id">
<p>

View File

@@ -45,6 +45,7 @@
</template>
</vxe-column>
<vxe-column field="type_id" width="100px" :title="$t('cmdb.ciType.ciType')"></vxe-column>
<vxe-column field="show_attr_value" width="100px" :title="$t('cmdb.ci.instance')"></vxe-column>
<vxe-column field="operate_type" width="89px" :title="$t('operation')">
<template #header="{ column }">
<span>{{ column.title }}</span>
@@ -314,7 +315,7 @@ export default {
}
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user', 'type_id']
const fields = ['created_at', 'user', 'type_id', 'show_attr_value']
const cellValue = row[column.property]
const created_at = row['created_at']
if (column.property === 'created_at') {
@@ -365,6 +366,22 @@ export default {
}
}
}
} else if (column.property === 'show_attr_value') {
if (cellValue && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
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() {

View File

@@ -85,7 +85,7 @@
@change="onChange"
format="YYYY-MM-DD HH:mm"
:placeholder="[$t('cmdb.history.startTime'), $t('cmdb.history.endTime')]"
v-else-if="attr.value_type === '3'"
v-else-if="item.value_type === '3'"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],

View File

@@ -7,6 +7,7 @@
:paneLengthPixel.sync="paneLengthPixel"
:appName="`cmdb-relation-views-${viewId}`"
:triggerLength="18"
calcBasedParent
>
<template #one>
<div class="relation-views-left" :style="{ height: `${windowHeight - 64}px` }">

View File

@@ -1,530 +1,106 @@
<template>
<div
class="resource-search"
:style="{ height: `${windowHeight - 93}px` }"
>
<div v-if="!isSearch" class="resource-search-before">
<div class="resource-search-title">
<ops-icon class="resource-search-title-icon" type="veops-resource11" />
<span class="resource-search-title-text">{{ $t('cmdb.ciType.resourceSearch') }}</span>
</div>
<SearchInput
ref="searchInputRef"
:CITypeGroup="CITypeGroup"
:allAttributesList="allAttributesList"
:searchValue="searchValue"
:selectCITypeIds="selectCITypeIds"
:expression="expression"
@changeFilter="changeFilter"
@updateAllAttributesList="updateAllAttributesList"
@saveCondition="saveCondition"
/>
<HistoryList
:recentList="recentList"
:favorList="favorList"
:detailCIId="detailCIId"
@clickRecent="clickRecent"
@deleteRecent="deleteRecent"
@clearRecent="clearRecent"
@deleteCollect="deleteCollect"
@showDetail="clickFavor"
/>
<img class="resource-search-before-bg" :src="require('@/modules/cmdb/assets/resourceSearch/resource_search_bg_1.png')" />
</div>
<div class="resource-search-after" v-else>
<div class="resource-search">
<div class="resource-search-tab">
<div
class="resource-search-after-left"
:style="{ width: showInstanceDetail ? '70%' : '100%' }"
v-for="(tab) in tabList"
:key="tab.value"
:class="['resource-search-tab-item', tabActive === tab.value ? 'resource-search-tab-item_active' : '']"
@click="tabActive = tab.value"
>
<SearchInput
ref="searchInputRef"
classType="after"
:CITypeGroup="CITypeGroup"
:allAttributesList="allAttributesList"
:searchValue="searchValue"
:selectCITypeIds="selectCITypeIds"
:expression="expression"
@changeFilter="changeFilter"
@updateAllAttributesList="updateAllAttributesList"
@saveCondition="saveCondition"
/>
<HistoryList
:recentList="recentList"
:favorList="favorList"
:detailCIId="detailCIId"
@clickRecent="clickRecent"
@deleteRecent="deleteRecent"
@clearRecent="clearRecent"
@deleteCollect="deleteCollect"
@showDetail="clickFavor"
/>
<div class="resource-search-divider"></div>
<InstanceList
:list="instanceList"
:tabList="ciTabList"
:referenceShowAttrNameMap="referenceShowAttrNameMap"
:referenceCIIdMap="referenceCIIdMap"
:favorList="favorList"
:detailCIId="detailCIId"
:searchValue="currentSearchValue"
@showDetail="showDetail"
@addCollect="addCollect"
@deleteCollect="deleteCollect"
/>
<div class="resource-search-pagination">
<a-pagination
:showSizeChanger="true"
:current="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@showSizeChange="handlePageSizeChange"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="changePage"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('all') }}</span>
</template>
</a-pagination>
</div>
</div>
<div v-if="showInstanceDetail" class="resource-search-after-right">
<InstanceDetail
:CIId="detailCIId"
:CITypeId="detailCITypeId"
:favorList="favorList"
@addCollect="addCollect"
@deleteCollect="deleteCollect"
@hideDetail="hideDetail"
/>
{{ $t(tab.lable) }}
</div>
</div>
<template v-if="isInit">
<ResourceSearchCom
v-show="tabActive === 'resourceSearch'"
:CITypeGroup="CITypeGroup"
:allCITypes="allCITypes"
/>
<RelationSearch
v-show="tabActive === 'relationSearch'"
:CITypeGroup="CITypeGroup"
:allCITypes="allCITypes"
/>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { getPreferenceSearch, savePreferenceSearch, getSubscribeAttributes, deletePreferenceSearch } from '@/modules/cmdb/api/preference'
import { getCITypeGroups } from '@/modules/cmdb/api/ciTypeGroup'
import { searchAttributes, getCITypeAttributesByTypeIds } from '@/modules/cmdb/api/CITypeAttr'
import { searchCI } from '@/modules/cmdb/api/ci'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { mapState } from 'vuex'
import SearchInput from './components/searchInput.vue'
import HistoryList from './components/historyList.vue'
import InstanceList from './components/instanceList.vue'
import InstanceDetail from './components/instanceDetail.vue'
import ResourceSearchCom from './resourceSearch/index.vue'
import RelationSearch from './relationSearch/index.vue'
export default {
name: 'ResourceSearch',
components: {
SearchInput,
HistoryList,
InstanceList,
InstanceDetail
ResourceSearchCom,
RelationSearch
},
data() {
return {
// 筛选条件
searchValue: '', // 搜索框
selectCITypeIds: [], // 已选模型
expression: '', // 筛选语句
currentSearchValue: '', // 当前已搜索语句
recentList: [], // 最近搜索
favorList: [], // 我的收藏
CITypeGroup: [], // CIType 分组
CITypes: [],
allAttributesList: [],
isSearch: false, // 是否搜索过
currentPage: 1,
pageSizeOptions: ['50', '100', '200', '100000'],
pageSize: 50,
totalNumber: 0,
ciTabList: [],
instanceList: [],
referenceShowAttrNameMap: {},
referenceCIIdMap: {},
showInstanceDetail: false,
detailCIId: -1,
detailCITypeId: -1,
tabActive: 'resourceSearch',
tabList: [
{
lable: 'cmdb.ciType.resourceSearch',
value: 'resourceSearch'
},
{
lable: 'cmdb.relationSearch.relationSearch',
value: 'relationSearch'
}
],
CITypeGroup: [],
allCITypes: [],
isInit: false,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
...mapState({
cmdbSearchValue: (state) => state.app.cmdbSearchValue,
}),
},
mounted() {
this.initData()
watch: {
cmdbSearchValue: {
immediate: true,
deep: true,
handler() {
this.tabActive = 'resourceSearch'
}
}
},
async mounted() {
try {
await Promise.all([
this.getCITypeGroups(),
this.getAllCITypes()
])
} catch (error) {
console.log('resource search mounted fail', error)
}
this.isInit = true
},
methods: {
async initData() {
await this.getRecentList()
await this.getFavorList()
await this.getCITypeGroups()
await this.getAllCITypes()
await this.getAllAttr()
},
async getRecentList() {
const recentList = await getPreferenceSearch({
name: '__recent__'
})
recentList.sort((a, b) => b.id - a.id)
this.recentList = recentList
},
async getFavorList() {
const favorList = await getPreferenceSearch({
name: '__favor__'
})
favorList.sort((a, b) => b.id - a.id)
this.favorList = favorList
},
async getCITypeGroups() {
const res = await getCITypeGroups({ need_other: true })
this.CITypeGroup = res
.filter((item) => item.ci_types && item.ci_types.length)
.filter((item) => item?.ci_types?.length)
.map((item) => {
item.id = `parent_${item.id || -1}`
return { ..._.cloneDeep(item) }
return item
})
},
async getAllCITypes() {
const res = await getCITypes()
this.CITypes = res?.ci_types
this.allCITypes = res?.ci_types
},
async getAllAttr() {
const res = await searchAttributes({ page_size: 9999 })
this.allAttributesList = res.attributes
this.originAllAttributesList = res.attributes
},
async updateAllAttributesList(value) {
if (value && value.length) {
const res = await getCITypeAttributesByTypeIds({ type_ids: value.join(',') })
this.allAttributesList = res.attributes
} else {
this.allAttributesList = this.originAllAttributesList
}
},
async saveCondition(isSubmit) {
if (
this.searchValue ||
this.expression ||
this.selectCITypeIds.length
) {
const needDeleteList = []
const differentList = []
this.recentList.forEach((item) => {
const option = item.option
if (
option.searchValue === this.searchValue &&
option.expression === this.expression &&
_.isEqual(option.ciTypeIds, this.selectCITypeIds)
) {
needDeleteList.push(item.id)
} else {
differentList.push(item.id)
}
})
if (differentList.length >= 10) {
needDeleteList.push(...differentList.slice(9))
}
if (needDeleteList.length) {
await Promise.all(
needDeleteList.map((id) => deletePreferenceSearch(id))
)
}
const ciTypeNames = this.selectCITypeIds.map((id) => {
const ciType = this.CITypes.find((item) => item.id === id)
return ciType?.alias || ciType?.name || id
})
await savePreferenceSearch({
option: {
searchValue: this.searchValue,
expression: this.expression,
ciTypeIds: this.selectCITypeIds,
ciTypeNames
},
name: '__recent__'
})
this.getRecentList()
}
if (isSubmit) {
this.isSearch = true
this.currentPage = 1
this.hideDetail()
this.loadInstance()
}
},
async deleteRecent(id) {
await deletePreferenceSearch(id)
this.getRecentList()
},
async clearRecent() {
const deletePromises = this.recentList.map((item) => {
return deletePreferenceSearch(item.id)
})
await Promise.all(deletePromises)
this.getRecentList()
},
async loadInstance() {
const { selectCITypeIds, expression, searchValue } = this
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
const ciTypeIds = [...selectCITypeIds]
if (!ciTypeIds.length) {
this.CITypeGroup.forEach((item) => {
const ids = item.ci_types.map((ci_type) => ci_type.id)
ciTypeIds.push(...ids)
})
}
const res = await searchCI({
q: `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${
searchValue ? `,*${searchValue}*` : ''
}`,
count: this.pageSize,
page: this.currentPage,
sort: '_type'
})
this.currentSearchValue = searchValue
this.totalNumber = res?.numfound ?? 0
if (!res?.result?.length) {
this.ciTabList = []
this.instanceList = []
}
const ciTabMap = new Map()
let list = res.result
list.forEach((item) => {
const ciType = this.CITypes.find((type) => type.id === item._type)
if (ciTabMap.has(item._type)) {
ciTabMap.get(item._type).count++
} else {
ciTabMap.set(item._type, {
id: item._type,
count: 1,
title: ciType?.alias || ciType?.name || '',
})
}
})
const mapEntries = [...ciTabMap.entries()]
const subscribedPromises = mapEntries.map((item) => {
return getSubscribeAttributes(item[0])
})
const subscribedRes = await Promise.all(subscribedPromises)
list = list.map((item) => {
const subscribedIndex = mapEntries.findIndex((mapValue) => mapValue[0] === item._type)
const subscribedAttr = subscribedRes?.[subscribedIndex]?.attributes || []
const obj = {
ci: item,
ciTypeObj: {},
attributes: subscribedAttr
}
const ciType = this.CITypes.find((type) => type.id === item._type)
obj.ciTypeObj = {
showAttrName: ciType?.show_name || ciType?.unique_key || '',
icon: ciType?.icon || '',
title: ciType?.alias || ciType?.name || '',
name: ciType?.name || '',
id: ciType.id
}
return obj
})
this.instanceList = list
const ciTabList = [...ciTabMap.values()]
if (list?.length) {
ciTabList.unshift({
id: -1,
title: this.$t('all'),
count: list?.length
})
}
this.ciTabList = ciTabList
// 处理引用属性
const allAttr = []
subscribedRes.map((item) => {
allAttr.push(...item.attributes)
})
this.handlePerference(_.uniqBy(allAttr, 'id'))
},
handlePerference(allAttr) {
let needRequiredCIType = []
allAttr.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
needRequiredCIType.push(attr)
}
})
needRequiredCIType = _.uniq(needRequiredCIType, 'id')
if (!needRequiredCIType.length) {
this.referenceShowAttrNameMap = {}
this.referenceCIIdMap = {}
return
}
this.handleReferenceShowAttrName(needRequiredCIType)
this.handleReferenceCIIdMap(needRequiredCIType)
},
async handleReferenceShowAttrName(needRequiredCIType) {
const res = await getCITypes({
type_ids: needRequiredCIType.map((col) => col.reference_type_id).join(',')
})
const map = {}
res.ci_types.forEach((ciType) => {
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
this.referenceShowAttrNameMap = map
},
async handleReferenceCIIdMap(needRequiredCIType) {
const map = {}
this.instanceList.forEach(({ ci }) => {
needRequiredCIType.forEach((col) => {
const ids = Array.isArray(ci[col.name]) ? ci[col.name] : ci[col.name] ? [ci[col.name]] : []
if (ids.length) {
if (!map?.[col.reference_type_id]) {
map[col.reference_type_id] = {}
}
ids.forEach((id) => {
map[col.reference_type_id][id] = {}
})
}
})
})
if (!Object.keys(map).length) {
this.referenceCIIdMap = {}
return
}
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
allRes.forEach((res) => {
res.result.forEach((item) => {
if (map?.[item._type]?.[item._id]) {
map[item._type][item._id] = item
}
})
})
this.referenceCIIdMap = map
},
clickRecent(data) {
this.updateAllAttributesList(data.ciTypeIds || [])
this.isSearch = true
this.currentPage = 1
this.searchValue = data?.searchValue || ''
this.expression = data?.expression || ''
this.selectCITypeIds = data?.ciTypeIds || []
this.hideDetail()
this.loadInstance()
},
handlePageSizeChange(_, pageSize) {
this.pageSize = pageSize
this.currentPage = 1
this.loadInstance()
},
changePage(page) {
this.currentPage = page
this.loadInstance()
},
changeFilter(data) {
this[data.name] = data.value
},
showDetail(data) {
this.detailCIId = data.id
this.detailCITypeId = data.ciTypeId
this.showInstanceDetail = true
},
hideDetail() {
this.detailCIId = -1
this.detailCITypeId = -1
this.showInstanceDetail = false
},
async addCollect(data) {
if (this?.favorList?.length >= 10) {
const deletePromises = this.favorList.slice(9).map((item) => {
return deletePreferenceSearch(item.id)
})
await Promise.all(deletePromises)
}
await savePreferenceSearch({
option: {
...data
},
name: '__favor__'
})
this.getFavorList()
},
async deleteCollect(id) {
await deletePreferenceSearch(id)
this.getFavorList()
},
clickFavor(data) {
this.isSearch = true
this.showDetail(data)
}
}
},
}
</script>
@@ -532,84 +108,32 @@ export default {
.resource-search {
width: 100%;
height: 100%;
position: relative;
&-before {
width: 100%;
max-width: 725px;
height: 100%;
margin: 0 auto;
padding-top: 100px;
display: flex;
flex-direction: column;
align-items: center;
& > div {
position: relative;
z-index: 1;
}
&-bg {
position: absolute;
left: -24px;
bottom: -24px;
width: calc(100% + 48px);
z-index: 0;
}
}
&-title {
&-tab {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 25px;
margin-bottom: 12px;
&-icon {
font-size: 28px;
}
&-item {
padding-right: 8px;
margin-right: 8px;
font-size: 14px;
font-weight: 400;
color: #86909C;
cursor: pointer;
&-text {
margin-left: 10px;
font-size: 20px;
font-weight: 700;
color: #1D2129;
}
}
&:not(:last-child) {
border-right: solid 1px #E4E7ED;
}
&-after {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
&:hover {
color: #2F54EB;
}
&-left {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
& > div {
flex-shrink: 0;
&_active {
color: #2F54EB;
}
}
&-right {
margin-left: 20px;
width: calc(30% - 20px);
flex-shrink: 0;
}
}
&-divider {
width: 100%;
height: 1px;
background-color: #E4E7ED;
margin: 20px 0;
}
&-pagination {
text-align: right;
margin: 12px 0px;
}
}
</style>

View File

@@ -0,0 +1,516 @@
<template>
<div class="search-table">
<div class="search-table-header">
<div class="table-tab">
<div
v-for="(tab) in tabList"
:key="tab.value"
:class="['table-tab-item', tabActive === tab.value ? 'table-tab-item_active' : '']"
@click="clickTab(tab.value)"
>
{{ tab.value }}
(<span class="table-tab-item-count">{{ tab.count }}</span>)
</div>
</div>
<a-button
v-if="tableData.ciList && tableData.ciList.length"
type="primary"
class="ops-button-ghost search-table-export"
ghost
@click="handleExport"
>
<ops-icon type="veops-export" />
{{ $t('export') }}
</a-button>
</div>
<ops-table
ref="xTable"
show-overflow
:data="tableData.ciList"
size="small"
:height="`${tableHeight}px`"
:cell-class-name="getCellClassName"
:header-cell-class-name="getHeaderCellClassName"
:checkbox-config="{ range: true }"
:loading="isSearchLoading"
:column-config="{ resizable: true }"
:resizable-config="{ minWidth: 60 }"
class="checkbox-hover-table"
>
<vxe-table-column
v-if="tableData.ciList && tableData.ciList.length"
align="center"
type="checkbox"
width="60"
>
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-table-column>
<template
v-if="returnPath && tableData.pathList && tableData.pathList.length"
>
<vxe-table-column
v-for="(path, index) in tableData.pathList"
class="table-path-column"
:key="`${path.id}-${index}`"
:title="tableData.pathList[index].name"
:field="path.id"
:show-header-overflow="false"
:width="index !== tableData.pathList.length - 1 ? 160 : 100"
>
<template #header>
<div class="table-path-header">
<span
class="table-path-header-name"
:style="{
maxWidth: tableData.pathList[index].relation ? '70px' : '100%'
}"
>
<a-tooltip :title="tableData.pathList[index].name">
{{ tableData.pathList[index].name }}
</a-tooltip>
</span>
<div
class="table-path-header-right"
v-if="tableData.pathList[index].relation"
>
<span class="table-path-header-line">
<a-icon
type="caret-right"
class="table-path-header-line-arrow"
/>
</span>
<span
class="table-path-header-relation"
>
<span class="table-path-header-relation-text">
<a-tooltip :title="tableData.pathList[index].relation">
{{ tableData.pathList[index].relation }}
</a-tooltip>
</span>
</span>
</div>
</div>
</template>
<template #default="{ row, columnIndex }">
<span
v-if="columnIndex === 1"
v-html="markSearchValue(row.pathCI[path.id])"
></span>
<span v-else >{{ row.pathCI[path.id] }}</span>
</template>
</vxe-table-column>
</template>
<template v-if="tableData.ciAttr && tableData.ciAttr.length">
<vxe-table-column
v-for="(attr, index) in tableData.ciAttr"
:key="`${attr.name}_${index}`"
:title="attr.alias || attr.name || ''"
:field="attr.name"
:width="attr.width"
:show-header-overflow="true"
>
<template #default="{ row }">
<AttrDisplay
:attr="attr"
:ci="row.targetCI"
:referenceShowAttrNameMap="referenceShowAttrNameMap"
:referenceCIIdMap="referenceCIIdMap"
/>
</template>
</vxe-table-column>
</template>
</ops-table>
<BatchDownload
ref="batchDownload"
:showFileTypeSelect="false"
@batchDownload="batchDownload"
/>
</div>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { mapState } from 'vuex'
import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
import AttrDisplay from '@/modules/cmdb/views/resource_search_2/resourceSearch/components/attrDisplay.vue'
import BatchDownload from '@/modules/cmdb/components/batchDownload/batchDownload.vue'
export default {
name: 'CITable',
components: {
AttrDisplay,
BatchDownload
},
props: {
allTableData: {
type: Object,
default: () => {}
},
tabActive: {
type: String,
default: ''
},
returnPath: {
type: Boolean,
default: false
},
isHideSearchCondition: {
type: Boolean,
default: false,
},
referenceShowAttrNameMap: {
type: Object,
default: () => {}
},
referenceCIIdMap: {
type: Object,
default: () => {}
},
searchValue: {
type: String,
default: ''
},
isSearchLoading: {
type: Boolean,
default: false
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return this.isHideSearchCondition ? this.windowHeight - 308 : this.windowHeight - 458
},
tableData() {
return this.allTableData?.[this.tabActive] || {}
},
tabList() {
const keys = Object.keys(this.allTableData) || []
return keys.map((key) => {
return {
value: key,
count: this.allTableData?.[key]?.count || 0
}
})
},
},
data() {
return {}
},
methods: {
markSearchValue(text) {
if (!text || !this.searchValue) {
return text
}
const regex = new RegExp(`(${this.searchValue})`, 'gi')
return String(text).replace(
regex,
`<span style="background-color: #D3EEFE; padding: 0 2px;">$1</span>`
)
},
clickTab(tab) {
this.$emit('updateTab', tab)
},
getRowSeq(row) {
const table = this.$refs?.['xTable']?.getVxetableRef?.() || null
return table?.getRowSeq?.(row)
},
getCellClassName({ columnIndex }) {
const pathLength = this.tableData?.pathList?.length
if (columnIndex <= pathLength && this.returnPath) {
return 'table-path-cell'
}
return ''
},
getHeaderCellClassName({ columnIndex }) {
const pathLength = this.tableData?.pathList?.length
if (columnIndex <= pathLength && this.returnPath) {
return 'table-path-header-cell'
}
return ''
},
handleExport() {
const preferenceAttrList = []
if (this.returnPath && this.tableData?.pathList?.length) {
preferenceAttrList.push(...this.tableData.pathList.map((path) => {
return {
name: path.id,
alias: path.name
}
}))
}
if (this.tableData?.ciAttr?.length) {
const ciAttr = _.cloneDeep(this.tableData.ciAttr)
ciAttr.forEach((attr) => {
attr.alias = attr.alias || attr.name
})
preferenceAttrList.push(...ciAttr)
}
this.$refs.batchDownload.open({
preferenceAttrList,
ciTypeName: this.tabActive || '',
})
},
batchDownload({ checkedKeys }) {
const excel_name = `cmdb-${this.tabActive}-${moment().format('YYYYMMDDHHmmss')}.xlsx`
const wb = new ExcelJS.Workbook()
const tableRef = this.$refs.xTable.getVxetableRef()
let tableData = _.cloneDeep([
...tableRef.getCheckboxReserveRecords(),
...tableRef.getCheckboxRecords(true),
])
if (!tableData.length) {
const { fullData } = tableRef.getTableData()
tableData = _.cloneDeep(fullData)
}
const ws = wb.addWorksheet(this.tabActive)
const pathColumns = []
const targetColumns = []
if (this.returnPath) {
const pathFilter = this.tableData.pathList.filter((path) => checkedKeys.includes(path.id))
pathFilter.forEach((path) => {
pathColumns.push({
header: path.name || '',
key: path.id,
width: 20,
})
})
}
const attrMap = new Map()
const attrFilter = this.tableData.ciAttr.filter((attr) => checkedKeys.includes(attr.name))
attrFilter.forEach((attr) => {
attrMap.set(attr.name, attr)
targetColumns.push({
header: attr.alias || attr.name || '',
key: attr.name,
width: 20,
})
})
ws.columns = [
...pathColumns,
...targetColumns
]
tableData.forEach(({ pathCI, targetCI }) => {
const row = {}
if (this.returnPath) {
pathColumns.forEach(({ key }) => {
row[key] = pathCI?.[key] || ''
})
}
targetColumns.forEach(({ key }) => {
const value = targetCI?.[key] ?? null
const attr = attrMap.get(key)
if (attr.valueType === '6') {
row[key] = value ? JSON.stringify(value) : value
} else if (attr.is_list && Array.isArray(value)) {
row[key] = value.join(',')
} else {
row[key] = value
}
})
ws.addRow(row)
})
wb.xlsx.writeBuffer().then((buffer) => {
const file = new Blob([buffer], {
type: 'application/octet-stream',
})
FileSaver.saveAs(file, excel_name)
})
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
}
}
}
</script>
<style lang="less" scoped>
.search-table {
width: 100%;
&-header {
display: flex;
align-items: baseline;
justify-content: space-between
}
&-export {
flex-shrink: 0;
margin-left: 12px;
}
.table-tab {
display: flex;
align-items: center;
column-gap: 35px;
padding-bottom: 6px;
margin-bottom: 18px;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
&-item {
font-size: 14px;
font-weight: 400;
color: #4E5969;
cursor: pointer;
flex-shrink: 0;
&-count {
color: #2F54EB;
}
&_active {
color: #2F54EB;
}
&:hover {
color: #2F54EB;
}
}
}
.table-path-header {
position: relative;
display: flex;
align-items: center;
&-name {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
position: relative;
z-index: 1;
flex-shrink: 0;
}
&-right {
display: flex;
align-items: center;
width: 100%;
margin-left: 10px;
margin-right: -5px;
position: relative;
}
&-line {
width: 100%;
height: 1px;
position: relative;
background-color: #CACDD9;
z-index: 0;
&-arrow {
position: absolute;
right: -6px;
top: -6px;
font-size: 12px;
color: #CACDD9;
}
}
&-relation {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #FFFFFF;
border: solid 1px #E4E7ED;
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px;
border-radius: 22px;
z-index: 2;
max-width: 70px;
width: fit-content;
&-text {
font-size: 12px;
font-weight: 400;
color: #A5A9BC;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
width: 100%;
}
}
}
.checkbox-hover-table {
/deep/ .vxe-table--body-wrapper {
.vxe-checkbox--label {
display: inline;
padding-left: 0px !important;
color: #bfbfbf;
}
.vxe-icon-checkbox-unchecked {
display: none;
}
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
display: none;
}
.vxe-cell--checkbox {
&:hover {
.vxe-icon-checkbox-unchecked {
display: inline;
}
.vxe-checkbox--label {
display: none;
}
}
}
}
}
/deep/ .table-path-header-cell {
background-color: #EBEFF8 !important;
.vxe-cell--title {
width: 100%;
overflow: visible;
}
}
/deep/ .table-path-cell {
background-color: #F9FBFF;
}
/deep/ .attr-display {
display: inline;
}
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<a-popover
v-model="visible"
trigger="click"
placement="bottomRight"
@visibleChange="handleVisibleChange"
>
<div class="search-condition-filter">
<a-icon class="search-condition-filter-icon" type="filter" />
<div
v-if="expression"
class="search-condition-filter-flag"
>
</div>
</div>
<template slot="content">
<div class="search-condition-content">
<div class="search-condition-content-title">
{{ $t('cmdb.relationSearch.conditionFilter') }}:
</div>
<ConditionFilter
ref="conditionFilterRef"
:canSearchPreferenceAttrList="allAttributesList"
:expression="expression"
:CITypeIds="selectCITypeIds"
:isDropdown="false"
@setExpFromFilter="setExpFromFilter"
/>
<div class="search-condition-filter-submit">
<a-button
type="primary"
size="small"
@click="clickSubmit()"
>
{{ $t('confirm') }}
</a-button>
</div>
</div>
</template>
</a-popover>
</template>
<script>
import ConditionFilter from '@/modules/cmdb/components/conditionFilter/index.vue'
export default {
name: 'FilterPopover',
components: {
ConditionFilter
},
props: {
allAttributesList: {
type: Array,
default: () => []
},
selectCITypeIds: {
type: Array,
default: () => []
},
expression: {
type: String,
default: ''
},
},
data() {
return {
visible: false
}
},
methods: {
handleVisibleChange(open) {
if (open) {
this.$nextTick(() => {
this.$refs.conditionFilterRef.init(true, false)
})
}
},
clickSubmit() {
this.$refs.conditionFilterRef.handleSubmit()
this.visible = false
},
setExpFromFilter(filterExp) {
const regSort = /(?<=sort=).+/g
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
if (expSort) {
expression += `&sort=${expSort}`
}
this.$emit('changeExpression', expression)
}
}
}
</script>
<style lang="less" scoped>
.search-condition-filter {
height: 32px;
width: 32px;
background-color: #FFFFFF;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
&-flag {
position: absolute;
right: -5px;
bottom: -5px;
width: 10px;
height: 10px;
border-radius: 10px;
background-color: #00B42A22;
display: flex;
align-items: center;
justify-content: center;
&::after {
content: '';
width: 5px;
height: 5px;
border-radius: 5px;
background-color: #00B42A;
}
}
&-icon {
font-size: 12px;
color: #A5A9BC;
}
&:hover {
.search-condition-filter-icon {
color: #2F54EB;
}
}
}
.search-condition-content {
min-width: 500px;
&-title {
font-size: 14px;
font-weight: 400;
color: #4E5969;
}
}
.search-condition-filter-submit {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<a-modal
:title="$t('cmdb.relationSearch.saveCondition')"
:visible="visible"
dialogClass="save-condition-modal"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form-model
ref="saveConditionForm"
:model="form"
:rules="formRule"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
>
<a-form-model-item
:label="$t('cmdb.relationSearch.conditionName')"
prop="name"
>
<a-input v-model="form.name" />
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
export default {
name: 'SaveConditionModal',
props: {
visible: {
type: Boolean,
default: false
}
},
computed: {
labelCol() {
return {
span: this.$i18n.locale === 'en' ? 7 : 4
}
},
wrapperCol() {
return {
span: this.$i18n.locale === 'en' ? 17 : 20
}
},
},
data() {
return {
form: {
name: ''
},
formRule: {
name: [
{ required: true, message: this.$t('placeholder1') }
],
}
}
},
methods: {
handleOk() {
this.$refs.saveConditionForm.validate((valid) => {
if (!valid) {
return
}
this.$emit('ok', {
name: this.form.name
})
this.handleCancel()
})
},
handleCancel() {
this.$refs.saveConditionForm.clearValidate()
this.form.name = ''
this.$emit('cancel')
}
}
}
</script>
<style lang="less" scoped>
.save-condition-modal {
/deep/ .ant-modal-close-x {
width: 48px;
height: 48px;
line-height: 48px;
}
/deep/ .ant-modal-body {
padding: 24px 18px;
}
/deep/ .ant-modal-footer {
padding: 10px 18px 18px;
}
}
</style>

View File

@@ -0,0 +1,728 @@
<template>
<div
class="search-condition"
:style="{
'--label-width': this.$i18n.locale === 'en' ? '90px' : '60px'
}"
>
<div class="search-condition-row">
<div class="search-condition-label">
{{ $t('cmdb.relationSearch.sourceCIType') }}
</div>
<div class="search-condition-control">
<treeselect
:value="sourceCIType"
class="custom-treeselect custom-treeselect-bgcAndBorder filter-content-ciTypes"
:style="{
width: '100%',
zIndex: '1000',
'--custom-height': '32px',
'--custom-bg-color': '#FFF',
'--custom-multiple-lineHeight': '32px',
}"
:multiple="false"
:clearable="true"
searchable
:options="CITypeGroup"
:limit="1"
:limitText="(count) => `+ ${count}`"
:disableBranchNodes="true"
:placeholder="$t('cmdb.relationSearch.sourceCITypeTip')"
:normalizer="
(node) => {
return {
id: node.id || -1,
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.ci_types,
}
}
"
@input="updateSourceCIType"
@open="handleSourceCITypeOpen"
>
<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-search
class="search-condition-input"
:placeholder="$t('cmdb.relationSearch.sourceCITYpeInput')"
:value="sourceCITypeSearchValue"
@change="handleSourceCITypeSearchValueChange"
/>
</div>
<FilterPopover
:allAttributesList="sourceAllAttributesList"
:selectCITypeIds="sourceCIType ? [sourceCIType] : []"
:expression="sourceExpression"
@changeExpression="changeSourceExpression"
/>
</div>
<div class="search-condition-row">
<div class="search-condition-label">
{{ $t('cmdb.relationSearch.targetCIType') }}
</div>
<div class="search-condition-control">
<a-select
:value="targetCITypes"
show-search
optionFilterProp="children"
mode="multiple"
:placeholder="$t('cmdb.relationSearch.targetCITypeTip')"
class="search-condition-select"
@change="handleTargetCITypeChange"
>
<a-select-opt-group
v-for="(key, index) in Object.keys(targetCITypeGroup)"
:key="key"
:label="$t('cmdb.relationSearch.level') + `${index + 1}`"
>
<a-select-option
v-for="citype in targetCITypeGroup[key]"
:key="citype.id"
:value="citype.id"
>
{{ citype.alias || citype.name }}
</a-select-option>
</a-select-opt-group>
</a-select>
</div>
<FilterPopover
:allAttributesList="targetAllAttributesList"
:selectCITypeIds="targetCITypes"
:expression="targetExpression"
@changeExpression="changeTargetExpression"
/>
</div>
<div class="search-condition-row">
<div class="search-condition-label">
{{ $t('cmdb.relationSearch.pathSelect') }}
</div>
<div class="search-condition-control">
<a-dropdown
v-model="pathSelectVisible"
:trigger="['click']"
:getPopupContainer="(trigger) => trigger.parentElement"
>
<a-input
:value="pathDisplay"
readOnly
:placeholder="$t('cmdb.relationSearch.pathSelectTip')"
class="search-condition-input"
@click="e => e.preventDefault()"
>
<a-icon
slot="suffix"
type="caret-down"
class="search-condition-input-suffix"
/>
</a-input>
<div @click="clickPathSelectDropdown" slot="overlay">
<template v-if="allPath.length" >
<a-checkbox-group
:value="selectedPath"
class="search-condition-checkbox"
@change="handlePathChange"
>
<a-checkbox
v-for="(path) in allPath"
:key="path.value"
:value="path.value"
class="search-condition-checkbox-item"
>
<a-tooltip :title="path.pathNames">
<span class="search-condition-checkbox-item-name">
{{ path.pathNames }}
</span>
</a-tooltip>
</a-checkbox>
</a-checkbox-group>
<div class="search-condition-path-divider"></div>
<div class="search-condition-path-switch">
<span>{{ $t('cmdb.relationSearch.returnPath') }}</span>
<a-switch
:checked="returnPath"
@change="handleReturnPathChange"
/>
</div>
</template>
<div
v-else
class="search-condition-path-null"
>
<img
:src="require('@/assets/data_empty.png')"
class="search-condition-path-null-img"
/>
<div class="search-condition-path-null-text">{{ $t('noData') }}</div>
</div>
</div>
</a-dropdown>
</div>
<div
:class="['search-condition-submit', isSearchLoading ? 'search-condition-submit-loading' : '']"
@click="clickSubmit"
>
<a-icon
:type="isSearchLoading ? 'loading' : 'search'"
class="search-condition-submit-icon"
/>
</div>
</div>
<div class="search-condition-favor">
<div class="search-condition-favor-list">
<div
v-for="(item) in favorList"
:key="item.id"
class="search-condition-favor-item"
@click="clickFavor(item)"
>
<div class="search-condition-favor-name">
{{ item.option.name }}
</div>
<a-icon
@click.stop="deleteFavor(item.id)"
type="close"
class="search-condition-favor-close"
/>
</div>
</div>
<div class="search-condition-favor-right">
<a
class="search-condition-save"
@click="saveCondition"
>
<ops-icon
type="veops-save"
class="search-condition-save-icon"
/>
<span class="search-condition-save-text">
{{ $t('cmdb.relationSearch.saveCondition') }}
</span>
</a>
<div
v-if="isSearch"
class="search-condition-hide"
@click="hideSearchCondition"
>
<a-icon
type="up"
class="search-condition-hide-icon"
/>
</div>
</div>
</div>
<SaveConditionModal
:visible="saveConditionVisible"
@ok="handleSaveConditionOk"
@cancel="saveConditionVisible = false"
/>
</div>
</template>
<script>
import { getPreferenceSearch, savePreferenceSearch, deletePreferenceSearch } from '@/modules/cmdb/api/preference'
import FilterPopover from './filterPopover.vue'
import SaveConditionModal from './saveConditionModal.vue'
export default {
name: 'SearchCondition',
components: {
FilterPopover,
SaveConditionModal
},
props: {
CITypeGroup: {
type: Array,
default: () => []
},
sourceCIType: {
type: [Number, undefined],
default: undefined
},
sourceCITypeSearchValue: {
type: String,
default: ''
},
sourceAllAttributesList: {
type: Array,
default: () => []
},
sourceExpression: {
type: String,
default: ''
},
targetCITypes: {
type: Array,
default: () => []
},
targetCITypeGroup: {
type: Object,
default: () => {}
},
targetAllAttributesList: {
type: Array,
default: () => []
},
targetExpression: {
type: String,
default: ''
},
returnPath: {
type: Boolean,
default: false
},
allPath: {
type: Array,
default: () => []
},
selectedPath: {
type: Array,
default: () => []
},
isSearch: {
type: Boolean,
default: false,
},
isSearchLoading: {
type: Boolean,
default: false
}
},
data() {
return {
oldsourceCIType: undefined,
saveConditionVisible: false,
pathSelectVisible: false,
favorList: [],
relationSearchFavorKey: '__relation_favor__'
}
},
computed: {
pathDisplay() {
return this.allPath?.filter((path) => this?.selectedPath?.includes?.(path?.value))?.map((path) => path?.pathNames)?.join(', ') || ''
}
},
mounted() {
this.getFavorList()
},
methods: {
async getFavorList() {
const favorList = await getPreferenceSearch({
name: this.relationSearchFavorKey
})
favorList.sort((a, b) => b.id - a.id)
this.favorList = favorList
},
updateSourceCIType(value) {
this.$emit('changeData', {
name: 'sourceCIType',
value
})
},
handleSourceCITypeSearchValueChange(e) {
const value = e.target.value
this.$emit('changeData', {
name: 'sourceCITypeSearchValue',
value
})
},
changeSourceExpression(expression) {
this.$emit('changeData', {
name: 'sourceExpression',
value: expression
})
},
handleTargetCITypeChange(value) {
this.$emit('changeData', {
name: 'targetCITypes',
value
})
},
changeTargetExpression(expression) {
this.$emit('changeData', {
name: 'targetExpression',
value: expression
})
},
handlePathChange(value) {
this.$emit('changeData', {
name: 'selectedPath',
value
})
},
handleReturnPathChange(checked) {
this.$emit('changeData', {
name: 'returnPath',
value: checked
})
},
clickSubmit() {
if (this.isSearchLoading) {
return
}
if (this.validateControl()) {
return
}
this.$emit('search')
},
validateControl() {
if (!this.sourceCIType) {
this.$message.warning(`${this.$t('placeholder2')} ${this.$t('cmdb.relationSearch.sourceCIType')}`)
return true
}
if (!this.targetCITypes.length) {
this.$message.warning(`${this.$t('placeholder2')} ${this.$t('cmdb.relationSearch.targetCIType')}`)
return true
}
if (!this.selectedPath.length) {
this.$message.warning(`${this.$t('placeholder2')} ${this.$t('cmdb.relationSearch.path')}`)
return true
}
return false
},
saveCondition() {
if (this.validateControl()) {
return
}
this.saveConditionVisible = true
},
async handleSaveConditionOk({ name }) {
if (this?.favorList?.length >= 10) {
const deletePromises = this.favorList.slice(9).map((item) => {
return deletePreferenceSearch(item.id)
})
await Promise.all(deletePromises)
}
const option = {
name,
sourceCIType: this.sourceCIType,
searchValue: this.sourceCITypeSearchValue,
sourceExpression: this.sourceExpression,
targetCITypes: this.targetCITypes,
targetExpression: this.targetExpression,
selectedPath: this.selectedPath,
}
savePreferenceSearch({
option: {
...option
},
name: this.relationSearchFavorKey
}).then(() => {
this.$message.success(this.$t('saveSuccess'))
this.getFavorList()
})
},
deleteFavor(id) {
deletePreferenceSearch(id).then(() => {
this.$message.success(this.$t('deleteSuccess'))
this.getFavorList()
})
},
hideSearchCondition() {
this.$emit('hideSearchCondition')
},
clickPathSelectDropdown(e) {
if (e.key === '3') {
this.pathSelectVisible = false
}
},
clickFavor(data) {
if (data?.option) {
this.$emit('clickFavor', data.option)
}
},
handleSourceCITypeOpen() {
this.pathSelectVisible = false
}
}
}
</script>
<style lang="less" scoped>
.search-condition {
&-row {
display: flex;
align-items: center;
margin-bottom: 24px;
column-gap: 15px;
}
&-label {
font-size: 14px;
font-weight: 400;
color: #000000;
width: var(--label-width);
}
&-control {
display: flex;
align-items: center;
column-gap: 12px;
width: 500px;
/deep/ .ant-dropdown-content {
background-color: #FFFFFF;
padding: 14px 18px;
width: 500px;
}
}
&-input {
width: 100%;
/deep/ .ant-input {
border: none;
box-shadow: none;
cursor: pointer;
}
&-suffix {
color: #CACDD9;
}
}
&-select {
width: 100%;
/deep/ .ant-select-selection {
border: none;
box-shadow: none;
}
}
&-path {
&-divider {
width: 100%;
margin: 20px 0;
height: 1px;
background-color: #E4E7ED;
}
&-switch {
display: flex;
align-items: center;
column-gap: 16px;
}
}
&-checkbox {
display: flex;
flex-direction: column;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
&-item {
margin: 0px;
display: flex;
align-items: center;
/deep/ & > span:first-child {
flex-shrink: 0;
}
/deep/ & > span:last-child {
width: 100%;
}
&-name {
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
display: inline-block;
max-width: 100%;
}
&:not(:last-child) {
margin-bottom: 16px;
}
}
}
&-path-null {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
&-img {
width: 100px;
}
&-text {
margin-top: 12px;
color: #A5A9BC;
}
}
&-submit {
width: 32px;
height: 32px;
cursor: pointer;
border-radius: 2px;
background-color: #2F54EB;
display: flex;
align-items: center;
justify-content: center;
&-icon {
font-size: 12px;
color: #FFFFFF;
}
&-loading {
background-color: #2F54EB90;
}
&:hover {
background-color: #2F54EB90;
}
}
&-favor {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 24px;
column-gap: 15px;
&-list {
max-width: 500px;
display: flex;
align-items: center;
column-gap: 14px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 4px;
}
&-name {
font-size: 12px;
font-weight: 400;
color: #4E5969;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-close {
font-size: 12px;
color: #4E5969;
flex-shrink: 0;
&:hover {
color: #4E596980;
}
}
&-right {
display: flex;
align-items: center;
flex-shrink: 0;
}
&-item {
display: flex;
align-items: center;
max-width: 150px;
background-color: #EBEFF8;
border-radius: 28px;
padding: 2px 12px;
column-gap: 3px;
cursor: pointer;
&:hover {
background-color: #D9E4FA;
.search-condition-favor-name {
color: #2F54EB;
}
.search-condition-favor-close {
color: #2F54EB;
}
}
}
}
&-save {
flex-shrink: 0;
display: flex;
align-items: center;
font-size: 12px;
column-gap: 7px;
}
&-hide {
width: 18px;
height: 18px;
background-color: #EBEFF8;
border-radius: 1px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-left: 22px;
&-icon {
font-size: 12px;
color: #86909C;
}
&:hover {
background-color: #D9E4FA;
.search-condition-hide-icon {
color: #2F54EB;
}
}
}
}
</style>

View File

@@ -0,0 +1,693 @@
<template>
<div
ref="relationSearchRef"
class="relation-search"
:style="{ height: `${windowHeight - 131}px` }"
>
<div class="relation-search-wrap">
<div
v-if="!isSearch"
class="relation-search-title"
>
<ops-icon class="relation-search-title-icon" type="veops-relationship2" />
<div class="relation-search-title-text">{{ $t('cmdb.relationSearch.relationSearch') }}</div>
</div>
<div
v-if="isHideSearchCondition"
class="relation-search-expand"
>
<div class="relation-search-expand-line"></div>
<div class="relation-search-expand-right">
<div
class="relation-search-expand-handle"
@click="isHideSearchCondition = false"
>
<a-icon
type="down"
class="relation-search-expand-icon"
/>
</div>
<div
class="relation-search-expand-text"
@click="isHideSearchCondition = false"
>
{{ $t('cmdb.relationSearch.expandCondition') }}
</div>
</div>
</div>
<SearchCondition
v-else
:CITypeGroup="CITypeGroup"
:sourceCIType="sourceCIType"
:sourceCITypeSearchValue="sourceCITypeSearchValue"
:sourceAllAttributesList="sourceAllAttributesList"
:sourceExpression="sourceExpression"
:targetCITypes="targetCITypes"
:targetCITypeGroup="targetCITypeGroup"
:targetAllAttributesList="targetAllAttributesList"
:targetExpression="targetExpression"
:returnPath="returnPath"
:allPath="allPath"
:selectedPath="selectedPath"
:isSearch="isSearch"
:isSearchLoading="isSearchLoading"
@changeData="changeData"
@search="handleSearch"
@hideSearchCondition="isHideSearchCondition = true"
@clickFavor="clickFavor"
/>
<div
v-if="isSearch"
class="relation-search-main"
>
<CITable
:allTableData="allTableData"
:tabActive="tableTabActive"
:returnPath="returnPath"
:isHideSearchCondition="isHideSearchCondition"
:referenceShowAttrNameMap="referenceShowAttrNameMap"
:referenceCIIdMap="referenceCIIdMap"
:searchValue="sourceCITypeSearchValue"
:isSearchLoading="isSearchLoading"
@updateTab="(tab) => tableTabActive = tab"
/>
<div class="relation-search-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@showSizeChange="handlePageSizeChange"
@change="changePage"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('all') }}</span>
</template>
</a-pagination>
</div>
</div>
</div>
<img
v-if="!isSearch"
class="relation-search-bg"
:src="require('@/modules/cmdb/assets/resourceSearch/resource_search_bg_1.png')"
/>
</div>
</template>
<script>
import _ from 'lodash'
import { getCITypeAttributesByTypeIds } from '@/modules/cmdb/api/CITypeAttr'
import { getRecursive_level2children, getCITypeRelationPath } from '@/modules/cmdb/api/CITypeRelation'
import { searchCIRelationPath } from '@/modules/cmdb/api/CIRelation'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { searchCI } from '@/modules/cmdb/api/ci'
import { strLength } from '@/modules/cmdb/utils/helper.js'
import SearchCondition from './components/searchCondition.vue'
import CITable from './components/ciTable.vue'
export default {
name: 'RelationSearch',
components: {
SearchCondition,
CITable
},
props: {
CITypeGroup: {
type: Array,
default: () => []
},
allCITypes: {
type: Array,
default: () => []
}
},
data() {
return {
isSearch: false, // 是否搜索
isHideSearchCondition: false, // 是否隐藏搜索条件
isWatchData: true, // 是否监听数据变化
isSearchLoading: false, // 搜索中
sourceCIType: undefined, // 已选源模型
sourceCITypeSearchValue: '', // 源模型搜索关键词
sourceAllAttributesList: [], // 源模型所有属性
sourceExpression: '', // 源模型表达式
targetCITypes: [], // 目标模型
targetCITypeGroup: {}, // 目标模型分组
targetAllAttributesList: [], // 目标模型所有属性
targetExpression: '', // 目标模型表达式
returnPath: true, // 表格是否展示路径详情
allPath: [], // 所有路径选项
selectedPath: [], // 已选择路径
// table
page: 1,
pageSize: 50,
pageSizeOptions: ['50', '100', '200'],
allTableData: {}, // 表格数据
totalNumber: 0, // 数据总数
tableTabActive: '', // 当前 table tab
referenceShowAttrNameMap: {},
referenceCIIdMap: {},
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
watchParams() {
return {
sourceCIType: this.sourceCIType,
targetCITypes: this.targetCITypes
}
},
},
watch: {
sourceCIType: {
immediate: true,
deep: true,
handler(id) {
if (this.isWatchData) {
this.sourceExpression = ''
this.targetCITypes = []
this.targetAllAttributesList = []
this.targetExpression = ''
this.selectedPath = []
this.getTargetCITypeGroup(id)
this.updateSourceAllAttributesList(id)
}
}
},
targetCITypes: {
immediate: true,
deep: true,
handler(ids) {
if (this.isWatchData) {
this.selectedPath = []
this.targetExpression = ''
this.updateTargetAllAttributesList(ids)
}
}
},
watchParams: {
immediate: true,
deep: true,
handler(data) {
if (this.isWatchData) {
this.updateAllPath(data)
}
}
}
},
methods: {
changeData(data) {
this[data.name] = data.value
},
async updateSourceAllAttributesList(id) {
if (id) {
const res = await getCITypeAttributesByTypeIds({ type_ids: id })
this.sourceAllAttributesList = res.attributes
} else {
this.sourceAllAttributesList = []
}
},
async getTargetCITypeGroup(id) {
let targetCITypeGroup = {}
if (id) {
const res = await getRecursive_level2children(id)
targetCITypeGroup = res
}
this.targetCITypeGroup = targetCITypeGroup
},
async updateTargetAllAttributesList(ids) {
if (ids?.length) {
const res = await getCITypeAttributesByTypeIds({ type_ids: ids.join(',') })
this.targetAllAttributesList = res.attributes
} else {
this.targetAllAttributesList = []
}
},
async updateAllPath(data) {
let allPath = []
if (
data.sourceCIType &&
data?.targetCITypes?.length
) {
const params = {
source_type_id: data.sourceCIType,
target_type_ids: data.targetCITypes.join(',')
}
const res = await getCITypeRelationPath(params)
if (res?.paths?.length) {
const sourceCIType = this.allCITypes.find((ciType) => ciType.id === data.sourceCIType)
const sourceCITypeName = sourceCIType?.alias || sourceCIType?.name || ''
const targetCITypeList = Object.values(this.targetCITypeGroup).reduce((acc, cur) => acc.concat(cur), [])
allPath = res.paths.map((ids) => {
const [sourceId, ...targetIds] = ids
const pathNames = [sourceCITypeName]
targetIds.forEach((id) => {
const ciType = targetCITypeList.find((item) => item.id === id)
if (ciType) {
pathNames.push(ciType.alias || ciType.name)
}
})
return {
value: ids.join(','),
sourceId,
targetIds,
pathNames: pathNames.join('-'),
}
})
}
}
this.allPath = allPath
},
async loadCI() {
this.isSearchLoading = true
const path = this.selectedPath.map((item) => {
return item?.split(',')?.map((id) => Number(id)) || []
})
const params = {
page: this.page,
page_size: this.pageSize,
source: {
type_id: this.sourceCIType
},
target: {
type_ids: this.targetCITypes
},
path
}
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const sourceExp = this.sourceExpression.match(regQ) ? this.sourceExpression.match(regQ)[0] : null
const targetExp = this.targetExpression.match(regQ) ? this.targetExpression.match(regQ)[0] : null
const sourceSearch = `${sourceExp ? `${sourceExp}` : ''}${this.sourceCITypeSearchValue ? `,*${this.sourceCITypeSearchValue}*` : ''}`
if (sourceSearch) {
params.source.q = sourceSearch
}
if (targetExp) {
params.target.q = targetExp
}
let res = {}
const tableData = {}
const typeId2Attr = {}
let pathKeyList = []
try {
res = await searchCIRelationPath(params)
pathKeyList = Object.keys(res.paths)
const filterAllPath = this.allPath.filter((path) => pathKeyList.includes(path.pathNames))
const typeIds = _.uniq(
filterAllPath.map((item) => item?.targetIds?.[item?.targetIds?.length - 1])
)
const promises = typeIds.map((id) => {
return getSubscribeAttributes(id)
})
const subscribedRes = await Promise.all(promises)
typeIds.forEach((id, index) => {
const attrList = subscribedRes?.[index]?.attributes || []
typeId2Attr[id] = attrList
})
} catch (error) {
this.isSearchLoading = false
this.allTableData = {}
this.totalNumber = 0
this.tableTabActive = ''
return
}
pathKeyList.forEach((key) => {
const pathObj = this.allPath.find((path) => path.pathNames === key)
const pathIdList = pathObj?.value?.split(',') || []
const pathNameList = key?.split('-') || []
const pathList = pathNameList.map((name, index) => {
let relation = ''
if (index < pathNameList.length - 1) {
const targetName = pathNameList[index + 1]
const sourceRelation = res?.relation_types?.[name]
if (sourceRelation) {
if (Object.keys(sourceRelation)?.includes?.(targetName)) {
relation = sourceRelation?.[targetName] || ''
}
}
}
return {
id: pathIdList?.[index] || '',
name,
relation
}
})
tableData[key] = {
key,
count: res.paths?.[key]?.length || 0,
pathList,
ciAttr: [],
ciList: []
}
if (pathObj) {
const firstIds = res?.paths?.[key]?.[0]
const targetId = firstIds[firstIds.length - 1]
const ciTypeId = (res?.id2ci?.[targetId] || {})?._type
if (ciTypeId) {
tableData[key].ciAttr = typeId2Attr[ciTypeId]
}
tableData[key].ciList = res.paths[key].map((ids) => {
const pathCI = {}
ids.map((id) => {
const ci = res?.id2ci?.[id] || {}
const showAttr = res?.type2show_key?.[ci._type] || ''
pathCI[ci._type] = ci?.[showAttr] ?? ''
})
const targetId = ids[ids.length - 1]
const targetCI = res?.id2ci?.[targetId] || {}
return {
pathCI,
targetCI
}
})
let totalWidth = 0
tableData[key].ciAttr.forEach((attr) => {
const lengthList = tableData[key].ciList.map(({ targetCI }) => {
return strLength(targetCI[attr.name])
})
attr.width = Math.round(Math.min(Math.max(100, ...lengthList), 350))
totalWidth += attr.width
})
// ci 表格宽度 = 容器宽度 - path 列宽 - checkbox 宽度
const wrapWidth = this.$refs?.relationSearchRef?.clientWidth - (tableData?.[key]?.pathList.length || 0) * 160 - 60
if (wrapWidth && totalWidth < wrapWidth) {
tableData[key].ciAttr.forEach((attr) => {
delete attr.width
})
}
}
})
this.$set(this, 'allTableData', tableData)
this.allTableData = tableData
this.totalNumber = res?.numfound ?? 0
this.tableTabActive = Object.keys(tableData)?.[0] || ''
this.isSearch = true
this.isSearchLoading = false
const allAttr = []
Object.values(typeId2Attr).map((attrList) => {
allAttr.push(...attrList)
})
this.handlePerference(_.uniqBy(allAttr, 'id'))
},
handlePerference(allAttr) {
let needRequiredCIType = []
allAttr.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
needRequiredCIType.push(attr)
}
})
needRequiredCIType = _.uniq(needRequiredCIType, 'id')
if (!needRequiredCIType.length) {
this.referenceShowAttrNameMap = {}
this.referenceCIIdMap = {}
return
}
this.handleReferenceShowAttrName(needRequiredCIType)
this.handleReferenceCIIdMap(needRequiredCIType)
},
async handleReferenceShowAttrName(needRequiredCIType) {
const res = await getCITypes({
type_ids: needRequiredCIType.map((col) => col.reference_type_id).join(',')
})
const map = {}
res.ci_types.forEach((ciType) => {
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
this.referenceShowAttrNameMap = map
},
async handleReferenceCIIdMap(needRequiredCIType) {
const map = {}
Object.values(this.allTableData).forEach((item) => {
const ciList = item?.ciList || []
ciList.forEach(({ targetCI }) => {
needRequiredCIType.forEach((col) => {
const ids = Array.isArray(targetCI[col.name]) ? targetCI[col.name] : targetCI[col.name] ? [targetCI[col.name]] : []
if (ids.length) {
if (!map?.[col.reference_type_id]) {
map[col.reference_type_id] = {}
}
ids.forEach((id) => {
map[col.reference_type_id][id] = {}
})
}
})
})
})
if (!Object.keys(map).length) {
this.referenceCIIdMap = {}
return
}
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
allRes.forEach((res) => {
res.result.forEach((item) => {
if (map?.[item._type]?.[item._id]) {
map[item._type][item._id] = item
}
})
})
this.referenceCIIdMap = map
},
handlePageSizeChange(_, pageSize) {
this.pageSize = pageSize
this.page = 1
this.loadCI()
},
changePage(page) {
this.page = page
this.loadCI()
},
handleSearch() {
this.page = 1
this.loadCI()
},
clickFavor(option) {
this.isWatchData = false
this.$nextTick(async () => {
this.sourceCIType = option?.sourceCIType || undefined
this.sourceCITypeSearchValue = option?.searchValue || ''
this.sourceExpression = option?.sourceExpression || ''
this.targetCITypes = option?.targetCITypes || []
this.targetExpression = option?.targetExpression || ''
this.selectedPath = option?.selectedPath || []
await Promise.all([
this.getTargetCITypeGroup(this.sourceCIType),
this.updateSourceAllAttributesList(this.sourceCIType),
this.updateTargetAllAttributesList(this.targetCITypes)
])
await this.updateAllPath({
sourceCIType: this.sourceCIType,
targetCITypes: this.targetCITypes
})
this.isWatchData = true
this.page = 1
this.loadCI()
})
}
}
}
</script>
<style lang="less" scoped>
.relation-search {
width: 100%;
height: 100%;
position: relative;
&-wrap {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
}
&-title {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
margin-top: 100px;
&-icon {
font-size: 28px;
margin-right: 10px;
}
&-text {
font-size: 20px;
font-weight: 700;
color: #1D2129;
}
}
&-expand {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
&-line {
width: 650px;
height: 1px;
background-color: #E4E7ED;
}
&-icon {
font-size: 12px;
color: #86909C;
}
&-text {
margin-left: 5px;
font-size: 12px;
font-weight: 400;
color: #A5A9BC;
}
&-handle {
width: 14px;
height: 14px;
background-color: #EBEFF8;
border-radius: 1px;
display: flex;
align-items: center;
justify-content: center;
}
&-right {
flex-shrink: 0;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
.relation-search-expand-handle {
background-color: #D9E4FA;
}
.relation-search-expand-icon {
color: #2F54EB;
}
.relation-search-expand-text {
color: #2F54EB;
}
}
}
}
&-bg {
position: absolute;
left: -24px;
bottom: -24px;
width: calc(100% + 48px);
z-index: 0;
}
&-main {
width: calc(100% + 48px);
// height: 100%;
background-color: #FFFFFF;
padding: 24px;
}
&-pagination {
text-align: right;
margin-top: 12px;
}
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<div :class="['attr-display', isEllipsis ? 'attr-display-ellipsis' : '']">
<template v-if="attr.is_reference && ci[attr.name]" >
<a
v-for="(ciId) in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
:key="ciId"
:href="`/cmdb/cidetail/${attr.reference_type_id}/${ciId}`"
target="_blank"
>
{{ getReferenceAttrValue(ciId) }}
</a>
</template>
<span v-else-if="attr.value_type === '6' && ci[attr.name]">{{ JSON.stringify(ci[attr.name]) }}</span>
<template v-else-if="attr.is_link && ci[attr.name]">
<a
v-for="(item, linkIndex) in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
:key="linkIndex"
:href="
item.startsWith('http') || item.startsWith('https')
? `${item}`
: `http://${item}`
"
target="_blank"
>
{{ getChoiceValueLabel(item) || item }}
</a>
</template>
<PasswordField
v-else-if="attr.is_password && ci[attr.name]"
:ci_id="ci._id"
:attr_id="attr.id"
></PasswordField>
<template v-else-if="attr.is_choice">
<span
v-for="value in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
:key="value"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
...getChoiceValueStyle(value),
}"
>
<ops-icon
:style="{ color: getChoiceValueIcon(attr, value).color }"
:type="getChoiceValueIcon(attr, value).name"
/>
<span
v-html="markSearchValue(getChoiceValueLabel(value) || value)"
></span>
</span>
</template>
<span
v-else
v-html="markSearchValue((attr.is_list && Array.isArray(ci[attr.name])) ? ci[attr.name].join(',') : ci[attr.name])"
></span>
</div>
</template>
<script>
import PasswordField from '@/modules/cmdb/components/passwordField/index.vue'
export default {
name: 'AttrDisplay',
components: {
PasswordField
},
props: {
attr: {
type: Object,
default: () => {}
},
ci: {
type: Object,
default: () => {}
},
isEllipsis: {
type: Boolean,
default: false
},
referenceShowAttrNameMap: {
type: Object,
default: () => {}
},
referenceCIIdMap: {
type: Object,
default: () => {}
},
searchValue: {
type: String,
default: ''
}
},
methods: {
markSearchValue(text) {
if (!text || !this.searchValue) {
return text
}
const regex = new RegExp(`(${this.searchValue})`, 'gi')
return String(text).replace(
regex,
`<span style="background-color: #D3EEFE; padding: 0 2px;">$1</span>`
)
},
getChoiceValueStyle(attrValue) {
const _find = this?.attr?.choice_value?.find?.((item) => String(item?.[0]) === String(attrValue))
if (_find) {
return _find?.[1]?.style || {}
}
return {}
},
getChoiceValueIcon(attrValue) {
const _find = this?.attr?.choice_value?.find((item) => String(item?.[0]) === String(attrValue))
if (_find) {
return _find?.[1]?.icon || {}
}
return {}
},
getChoiceValueLabel(attrValue) {
const _find = this?.attr?.choice_value?.find((item) => String(item?.[0]) === String(attrValue))
if (_find) {
return _find?.[1]?.label || ''
}
return ''
},
getReferenceAttrValue(id) {
if (this.attr.referenceShowAttrNameMap?.[id]) {
return this.attr.referenceShowAttrNameMap[id]
}
const ci = this?.referenceCIIdMap?.[this?.attr?.reference_type_id]?.[id]
if (!ci) {
return id
}
const attrName = this.referenceShowAttrNameMap?.[this?.attr.reference_type_id]
return ci?.[attrName] || id
},
}
}
</script>
<style lang="less" scoped>
.attr-display {
width: 100%;
font-size: 14px;
font-weight: 400;
word-break: break-all;
&-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
}
</style>

View File

@@ -0,0 +1,235 @@
<template>
<a-popover
v-model="visible"
trigger="click"
placement="bottom"
@visibleChange="handleVisibleChange"
>
<div class="filter-btn">
<a-icon class="filter-btn-icon" type="filter" />
<span class="filter-btn-title">{{ $t('cmdb.ciType.advancedFilter') }}</span>
</div>
<template slot="content">
<div class="filter-content">
<a-form :form="form">
<a-form-item
:label="$t('cmdb.ciType.ciType')"
:label-col="formLayout.labelCol"
:wrapper-col="formLayout.wrapperCol"
>
<treeselect
:value="selectCITypeIds"
class="custom-treeselect custom-treeselect-bgcAndBorder filter-content-ciTypes"
:style="{
width: '400px',
zIndex: '1000',
'--custom-height': '32px',
'--custom-bg-color': '#FFF',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '32px',
}"
:multiple="true"
:clearable="true"
searchable
:options="CITypeGroup"
:limit="1"
:limitText="(count) => `+ ${count}`"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.ciType')"
@close="closeCiTypeGroup"
@open="openCiTypeGroup"
@input="inputCiTypeGroup"
:normalizer="
(node) => {
return {
id: node.id || -1,
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.ci_types,
}
}
"
>
<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-form-item
:label="$t('cmdb.ciType.filterPopoverLabel')"
:label-col="formLayout.labelCol"
:wrapper-col="formLayout.wrapperCol"
class="filter-content-condition-filter"
>
<ConditionFilter
ref="conditionFilterRef"
:canSearchPreferenceAttrList="allAttributesList"
:expression="expression"
:CITypeIds="selectCITypeIds"
:isDropdown="false"
@setExpFromFilter="setExpFromFilter"
/>
</a-form-item>
</a-form>
<div class="filter-content-action">
<a-button
size="small"
@click="saveCondition(false)"
>
{{ $t('cmdb.ciType.saveCondition') }}
</a-button>
<a-button
type="primary"
size="small"
@click="saveCondition(true)"
>
{{ $t('confirm') }}
</a-button>
</div>
</div>
</template>
</a-popover>
</template>
<script>
import _ from 'lodash'
import ConditionFilter from '@/modules/cmdb/components/conditionFilter/index.vue'
export default {
name: 'FilterPopover',
components: {
ConditionFilter
},
data() {
return {
visible: false,
form: this.$form.createForm(this),
formLayout: {
labelCol: { span: 3 },
wrapperCol: { span: 15 },
},
lastCiType: [],
}
},
props: {
expression: {
type: String,
default: ''
},
selectCITypeIds: {
type: Array,
default: () => []
},
CITypeGroup: {
type: Array,
default: () => []
},
allAttributesList: {
type: Array,
default: () => []
}
},
methods: {
handleVisibleChange(open) {
if (open) {
this.$nextTick(() => {
this.$refs.conditionFilterRef.init(true, false)
})
}
},
openCiTypeGroup() {
this.lastCiType = _.cloneDeep(this.selectCITypeIds)
},
closeCiTypeGroup(value) {
if (!_.isEqual(value, this.lastCiType)) {
this.$emit('updateAllAttributesList', value)
}
},
inputCiTypeGroup(value) {
if (!value || !value.length) {
this.$emit('updateAllAttributesList', value)
}
this.$emit('changeFilter', {
name: 'selectCITypeIds',
value
})
},
setExpFromFilter(filterExp) {
const regSort = /(?<=sort=).+/g
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
if (expSort) {
expression += `&sort=${expSort}`
}
this.$emit('changeFilter', {
name: 'expression',
value: expression
})
},
saveCondition(isSubmit) {
this.$refs.conditionFilterRef.handleSubmit()
this.$nextTick(() => {
this.$emit('saveCondition', isSubmit)
this.visible = false
})
},
}
}
</script>
<style lang="less" scoped>
.filter-btn {
flex-shrink: 0;
display: flex;
align-items: center;
margin-left: 13px;
cursor: pointer;
&-icon {
color: #2F54EB;
font-size: 12px;
}
&-title {
font-size: 14px;
font-weight: 400;
color: #2F54EB;
margin-left: 3px;
}
}
.filter-content {
width: 600px;
&-ciTypes {
/deep/ .vue-treeselect__value-container {
line-height: 32px;
}
}
&-condition-filter {
max-height: 250px;
// overflow-y: auto;
margin-bottom: 0px;
}
&-action {
width: 100%;
margin-top: 12px;
display: flex;
justify-content: flex-end;
align-items: center;
column-gap: 21px;
}
}
</style>

View File

@@ -0,0 +1,310 @@
<template>
<div class="history-list">
<div
class="history-recent"
v-if="recentList.length"
>
<div class="history-title">
<a-icon type="eye" class="history-title-icon" />
<div class="history-title-text">{{ $t('cmdb.ciType.recentSearch') }}</div>
<a-popconfirm
:title="$t('cmdb.ciType.confirmClear')"
placement="topRight"
@confirm="clearRecent"
>
<a-tooltip :title="$t('clear')" >
<a-icon
type="delete"
class="history-title-clear"
/>
</a-tooltip>
</a-popconfirm>
</div>
<div class="recent-list">
<div
v-for="(item) in recentList.slice(0, 10)"
:key="item.id"
class="recent-list-item"
@click="clickRecent(item.option)"
>
<div class="recent-list-item-text">
{{ getRecentSearchText(item.option) }}
</div>
<a-icon
type="close"
class="recent-list-item-close"
@click.stop="deleteRecent(item.id)"
/>
</div>
</div>
</div>
<div
class="history-favor"
v-if="favorList.length"
>
<div class="history-title">
<ops-icon type="veops-collect" class="history-title-icon" />
<div class="history-title-text">{{ $t('cmdb.ciType.myCollection') }}</div>
<div class="history-title-count">({{ favorList.length }})</div>
<ops-icon
type="veops-expand"
class="history-title-expand"
:style="{
transform: `rotate(${isExpand ? '180deg' : '0deg'})`
}"
@click="isExpand = !isExpand"
/>
</div>
<div
class="favor-list"
:style="{ height: isExpand ? 'auto' : '30px' }"
>
<div
v-for="(item) in favorList"
:key="item.id"
:class="['favor-list-item', detailCIId === item.option.CIId ? 'favor-list-item-selected' : '']"
@click="showDetail(item.option)"
>
<CIIcon
:icon="item.option.icon"
:title="item.option.CITypeTitle"
/>
<div class="favor-list-item-title">
{{ item.option.title }}
</div>
<ops-icon
type="veops-collected"
class="favor-list-item-collected"
@click.stop="deleteCollect(item.id)"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
export default {
name: 'HistoryList',
components: {
CIIcon
},
props: {
recentList: {
type: Array,
default: () => []
},
favorList: {
type: Array,
default: () => []
},
detailCIId: {
type: [String, Number],
default: -1
}
},
data() {
return {
isExpand: false,
}
},
methods: {
getRecentSearchText(option) {
const textArray = []
if (option.searchValue) {
textArray.push(`${this.$t('cmdb.ciType.keyword')}: ${option.searchValue}`)
}
if (option?.ciTypeNames?.length) {
textArray.push(`${this.$t('cmdb.ciType.CIType')}: ${option.ciTypeNames.join(',')}`)
}
if (option.expression) {
textArray.push(`${this.$t('cmdb.ciType.conditionFilter')}: ${option.expression}`)
}
return textArray.join('; ')
},
clickRecent(data) {
this.$emit('clickRecent', data)
},
deleteRecent(id) {
this.$emit('deleteRecent', id)
},
deleteCollect(id) {
this.$emit('deleteCollect', id)
},
showDetail(data) {
this.$emit('showDetail', {
id: data.CIId,
ciTypeId: data.CITypeId
})
},
clearRecent() {
this.$emit('clearRecent')
}
}
}
</script>
<style lang="less" scoped>
.history-list {
width: 100%;
.history-title {
display: flex;
align-items: center;
&-icon {
font-size: 12px;
color: #2F54EB;
}
&-text {
font-size: 14px;
font-weight: 400;
color: #4E5969;
margin-left: 4px;
}
&-count {
font-size: 14px;
font-weight: 400;
color: #86909C;
}
&-clear {
margin-left: auto;
cursor: pointer;
}
&-expand {
margin-left: auto;
cursor: pointer;
}
}
.history-recent {
width: 100%;
margin-top: 15px;
.recent-list {
margin-top: 10px;
display: flex;
align-items: center;
flex-wrap: wrap;
column-gap: 16px;
row-gap: 8px;
&-item {
flex-shrink: 0;
padding: 4px 13px;
display: flex;
align-items: center;
border-radius: 22px;
background: rgba(255, 255, 255, 0.50);
cursor: pointer;
max-width: calc((100% - 16px) / 2);
&-text {
font-size: 12px;
font-weight: 400;
color: #1D2129;
max-width: 100%;
text-wrap: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&-close {
font-size: 12px;
margin-left: 4px;
color: #A5A9BC;
display: none;
}
&:hover {
.recent-list-item-text {
color: #2F54EB;
}
.recent-list-item-close {
display: inline-block;
}
}
}
}
}
.history-favor {
width: 100%;
margin-top: 15px;
.favor-list {
margin-top: 10px;
display: flex;
align-items: center;
flex-wrap: wrap;
column-gap: 16px;
row-gap: 8px;
overflow: hidden;
min-height: 30px;
&-item {
flex-shrink: 0;
padding: 4px 13px;
display: flex;
align-items: center;
border-radius: 22px;
background: rgba(255, 255, 255, 0.90);
cursor: pointer;
max-width: calc((100% - 16px) / 2);
&-title {
font-size: 12px;
font-weight: 400;
margin-left: 4px;
color: #1D2129;
max-width: 100%;
text-overflow: ellipsis;
text-wrap: nowrap;
overflow: hidden;
}
&-collected {
font-size: 14px;
margin-left: 4px;
color: #FAD337;
}
&-selected {
border: 1px solid #7F97FA;
background-color: rgba(255, 255, 255, 0.90);
.favor-list-item-title {
color: #2F54EB;
}
}
&:hover {
.favor-list-item-title {
color: #2F54EB;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,438 @@
<template>
<div class="instance-detail">
<div
class="instance-detail-hide"
@click="hideDetail"
>
<a-icon class="instance-detail-hide-icon" type="right" />
</div>
<div class="instance-detail-null" v-if="!ci._id" >
<img
:src="require('@/modules/cmdb/assets/no_permission.png')"
class="instance-detail-null-img"
/>
<span class="instance-detail-null-text" >{{ $t('noData') }}</span>
</div>
<template v-else>
<div
class="instance-detail-header"
>
<div class="instance-detail-header-line-1"></div>
<div class="instance-detail-header-line-2"></div>
<div class="instance-detail-header-row">
<CIIcon
:icon="ciType.icon"
:title="ciType.name || ''"
:size="20"
/>
<div class="instance-detail-header-title">
{{ detailTitle }}
</div>
<ops-icon
:type="favorId ? 'veops-collected' : 'veops-collect'"
:style="{ color: favorId ? '#FAD337' : '#A5A9BC' }"
class="instance-detail-header-collect"
@click="clickCollect"
/>
<a class="instance-detail-header-share" @click="shareCi">
<a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }}
</a>
</div>
</div>
<div class="instance-detail-attr">
<div
v-for="(group) in attributeGroups"
:key="group.id"
class="instance-detail-attr-group"
>
<span class="instance-detail-attr-group-name">{{ group.name || $t('other') }}</span>
<div class="instance-detail-attr-list">
<div
v-for="(attr) in group.attributes"
:key="attr.id"
class="instance-detail-attr-item"
>
<a-tooltip :title="attr.alias || attr.name || ''">
<div class="instance-detail-attr-item-label">
<span class="instance-detail-attr-item-label-text">
{{ attr.alias || attr.name || '' }}
</span>
<span class="instance-detail-attr-item-label-colon">:</span>
</div>
</a-tooltip>
<div class="instance-detail-attr-item-value">
<AttrDisplay
:attr="attr"
:ci="ci"
/>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { getCIById, searchCI } from '@/modules/cmdb/api/ci'
import { getCITypeGroupById, getCITypes, getCIType } from '@/modules/cmdb/api/CIType'
import AttrDisplay from './attrDisplay.vue'
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
export default {
name: 'InstanceDetail',
components: {
AttrDisplay,
CIIcon
},
props: {
CIId: {
type: [String, Number],
default: -1
},
CITypeId: {
type: [String, Number],
default: -1
},
favorList: {
type: Array,
default: () => []
}
},
data() {
return {
ci: {},
ciType: {},
attributeGroups: [],
isNullData: false,
}
},
computed: {
watchParams() {
return {
CIId: this.CIId,
CITypeId: this.CITypeId
}
},
detailTitle() {
const attrName = this?.ciType?.show_name || this?.ciType?.unique_name || ''
return attrName ? (this?.ci?.[attrName] || '') : ''
},
favorId() {
const id = this.favorList.find((item) => item?.option?.CIId === this.CIId)?.id
return id ?? null
}
},
watch: {
watchParams: {
immediate: true,
deep: true,
handler(newVal) {
if (newVal?.CIId !== -1 && newVal?.CITypeId !== -1) {
this.initData()
}
}
}
},
methods: {
async initData() {
const ci = await this.getCI()
if (!ci) {
this.isNullData = true
return
}
await this.getCIType()
await this.getAttributes()
},
async getCI() {
const res = await getCIById(this.CIId)
const ci = res.result?.[0] || {}
this.ci = ci
return ci
},
async getCIType() {
const res = await getCIType(this.CITypeId)
this.ciType = res?.ci_types?.[0] || {}
},
async getAttributes() {
const res = await getCITypeGroupById(this.CITypeId, { need_other: 1 })
this.attributeGroups = res
this.handleReferenceAttr()
},
async handleReferenceAttr() {
const map = {}
this.attributeGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
if (ids.length) {
if (!map?.[attr.reference_type_id]) {
map[attr.reference_type_id] = {}
}
ids.forEach((id) => {
map[attr.reference_type_id][id] = {}
})
}
}
})
})
if (!Object.keys(map).length) {
return
}
const ciTypesRes = await getCITypes({
type_ids: Object.keys(map).join(',')
})
const showAttrNameMap = {}
ciTypesRes.ci_types.forEach((ciType) => {
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
const ciNameMap = {}
allRes.forEach((res) => {
res.result.forEach((item) => {
ciNameMap[item._id] = item
})
})
const newAttrGroups = _.cloneDeep(this.attributeGroups)
newAttrGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
const referenceShowAttrNameMap = {}
const referenceCIIds = this.ci[attr.name];
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
})
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
}
})
})
this.$set(this, 'attributeGroups', newAttrGroups)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.CITypeId}/${this.CIId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
clickCollect() {
if (this.favorId) {
this.$emit('deleteCollect', this.favorId)
} else {
this.$emit('addCollect', {
CIId: this.CIId,
CITypeId: this.CITypeId,
title: this.detailTitle,
icon: this.ciType?.icon,
CITypeTitle: this.ciType?.name || ''
})
}
},
hideDetail() {
this.$emit('hideDetail')
}
}
}
</script>
<style lang="less" scoped>
.instance-detail {
width: 100%;
height: 100%;
border-radius: 2px;
border: 1px solid #E4E7ED;
background-color: #FFFFFF;
display: flex;
flex-direction: column;
position: relative;
&-hide {
position: absolute;
left: 0;
top: 50%;
margin-top: -21px;
border-radius: 0px 2px 2px 0px;
background-color: #2f54eb;
width: 13px;
height: 43px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
cursor: pointer;
&-icon {
color: #FFFFFF;
font-size: 12px;
}
&:hover {
background-color: #597ef7;
}
}
&-null {
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
padding-top: 100px;
&-img {
width: 180px;
}
&-text {
color: #86909C;
margin-top: 20px;
}
}
&-header {
width: 100%;
height: 75px;
background-color: #EBF0F9;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
padding: 0 20px;
flex-shrink: 0;
&-line-1 {
height: 44px;
width: 300px;
position: absolute;
right: -20px;
top: 0px;
transform: rotate(40deg);
background: rgba(248, 249, 253, 0.60);
}
&-line-2 {
height: 44px;
width: 300px;
position: absolute;
right: -110px;
top: 0px;
transform: rotate(40deg);
background: rgba(248, 249, 253, 0.60);
}
&-row {
width: 100%;
height: 100%;
display: flex;
align-items: center;
position: relative;
z-index: 2;
}
&-title {
font-size: 16px;
font-weight: 700;
color: #1D2129;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
margin-left: 9px;
}
&-collect {
margin-left: 8px;
margin-right: 8px;
}
&-share {
margin-left: auto;
flex-shrink: 0;
}
}
&-attr {
width: 100%;
overflow-y: auto;
height: 100%;
padding: 20px;
&-group {
&:not(:first-child) {
margin-top: 15px;
}
&-name {
font-size: 14px;
font-weight: 700;
color: #1D2129;
}
}
&-item {
margin-top: 15px;
display: flex;
align-items: flex-start;
&-label {
font-size: 14px;
font-weight: 400;
color: #86909C;
width: 25%;
flex-shrink: 0;
display: flex;
align-items: center;
&-text {
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-colon {
flex-shrink: 0;
}
}
&-value {
margin-left: 12px;
}
}
}
}
</style>

View File

@@ -0,0 +1,443 @@
<template>
<div class="list-wrap">
<div class="list-wrap-bg" v-if="!filterList.length">
<img :src="require('@/modules/cmdb/assets/resourceSearch/resource_search_bg_2.png')" />
</div>
<div v-if="tabList.length" class="list-tab">
<div class="list-tab-left">
<div class="list-tab-label">{{ $t('cmdb.ciType.currentPage') }}: </div>
<div
v-for="(tab) in tabList"
:key="tab.id"
:class="['list-tab-item', tab.id === currentTab ? 'list-tab-item-active' : '']"
@click="clickTab(tab.id)"
>
<span class="list-tab-item-title">{{ tab.title }}</span>
(<span class="list-tab-item-count">{{ tab.count }}</span>)
</div>
</div>
<a-button
icon="download"
type="primary"
class="ops-button-ghost list-tab-export"
ghost
@click="handleExport"
>
{{ $t('download') }}
</a-button>
</div>
<div v-if="filterList.length" class="list-container">
<div
v-for="(item) in filterList"
:key="item._id"
:class="['list-card', detailCIId === item.ci._id ? 'list-card-selected' : '']"
@click="clickInstance(item.ci._id, item.ciTypeObj.id)"
>
<div class="list-card-header">
<div class="list-card-model">
<CIIcon
:icon="item.ciTypeObj.icon"
:title="item.ciTypeObj.name"
/>
<span class="list-card-model-title">{{ item.ciTypeObj.title }}</span>
</div>
<div class="list-card-title">{{ item.ci[item.ciTypeObj.showAttrName] }}</div>
<ops-icon
v-if="getFavorId(item.ci._id)"
type="veops-collected"
class="list-card-collect"
:style="{ color: '#FAD337' }"
@click.stop="deleteCollect(item.ci._id)"
/>
<ops-icon
v-else
type="veops-collect"
class="list-card-collect"
:style="{ color: '#A5A9BC' }"
@click.stop="addCollect(item)"
/>
</div>
<div class="list-card-attr">
<div
v-for="(attr) in item.attributes"
:key="attr.name"
class="list-card-attr-item"
>
<div class="list-card-attr-item-label">{{ attr.alias || attr.name || '' }}: </div>
<div class="list-card-attr-item-value">
<AttrDisplay
:attr="attr"
:ci="item.ci"
:referenceShowAttrNameMap="referenceShowAttrNameMap"
:referenceCIIdMap="referenceCIIdMap"
:isEllipsis="true"
:searchValue="searchValue"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
import moment from 'moment'
import AttrDisplay from './attrDisplay.vue'
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
export default {
name: 'InstanceList',
components: {
AttrDisplay,
CIIcon
},
props: {
list: {
type: Array,
default: () => []
},
tabList: {
type: Array,
default: () => []
},
referenceShowAttrNameMap: {
type: Object,
default: () => {}
},
referenceCIIdMap: {
type: Object,
default: () => {}
},
favorList: {
type: Array,
default: () => []
},
detailCIId: {
type: [String, Number],
default: -1
},
searchValue: {
type: String,
default: ''
}
},
data() {
return {
currentTab: ''
}
},
computed: {
filterList() {
if (!this.currentTab || this.currentTab === -1) {
return this.list
}
return this.list.filter((item) => item.ciTypeObj.id === this.currentTab)
}
},
watch: {
tabList: {
immediate: true,
deep: true,
handler(newVal) {
this.currentTab = newVal?.[0]?.id ?? ''
}
}
},
methods: {
clickTab(id) {
this.currentTab = id
},
getAttrLabel(attrName, attributes) {
const label = attributes.find((attr) => attr.name === attrName)?.alias
return label || attrName
},
clickInstance(id, ciTypeId) {
this.$emit('showDetail', {
id,
ciTypeId,
})
},
getFavorId(ciId) {
const id = this.favorList.find((item) => item?.option?.CIId === ciId)?.id
return id ?? null
},
addCollect(data) {
this.$emit('addCollect', {
CIId: data.ci._id,
CITypeId: data.ciTypeObj.id,
title: data.ci[data.ciTypeObj.showAttrName],
icon: data.ciTypeObj.icon,
CITypeTitle: data.ciTypeObj.name
})
},
deleteCollect(ciId) {
const favorId = this.getFavorId(ciId)
if (favorId) {
this.$emit('deleteCollect', favorId)
}
},
handleExport() {
const excel_name = `cmdb-${this.$t('cmdb.ciType.resourceSearch')}-${moment().format('YYYYMMDDHHmmss')}.xlsx`
const wb = new ExcelJS.Workbook()
this.tabList.map((sheet) => {
if (sheet.id === -1) {
return
}
const ws = wb.addWorksheet(sheet.title)
this.handleSheetData({
ws,
sheet
})
})
wb.xlsx.writeBuffer().then((buffer) => {
const file = new Blob([buffer], {
type: 'application/octet-stream',
})
FileSaver.saveAs(file, excel_name)
})
},
handleSheetData({
ws,
sheet
}) {
const listData = this.list.filter((item) => item.ciTypeObj.id === sheet.id)
if (!listData.length) {
return
}
const columnMap = new Map()
const columns = listData[0].attributes.filter((attr) => !attr.is_password).map((attr) => {
columnMap.set(attr.name, attr)
return {
header: attr.alias || attr.name || '',
key: attr.name,
width: 20,
}
})
ws.columns = columns
listData.forEach((data) => {
const row = {}
columns.forEach(({ key }) => {
const value = data?.ci?.[key] ?? null
const attr = columnMap.get(key)
if (attr.valueType === '6') {
row[key] = value ? JSON.stringify(value) : value
} else if (attr.is_list && Array.isArray(value)) {
row[key] = value.join(',')
} else {
row[key] = value
}
})
ws.addRow(row)
})
}
}
}
</script>
<style lang="less" scoped>
.list-wrap {
width: 100%;
height: 100%;
flex-shrink: 1 !important;
overflow: hidden;
display: flex;
flex-direction: column;
&-bg {
width: 100%;
padding-top: 90px;
display: flex;
justify-content: center;
img {
width: 300px;
}
}
.list-tab {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
column-gap: 14px;
&-left {
display: flex;
align-items: center;
column-gap: 14px;
row-gap: 7px;
overflow-x: auto;
max-width: 100%;
}
&-label {
font-size: 14px;
font-weight: 400;
color: #4E5969;
flex-shrink: 0;
}
&-item {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 400;
color: #4E5969;
cursor: pointer;
flex-shrink: 0;
&-count {
color: #2F54EB;
}
&-active {
color: #2F54EB;
}
&:hover {
color: #2F54EB;
}
}
&-export {
margin-left: auto;
flex-shrink: 0;
}
}
.list-container {
width: 100%;
margin-top: 12px;
height: 100%;
overflow-y: auto;
flex-shrink: 1;
flex-grow: 0;
.list-card {
width: 100%;
background-color: #FFF;
border-radius: 4px;
padding: 15px;
cursor: pointer;
&:not(:first-child) {
margin-top: 16px;
}
&-selected {
border: 1px solid #7F97FA;
background-color: #F9FBFF;
}
&-header {
display: flex;
align-items: center;
}
&-model {
border-radius: 24px;
border: 1px solid #E4E7ED;
background-color: #FFF;
display: flex;
align-items: center;
justify-content: center;
height: 24px;
padding: 0 13px;
flex-shrink: 0;
&-title {
font-size: 12px;
font-weight: 400;
line-height: 24px;
color: #1D2129;
margin-left: 4px;
}
}
&-title {
margin-left: 11px;
font-size: 14px;
font-weight: 700;
color: #1D2129;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-collect {
font-size: 12px;
margin-left: 9px;
display: none;
}
&-attr {
display: flex;
flex-wrap: wrap;
align-items: center;
overflow: hidden;
height: 25px;
column-gap: 40px;
row-gap: 20px;
margin-top: 12px;
&-item {
flex-shrink: 0;
max-width: calc((100% - 160px) / 5);
display: flex;
align-items: center;
overflow: hidden;
&-label {
color: #86909C;
font-size: 14px;
font-weight: 400;
flex-shrink: 0;
}
&-value {
color: #1D2129;
font-size: 14px;
font-weight: 400;
margin-left: 12px;
overflow: hidden;
}
}
}
&:hover {
box-shadow: 0px 2px 12px 0px rgba(147, 168, 223, 0.20);
.list-card-collect {
display: inline-block;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,201 @@
<template>
<div :class="['search-input', classType ? 'search-input-' + classType : '']">
<a-input
:value="searchValue"
class="search-input-component"
:placeholder="$t('cmdb.ciType.searchInputTip')"
@change="handleChangeSearchValue"
@pressEnter="saveCondition(true)"
>
<a-icon
class="search-input-component-icon"
slot="prefix"
type="search"
@click="saveCondition(true)"
/>
</a-input>
<FilterPopover
ref="filterPpoverRef"
:CITypeGroup="CITypeGroup"
:allAttributesList="allAttributesList"
:expression="expression"
:selectCITypeIds="selectCITypeIds"
@changeFilter="changeFilter"
@updateAllAttributesList="updateAllAttributesList"
@saveCondition="saveCondition"
/>
<div v-if="copyText" class="expression-display">
<span class="expression-display-text">{{ copyText }}</span>
<a-icon
slot="suffix"
type="check-circle"
class="expression-display-icon"
@click="handleCopyExpression"
/>
</div>
</div>
</template>
<script>
import FilterPopover from './filterPopover.vue'
export default {
name: 'SearchInput',
components: {
FilterPopover
},
props: {
searchValue: {
type: String,
default: ''
},
expression: {
type: String,
default: ''
},
selectCITypeIds: {
type: Array,
default: () => []
},
CITypeGroup: {
type: Array,
default: () => []
},
allAttributesList: {
type: Array,
default: () => []
},
classType: {
type: String,
default: ''
}
},
data() {
return {}
},
computed: {
// 复制文字展示与实际文本复制内容区别在于未选择模型时不展示所有模型拼接数据
copyText() {
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(regQ) ? this.expression.match(regQ)[0] : null
const textArray = []
if (this.selectCITypeIds?.length) {
textArray.push(`_type:(${this.selectCITypeIds.join(';')})`)
}
if (exp) {
textArray.push(exp)
}
if (this.searchValue) {
textArray.push(`*${this.searchValue}*`)
}
return textArray.length ? `q=${textArray.join(',')}` : ''
},
},
methods: {
updateAllAttributesList(value) {
this.$emit('updateAllAttributesList', value)
},
saveCondition(isSubmit) {
this.$emit('saveCondition', isSubmit)
},
handleChangeSearchValue(e) {
const value = e.target.value
this.changeFilter({
name: 'searchValue',
value
})
},
changeFilter(data) {
this.$emit('changeFilter', data)
},
handleCopyExpression() {
const { selectCITypeIds, expression, searchValue } = this
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
const ciTypeIds = [...selectCITypeIds]
if (!ciTypeIds.length) {
this.CITypeGroup.forEach((item) => {
const ids = item.ci_types.map((ci_type) => ci_type.id)
ciTypeIds.push(...ids)
})
}
const copyText = `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${searchValue ? `,*${searchValue}*` : ''}`
this.$copyText(copyText)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
}
}
}
</script>
<style lang="less" scoped>
.search-input {
width: 100%;
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
&-component {
height: 100%;
flex-grow: 1;
background-color: #FFFFFF;
border: none;
font-size: 14px;
border-radius: 48px;
overflow: hidden;
&-icon {
color: #2F54EB;
font-size: 14px;
}
/deep/ & > input {
height: 100%;
margin-left: 10px;
border: none;
box-shadow: none;
}
}
&-after {
height: 38px;
justify-content: flex-start;
.search-input-component {
max-width: 524px;
}
}
.expression-display {
display: flex;
align-items: center;
margin-left: 20px;
max-width: 30%;
&-text {
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
}
&-icon {
margin-left: 8px;
color: #00b42a;
cursor: pointer;
}
}
}
</style>

View File

@@ -0,0 +1,625 @@
<template>
<div
class="resource-search"
:style="{ height: `${windowHeight - 131}px` }"
>
<div v-if="!isSearch" class="resource-search-before">
<div class="resource-search-title">
<ops-icon class="resource-search-title-icon" type="veops-resource11" />
<span class="resource-search-title-text">{{ $t('cmdb.ciType.resourceSearch') }}</span>
</div>
<SearchInput
ref="searchInputRef"
:CITypeGroup="CITypeGroup"
:allAttributesList="allAttributesList"
:searchValue="searchValue"
:selectCITypeIds="selectCITypeIds"
:expression="expression"
@changeFilter="changeFilter"
@updateAllAttributesList="updateAllAttributesList"
@saveCondition="saveCondition"
/>
<HistoryList
:recentList="recentList"
:favorList="favorList"
:detailCIId="detailCIId"
@clickRecent="clickRecent"
@deleteRecent="deleteRecent"
@clearRecent="clearRecent"
@deleteCollect="deleteCollect"
@showDetail="clickFavor"
/>
<img class="resource-search-before-bg" :src="require('@/modules/cmdb/assets/resourceSearch/resource_search_bg_1.png')" />
</div>
<div class="resource-search-after" v-else>
<div
class="resource-search-after-left"
:style="{ width: showInstanceDetail ? '70%' : '100%' }"
>
<SearchInput
ref="searchInputRef"
classType="after"
:CITypeGroup="CITypeGroup"
:allAttributesList="allAttributesList"
:searchValue="searchValue"
:selectCITypeIds="selectCITypeIds"
:expression="expression"
@changeFilter="changeFilter"
@updateAllAttributesList="updateAllAttributesList"
@saveCondition="saveCondition"
/>
<HistoryList
:recentList="recentList"
:favorList="favorList"
:detailCIId="detailCIId"
@clickRecent="clickRecent"
@deleteRecent="deleteRecent"
@clearRecent="clearRecent"
@deleteCollect="deleteCollect"
@showDetail="clickFavor"
/>
<div class="resource-search-divider"></div>
<InstanceList
:list="instanceList"
:tabList="ciTabList"
:referenceShowAttrNameMap="referenceShowAttrNameMap"
:referenceCIIdMap="referenceCIIdMap"
:favorList="favorList"
:detailCIId="detailCIId"
:searchValue="currentSearchValue"
@showDetail="showDetail"
@addCollect="addCollect"
@deleteCollect="deleteCollect"
/>
<div class="resource-search-pagination">
<a-pagination
:showSizeChanger="true"
:current="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@showSizeChange="handlePageSizeChange"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="changePage"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('all') }}</span>
</template>
</a-pagination>
</div>
</div>
<div v-if="showInstanceDetail" class="resource-search-after-right">
<InstanceDetail
:CIId="detailCIId"
:CITypeId="detailCITypeId"
:favorList="favorList"
@addCollect="addCollect"
@deleteCollect="deleteCollect"
@hideDetail="hideDetail"
/>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { mapState } from 'vuex'
import { getPreferenceSearch, savePreferenceSearch, getSubscribeAttributes, deletePreferenceSearch } from '@/modules/cmdb/api/preference'
import { searchAttributes, getCITypeAttributesByTypeIds } from '@/modules/cmdb/api/CITypeAttr'
import { searchCI } from '@/modules/cmdb/api/ci'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import SearchInput from './components/searchInput.vue'
import HistoryList from './components/historyList.vue'
import InstanceList from './components/instanceList.vue'
import InstanceDetail from './components/instanceDetail.vue'
export default {
name: 'ResourceSearchCom',
components: {
SearchInput,
HistoryList,
InstanceList,
InstanceDetail
},
props: {
CITypeGroup: {
type: Array,
default: () => []
},
allCITypes: {
type: Array,
default: () => []
}
},
data() {
return {
// 筛选条件
searchValue: '', // 搜索框
selectCITypeIds: [], // 已选模型
expression: '', // 筛选语句
currentSearchValue: '', // 当前已搜索语句
recentList: [], // 最近搜索
favorList: [], // 我的收藏
allAttributesList: [],
isSearch: false, // 是否搜索过
currentPage: 1,
pageSizeOptions: ['50', '100', '200', '100000'],
pageSize: 50,
totalNumber: 0,
ciTabList: [],
instanceList: [],
referenceShowAttrNameMap: {},
referenceCIIdMap: {},
showInstanceDetail: false,
detailCIId: -1,
detailCITypeId: -1,
}
},
computed: {
...mapState({
cmdbSearchValue: (state) => state.app.cmdbSearchValue,
}),
windowHeight() {
return this.$store.state.windowHeight
},
},
watch: {
cmdbSearchValue: {
handler(value) {
this.searchValue = value
this.saveCondition(true)
}
}
},
mounted() {
this.initData()
},
methods: {
async initData() {
await this.getFavorList()
this.$nextTick(async () => {
if (this.cmdbSearchValue) {
this.searchValue = this.cmdbSearchValue
this.saveCondition(true)
} else {
await this.getRecentList()
}
})
await this.getAllAttr()
},
async getRecentList() {
const recentList = await getPreferenceSearch({
name: '__recent__'
})
recentList.sort((a, b) => b.id - a.id)
this.recentList = recentList
},
async getFavorList() {
const favorList = await getPreferenceSearch({
name: '__favor__'
})
favorList.sort((a, b) => b.id - a.id)
this.favorList = favorList
},
async getAllAttr() {
const res = await searchAttributes({ page_size: 9999 })
this.allAttributesList = res.attributes
this.originAllAttributesList = res.attributes
},
async updateAllAttributesList(value) {
if (value && value.length) {
const res = await getCITypeAttributesByTypeIds({ type_ids: value.join(',') })
this.allAttributesList = res.attributes
} else {
this.allAttributesList = this.originAllAttributesList
}
},
async saveCondition(isSubmit) {
if (
this.searchValue ||
this.expression ||
this.selectCITypeIds.length
) {
const needDeleteList = []
const differentList = []
this.recentList.forEach((item) => {
const option = item.option
if (
option.searchValue === this.searchValue &&
option.expression === this.expression &&
_.isEqual(option.ciTypeIds, this.selectCITypeIds)
) {
needDeleteList.push(item.id)
} else {
differentList.push(item.id)
}
})
if (differentList.length >= 10) {
needDeleteList.push(...differentList.slice(9))
}
if (needDeleteList.length) {
await Promise.all(
needDeleteList.map((id) => deletePreferenceSearch(id))
)
}
const ciTypeNames = this.selectCITypeIds.map((id) => {
const ciType = this.allCITypes.find((item) => item.id === id)
return ciType?.alias || ciType?.name || id
})
await savePreferenceSearch({
option: {
searchValue: this.searchValue,
expression: this.expression,
ciTypeIds: this.selectCITypeIds,
ciTypeNames
},
name: '__recent__'
})
this.getRecentList()
}
if (isSubmit) {
this.isSearch = true
this.currentPage = 1
this.hideDetail()
this.loadInstance()
}
},
async deleteRecent(id) {
await deletePreferenceSearch(id)
this.getRecentList()
},
async clearRecent() {
const deletePromises = this.recentList.map((item) => {
return deletePreferenceSearch(item.id)
})
await Promise.all(deletePromises)
this.getRecentList()
},
async loadInstance() {
const { selectCITypeIds, expression, searchValue } = this
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
const ciTypeIds = [...selectCITypeIds]
if (!ciTypeIds.length) {
this.CITypeGroup.forEach((item) => {
const ids = item.ci_types.map((ci_type) => ci_type.id)
ciTypeIds.push(...ids)
})
}
const res = await searchCI({
q: `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${
searchValue ? `,*${searchValue}*` : ''
}`,
count: this.pageSize,
page: this.currentPage,
sort: '_type'
})
this.currentSearchValue = searchValue
this.totalNumber = res?.numfound ?? 0
if (!res?.result?.length) {
this.ciTabList = []
this.instanceList = []
}
const ciTabMap = new Map()
let list = res.result
list.forEach((item) => {
const ciType = this.allCITypes.find((type) => type.id === item._type)
if (ciTabMap.has(item._type)) {
ciTabMap.get(item._type).count++
} else {
ciTabMap.set(item._type, {
id: item._type,
count: 1,
title: ciType?.alias || ciType?.name || '',
})
}
})
const mapEntries = [...ciTabMap.entries()]
const subscribedPromises = mapEntries.map((item) => {
return getSubscribeAttributes(item[0])
})
const subscribedRes = await Promise.all(subscribedPromises)
list = list.map((item) => {
const subscribedIndex = mapEntries.findIndex((mapValue) => mapValue[0] === item._type)
const subscribedAttr = subscribedRes?.[subscribedIndex]?.attributes || []
const obj = {
ci: item,
ciTypeObj: {},
attributes: subscribedAttr
}
const ciType = this.allCITypes.find((type) => type.id === item._type)
obj.ciTypeObj = {
showAttrName: ciType?.show_name || ciType?.unique_key || '',
icon: ciType?.icon || '',
title: ciType?.alias || ciType?.name || '',
name: ciType?.name || '',
id: ciType.id
}
return obj
})
this.instanceList = list
const ciTabList = [...ciTabMap.values()]
if (list?.length) {
ciTabList.unshift({
id: -1,
title: this.$t('all'),
count: list?.length
})
}
this.ciTabList = ciTabList
// 处理引用属性
const allAttr = []
subscribedRes.map((item) => {
allAttr.push(...item.attributes)
})
this.handlePerference(_.uniqBy(allAttr, 'id'))
},
handlePerference(allAttr) {
let needRequiredCIType = []
allAttr.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
needRequiredCIType.push(attr)
}
})
needRequiredCIType = _.uniq(needRequiredCIType, 'id')
if (!needRequiredCIType.length) {
this.referenceShowAttrNameMap = {}
this.referenceCIIdMap = {}
return
}
this.handleReferenceShowAttrName(needRequiredCIType)
this.handleReferenceCIIdMap(needRequiredCIType)
},
async handleReferenceShowAttrName(needRequiredCIType) {
const res = await getCITypes({
type_ids: needRequiredCIType.map((col) => col.reference_type_id).join(',')
})
const map = {}
res.ci_types.forEach((ciType) => {
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
this.referenceShowAttrNameMap = map
},
async handleReferenceCIIdMap(needRequiredCIType) {
const map = {}
this.instanceList.forEach(({ ci }) => {
needRequiredCIType.forEach((col) => {
const ids = Array.isArray(ci[col.name]) ? ci[col.name] : ci[col.name] ? [ci[col.name]] : []
if (ids.length) {
if (!map?.[col.reference_type_id]) {
map[col.reference_type_id] = {}
}
ids.forEach((id) => {
map[col.reference_type_id][id] = {}
})
}
})
})
if (!Object.keys(map).length) {
this.referenceCIIdMap = {}
return
}
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
allRes.forEach((res) => {
res.result.forEach((item) => {
if (map?.[item._type]?.[item._id]) {
map[item._type][item._id] = item
}
})
})
this.referenceCIIdMap = map
},
clickRecent(data) {
this.updateAllAttributesList(data.ciTypeIds || [])
this.isSearch = true
this.currentPage = 1
this.searchValue = data?.searchValue || ''
this.expression = data?.expression || ''
this.selectCITypeIds = data?.ciTypeIds || []
this.hideDetail()
this.loadInstance()
},
handlePageSizeChange(_, pageSize) {
this.pageSize = pageSize
this.currentPage = 1
this.loadInstance()
},
changePage(page) {
this.currentPage = page
this.loadInstance()
},
changeFilter(data) {
this[data.name] = data.value
},
showDetail(data) {
this.detailCIId = data.id
this.detailCITypeId = data.ciTypeId
this.showInstanceDetail = true
},
hideDetail() {
this.detailCIId = -1
this.detailCITypeId = -1
this.showInstanceDetail = false
},
async addCollect(data) {
if (this?.favorList?.length >= 10) {
const deletePromises = this.favorList.slice(9).map((item) => {
return deletePreferenceSearch(item.id)
})
await Promise.all(deletePromises)
}
await savePreferenceSearch({
option: {
...data
},
name: '__favor__'
})
this.getFavorList()
},
async deleteCollect(id) {
await deletePreferenceSearch(id)
this.getFavorList()
},
clickFavor(data) {
this.isSearch = true
this.showDetail(data)
}
}
}
</script>
<style lang="less" scoped>
.resource-search {
width: 100%;
height: 100%;
position: relative;
&-before {
width: 100%;
max-width: 725px;
height: 100%;
margin: 0 auto;
padding-top: 100px;
display: flex;
flex-direction: column;
align-items: center;
& > div {
position: relative;
z-index: 1;
}
&-bg {
position: absolute;
left: -24px;
bottom: -24px;
width: calc(100% + 48px);
z-index: 0;
}
}
&-title {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 25px;
&-icon {
font-size: 28px;
}
&-text {
margin-left: 10px;
font-size: 20px;
font-weight: 700;
color: #1D2129;
}
}
&-after {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
&-left {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
& > div {
flex-shrink: 0;
}
}
&-right {
margin-left: 20px;
width: calc(30% - 20px);
flex-shrink: 0;
}
}
&-divider {
width: 100%;
height: 1px;
background-color: #E4E7ED;
margin: 20px 0;
}
&-pagination {
text-align: right;
margin: 12px 0px;
}
}
</style>

View File

@@ -15,6 +15,7 @@
:paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-topo-views"
:triggerLength="18"
calcBasedParent
>
<template #one>
<a-input

View File

@@ -10,6 +10,7 @@
:paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-tree-views"
:triggerLength="18"
calcBasedParent
>
<template #one>
<div class="tree-views-left" :style="{ height: `${windowHeight - 64}px` }">

View File

@@ -25,7 +25,7 @@ const app = {
color: null,
weak: false,
multiTab: false,
cmdbSearchValue: '',
},
mutations: {
SET_SIDEBAR_TYPE: (state, type) => {
@@ -76,7 +76,9 @@ const app = {
Vue.ls.set(DEFAULT_MULTI_TAB, bool)
state.multiTab = bool
},
UPDATE_CMDB_SEARCH_VALUE: (state, value) => {
state.cmdbSearchValue = value
}
},
actions: {
setSidebar({ commit }, type) {
@@ -118,6 +120,9 @@ const app = {
ToggleMultiTab({ commit }, bool) {
commit('TOGGLE_MULTI_TAB', bool)
},
UpdateCMDBSEarchValue({ commit }, value) {
commit('UPDATE_CMDB_SEARCH_VALUE', value)
}
}
}

View File

@@ -0,0 +1,11 @@
const company = {
state: {
name: undefined
},
mutations: {
SET_COMPANY_NAME: (state, name) => {
state.name = name
},
},
}
export default company

View File

@@ -46,7 +46,8 @@ const user = {
sex: '',
position_name: '',
direct_supervisor_id: null,
auth_enable: {}
auth_enable: {},
notice_info: {}
},
mutations: {
@@ -54,7 +55,7 @@ const user = {
state.token = token
},
SET_USER_INFO: (state, { name, welcome, avatar, roles, info, uid, rid, username, mobile, department_id, employee_id, email, nickname, sex, position_name, direct_supervisor_id, annual_leave }) => {
SET_USER_INFO: (state, { name, welcome, avatar, roles, info, uid, rid, username, mobile, department_id, employee_id, email, nickname, sex, position_name, direct_supervisor_id, annual_leave, notice_info }) => {
state.name = name
state.welcome = welcome
state.avatar = avatar
@@ -73,6 +74,7 @@ const user = {
state.position_name = position_name
state.direct_supervisor_id = direct_supervisor_id
state.annual_leave = annual_leave
state.notice_info = notice_info
},
LOAD_ALL_USERS: (state, users) => {
@@ -160,7 +162,8 @@ const user = {
sex: res.sex,
position_name: res.position_name,
direct_supervisor_id: res.direct_supervisor_id,
annual_leave: res.annual_leave
annual_leave: res.annual_leave,
notice_info: res.notice_info
})
})
}).catch(error => {

View File

@@ -7,6 +7,7 @@ import user from './global/user'
import routes from './global/routes'
import notice from './global/notice'
import getters from './global/getters'
import company from './global/company'
import appConfig from '@/config/app'
Vue.use(Vuex)
@@ -16,7 +17,8 @@ const store = new Vuex.Store({
app,
user,
routes,
notice
notice,
company
},
state: {
windowWidth: 800,

View File

@@ -860,20 +860,30 @@ body {
.vue-treeselect__control {
border-radius: 2px !important;
height: 32px;
border-color: #e4e7ed;
.vue-treeselect__value-container{
height: 30px;
}
.vue-treeselect__input-container{
display: flex;
align-items: center;
height: 30px;
}
}
.vue-treeselect__placeholder,
.vue-treeselect__single-value {
line-height: 32px !important;
line-height: 28px !important;
}
.vue-treeselect__input {
height: 32px !important;
line-height: 32px !important;
height: 28px !important;
line-height: 28px !important;
}
}
// vue-treeselect 多选样式
.ops-setting-treeselect.vue-treeselect--multi {
.vue-treeselect__control {
border-radius: 2px !important;
border-color: #e4e7ed;
}
}

View File

@@ -47,6 +47,7 @@
</template>
<script>
import { mapMutations } from 'vuex'
import { getCompanyInfo, postCompanyInfo, putCompanyInfo } from '@/api/company'
import SpanTitle from '../components/spanTitle.vue'
import { mixinPermissions } from '@/utils/mixin'
@@ -82,6 +83,7 @@ export default {
} else {
this.infoData = res.info
this.getId = res.id
this.SET_COMPANY_NAME(res?.info?.name || '')
}
},
computed: {
@@ -122,6 +124,7 @@ export default {
},
},
methods: {
...mapMutations(['SET_COMPANY_NAME']),
async onSubmit() {
this.$refs.infoData.validate(async (valid) => {
if (valid) {
@@ -130,6 +133,7 @@ export default {
} else {
await putCompanyInfo(this.getId, this.infoData)
}
this.SET_COMPANY_NAME(this.infoData.name || '')
this.$message.success(this.$t('saveSuccess'))
} else {
this.$message.warning(this.$t('cs.companyInfo.checkInputCorrect'))

View File

@@ -1,393 +0,0 @@
<template>
<a-modal
:visible="visible"
:title="$t('cs.companyStructure.batchImport')"
dialogClass="ops-modal setting-structure-upload"
:width="800"
@cancel="close"
>
<div class="setting-structure-upload-steps">
<div
:class="{ 'setting-structure-upload-step': true, selected: index + 1 <= currentStep }"
v-for="(step, index) in stepList"
:key="step.value"
>
<div :class="{ 'setting-structure-upload-step-icon': true }">
<ops-icon :type="step.icon" />
</div>
<span>{{ step.label }}</span>
</div>
</div>
<template v-if="currentStep === 1">
<a-upload :multiple="false" :customRequest="customRequest" accept=".xlsx" :showUploadList="false">
<a-button :style="{ marginBottom: '20px' }" type="primary"> <a-icon type="upload" />{{ $t('cs.companyStructure.selectFile') }}</a-button>
</a-upload>
<p><a @click="download">{{ $t('cs.companyStructure.clickDownloadImportTemplate') }}</a></p>
</template>
<div
:style="{
height: '60px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
whiteSpace: 'pre-wrap',
}"
v-if="currentStep === 3"
>
{{ $t('cs.companyStructure.importSuccess', { allCount: allCount })
}}<span :style="{ color: '#2362FB' }"> {{ allCount - errorCount }} </span>{{ $t('cs.companyStructure.count') }},
{{ $t('cs.companyStructure.importFailed') }}<span :style="{ color: '#D81E06' }"> {{ errorCount }} </span
>{{ $t('cs.companyStructure.count') }}
</div>
<vxe-table
v-if="currentStep === 2 || has_error"
ref="employeeTable"
stripe
:data="importData"
show-overflow
show-header-overflow
highlight-hover-row
size="small"
class="ops-stripe-table"
:max-height="400"
:column-config="{ resizable: true }"
>
<vxe-column field="email" :title="$t('cs.companyStructure.email')" min-width="120" fixed="left"></vxe-column>
<vxe-column field="username" :title="$t('cs.companyStructure.username')" min-width="80" ></vxe-column>
<vxe-column field="nickname" :title="$t('cs.companyStructure.nickname')" min-width="80"></vxe-column>
<vxe-column field="password" :title="$t('cs.companyStructure.password')" min-width="80"></vxe-column>
<vxe-column field="sex" :title="$t('cs.companyStructure.sex')" min-width="60"></vxe-column>
<vxe-column field="mobile" :title="$t('cs.companyStructure.mobile')" min-width="80"></vxe-column>
<vxe-column field="position_name" :title="$t('cs.companyStructure.positionName')" min-width="80"></vxe-column>
<vxe-column field="department_name" :title="$t('cs.companyStructure.departmentName')" min-width="80"></vxe-column>
<vxe-column v-if="has_error" field="err" :title="$t('cs.companyStructure.importFailedReason')" min-width="120" fixed="right">
<template #default="{ row }">
<span :style="{ color: '#D81E06' }">{{ row.err }}</span>
</template>
</vxe-column>
</vxe-table>
<a-space slot="footer">
<a-button size="small" type="primary" ghost @click="close">{{ $t('cancel') }}</a-button>
<a-button v-if="currentStep !== 1" size="small" type="primary" ghost @click="goPre">{{ $t('cs.companyStructure.prevStep') }}</a-button>
<a-button v-if="currentStep !== 3" size="small" type="primary" @click="goNext">{{ $t('cs.companyStructure.nextStep') }}</a-button>
<a-button v-else size="small" type="primary" @click="close">{{ $t('cs.companyStructure.done') }}</a-button>
</a-space>
</a-modal>
</template>
<script>
import { downloadExcel, excel2Array } from '@/utils/download'
import { importEmployee } from '@/api/employee'
export default {
name: 'BatchUpload',
data() {
const stepList = [
{
value: 1,
label: this.$t('cs.companyStructure.uploadFile'),
icon: 'icon-shidi-tianjia',
},
{
value: 2,
label: this.$t('cs.companyStructure.confirmData'),
icon: 'icon-shidi-yunshangchuan',
},
{
value: 3,
label: this.$t('cs.companyStructure.uploadDone'),
icon: 'icon-shidi-queren',
},
]
const common_importParamsList = [
'email',
'username',
'nickname',
'password',
'sex',
'mobile',
'position_name',
'department_name',
'entry_date',
'is_internship',
'leave_date',
'id_card',
'nation',
'id_place',
'party',
'household_registration_type',
'hometown',
'marry',
'max_degree',
'emergency_person',
'emergency_phone',
'bank_card_number',
'bank_card_name',
'opening_bank',
'account_opening_location',
'school',
'major',
'education',
'graduation_year',
]
return {
stepList,
common_importParamsList,
visible: false,
currentStep: 1,
importData: [],
has_error: false,
allCount: 0,
errorCount: 0,
}
},
methods: {
open() {
this.importData = []
this.has_error = false
this.errorCount = 0
this.visible = true
},
close() {
this.currentStep = 1
this.visible = false
},
async goNext() {
if (this.currentStep === 2) {
// 此处调用后端接口
this.allCount = this.importData.length
const importData = this.importData.map((item) => {
const { _X_ROW_KEY, ...rest } = item
const keyArr = Object.keys(rest)
keyArr.forEach((key) => {
if (rest[key]) {
rest[key] = rest[key] + ''
}
})
rest.educational_experience = [
{
school: rest.school,
major: rest.major,
education: rest.education,
graduation_year: rest.graduation_year,
},
]
delete rest.school
delete rest.major
delete rest.education
delete rest.graduation_year
return rest
})
const res = await importEmployee({ employee_list: importData })
if (res.length) {
const errData = res.filter((item) => {
return item.err.length
})
console.log('err', errData)
this.has_error = true
this.errorCount = errData.length
this.currentStep += 1
this.importData = errData
this.$message.error(this.$t('cs.companyStructure.dataErr'))
} else {
this.currentStep += 1
this.$message.success(this.$t('cs.companyStructure.opSuccess'))
}
this.$emit('refresh')
}
},
goPre() {
this.has_error = false
this.errorCount = 0
this.currentStep -= 1
},
download() {
const data = [
[
{
v: '1、表头标“*”的红色字体为必填项\n2、邮箱、用户名不允许重复\n3、登录密码密码由6-20位字母、数字组成\n4、部门上下级部门间用"/"隔开,且从最上级部门开始,例如“深圳分公司/IT部/IT二部”。如出现相同的部门则默认导入组织架构中顺序靠前的部门',
t: 's',
s: {
alignment: {
wrapText: true,
vertical: 'center',
},
},
},
],
[
{
v: '*邮箱',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*用户名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*姓名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*密码',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '性别',
t: 's',
},
{
v: '手机号',
t: 's',
},
{
v: '岗位',
t: 's',
},
{
v: '部门',
t: 's',
},
],
]
data[1] = data[1].filter((item) => item['v'] !== '目前所属主体')
data[1] = data[1].filter((item) => item['v'] !== '初始入职日期')
downloadExcel(data, this.$t('cs.companyStructure.downloadTemplateName'))
},
customRequest(data) {
this.fileList = [data.file]
excel2Array(data.file).then((res) => {
res = res.filter((item) => item.length)
this.importData = res.slice(2).map((item) => {
const obj = {}
// 格式化日期字段
item[8] = this.formatDate(item[8]) // 目前主体入职日期
item[10] = this.formatDate(item[10]) // 离职日期
item[28] = this.formatDate(item[28]) // 毕业年份
item.forEach((ele, index) => {
obj[this.common_importParamsList[index]] = ele
})
return obj
})
this.currentStep = 2
})
},
formatDate(numb) {
if (numb) {
const time = new Date((numb - 1) * 24 * 3600000 + 1)
time.setYear(time.getFullYear() - 70)
time.setMonth(time.getMonth())
time.setHours(time.getHours() - 8)
time.setMinutes(time.getMinutes())
time.setMilliseconds(time.getMilliseconds())
// return time.valueOf()
// 日期格式
const format = 'Y-m-d'
const year = time.getFullYear()
// 由于 getMonth 返回值会比正常月份小 1
let month = time.getMonth() + 1
let day = time.getDate()
month = month > 9 ? month : `0${month}`
day = day > 9 ? day : `0${day}`
const hash = {
Y: year,
m: month,
d: day,
}
return format.replace(/\w/g, (o) => {
return hash[o]
})
} else {
return null
}
},
},
}
</script>
<style lang="less">
.setting-structure-upload {
.ant-modal-body {
padding: 24px 48px;
}
.setting-structure-upload-steps {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 20px;
.setting-structure-upload-step {
display: inline-block;
text-align: center;
position: relative;
.setting-structure-upload-step-icon {
width: 86px;
height: 86px;
display: flex;
align-items: center;
justify-content: center;
background-image: url('../../../assets/icon-bg.png');
margin-bottom: 20px;
> i {
font-size: 40px;
color: #fff;
}
}
> span {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.5);
}
}
.setting-structure-upload-step:not(:first-child)::before {
content: '';
height: 2px;
width: 223px;
position: absolute;
background-color: #e7ecf3;
left: -223px;
top: 43px;
z-index: 0;
}
.selected.setting-structure-upload-step {
&:not(:first-child)::before {
background-color: #7eb0ff;
}
}
.selected {
.setting-structure-upload-step-icon {
background-image: url('../../../assets/icon-bg-selected.png');
}
> span {
color: rgba(0, 0, 0, 0.8);
}
}
}
}
</style>

View File

@@ -10,10 +10,22 @@
:body-style="{ height: `${windowHeight - 320}px`, overflow: 'hidden', overflowY: 'scroll' }"
>
<a-form-model ref="employeeFormData" :model="employeeFormData" :rules="rules" :colon="false">
<a-form-model-item ref="email" :label="$t('cs.companyStructure.email')" prop="email" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='email')!==-1">
<a-input v-model="employeeFormData.email" :placeholder="$t('cs.companyStructure.emailPlaceholder')"/>
<a-form-model-item
ref="email"
:label="$t('cs.companyStructure.email')"
prop="email"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'email') !== -1"
>
<a-input v-model="employeeFormData.email" :placeholder="$t('cs.companyStructure.emailPlaceholder')" />
</a-form-model-item>
<a-form-model-item ref="username" :label="$t('cs.companyStructure.username')" prop="username" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='username')!==-1">
<a-form-model-item
ref="username"
:label="$t('cs.companyStructure.username')"
prop="username"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'username') !== -1"
>
<a-input v-model="employeeFormData.username" :placeholder="$t('cs.companyStructure.usernamePlaceholder')" />
</a-form-model-item>
<a-form-model-item
@@ -23,31 +35,82 @@
prop="password"
:style="formModalItemStyle"
>
<a-input-password v-model="employeeFormData.password" :placeholder="$t('cs.companyStructure.passwordPlaceholder')" />
<a-input-password
v-model="employeeFormData.password"
:placeholder="$t('cs.companyStructure.passwordPlaceholder')"
/>
</a-form-model-item>
<a-form-model-item ref="nickname" :label="$t('cs.companyStructure.nickname')" prop="nickname" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='nickname')!==-1">
<a-form-model-item
ref="nickname"
:label="$t('cs.companyStructure.nickname')"
prop="nickname"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'nickname') !== -1"
>
<a-input v-model="employeeFormData.nickname" :placeholder="$t('cs.companyStructure.nicknamePlaceholder')" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.companyStructure.sex')" prop="sex" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='sex')!==-1">
<a-form-model-item
:label="$t('cs.companyStructure.sex')"
prop="sex"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'sex') !== -1"
>
<a-select v-model="employeeFormData.sex" :placeholder="$t('cs.companyStructure.sexPlaceholder')">
<a-select-option value=""> {{ $t('cs.companyStructure.male') }} </a-select-option>
<a-select-option value=""> {{ $t('cs.companyStructure.female') }} </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item ref="mobile" :label="$t('cs.companyStructure.mobile')" prop="mobile" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='mobile')!==-1">
<a-form-model-item
ref="mobile"
:label="$t('cs.companyStructure.mobile')"
prop="mobile"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'mobile') !== -1"
>
<a-input v-model="employeeFormData.mobile" :placeholder="$t('cs.companyStructure.mobilePlaceholder')" />
</a-form-model-item>
<div :style="{ width: '361px', display: 'inline-block', margin: '0 7px' }" v-if="attributes.findIndex(v=>v=='department_id')!==-1">
<div :style="{ height: '41px', lineHeight: '40px' }">{{ $t('cs.companyStructure.departmentName') }}</div>
<DepartmentTreeSelect v-model="employeeFormData.department_id" />
</div>
<a-form-model-item ref="position_name" :label="$t('cs.companyStructure.positionName')" prop="position_name" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='position_name')!==-1">
<a-input v-model="employeeFormData.position_name" :placeholder="$t('cs.companyStructure.positionNamePlaceholder')" />
<a-form-model-item
ref="department_id"
:label="$t('cs.companyStructure.departmentName')"
prop="department_id"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'department_id') !== -1"
>
<DepartmentTreeSelect style="margin-top: 4px" v-model="employeeFormData.department_id" />
</a-form-model-item>
<a-form-model-item
ref="position_name"
:label="$t('cs.companyStructure.positionName')"
prop="position_name"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'position_name') !== -1"
>
<a-input
v-model="employeeFormData.position_name"
:placeholder="$t('cs.companyStructure.positionNamePlaceholder')"
/>
</a-form-model-item>
<a-form-model-item
ref="direct_supervisor_id"
:label="$t('cs.companyStructure.selectDirectSupervisor')"
prop="direct_supervisor_id"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'direct_supervisor_id') !== -1"
>
<EmployeeTreeSelect style="margin-top: 4px" v-model="employeeFormData.direct_supervisor_id" />
</a-form-model-item>
<a-form-model-item
ref="work_region"
:label="$t('cs.companyStructure.work_region')"
prop="work_region"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'work_region') !== -1"
>
<a-select v-model="employeeFormData.work_region" :placeholder="$t('cs.companyStructure.workRegionPlaceholder')">
<a-select-option value="china_mainland"> {{ $t('cs.companyStructure.china_mainland') }} </a-select-option>
<a-select-option value="china_hk"> {{ $t('cs.companyStructure.china_hk') }} </a-select-option>
</a-select>
</a-form-model-item>
<div :style="{ width: '361px', display: 'inline-block', margin: '0 7px' }" v-if="attributes.findIndex(v=>v=='direct_supervisor_id')!==-1">
<div :style="{ height: '41px', lineHeight: '40px' }">{{ $t('cs.companyStructure.selectDirectSupervisor') }}</div>
<EmployeeTreeSelect v-model="employeeFormData.direct_supervisor_id" />
</div>
</a-form-model>
<template slot="footer">
<a-button key="back" @click="close"> {{ $t('cancel') }} </a-button>
@@ -75,7 +138,7 @@ export default {
educational_experience: [],
children_information: [],
file_is_show: true,
attributes: []
attributes: [],
}
},
created() {
@@ -85,7 +148,7 @@ export default {
},
inject: ['provide_allTreeDepartment', 'provide_allFlatEmployees'],
computed: {
...mapState({
...mapState({
windowHeight: (state) => state.windowHeight,
}),
departemntTreeSelectOption() {
@@ -103,7 +166,12 @@ export default {
rules() {
return {
email: [
{ required: true, whitespace: true, message: this.$t('cs.companyStructure.emailPlaceholder'), trigger: 'blur' },
{
required: true,
whitespace: true,
message: this.$t('cs.companyStructure.emailPlaceholder'),
trigger: 'blur',
},
{
pattern: /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/,
message: this.$t('cs.companyStructure.emailFormatErr'),
@@ -112,12 +180,29 @@ export default {
{ max: 50, message: this.$t('cs.person.inputStrCountLimit', { limit: 50 }) },
],
username: [
{ required: true, whitespace: true, message: this.$t('cs.companyStructure.usernamePlaceholder'), trigger: 'blur' },
{
required: true,
whitespace: true,
message: this.$t('cs.companyStructure.usernamePlaceholder'),
trigger: 'blur',
},
{ max: 20, message: this.$t('cs.person.inputStrCountLimit', { limit: 20 }) },
],
password: [{ required: true, whitespace: true, message: this.$t('cs.companyStructure.passwordPlaceholder'), trigger: 'blur' }],
password: [
{
required: true,
whitespace: true,
message: this.$t('cs.companyStructure.passwordPlaceholder'),
trigger: 'blur',
},
],
nickname: [
{ required: true, whitespace: true, message: this.$t('cs.companyStructure.nicknamePlaceholder'), trigger: 'blur' },
{
required: true,
whitespace: true,
message: this.$t('cs.companyStructure.nicknamePlaceholder'),
trigger: 'blur',
},
{ max: 20, message: this.$t('cs.person.inputStrCountLimit', { limit: 20 }) },
],
mobile: [
@@ -128,7 +213,7 @@ export default {
},
],
}
}
},
},
beforeDestroy() {
Bus.$off('getAttributes')
@@ -136,7 +221,8 @@ export default {
methods: {
async open(getData, type) {
// 提交时去掉school, major, education, graduation_year, name, gender, birthday, parental_leave_left
const { school, major, education, graduation_year, name, gender, birthday, parental_leave_left, ...newGetData } = getData
const { school, major, education, graduation_year, name, gender, birthday, parental_leave_left, ...newGetData } =
getData
const _getData = _.cloneDeep(newGetData)
const { direct_supervisor_id } = newGetData
if (direct_supervisor_id) {
@@ -149,46 +235,54 @@ export default {
// if (type !== 'add' && this.employeeFormData.educational_experience.length !== 0) {
// this.educational_experience = this.employeeFormData.educational_experience
// }
this.children_information = this.formatChildrenInformationList() || [{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0
}]
this.educational_experience = this.formatEducationalExperienceList() || [{
id: uuidv4(),
school: '',
major: '',
education: undefined,
graduation_year: null
}]
this.children_information = this.formatChildrenInformationList() || [
{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0,
},
]
this.educational_experience = this.formatEducationalExperienceList() || [
{
id: uuidv4(),
school: '',
major: '',
education: undefined,
graduation_year: null,
},
]
this.type = type
this.visible = true
},
close() {
this.$refs.employeeFormData.resetFields()
this.educational_experience = [{
school: '',
major: '',
education: undefined,
graduation_year: null
}]
this.children_information = [{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0
}]
this.educational_experience = [
{
school: '',
major: '',
education: undefined,
graduation_year: null,
},
]
this.children_information = [
{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0,
},
]
this.visible = false
},
formatChildrenInformationList() {
let arr = []
arr = this.employeeFormData.children_information ? this.employeeFormData.children_information : undefined
if (arr && arr.length) {
arr.forEach(item => {
arr.forEach((item) => {
item.id = uuidv4()
})
return arr
@@ -199,7 +293,7 @@ export default {
let arr = []
arr = this.employeeFormData.educational_experience ? this.employeeFormData.educational_experience : undefined
if (arr && arr.length) {
arr.forEach(item => {
arr.forEach((item) => {
item.id = uuidv4()
})
return arr
@@ -212,12 +306,12 @@ export default {
school: '',
major: '',
education: undefined,
graduation_year: null
graduation_year: null,
}
this.educational_experience.push(newEducational_experience)
},
removeEducation(removeId) {
const _idx = this.educational_experience.findIndex(item => item.id === removeId)
const _idx = this.educational_experience.findIndex((item) => item.id === removeId)
if (_idx !== -1) {
this.educational_experience.splice(_idx, 1)
}
@@ -228,12 +322,12 @@ export default {
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0
parental_leave_left: 0,
}
this.children_information.push(newChildrenInfo)
},
removeChildren(removeId) {
const _idx = this.children_information.findIndex(item => item.id === removeId)
const _idx = this.children_information.findIndex((item) => item.id === removeId)
if (_idx !== -1) {
this.children_information.splice(_idx, 1)
}
@@ -254,10 +348,10 @@ export default {
// }
if (date !== null) {
if (param === 'graduation_year') {
const _idx = this.educational_experience.findIndex(item => item.id === id)
const _idx = this.educational_experience.findIndex((item) => item.id === id)
this.educational_experience[_idx].graduation_year = moment(date).format('YYYY-MM')
} else if (param === 'birthday') {
const _idx = this.children_information.findIndex(item => item.id === id)
const _idx = this.children_information.findIndex((item) => item.id === id)
this.children_information[_idx].birthday = moment(date).format('YYYY-MM-DD')
} else {
this.employeeFormData[param] = moment(date).format('YYYY-MM-DD')
@@ -303,7 +397,7 @@ export default {
</script>
<style lang="less" scoped>
.el-date-picker {
width: 100%;
height: 36px;
width: 100%;
height: 36px;
}
</style>

View File

@@ -16,17 +16,17 @@
:key="group.id"
>
<div
class="ops-setting-structure-sidebar-group-header"
:class="{ 'group-selected': groupIndex === activeGroupIndex }"
:class="{
'ops-setting-structure-sidebar-group-header': true,
'group-selected': groupIndex === activeGroupIndex,
}"
>
<div class="ops-setting-structure-sidebar-group-header-avatar">
<a-icon :type="group.icon"/>
<a-icon :type="group.icon" />
</div>
<span
class="ops-setting-structure-sidebar-group-header-title"
@click="
clickSelectGroup(groupIndex)
"
@click="clickSelectGroup(groupIndex)"
:id="[group.id === 0 ? 'employee' : 'department']"
>
{{ group.title }}
@@ -100,7 +100,7 @@
/>
</div>
<!-- 筛选框 -->
<div class="Screening-box" v-if="activeGroupIndex === 1" style="background-color: rgb(240, 245, 255) ;">
<div class="Screening-box" v-if="activeGroupIndex === 1" style="background-color: rgb(240, 245, 255)">
<a-popover
@visibleChange="visibleChange"
trigger="click"
@@ -122,16 +122,17 @@
</div>
</template>
<span :style="{ whiteSpace: 'nowrap' }">
<a-icon class="screening-box-scene-icon" type="filter"/>
<a-icon class="screening-box-scene-icon" type="filter" />
{{ getCurrentSceneLabel() }}
<a-icon class="screening-box-scene-icon" :type="displayTimeIcon"/>
<a-icon class="screening-box-scene-icon" :type="displayTimeIcon" />
</span>
</a-popover>
</div>
<SearchForm
ref="search"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@refresh="handleSearch"/>
@refresh="handleSearch"
/>
</div>
<div>
<a-space v-if="isEditable">
@@ -168,23 +169,24 @@
<div>
<div :style="{ marginTop: '8px' }" class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="downloadEmployeeAll">{{ $t('cs.companyStructure.downloadAll') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="exportSelectEvent">{{ $t('cs.companyStructure.downloadSelected') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('department_id')">{{ $t('cs.companyStructure.editDepartment') }}</span>
<a-divider type="vertical"/>
<span
@click="openBatchModal('direct_supervisor_id')">{{ $t('cs.companyStructure.editDirectSupervisor') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('direct_supervisor_id')">{{
$t('cs.companyStructure.editDirectSupervisor')
}}</span>
<a-divider type="vertical" />
<span @click="openBatchModal('position_name')">{{ $t('cs.companyStructure.editPosition') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('password')">{{ $t('cs.companyStructure.resetPassword') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('block', null, 1)">{{ $t('cs.companyStructure.block') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('block', null, 0)">{{ $t('cs.companyStructure.recover') }}</span>
<a-divider type="vertical"/>
<span>{{ $t('selectRows', {rows: selectedRowKeys.length}) }}</span>
<a-divider type="vertical" />
<span>{{ $t('selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</div>
<!-- <div>
@@ -195,11 +197,11 @@
</div>
</div>
<!-- 批量操作对话框 -->
<BatchModal ref="BatchModal" @refresh="updateAll"/>
<BatchModal ref="BatchModal" @refresh="updateAll" />
<!-- 部门表单对话框 -->
<DepartmentModal ref="DepartmentModal" @refresh="clickSelectGroup(1)"/>
<DepartmentModal ref="DepartmentModal" @refresh="clickSelectGroup(1)" />
<!-- 员工表单对话框 -->
<EmployeeModal ref="EmployeeModal" @refresh="updateAll"/>
<EmployeeModal ref="EmployeeModal" @refresh="updateAll" />
<!-- 表格展示 -->
<EmployeeTable
@@ -211,7 +213,7 @@
@onSelectChange="onSelectChange"
@openEmployeeModal="openEmployeeModal"
@openBatchModal="openBatchModal"
@tranferAttributes="getAttributes"
@transferAttributes="getAttributes"
:isEditable="isEditable"
:loading="loading"
>
@@ -225,7 +227,9 @@
:page-size-options="pageSizeOptions"
:current="tablePage.currentPage"
:total="tablePage.totalResult"
:show-total="(total, range) => $t('pagination.total', { range0: range[0], range1: range[1], total:total })"
:show-total="
(total, range) => $t('pagination.total', { range0: range[0], range1: range[1], total: total })
"
:page-size="tablePage.pageSize"
:default-current="1"
@change="pageOrSizeChange"
@@ -252,6 +256,9 @@
clickSelectGroup(1)
}
"
@downloadTemplate="downloadTemplate"
@customRequest="customRequest"
@import="importEmployee"
/>
</div>
</template>
@@ -262,15 +269,22 @@ import SplitPane from '@/components/SplitPane'
import CollapseTransition from '@/components/CollapseTransition'
import Bus from './eventBus/bus'
import CategroyTree from './CategoryTree'
import BatchUpload from './BatchUpload'
import BatchUpload from '../components/BatchUpload.vue'
import BatchModal from './BatchModal.vue'
import EmployeeModal from './EmployeeModal.vue'
import DepartmentModal from './DepartmentModal.vue'
import EmployeeTable from '../components/employeeTable.vue'
import { getDepartmentList, deleteDepartmentById, getAllDepartmentList, getAllDepAndEmployee } from '@/api/company'
import { getEmployeeList, getEmployeeCount, downloadAllEmployee, getEmployeeListByFilter } from '@/api/employee'
import {
getEmployeeList,
getEmployeeCount,
downloadAllEmployee,
getEmployeeListByFilter,
importEmployee,
} from '@/api/employee'
import { mixinPermissions } from '@/utils/mixin'
import SearchForm from '../components/SearchForm.vue'
import { downloadExcel, excel2Array } from '@/utils/download'
export default {
name: 'CompanyStructure',
@@ -284,10 +298,22 @@ export default {
EmployeeModal,
DepartmentModal,
EmployeeTable,
SearchForm
SearchForm,
},
data() {
const common_importParamsList = [
'email',
'username',
'nickname',
'password',
'sex',
'mobile',
'position_name',
'department_name',
'work_region',
]
return {
common_importParamsList,
isActive: '',
visible: true,
localStorageKey: 'itsm-company-strcutre',
@@ -322,7 +348,7 @@ export default {
attributes: [],
pageSizeOptions: ['50', '100', '200', '9999'],
expression: [],
loading: false
loading: false,
}
},
// created() {
@@ -363,16 +389,26 @@ export default {
value: 'sex',
is_choice: true,
choice_value: [
{ label: this.$t('cs.companyStructure.male'), value: '' },
{ label: this.$t('cs.companyStructure.female'), value: '' }]
{ label: this.$t('cs.companyStructure.male'), value: '' },
{ label: this.$t('cs.companyStructure.female'), value: '' },
],
},
{ label: this.$t('cs.companyStructure.mobile'), value: 'mobile' },
{ label: this.$t('cs.companyStructure.departmentName'), value: 'department_name' },
{ label: this.$t('cs.companyStructure.positionName'), value: 'position_name' },
{ label: this.$t('cs.companyStructure.supervisor'), value: 'direct_supervisor_id' },
{
label: this.$t('cs.companyStructure.work_region'),
value: 'work_region',
is_choice: true,
choice_value: [
{ label: this.$t('cs.companyStructure.china_mainland'), value: 'china_mainland' },
{ label: this.$t('cs.companyStructure.china_hk'), value: 'china_hk' },
],
},
]
},
sceneList () {
sceneList() {
return [
{
label: this.$t('all'),
@@ -388,7 +424,7 @@ export default {
},
]
},
groupData () {
groupData() {
return [
{
id: 0,
@@ -396,7 +432,7 @@ export default {
icon: 'user',
},
]
}
},
},
provide() {
return {
@@ -426,11 +462,8 @@ export default {
} else {
this.currentScene = 0
}
// console.log(this.currentScene)
// this.init()
this.clickSelectGroup(0).then(val => {
this.clickSelectItem(0)
})
this.updateCount()
this.clickSelectItem(0)
Bus.$on('updataAllIncludeEmployees', () => {
this.getAllFlatEmployees()
this.getAllDepAndEmployee()
@@ -492,7 +525,7 @@ export default {
setSearchPreferenceAttrList() {
this.canSearchPreferenceAttrList.forEach((item) => {
if (!this.attributes.includes(item.value)) {
this.canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter(v => v.value !== item.value)
this.canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((v) => v.value !== item.value)
}
})
},
@@ -504,9 +537,6 @@ export default {
this.$refs.ScreeningBoxScenePopover.$refs.tooltip.onVisibleChange(false)
}
document.getElementById('department').click()
// this.currentPage = 1
// this.updateTableData(1)
// this.departmentList = this.reqDepartmentList(-1)
},
clickHandler(event) {
this.isActive = event.target.innerText
@@ -544,37 +574,6 @@ export default {
this.activeEmployeeCount = res1.employee_count
this.deactiveEmployeeCount = res2.employee_count
},
async updateTableData(currentPage = 1, pageSize = this.tablePage.pageSize) {
this.selectedRowKeys = []
let reqEmployeeData = null
if (this.activeGroupIndex === 0) {
reqEmployeeData = await getEmployeeList({
...this.tableFilterData,
block_status: this.block_status,
page: currentPage,
page_size: pageSize,
search: this.filterName,
order: this.tableSortData || 'direct_supervisor_id',
})
} else if (this.activeGroupIndex === 1) {
reqEmployeeData = await getEmployeeList({
...this.tableFilterData,
block_status: this.currentScene,
department_id: this.selectDepartment.id,
page: currentPage,
page_size: pageSize,
search: this.filterName,
order: this.tableSortData || 'direct_supervisor_id',
})
}
this.tableData = this.FilterTableData(reqEmployeeData)
this.tablePage = {
...this.tablePage,
currentPage: reqEmployeeData.page,
pageSize: reqEmployeeData.page_size,
totalResult: reqEmployeeData.total,
}
},
async updateTableDataByFilter(currentPage = 1, pageSize = this.tablePage.pageSize) {
this.loading = true
this.selectedRowKeys = []
@@ -623,7 +622,8 @@ export default {
let max_index = 0
educational_experience.forEach((item, index) => {
if (index < educational_experience.length - 1) {
max_index = item.graduation_year > educational_experience[index + 1].graduation_year ? index : index + 1
max_index =
item.graduation_year > educational_experience[index + 1].graduation_year ? index : index + 1
}
})
tableData[index].school = educational_experience[max_index].school
@@ -708,7 +708,7 @@ export default {
} else {
block_status = this.currentScene
}
downloadAllEmployee({ block_status: block_status }).then(res => {
downloadAllEmployee({ block_status: block_status }).then((res) => {
const content = res
const blob = new Blob([content], { type: 'application/vnd.ms-excel' })
const url = window.URL.createObjectURL(blob)
@@ -787,7 +787,8 @@ export default {
},
// 请求部门数据
async reqDepartmentList(departmentId) {
const res = (await getDepartmentList({ department_parent_id: departmentId, block: this.currentScene })).departments
const res = (await getDepartmentList({ department_parent_id: departmentId, block: this.currentScene }))
.departments
return this.transformDepartmentData(res)
},
openDepartmentModal(type) {
@@ -828,10 +829,10 @@ export default {
},
sortChangeEvent({ sortList }) {
this.tableSortData = sortList
.map((item) => {
return `${item.order === 'asc' ? '' : '-'}${item.property}`
})
.join(',')
.map((item) => {
return `${item.order === 'asc' ? '' : '-'}${item.property}`
})
.join(',')
this.updateTableDataByFilter()
},
filterChangeEvent({ column, property, values, datas, filterList, $event }) {
@@ -852,6 +853,137 @@ export default {
exportSelectEvent() {
Bus.$emit('reqExportSelectEvent')
},
downloadTemplate() {
const data = [
[
{
v: '1、表头标“*”的红色字体为必填项\n2、邮箱、用户名不允许重复\n3、登录密码密码由6-20位字母、数字组成\n4、部门上下级部门间用"/"隔开,且从最上级部门开始,例如“深圳分公司/IT部/IT二部”。如出现相同的部门则默认导入组织架构中顺序靠前的部门',
t: 's',
s: {
alignment: {
wrapText: true,
vertical: 'center',
},
},
},
],
[
{
v: '*邮箱',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*用户名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*姓名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*密码',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '性别',
t: 's',
},
{
v: '手机号',
t: 's',
},
{
v: '部门',
t: 's',
},
{
v: '岗位',
t: 's',
},
{
v: '工作地区',
t: 's',
},
],
]
downloadExcel(data, this.$t('cs.companyStructure.downloadTemplateName'))
},
customRequest({ data }, callback) {
excel2Array(data.file).then((res) => {
res = res.filter((item) => item.length)
callback(
res.slice(2).map((item) => {
const obj = {}
item.forEach((ele, index) => {
obj[this.common_importParamsList[index]] = ele
})
return obj
})
)
})
},
async importEmployee({ importData }, callback) {
this.allCount = importData.length
const _importData = importData.map((item) => {
const { _X_ROW_KEY, ...rest } = item
const keyArr = Object.keys(rest)
keyArr.forEach((key) => {
if (rest[key]) {
rest[key] = rest[key] + ''
}
})
rest.educational_experience = [
{
school: rest.school,
major: rest.major,
education: rest.education,
graduation_year: rest.graduation_year,
},
]
delete rest.school
delete rest.major
delete rest.education
delete rest.graduation_year
const regionMap = {
中国大陆: 'china_mainland',
中国香港: 'china_hk',
'Chinese Mainland': 'china_mainland',
'HK China': 'china_hk',
}
rest.work_region = regionMap[rest.work_region] ?? res.work_region
return rest
})
const res = await importEmployee({ employee_list: _importData })
callback(res)
},
},
}
</script>
@@ -1051,7 +1183,6 @@ export default {
color: @primary-color;
font-size: 12px;
}
}
}
}
@@ -1068,7 +1199,6 @@ export default {
}
}
}
}
.ops-setting-structure-main-pagination {
width: 100%;

View File

@@ -0,0 +1,243 @@
<template>
<a-modal
:visible="visible"
:title="$t('cs.companyStructure.batchImport')"
dialogClass="ops-modal setting-structure-upload"
:width="800"
@cancel="close"
>
<div class="setting-structure-upload-steps">
<div
:class="{ 'setting-structure-upload-step': true, selected: index + 1 <= currentStep }"
v-for="(step, index) in stepList"
:key="step.value"
>
<div :class="{ 'setting-structure-upload-step-icon': true }">
<ops-icon :type="step.icon" />
</div>
<span>{{ step.label }}</span>
</div>
</div>
<template v-if="currentStep === 1">
<a-upload :multiple="false" :customRequest="customRequest" accept=".xlsx" :showUploadList="false">
<a-button :style="{ marginBottom: '20px' }" type="primary">
<a-icon type="upload" />{{ $t('cs.companyStructure.selectFile') }}</a-button
>
</a-upload>
<p>
<a @click="download">
<slot name="downloadTemplateText">{{ $t('cs.companyStructure.clickDownloadImportTemplate') }}</slot>
</a>
</p>
</template>
<div
:style="{
height: '60px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
whiteSpace: 'pre-wrap',
}"
v-if="currentStep === 3"
>
{{ $t('cs.companyStructure.importSuccess', { allCount: allCount })
}}<span :style="{ color: '#2362FB' }"> {{ allCount - errorCount }} </span>{{ $t('cs.companyStructure.count') }},
{{ $t('cs.companyStructure.importFailed') }}<span :style="{ color: '#D81E06' }"> {{ errorCount }} </span
>{{ $t('cs.companyStructure.count') }}
</div>
<slot>
<vxe-table
v-if="currentStep === 2 || has_error"
ref="employeeTable"
stripe
:data="importData"
show-overflow
show-header-overflow
highlight-hover-row
size="small"
class="ops-stripe-table"
:max-height="400"
:column-config="{ resizable: true }"
>
<vxe-column field="email" :title="$t('cs.companyStructure.email')" min-width="120" fixed="left"></vxe-column>
<vxe-column field="username" :title="$t('cs.companyStructure.username')" min-width="80"></vxe-column>
<vxe-column field="nickname" :title="$t('cs.companyStructure.nickname')" min-width="80"></vxe-column>
<vxe-column field="password" :title="$t('cs.companyStructure.password')" min-width="80"></vxe-column>
<vxe-column field="sex" :title="$t('cs.companyStructure.sex')" min-width="60"></vxe-column>
<vxe-column field="mobile" :title="$t('cs.companyStructure.mobile')" min-width="80"></vxe-column>
<vxe-column
field="department_name"
:title="$t('cs.companyStructure.departmentName')"
min-width="80"
></vxe-column>
<vxe-column field="position_name" :title="$t('cs.companyStructure.positionName')" min-width="80"></vxe-column>
<vxe-column field="work_region" :title="$t('cs.companyStructure.work_region')" min-width="80"></vxe-column>
<vxe-column
v-if="has_error"
field="err"
:title="$t('cs.companyStructure.importFailedReason')"
min-width="120"
fixed="right"
>
<template #default="{ row }">
<span :style="{ color: '#D81E06' }">{{ row.err }}</span>
</template>
</vxe-column>
</vxe-table>
</slot>
<a-space slot="footer">
<a-button size="small" type="primary" ghost @click="close">{{ $t('cancel') }}</a-button>
<a-button v-if="currentStep !== 1" size="small" type="primary" ghost @click="goPre">{{
$t('cs.companyStructure.prevStep')
}}</a-button>
<a-button v-if="currentStep !== 3" size="small" type="primary" @click="goNext">{{
$t('cs.companyStructure.nextStep')
}}</a-button>
<a-button v-else size="small" type="primary" @click="close">{{ $t('cs.companyStructure.done') }}</a-button>
</a-space>
</a-modal>
</template>
<script>
export default {
name: 'BatchUpload',
data() {
const stepList = [
{
value: 1,
label: this.$t('cs.companyStructure.uploadFile'),
icon: 'icon-shidi-tianjia',
},
{
value: 2,
label: this.$t('cs.companyStructure.confirmData'),
icon: 'icon-shidi-yunshangchuan',
},
{
value: 3,
label: this.$t('cs.companyStructure.uploadDone'),
icon: 'icon-shidi-queren',
},
]
return {
stepList,
visible: false,
currentStep: 1,
importData: [],
has_error: false,
allCount: 0,
errorCount: 0,
}
},
methods: {
open() {
this.importData = []
this.has_error = false
this.errorCount = 0
this.visible = true
},
close() {
this.currentStep = 1
this.visible = false
},
async goNext() {
if (this.currentStep === 2) {
this.allCount = this.importData.length
this.$emit('import', { importData: this.importData }, (res) => {
if (res.length) {
const errData = res.filter((item) => {
return item.err.length
})
console.log('err', errData)
this.has_error = true
this.errorCount = errData.length
this.currentStep += 1
this.importData = errData
this.$message.error(this.$t('cs.companyStructure.dataErr'))
} else {
this.currentStep += 1
this.$message.success(this.$t('cs.companyStructure.opSuccess'))
}
this.$emit('refresh')
})
}
},
goPre() {
this.has_error = false
this.errorCount = 0
this.currentStep -= 1
},
download() {
this.$emit('downloadTemplate')
},
customRequest(data) {
this.fileList = [data.file]
this.$emit('customRequest', { data }, (importData) => {
this.importData = importData
this.currentStep = 2
})
},
},
}
</script>
<style lang="less">
.setting-structure-upload {
.ant-modal-body {
padding: 24px 48px;
}
.setting-structure-upload-steps {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 20px;
.setting-structure-upload-step {
display: inline-block;
text-align: center;
position: relative;
.setting-structure-upload-step-icon {
width: 86px;
height: 86px;
display: flex;
align-items: center;
justify-content: center;
background-image: url('../../../assets/icon-bg.png');
margin-bottom: 20px;
> i {
font-size: 40px;
color: #fff;
}
}
> span {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.5);
}
}
.setting-structure-upload-step:not(:first-child)::before {
content: '';
height: 2px;
width: 223px;
position: absolute;
background-color: #e7ecf3;
left: -223px;
top: 43px;
z-index: 0;
}
.selected.setting-structure-upload-step {
&:not(:first-child)::before {
background-color: #7eb0ff;
}
}
.selected {
.setting-structure-upload-step-icon {
background-image: url('../../../assets/icon-bg-selected.png');
}
> span {
color: rgba(0, 0, 0, 0.8);
}
}
}
}
</style>

View File

@@ -92,7 +92,7 @@
<span>{{ $t('cs.companyStructure.sex') }}</span>
</span>
</template>
<template #default="{row}">
<template #default="{ row }">
<span v-if="row.sex === ''">{{ $t('cs.companyStructure.male') }}</span>
<span v-if="row.sex === ''">{{ $t('cs.companyStructure.female') }}</span>
</template>
@@ -175,6 +175,26 @@
}}</span>
</template>
</vxe-column>
<vxe-column
field="work_region"
v-if="
checkedCols.findIndex((v) => v == 'work_region') !== -1 &&
attributes.findIndex((v) => v == 'work_region') !== -1
"
:title="$t('cs.companyStructure.work_region')"
min-width="120px"
key="work_region"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon class="move-icon" />
<span>{{ $t('cs.companyStructure.work_region') }}</span>
</span>
</template>
<template #default="{ row }">
{{ $t(`cs.companyStructure.${row.work_region}`) }}
</template>
</vxe-column>
<vxe-column
field="control"
width="100px"
@@ -197,7 +217,7 @@
>
<template slot="content">
<div :style="{ maxHeight: `${windowHeight - 320}px`, overflowY: 'auto', width: '160px' }">
<a-checkbox-group v-model="unsbmitCheckedCols" :options="options" style="display: grid;">
<a-checkbox-group v-model="unsbmitCheckedCols" :options="options" style="display: grid">
</a-checkbox-group>
</div>
<div
@@ -209,22 +229,24 @@
justifyContent: 'flex-end',
}"
>
<a-button :style="{ marginRight: '10px' }" size="small" @click="handleCancel">{{ $t('cancel') }}</a-button>
<a-button :style="{ marginRight: '10px' }" size="small" @click="handleCancel">{{
$t('cancel')
}}</a-button>
<a-button size="small" @click="handleSubmit" type="primary">{{ $t('confirm') }}</a-button>
</div>
</template>
<a-icon type="control" style="cursor: pointer;" />
<a-icon type="control" style="cursor: pointer" />
</a-popover>
</template>
</template>
<template #default="{ row }">
<a-space v-if="tableType === 'structure'">
<a><a-icon type="edit" @click="openEmployeeModal(row, 'edit')"/></a>
<a><a-icon type="edit" @click="openEmployeeModal(row, 'edit')" /></a>
<a-tooltip>
<template slot="title">
{{ $t('cs.companyStructure.resetPassword') }}
</template>
<a><a-icon type="reload" @click="openBatchModal('password', row)"/></a>
<a><a-icon type="reload" @click="openBatchModal('password', row)" /></a>
</a-tooltip>
<a-tooltip v-if="!row.block">
<template slot="title">
@@ -305,6 +327,7 @@ export default {
{ label: this.$t('cs.companyStructure.departmentName'), value: 'department_name' },
{ label: this.$t('cs.companyStructure.positionName'), value: 'position_name' },
{ label: this.$t('cs.companyStructure.supervisor'), value: 'direct_supervisor_id' },
{ label: this.$t('cs.companyStructure.work_region'), value: 'work_region' },
]
const checkedCols = JSON.parse(localStorage.getItem('setting-table-CheckedCols')) || [
'nickname',
@@ -315,6 +338,7 @@ export default {
'department_name',
'position_name',
'direct_supervisor_id',
'work_region',
]
return {
filterRoleList: [],
@@ -442,7 +466,7 @@ export default {
}
}
Bus.$emit('getAttributes', this.attributes)
this.$emit('tranferAttributes', this.attributes)
this.$emit('transferAttributes', this.attributes)
},
getIsInterInship(is_internship) {
return this.internMap.filter((item) => item.id === is_internship)[0]['label']
@@ -541,7 +565,7 @@ export default {
useStyle: true, // 是否导出样式
isFooter: false, // 是否导出表尾比如合计
// 过滤那个字段导出
columnFilterMethod: function(column, $columnIndex) {
columnFilterMethod: function (column, $columnIndex) {
return !(column.$columnIndex === 0)
// 0是复选框 不导出
},

View File

@@ -1,106 +1,121 @@
<template>
<treeselect
:disable-branch-nodes="multiple ? false : true"
:multiple="multiple"
:options="employeeTreeSelectOption"
:placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')"
v-model="treeValue"
:max-height="200"
:noChildrenText="$t('cs.components.empty')"
:noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY"
:limit="limit"
:limitText="(count) => `+ ${count}`"
v-bind="$attrs"
appendToBody
:zIndex="1050"
:flat="flat"
>
</treeselect>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import { formatOption } from '@/utils/util'
export default {
name: 'EmployeeTreeSelect',
components: {
Treeselect,
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array, Number, null],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
className: {
type: String,
default: 'ops-setting-treeselect',
},
placeholder: {
type: String,
default: '',
},
idType: {
type: Number,
default: 1,
},
departmentKey: {
type: String,
default: 'department_id',
},
employeeKey: {
type: String,
default: 'employee_id',
},
limit: {
type: Number,
default: 20,
},
flat: {
type: Boolean,
default: false,
},
},
data() {
return {}
},
inject: {
provide_allTreeDepAndEmp: {
from: 'provide_allTreeDepAndEmp',
},
readOnly: {
from: 'readOnly',
default: false,
},
},
computed: {
treeValue: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
allTreeDepAndEmp() {
return this.provide_allTreeDepAndEmp()
},
employeeTreeSelectOption() {
return formatOption(this.allTreeDepAndEmp, this.idType, false, this.departmentKey, this.employeeKey)
},
},
methods: {},
}
</script>
<style scoped></style>
<template>
<treeselect
:disable-branch-nodes="multiple ? false : true"
:multiple="multiple"
:options="employeeTreeSelectOption"
:placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')"
v-model="treeValue"
:max-height="200"
:noChildrenText="$t('cs.components.empty')"
:noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY"
:limit="limit"
:limitText="(count) => `+ ${count}`"
v-bind="$attrs"
appendToBody
:zIndex="1050"
:flat="flat"
>
</treeselect>
</template>
<script>
import _ from 'lodash'
import Treeselect from '@riophae/vue-treeselect'
import { formatOption } from '@/utils/util'
export default {
name: 'EmployeeTreeSelect',
components: {
Treeselect,
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array, Number, null],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
className: {
type: String,
default: 'ops-setting-treeselect',
},
placeholder: {
type: String,
default: '',
},
idType: {
type: Number,
default: 1,
},
departmentKey: {
type: String,
default: 'department_id',
},
employeeKey: {
type: String,
default: 'employee_id',
},
limit: {
type: Number,
default: 20,
},
flat: {
type: Boolean,
default: false,
},
otherOptions: {
type: Array,
default: () => [],
}
},
data() {
return {}
},
inject: {
provide_allTreeDepAndEmp: {
from: 'provide_allTreeDepAndEmp',
},
readOnly: {
from: 'readOnly',
default: false,
},
},
computed: {
treeValue: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
allTreeDepAndEmp() {
return this.provide_allTreeDepAndEmp()
},
employeeTreeSelectOption() {
return formatOption(
[
..._.cloneDeep((Array.isArray(this.allTreeDepAndEmp) ? this.allTreeDepAndEmp : [])),
..._.cloneDeep((Array.isArray(this.otherOptions) ? this.otherOptions : []))
],
this.idType,
false,
this.departmentKey,
this.employeeKey
)
},
},
methods: {},
}
</script>
<style scoped></style>

View File

@@ -19,7 +19,7 @@ const cs_en = {
app: 'APP Authority',
basic: 'Basic Settings',
theme: 'Theme Settings',
security: 'Security Settings'
security: 'Security Settings',
},
companyInfo: {
spanCompany: 'Description',
@@ -201,7 +201,11 @@ const cs_en = {
createEmployee: 'Create Employee',
editEmployee: 'Edit Employee',
role: 'Role',
selectDisplayColumn: 'Please select columns to display'
selectDisplayColumn: 'Please select columns to display',
work_region: 'Work Region',
workRegionPlaceholder: 'Please select work region',
china_mainland: 'Chinese Mainland',
china_hk: 'HK China',
},
auth: {
basic: 'Basic',
@@ -220,7 +224,7 @@ const cs_en = {
user: 'User',
username: 'Username',
userPlaceholder: 'Please enter username',
userHelp: 'User DN: cn={},ou=users,dc=xxx,dc=com {} will be replaced by username'
userHelp: 'User DN: cn={},ou=users,dc=xxx,dc=com {} will be replaced by username',
},
cas: {
server: 'Server Address',
@@ -237,10 +241,11 @@ const cs_en = {
validateRoutePlaceholder: 'Please enter validate route',
afterLoginRoute: 'Redirect Route',
afterLoginRoutePlaceholder: 'Please enter redirect route',
userMap: 'User Attribute Mapping'
userMap: 'User Attribute Mapping',
},
autoRedirectLogin: 'Auto Redirect to Third-party Login Page',
autoRedirectLoginHelp: 'If disabled, a confirmation will be displayed to redirect to third-party login page. Click the Cancel button will go to the built-in login page',
autoRedirectLoginHelp:
'If disabled, a confirmation will be displayed to redirect to third-party login page. Click the Cancel button will go to the built-in login page',
usernameOrEmail: 'Username/Email',
usernameOrEmailPlaceholder: 'Please enter username/email',
password: 'Password',
@@ -257,7 +262,7 @@ const cs_en = {
userInfo: 'User Info',
scopes: 'Scopes',
scopesPlaceholder: 'Please enter scopes',
}
},
},
duty: {
basicSetting: 'Basic Settings',
@@ -274,13 +279,13 @@ const cs_en = {
mainDutyPeople: 'Main Duty Person',
deputyDutyPeople: 'Deputy Duty Person',
dutyRule: 'Duty Rule',
'一': 'Mon',
'二': 'Tue',
'三': 'Wed',
'四': 'Thu',
'五': 'Fri',
'六': 'Sat',
'日': 'Sun',
: 'Mon',
: 'Tue',
: 'Wed',
: 'Thu',
: 'Fri',
: 'Sat',
: 'Sun',
searchPlaceholder: 'Please search',
dutyTable: 'Duty Schedule',
dutyMember: 'Duty Member',
@@ -304,7 +309,7 @@ const cs_en = {
offDutyReceiverPlaceholder: 'Please select off-duty receiver',
titleLimit: 'Please enter title (20 characters)',
remarkLimit: 'Remark 150 characters max',
frequencyLimit: 'Please enter duty frequency (positive integer)'
frequencyLimit: 'Please enter duty frequency (positive integer)',
},
group: {
groupName: 'User Group',
@@ -329,7 +334,7 @@ const cs_en = {
moreThan: 'More Than',
lessThan: 'Less Than',
operatorInPlaceholder: 'Separate by ;',
selectEmployee: 'Select Employee'
selectEmployee: 'Select Employee',
},
notice: {
corpid: 'Corp ID',
@@ -368,7 +373,8 @@ const cs_en = {
disableCreationOfRequestsViaEmail: 'Disable Creation of Requests Via Email',
specifyAllowedEmails: 'Specify Allowed Emails/Domains, Separate Multiple Values By Comma',
specifyAllowedEmailsExample: 'E.g. user@domain.com,*@domain.com',
specifyAllowedEmailsLimit: 'Limit cannot apply to requests already in sessions, it will aggregate to its parent ticket',
specifyAllowedEmailsLimit:
'Limit cannot apply to requests already in sessions, it will aggregate to its parent ticket',
messageConfig: 'Message Settings',
moveWrongMessagesToFolder: 'Move Messages to Wrong Folder',
knowMore: 'Learn More',
@@ -438,7 +444,7 @@ const cs_en = {
myDepartmentAndSubordinateDepartments: 'My Department And Subordinate Departments',
test: 'Test',
selectApp: 'Select App',
}
},
}
export default cs_en

View File

@@ -19,7 +19,7 @@ const cs_zh = {
app: '应用权限',
basic: '基础设置',
theme: '主题配置',
security: '安全配置'
security: '安全配置',
},
companyInfo: {
spanCompany: '公司描述',
@@ -201,7 +201,11 @@ const cs_zh = {
createEmployee: '新建员工',
editEmployee: '编辑员工',
role: '角色',
selectDisplayColumn: '请选择需要展示的列'
selectDisplayColumn: '请选择需要展示的列',
work_region: '工作地区',
workRegionPlaceholder: '请选择工作地区',
china_mainland: '中国大陆',
china_hk: '中国香港',
},
auth: {
basic: '基本',
@@ -220,7 +224,7 @@ const cs_zh = {
user: '用户',
username: '用户名称',
userPlaceholder: '请输入用户名称',
userHelp: '用户dn: cn={},ou=users,dc=xxx,dc=com {}会替换成用户名'
userHelp: '用户dn: cn={},ou=users,dc=xxx,dc=com {}会替换成用户名',
},
cas: {
server: '服务端地址',
@@ -237,7 +241,7 @@ const cs_zh = {
validateRoutePlaceholder: '请输入验证路由',
afterLoginRoute: '重定向路由',
afterLoginRoutePlaceholder: '请输入重定向路由',
userMap: '用户属性映射'
userMap: '用户属性映射',
},
autoRedirectLogin: '自动跳转到第三方登录页',
autoRedirectLoginHelp: '如果关闭,则会弹出跳转到第三方登录页的确认,点取消按钮会进入系统内置的登录页',
@@ -257,7 +261,7 @@ const cs_zh = {
userInfo: '用户信息',
scopes: '授权范围',
scopesPlaceholder: '请输入授权范围',
}
},
},
duty: {
basicSetting: '基础设置',
@@ -274,13 +278,13 @@ const cs_zh = {
mainDutyPeople: '主值班人',
deputyDutyPeople: '副值班人',
dutyRule: '排班规则',
'一': '一',
'二': '二',
'三': '三',
'四': '四',
'五': '五',
'六': '六',
'日': '日',
: '一',
: '二',
: '三',
: '四',
: '五',
: '六',
: '日',
searchPlaceholder: '请查找',
dutyTable: '值班表',
dutyMember: '值班人员',
@@ -304,7 +308,7 @@ const cs_zh = {
offDutyReceiverPlaceholder: '请选择非值班时间接收人',
titleLimit: '请输入标题20个字符',
remarkLimit: '备注150个字符以内',
frequencyLimit: '请输入值班频次(正整数)'
frequencyLimit: '请输入值班频次(正整数)',
},
group: {
groupName: '用户分组',
@@ -329,7 +333,7 @@ const cs_zh = {
moreThan: '大于',
lessThan: '小于',
operatorInPlaceholder: '以 ; 分隔',
selectEmployee: '选择员工'
selectEmployee: '选择员工',
},
notice: {
corpid: '企业ID',
@@ -438,6 +442,6 @@ const cs_zh = {
myDepartmentAndSubordinateDepartments: '本部门及下属部门',
test: '测试',
selectApp: '选择应用',
}
},
}
export default cs_zh

View File

@@ -2,27 +2,13 @@
<div class="setting-person">
<div class="setting-person-left">
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '1'
})
}
"
@click="clickSideItem('1')"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '1' }"
>
<ops-icon type="icon-shidi-yonghu" />{{ $t('cs.person.spanTitle') }}
</div>
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '2'
})
}
"
@click="clickSideItem('2')"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '2' }"
>
<a-icon type="unlock" theme="filled" />{{ $t('cs.person.accountAndPassword') }}
@@ -240,7 +226,14 @@ export default {
}
},
},
beforeDestroy() {
this.$bus.$off('changeSettingPersonCurrent', this.clickSideItemv)
},
mounted() {
this.$bus.$on('changeSettingPersonCurrent', this.clickSideItem)
if (this.$route?.query?.current) {
this.current = this.$route.query.current
}
this.getAllFlatEmployees()
this.getAllFlatDepartment()
this.getEmployeeByUid()
@@ -249,6 +242,12 @@ export default {
...mapActions(['GetInfo']),
getDepartmentName,
getDirectorName,
clickSideItem(type) {
this.$refs.personForm.clearValidate()
this.$nextTick(() => {
this.current = type
})
},
getEmployeeByUid() {
getEmployeeByUid(this.uid).then((res) => {
this.form = { ...res }

View File

@@ -41,7 +41,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.12
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.13
container_name: cmdb-api
env_file:
- .env
@@ -71,7 +71,7 @@ services:
flask cmdb-init-acl
flask init-import-user-from-acl
flask init-department
flask cmdb-patch -v 2.4.12
flask cmdb-patch -v 2.4.13
flask cmdb-counter > counter.log 2>&1
networks:
new:
@@ -84,7 +84,7 @@ services:
test: "ps aux|grep -v grep|grep -v '1 root'|grep gunicorn || exit 1"
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.12
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.13
container_name: cmdb-ui
depends_on:
cmdb-api: