mirror of https://github.com/veops/cmdb.git
commit
aae43a53b5
|
@ -54,6 +54,108 @@
|
|||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ops-setting-holiday_management-copy</div>
|
||||
<div class="code-name">&#xe9fa;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-system_log</div>
|
||||
<div class="code-name">&#xe9f8;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ops-setting-adjustday</div>
|
||||
<div class="code-name">&#xe9f6;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ops-setting-holiday</div>
|
||||
<div class="code-name">&#xe9f7;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ops-setting-festival</div>
|
||||
<div class="code-name">&#xe9f5;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-count</div>
|
||||
<div class="code-name">&#xe9f4;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-satisfaction</div>
|
||||
<div class="code-name">&#xe9f3;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-folder</div>
|
||||
<div class="code-name">&#xe9f2;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-entire_network_</div>
|
||||
<div class="code-name">&#xe9f1;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-subnet</div>
|
||||
<div class="code-name">&#xe9f0;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-map_view</div>
|
||||
<div class="code-name">&#xe9ef;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-recycle</div>
|
||||
<div class="code-name">&#xe9ee;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-catalog</div>
|
||||
<div class="code-name">&#xe9ed;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-ipam</div>
|
||||
<div class="code-name">&#xe9ec;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">cmdb-calc</div>
|
||||
<div class="code-name">&#xe9eb;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-users</div>
|
||||
<div class="code-name">&#xe9ea;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-tokens</div>
|
||||
<div class="code-name">&#xe9e9;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-mysql</div>
|
||||
|
@ -6000,9 +6102,9 @@
|
|||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1729157759723') format('woff2'),
|
||||
url('iconfont.woff?t=1729157759723') format('woff'),
|
||||
url('iconfont.ttf?t=1729157759723') format('truetype');
|
||||
src: url('iconfont.woff2?t=1731312848138') format('woff2'),
|
||||
url('iconfont.woff?t=1731312848138') format('woff'),
|
||||
url('iconfont.ttf?t=1731312848138') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
|
@ -6028,6 +6130,159 @@
|
|||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-holidays"></span>
|
||||
<div class="name">
|
||||
ops-setting-holiday_management-copy
|
||||
</div>
|
||||
<div class="code-name">.ops-setting-holidays
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-itsm-logs"></span>
|
||||
<div class="name">
|
||||
itsm-system_log
|
||||
</div>
|
||||
<div class="code-name">.ops-itsm-logs
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-workday"></span>
|
||||
<div class="name">
|
||||
ops-setting-adjustday
|
||||
</div>
|
||||
<div class="code-name">.ops-setting-workday
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-holiday"></span>
|
||||
<div class="name">
|
||||
ops-setting-holiday
|
||||
</div>
|
||||
<div class="code-name">.ops-setting-holiday
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-festival"></span>
|
||||
<div class="name">
|
||||
ops-setting-festival
|
||||
</div>
|
||||
<div class="code-name">.ops-setting-festival
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-calc"></span>
|
||||
<div class="name">
|
||||
itsm-count
|
||||
</div>
|
||||
<div class="code-name">.itsm-calc
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-reports_4"></span>
|
||||
<div class="name">
|
||||
itsm-satisfaction
|
||||
</div>
|
||||
<div class="code-name">.itsm-reports_4
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-folder"></span>
|
||||
<div class="name">
|
||||
veops-folder
|
||||
</div>
|
||||
<div class="code-name">.veops-folder
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-entire_network_"></span>
|
||||
<div class="name">
|
||||
veops-entire_network_
|
||||
</div>
|
||||
<div class="code-name">.veops-entire_network_
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-subnet"></span>
|
||||
<div class="name">
|
||||
veops-subnet
|
||||
</div>
|
||||
<div class="code-name">.veops-subnet
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-map_view"></span>
|
||||
<div class="name">
|
||||
veops-map_view
|
||||
</div>
|
||||
<div class="code-name">.veops-map_view
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-recycle"></span>
|
||||
<div class="name">
|
||||
veops-recycle
|
||||
</div>
|
||||
<div class="code-name">.veops-recycle
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-catalog"></span>
|
||||
<div class="name">
|
||||
veops-catalog
|
||||
</div>
|
||||
<div class="code-name">.veops-catalog
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-ipam"></span>
|
||||
<div class="name">
|
||||
veops-ipam
|
||||
</div>
|
||||
<div class="code-name">.veops-ipam
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont cmdb-calc"></span>
|
||||
<div class="name">
|
||||
cmdb-calc
|
||||
</div>
|
||||
<div class="code-name">.cmdb-calc
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-users"></span>
|
||||
<div class="name">
|
||||
ai-users
|
||||
</div>
|
||||
<div class="code-name">.ai-users
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-tokens"></span>
|
||||
<div class="name">
|
||||
ai-tokens
|
||||
</div>
|
||||
<div class="code-name">.ai-tokens
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont oneterm-mysql"></span>
|
||||
<div class="name">
|
||||
|
@ -8162,20 +8417,20 @@
|
|||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-duration"></span>
|
||||
<span class="icon iconfont itsm-reports_3"></span>
|
||||
<div class="name">
|
||||
itsm-duration
|
||||
</div>
|
||||
<div class="code-name">.itsm-duration
|
||||
<div class="code-name">.itsm-reports_3
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-workload"></span>
|
||||
<span class="icon iconfont itsm-reports_2"></span>
|
||||
<div class="name">
|
||||
itsm-workload (1)
|
||||
</div>
|
||||
<div class="code-name">.itsm-workload
|
||||
<div class="code-name">.itsm-reports_2
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
@ -14947,6 +15202,142 @@
|
|||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-holidays"></use>
|
||||
</svg>
|
||||
<div class="name">ops-setting-holiday_management-copy</div>
|
||||
<div class="code-name">#ops-setting-holidays</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-itsm-logs"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-system_log</div>
|
||||
<div class="code-name">#ops-itsm-logs</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-workday"></use>
|
||||
</svg>
|
||||
<div class="name">ops-setting-adjustday</div>
|
||||
<div class="code-name">#ops-setting-workday</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-holiday"></use>
|
||||
</svg>
|
||||
<div class="name">ops-setting-holiday</div>
|
||||
<div class="code-name">#ops-setting-holiday</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-festival"></use>
|
||||
</svg>
|
||||
<div class="name">ops-setting-festival</div>
|
||||
<div class="code-name">#ops-setting-festival</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-calc"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-count</div>
|
||||
<div class="code-name">#itsm-calc</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-reports_4"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-satisfaction</div>
|
||||
<div class="code-name">#itsm-reports_4</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-folder"></use>
|
||||
</svg>
|
||||
<div class="name">veops-folder</div>
|
||||
<div class="code-name">#veops-folder</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-entire_network_"></use>
|
||||
</svg>
|
||||
<div class="name">veops-entire_network_</div>
|
||||
<div class="code-name">#veops-entire_network_</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-subnet"></use>
|
||||
</svg>
|
||||
<div class="name">veops-subnet</div>
|
||||
<div class="code-name">#veops-subnet</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-map_view"></use>
|
||||
</svg>
|
||||
<div class="name">veops-map_view</div>
|
||||
<div class="code-name">#veops-map_view</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-recycle"></use>
|
||||
</svg>
|
||||
<div class="name">veops-recycle</div>
|
||||
<div class="code-name">#veops-recycle</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-catalog"></use>
|
||||
</svg>
|
||||
<div class="name">veops-catalog</div>
|
||||
<div class="code-name">#veops-catalog</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-ipam"></use>
|
||||
</svg>
|
||||
<div class="name">veops-ipam</div>
|
||||
<div class="code-name">#veops-ipam</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#cmdb-calc"></use>
|
||||
</svg>
|
||||
<div class="name">cmdb-calc</div>
|
||||
<div class="code-name">#cmdb-calc</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-users"></use>
|
||||
</svg>
|
||||
<div class="name">ai-users</div>
|
||||
<div class="code-name">#ai-users</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-tokens"></use>
|
||||
</svg>
|
||||
<div class="name">ai-tokens</div>
|
||||
<div class="code-name">#ai-tokens</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#oneterm-mysql"></use>
|
||||
|
@ -16845,18 +17236,18 @@
|
|||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-duration"></use>
|
||||
<use xlink:href="#itsm-reports_3"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-duration</div>
|
||||
<div class="code-name">#itsm-duration</div>
|
||||
<div class="code-name">#itsm-reports_3</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-workload"></use>
|
||||
<use xlink:href="#itsm-reports_2"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-workload (1)</div>
|
||||
<div class="code-name">#itsm-workload</div>
|
||||
<div class="code-name">#itsm-reports_2</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1729157759723') format('woff2'),
|
||||
url('iconfont.woff?t=1729157759723') format('woff'),
|
||||
url('iconfont.ttf?t=1729157759723') format('truetype');
|
||||
src: url('iconfont.woff2?t=1731312848138') format('woff2'),
|
||||
url('iconfont.woff?t=1731312848138') format('woff'),
|
||||
url('iconfont.ttf?t=1731312848138') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
|
@ -13,6 +13,74 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.ops-setting-holidays:before {
|
||||
content: "\e9fa";
|
||||
}
|
||||
|
||||
.ops-itsm-logs:before {
|
||||
content: "\e9f8";
|
||||
}
|
||||
|
||||
.ops-setting-workday:before {
|
||||
content: "\e9f6";
|
||||
}
|
||||
|
||||
.ops-setting-holiday:before {
|
||||
content: "\e9f7";
|
||||
}
|
||||
|
||||
.ops-setting-festival:before {
|
||||
content: "\e9f5";
|
||||
}
|
||||
|
||||
.itsm-calc:before {
|
||||
content: "\e9f4";
|
||||
}
|
||||
|
||||
.itsm-reports_4:before {
|
||||
content: "\e9f3";
|
||||
}
|
||||
|
||||
.veops-folder:before {
|
||||
content: "\e9f2";
|
||||
}
|
||||
|
||||
.veops-entire_network_:before {
|
||||
content: "\e9f1";
|
||||
}
|
||||
|
||||
.veops-subnet:before {
|
||||
content: "\e9f0";
|
||||
}
|
||||
|
||||
.veops-map_view:before {
|
||||
content: "\e9ef";
|
||||
}
|
||||
|
||||
.veops-recycle:before {
|
||||
content: "\e9ee";
|
||||
}
|
||||
|
||||
.veops-catalog:before {
|
||||
content: "\e9ed";
|
||||
}
|
||||
|
||||
.veops-ipam:before {
|
||||
content: "\e9ec";
|
||||
}
|
||||
|
||||
.cmdb-calc:before {
|
||||
content: "\e9eb";
|
||||
}
|
||||
|
||||
.ai-users:before {
|
||||
content: "\e9ea";
|
||||
}
|
||||
|
||||
.ai-tokens:before {
|
||||
content: "\e9e9";
|
||||
}
|
||||
|
||||
.oneterm-mysql:before {
|
||||
content: "\e9e8";
|
||||
}
|
||||
|
@ -961,11 +1029,11 @@
|
|||
content: "\e914";
|
||||
}
|
||||
|
||||
.itsm-duration:before {
|
||||
.itsm-reports_3:before {
|
||||
content: "\e913";
|
||||
}
|
||||
|
||||
.itsm-workload:before {
|
||||
.itsm-reports_2:before {
|
||||
content: "\e912";
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,125 @@
|
|||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "42337844",
|
||||
"name": "ops-setting-holiday_management-copy",
|
||||
"font_class": "ops-setting-holidays",
|
||||
"unicode": "e9fa",
|
||||
"unicode_decimal": 59898
|
||||
},
|
||||
{
|
||||
"icon_id": "42335414",
|
||||
"name": "itsm-system_log",
|
||||
"font_class": "ops-itsm-logs",
|
||||
"unicode": "e9f8",
|
||||
"unicode_decimal": 59896
|
||||
},
|
||||
{
|
||||
"icon_id": "42334782",
|
||||
"name": "ops-setting-adjustday",
|
||||
"font_class": "ops-setting-workday",
|
||||
"unicode": "e9f6",
|
||||
"unicode_decimal": 59894
|
||||
},
|
||||
{
|
||||
"icon_id": "42334768",
|
||||
"name": "ops-setting-holiday",
|
||||
"font_class": "ops-setting-holiday",
|
||||
"unicode": "e9f7",
|
||||
"unicode_decimal": 59895
|
||||
},
|
||||
{
|
||||
"icon_id": "42334734",
|
||||
"name": "ops-setting-festival",
|
||||
"font_class": "ops-setting-festival",
|
||||
"unicode": "e9f5",
|
||||
"unicode_decimal": 59893
|
||||
},
|
||||
{
|
||||
"icon_id": "42281202",
|
||||
"name": "itsm-count",
|
||||
"font_class": "itsm-calc",
|
||||
"unicode": "e9f4",
|
||||
"unicode_decimal": 59892
|
||||
},
|
||||
{
|
||||
"icon_id": "42270632",
|
||||
"name": "itsm-satisfaction",
|
||||
"font_class": "itsm-reports_4",
|
||||
"unicode": "e9f3",
|
||||
"unicode_decimal": 59891
|
||||
},
|
||||
{
|
||||
"icon_id": "42205149",
|
||||
"name": "veops-folder",
|
||||
"font_class": "veops-folder",
|
||||
"unicode": "e9f2",
|
||||
"unicode_decimal": 59890
|
||||
},
|
||||
{
|
||||
"icon_id": "42205128",
|
||||
"name": "veops-entire_network_",
|
||||
"font_class": "veops-entire_network_",
|
||||
"unicode": "e9f1",
|
||||
"unicode_decimal": 59889
|
||||
},
|
||||
{
|
||||
"icon_id": "42205094",
|
||||
"name": "veops-subnet",
|
||||
"font_class": "veops-subnet",
|
||||
"unicode": "e9f0",
|
||||
"unicode_decimal": 59888
|
||||
},
|
||||
{
|
||||
"icon_id": "42201912",
|
||||
"name": "veops-map_view",
|
||||
"font_class": "veops-map_view",
|
||||
"unicode": "e9ef",
|
||||
"unicode_decimal": 59887
|
||||
},
|
||||
{
|
||||
"icon_id": "42201676",
|
||||
"name": "veops-recycle",
|
||||
"font_class": "veops-recycle",
|
||||
"unicode": "e9ee",
|
||||
"unicode_decimal": 59886
|
||||
},
|
||||
{
|
||||
"icon_id": "42201586",
|
||||
"name": "veops-catalog",
|
||||
"font_class": "veops-catalog",
|
||||
"unicode": "e9ed",
|
||||
"unicode_decimal": 59885
|
||||
},
|
||||
{
|
||||
"icon_id": "42201534",
|
||||
"name": "veops-ipam",
|
||||
"font_class": "veops-ipam",
|
||||
"unicode": "e9ec",
|
||||
"unicode_decimal": 59884
|
||||
},
|
||||
{
|
||||
"icon_id": "42179262",
|
||||
"name": "cmdb-calc",
|
||||
"font_class": "cmdb-calc",
|
||||
"unicode": "e9eb",
|
||||
"unicode_decimal": 59883
|
||||
},
|
||||
{
|
||||
"icon_id": "42161413",
|
||||
"name": "ai-users",
|
||||
"font_class": "ai-users",
|
||||
"unicode": "e9ea",
|
||||
"unicode_decimal": 59882
|
||||
},
|
||||
{
|
||||
"icon_id": "42161417",
|
||||
"name": "ai-tokens",
|
||||
"font_class": "ai-tokens",
|
||||
"unicode": "e9e9",
|
||||
"unicode_decimal": 59881
|
||||
},
|
||||
{
|
||||
"icon_id": "42155223",
|
||||
"name": "oneterm-mysql",
|
||||
|
@ -1667,14 +1786,14 @@
|
|||
{
|
||||
"icon_id": "39926816",
|
||||
"name": "itsm-duration",
|
||||
"font_class": "itsm-duration",
|
||||
"font_class": "itsm-reports_3",
|
||||
"unicode": "e913",
|
||||
"unicode_decimal": 59667
|
||||
},
|
||||
{
|
||||
"icon_id": "39926833",
|
||||
"name": "itsm-workload (1)",
|
||||
"font_class": "itsm-workload",
|
||||
"font_class": "itsm-reports_2",
|
||||
"unicode": "e912",
|
||||
"unicode_decimal": 59666
|
||||
},
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,109 @@
|
|||
import { axios } from '@/utils/request'
|
||||
|
||||
export function getIPAMSubnet() {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMSubnet(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMSubnetById(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function putIPAMSubnet(id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteIPAMSubnet(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMScope(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/scope',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function putIPAMScope(id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/scope/${id}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteIPAMScope(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/scope/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMAddress(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/address',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHosts(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet/hosts',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMAddress(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/address',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHistoryOperate(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/history/operate',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHistoryScan(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/history/scan',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMStats(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/stats',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import { axios } from '@/utils/request'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
import i18n from '@/lang'
|
||||
|
||||
export function getPreference(instance = true, tree = null) {
|
||||
return axios({
|
||||
|
@ -16,10 +18,34 @@ export function getPreference2(instance = true, tree = null) {
|
|||
})
|
||||
}
|
||||
|
||||
export function getSubscribeAttributes(ciTypeId) {
|
||||
return axios({
|
||||
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
|
||||
method: 'GET'
|
||||
export function getSubscribeAttributes(ciTypeId, formatDefaultAttr = true) {
|
||||
return new Promise(async (resolve) => {
|
||||
const res = await axios({
|
||||
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (
|
||||
formatDefaultAttr &&
|
||||
res?.attributes?.length
|
||||
) {
|
||||
res.attributes.forEach((item) => {
|
||||
switch (item.name) {
|
||||
case CI_DEFAULT_ATTR.UPDATE_USER:
|
||||
item.id = item.name
|
||||
item.alias = i18n.t('cmdb.components.updater')
|
||||
break
|
||||
case CI_DEFAULT_ATTR.UPDATE_TIME:
|
||||
item.id = item.name
|
||||
item.alias = i18n.t('cmdb.components.updateTime')
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resolve(res)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -22,7 +22,7 @@
|
|||
<draggable :value="targetKeys" animation="300" @end="dragEnd" :disabled="!isSortable">
|
||||
<div
|
||||
@dblclick="changeSingleItem(item)"
|
||||
v-for="item in filteredItems"
|
||||
v-for="item in filterDefaultAttr(filteredItems)"
|
||||
:key="item.key"
|
||||
:style="{ height: '38px' }"
|
||||
>
|
||||
|
@ -54,11 +54,44 @@
|
|||
</li>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
<div
|
||||
v-if="rightDefaultAttrList.length"
|
||||
class="default-attr"
|
||||
>
|
||||
<a-divider>
|
||||
<span class="default-attr-divider">
|
||||
{{ $t('cmdb.components.default') }}
|
||||
</span>
|
||||
</a-divider>
|
||||
|
||||
<div
|
||||
v-for="(item) in rightDefaultAttrList"
|
||||
:key="item.key"
|
||||
:class="['default-attr-item', selectedKeys.includes(item.key) ? 'default-attr-item-selected' : '']"
|
||||
@click="setSelectedKeys(item)"
|
||||
@dblclick="changeSingleItem(item)"
|
||||
>
|
||||
<div
|
||||
class="default-attr-arrow"
|
||||
style="left: 17px"
|
||||
@click.stop="changeSingleItem(item)"
|
||||
>
|
||||
<a-icon type="left" />
|
||||
</div>
|
||||
<div class="default-attr-title">
|
||||
{{ $t(item.title) }}
|
||||
</div>
|
||||
<div class="default-attr-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="direction === 'left'" class="ant-transfer-list-content">
|
||||
<div
|
||||
@dblclick="changeSingleItem(item)"
|
||||
v-for="item in filteredItems"
|
||||
v-for="item in filterDefaultAttr(filteredItems)"
|
||||
:key="item.key"
|
||||
:style="{ height: '38px' }"
|
||||
>
|
||||
|
@ -82,6 +115,39 @@
|
|||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="leftDefaultAttrList.length"
|
||||
class="default-attr"
|
||||
>
|
||||
<a-divider>
|
||||
<span class="default-attr-divider">
|
||||
{{ $t('cmdb.components.default') }}
|
||||
</span>
|
||||
</a-divider>
|
||||
|
||||
<div
|
||||
v-for="(item) in leftDefaultAttrList"
|
||||
:key="item.key"
|
||||
:class="['default-attr-item', selectedKeys.includes(item.key) ? 'default-attr-item-selected' : '']"
|
||||
@click="setSelectedKeys(item)"
|
||||
@dblclick="changeSingleItem(item)"
|
||||
>
|
||||
<div
|
||||
class="default-attr-arrow"
|
||||
style="left: 2px"
|
||||
@click.stop="changeSingleItem(item)"
|
||||
>
|
||||
<a-icon type="right" />
|
||||
</div>
|
||||
<div class="default-attr-title">
|
||||
{{ $t(item.title) }}
|
||||
</div>
|
||||
<div class="default-attr-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-transfer>
|
||||
|
@ -95,6 +161,7 @@
|
|||
import _ from 'lodash'
|
||||
import draggable from 'vuedraggable'
|
||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
|
||||
export default {
|
||||
name: 'AttributesTransfer',
|
||||
|
@ -130,10 +197,41 @@ export default {
|
|||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
showDefaultAttr: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedKeys: [],
|
||||
defaultAttrList: [
|
||||
{
|
||||
title: 'cmdb.components.updater',
|
||||
name: 'updater',
|
||||
key: CI_DEFAULT_ATTR.UPDATE_USER
|
||||
},
|
||||
{
|
||||
title: 'cmdb.components.updateTime',
|
||||
name: 'update time',
|
||||
key: CI_DEFAULT_ATTR.UPDATE_TIME
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rightDefaultAttrList() {
|
||||
if (!this.showDefaultAttr) {
|
||||
return []
|
||||
}
|
||||
return this.defaultAttrList.filter((item) => this.targetKeys.includes(item.key))
|
||||
},
|
||||
|
||||
leftDefaultAttrList() {
|
||||
if (!this.showDefaultAttr) {
|
||||
return []
|
||||
}
|
||||
return this.defaultAttrList.filter((item) => !this.targetKeys.includes(item.key))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -216,6 +314,10 @@ export default {
|
|||
}
|
||||
this.$emit('setFixedList', _fixedList)
|
||||
},
|
||||
|
||||
filterDefaultAttr(list) {
|
||||
return this.showDefaultAttr ? list.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.key)) : list
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -296,5 +398,67 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.default-attr {
|
||||
.ant-divider {
|
||||
margin: 7px 0;
|
||||
padding: 0 15px;
|
||||
|
||||
.ant-divider-inner-text {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-divider {
|
||||
font-size: 12px;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
color: rgb(163, 163, 163);
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
background-color: #fff;
|
||||
color: @primary-color;
|
||||
border-radius: 4px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
padding-left: 34px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
position: relative;
|
||||
border-left: solid 2px transparent;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&-selected {
|
||||
background-color: #f0f5ff;
|
||||
border-color: #2f54eb;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f5ff;
|
||||
|
||||
.default-attr-arrow {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -129,6 +129,8 @@ import Treeselect from '@riophae/vue-treeselect'
|
|||
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
import { getCITypeGroups } from '../../api/ciTypeGroup'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
|
||||
export default {
|
||||
name: 'SearchForm',
|
||||
components: { MetadataDrawer, FilterComp, Treeselect },
|
||||
|
@ -176,7 +178,9 @@ export default {
|
|||
return '200px'
|
||||
},
|
||||
canSearchPreferenceAttrList() {
|
||||
return this.preferenceAttrList.filter((item) => item.value_type !== '6')
|
||||
return this.preferenceAttrList.filter((item) => {
|
||||
return item.value_type !== '6' && ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.name)
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
:fixedList="fixedList"
|
||||
@setFixedList="setFixedList"
|
||||
:height="windowHeight - 170"
|
||||
:showDefaultAttr="true"
|
||||
/>
|
||||
<div class="custom-drawer-bottom-action">
|
||||
<a-button @click="subInstanceSubmit" type="primary">{{ $t('cmdb.preference.sub') }}</a-button>
|
||||
|
@ -64,7 +65,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="cmdb-subscribe-drawer-tree-main" :style="{ maxHeight: `${((windowHeight - 170) * 2) / 3}px` }">
|
||||
<div @click="changeTreeViews(attr)" v-for="attr in attrList" :key="attr.name">
|
||||
<div @click="changeTreeViews(attr)" v-for="attr in treeViewAttrList" :key="attr.name">
|
||||
<a-checkbox :checked="treeViews.includes(attr.name)" />
|
||||
{{ attr.title }}
|
||||
</div>
|
||||
|
@ -90,6 +91,8 @@ import {
|
|||
} from '@/modules/cmdb/api/preference'
|
||||
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import AttributesTransfer from '../attributesTransfer'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
|
||||
export default {
|
||||
name: 'SubscribeSetting',
|
||||
components: { AttributesTransfer },
|
||||
|
@ -110,16 +113,32 @@ export default {
|
|||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
treeViewAttrList() {
|
||||
return this.attrList.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.name))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(ciType = {}, activeKey = '1') {
|
||||
this.ciType = ciType
|
||||
this.activeKey = activeKey
|
||||
const updatedByKey = CI_DEFAULT_ATTR.UPDATE_USER
|
||||
const updatedAtKey = CI_DEFAULT_ATTR.UPDATE_TIME
|
||||
|
||||
getCITypeAttributesByName(ciType.type_id).then((res) => {
|
||||
const attributes = res.attributes
|
||||
const attributes = res.attributes.filter((item) => ![updatedByKey, updatedAtKey].includes(item.name))
|
||||
|
||||
;[updatedByKey, updatedAtKey].map((key) => {
|
||||
attributes.push({
|
||||
alias: key,
|
||||
name: key,
|
||||
id: key
|
||||
})
|
||||
})
|
||||
|
||||
getSubscribeAttributes(ciType.type_id).then((_res) => {
|
||||
this.instanceSubscribed = _res.is_subscribed
|
||||
const selectedAttrList = _res.attributes.map((item) => item.id.toString())
|
||||
|
||||
const attrList = attributes.map((item) => {
|
||||
return {
|
||||
key: item.id.toString(),
|
||||
|
@ -188,9 +207,20 @@ export default {
|
|||
})
|
||||
},
|
||||
subInstanceSubmit() {
|
||||
const customAttr = []
|
||||
const defaultAttr = []
|
||||
this.selectedAttrList.map((attr) => {
|
||||
if ([CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(attr)) {
|
||||
defaultAttr.push(attr)
|
||||
} else {
|
||||
customAttr.push(attr)
|
||||
}
|
||||
})
|
||||
const selectedAttrList = [...customAttr, ...defaultAttr]
|
||||
|
||||
subscribeCIType(
|
||||
this.ciType.type_id,
|
||||
this.selectedAttrList.map((item) => {
|
||||
selectedAttrList.map((item) => {
|
||||
return [item, !!this.fixedList.includes(item)]
|
||||
})
|
||||
).then((res) => {
|
||||
|
|
|
@ -24,7 +24,8 @@ const cmdb_en = {
|
|||
operationHistory: 'Operation Audit',
|
||||
relationType: 'Relation Type',
|
||||
ad: 'AutoDiscovery',
|
||||
cidetail: 'CI Detail'
|
||||
cidetail: 'CI Detail',
|
||||
scene: 'Scene'
|
||||
},
|
||||
ciType: {
|
||||
ciType: 'CIType',
|
||||
|
@ -377,6 +378,9 @@ const cmdb_en = {
|
|||
param: 'Parameter{param}',
|
||||
value: 'Value{value}',
|
||||
clear: 'Clear',
|
||||
updater: 'Update User',
|
||||
updateTime: 'Update Time',
|
||||
default: 'Default'
|
||||
},
|
||||
batch: {
|
||||
downloadFailed: 'Download failed',
|
||||
|
@ -757,6 +761,83 @@ if __name__ == "__main__":
|
|||
conditionName: 'Condition Name',
|
||||
path: 'Path',
|
||||
expandCondition: 'Expand Condition',
|
||||
},
|
||||
ipam: {
|
||||
overview: 'Overview',
|
||||
addressAssign: 'Address Assign',
|
||||
ipSearch: 'IP Search',
|
||||
subnetList: 'Subnet List',
|
||||
history: 'History Log',
|
||||
ticket: 'Related Tickets',
|
||||
addSubnet: 'Add Subnet',
|
||||
editSubnet: 'Edit Subnet',
|
||||
addCatalog: 'Add Catalog',
|
||||
editCatalog: 'Edit Catalog',
|
||||
catalogName: 'Catalog Name',
|
||||
editName: 'Edit Name',
|
||||
editNode: 'Edit Node',
|
||||
deleteNode: 'Delete Node',
|
||||
basicInfo: 'Basic Info',
|
||||
scanRule: 'Scan Rule',
|
||||
adExecTarget: 'Execute targets',
|
||||
masterMachineTip: 'The machine where OneMaster is installed',
|
||||
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x ID',
|
||||
selectFromCMDBTips: 'Select from CMDB',
|
||||
adInterval: 'Collection frequency',
|
||||
cronTips: 'The format is the same as crontab, for example: 0 15 * * 1-5',
|
||||
masterMachine: 'Master machine',
|
||||
specifyMachine: 'Specify machine',
|
||||
specifyMachineTips: 'Please fill in the specify machine!',
|
||||
cronRequiredTip: 'Acquisition frequency cannot be null',
|
||||
addressNullTip: ' Please select the leaf node of the left subnet tree first',
|
||||
addressNullTip2: 'Subnet prefix length must be >= 16',
|
||||
assignedOnline: 'Assigned Online',
|
||||
assignedOffline: 'Assigned Offline',
|
||||
unassignedOnline: 'Unassigned Online',
|
||||
unused: 'Unused',
|
||||
allStatus: 'All Status',
|
||||
editAssignAddress: 'Edit Assign Address',
|
||||
assign: 'Assign',
|
||||
recycle: 'Recycle',
|
||||
assignStatus: 'Assign Status',
|
||||
reserved: 'Reserved',
|
||||
assigned: 'Assigned',
|
||||
recycleTip: 'Confirmed for recycle? After recycling, the status of the segment will be changed to unassigned.',
|
||||
recycleSuccess: '{ip} Recycled successfully, status changed to: unassigned.',
|
||||
operationLog: 'Operation Log',
|
||||
scanLog: 'Scan Log',
|
||||
updateCatalog: 'Update Catalog',
|
||||
deleteCatalog: 'Delete Catalog',
|
||||
updateSubnet: 'Update Subnet',
|
||||
deleteSubnet: 'Delete Subnet',
|
||||
revokeAddress: 'Revoke Address',
|
||||
operateTime: 'Operate Time',
|
||||
operateUser: 'Operate User',
|
||||
operateType: 'Operate Type',
|
||||
subnet: 'Subnet',
|
||||
description: 'Description',
|
||||
ipNumber: 'Number of online IP',
|
||||
startTime: 'Start Time',
|
||||
endTime: 'End Time',
|
||||
scanningTime: 'Scanning Time',
|
||||
viewResult: 'View Result',
|
||||
scannedIP: 'Scanned IP',
|
||||
subnetStats: 'Subnet Stats',
|
||||
addressStats: 'Address Stats',
|
||||
onlineStats: 'Online Stats',
|
||||
assignStats: 'Assign Stats',
|
||||
total: 'Total',
|
||||
free: 'Free',
|
||||
unassigned: 'Unassigned',
|
||||
online: 'Online',
|
||||
offline: 'Offline',
|
||||
onlineUsageStats: 'Subnet Online Stats',
|
||||
subnetName: 'Subnet Name',
|
||||
addressCount: 'Address Count',
|
||||
onlineRatio: 'Online Ratio',
|
||||
scanEnable: 'Scan Enable',
|
||||
lastScanTime: 'Last Scan Time',
|
||||
isSuccess: 'Is Success'
|
||||
}
|
||||
}
|
||||
export default cmdb_en
|
||||
|
|
|
@ -24,7 +24,8 @@ const cmdb_zh = {
|
|||
operationHistory: '操作审计',
|
||||
relationType: '关系类型',
|
||||
ad: '自动发现',
|
||||
cidetail: 'CI 详情'
|
||||
cidetail: 'CI 详情',
|
||||
scene: '场景'
|
||||
},
|
||||
ciType: {
|
||||
ciType: '模型',
|
||||
|
@ -377,6 +378,9 @@ const cmdb_zh = {
|
|||
param: '参数{param}',
|
||||
value: '值{value}',
|
||||
clear: '清空',
|
||||
updater: '更新人',
|
||||
updateTime: '更新时间',
|
||||
default: '默认'
|
||||
},
|
||||
batch: {
|
||||
downloadFailed: '失败下载',
|
||||
|
@ -756,6 +760,83 @@ if __name__ == "__main__":
|
|||
conditionName: '条件命名',
|
||||
path: '路径',
|
||||
expandCondition: '展开条件',
|
||||
},
|
||||
ipam: {
|
||||
overview: '概览',
|
||||
addressAssign: '地址分配',
|
||||
ipSearch: 'IP查询',
|
||||
subnetList: '子网列表',
|
||||
history: '历史记录',
|
||||
ticket: '关联工单',
|
||||
addSubnet: '新增子网',
|
||||
editSubnet: '编辑子网',
|
||||
addCatalog: '新增目录',
|
||||
editCatalog: '修改目录',
|
||||
catalogName: '目录名称',
|
||||
editName: '修改名称',
|
||||
editNode: '修改节点',
|
||||
deleteNode: '删除节点',
|
||||
basicInfo: '基本信息',
|
||||
scanRule: '扫描规则',
|
||||
adExecTarget: '执行机器',
|
||||
masterMachineTip: '安装OneMaster的所在机器',
|
||||
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
|
||||
selectFromCMDBTips: '从CMDB中选择',
|
||||
adInterval: '采集频率',
|
||||
cronTips: '格式同crontab, 例如:0 15 * * 1-5',
|
||||
masterMachine: 'Master机器',
|
||||
specifyMachine: '指定机器',
|
||||
specifyMachineTips: '请填写指定机器!',
|
||||
cronRequiredTip: '采集频率不能为空',
|
||||
addressNullTip: '请先选择左侧子网树的叶子节点',
|
||||
addressNullTip2: '子网前缀长度必须 >= 16',
|
||||
assignedOnline: '已分配在线',
|
||||
assignedOffline: '已分配离线',
|
||||
unassignedOnline: '未分配在线',
|
||||
unused: '空闲',
|
||||
allStatus: '全部状态',
|
||||
editAssignAddress: '编辑分配地址',
|
||||
assign: '分配',
|
||||
recycle: '回收',
|
||||
assignStatus: '分配状态',
|
||||
reserved: '预留',
|
||||
assigned: '已分配',
|
||||
recycleTip: '确认要回收吗?回收后该网段状态变更为:未分配',
|
||||
recycleSuccess: '{ip} 回收成功,状态变更为: 未分配',
|
||||
operationLog: '操作记录',
|
||||
scanLog: '扫描记录',
|
||||
updateCatalog: '更新目录',
|
||||
deleteCatalog: '删除目录',
|
||||
updateSubnet: '修改子网',
|
||||
deleteSubnet: '删除子网',
|
||||
revokeAddress: '地址回收',
|
||||
operateTime: '操作时间',
|
||||
operateUser: '操作人',
|
||||
operateType: '操作类型',
|
||||
subnet: '子网',
|
||||
description: '描述',
|
||||
ipNumber: '在线IP地址数',
|
||||
startTime: '开始时间',
|
||||
endTime: '结束时间',
|
||||
scanningTime: '扫描耗时',
|
||||
viewResult: '查看结果',
|
||||
scannedIP: '已扫描的IP',
|
||||
subnetStats: '子网统计',
|
||||
addressStats: '地址数统计',
|
||||
onlineStats: '在线统计',
|
||||
assignStats: '分配统计',
|
||||
total: '总数',
|
||||
free: '空闲',
|
||||
unassigned: '未分配',
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
onlineUsageStats: '子网在线统计',
|
||||
subnetName: '子网名称',
|
||||
addressCount: '地址数',
|
||||
onlineRatio: '在线率',
|
||||
scanEnable: '是否扫描',
|
||||
lastScanTime: '最后扫描时间',
|
||||
isSuccess: '是否成功'
|
||||
}
|
||||
}
|
||||
export default cmdb_zh
|
||||
|
|
|
@ -70,6 +70,17 @@ const genCmdbRoutes = async () => {
|
|||
meta: { title: 'cmdb.menu.cidetail', keepAlive: false },
|
||||
component: () => import('../views/ci/ciDetailPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/cmdb/disabled4',
|
||||
name: 'cmdb_disabled4',
|
||||
meta: { title: 'cmdb.menu.scene', appName: 'cmdb', disabled: true, permission: ['admin', 'cmdb_admin'] },
|
||||
},
|
||||
{
|
||||
path: '/cmdb/ipam',
|
||||
component: () => import('../views/ipam'),
|
||||
name: 'cmdb_ipam',
|
||||
meta: { title: 'IPAM', appName: 'cmdb', icon: 'veops-ipam', selectedIcon: 'veops-ipam', keepAlive: false, permission: ['admin', 'cmdb_admin'] }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/disabled2',
|
||||
name: 'cmdb_disabled2',
|
||||
|
|
|
@ -33,3 +33,8 @@ export const defautValueColor = [
|
|||
]
|
||||
|
||||
export const defaultBGColors = ['#ffccc7', '#ffd8bf', '#ffe7ba', '#fff1b8', '#d9f7be', '#b5f5ec', '#bae7ff', '#d6e4ff', '#efdbff', '#ffd6e7']
|
||||
|
||||
export const CI_DEFAULT_ATTR = {
|
||||
UPDATE_USER: '_updated_by',
|
||||
UPDATE_TIME: '_updated_at'
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
import _ from 'lodash'
|
||||
import XLSX from 'xlsx'
|
||||
import XLSXS from 'xlsx-js-style'
|
||||
import { CI_DEFAULT_ATTR } from './const.js'
|
||||
import i18n from '@/lang'
|
||||
|
||||
export function sum(arr) {
|
||||
if (!arr.length) {
|
||||
return 0
|
||||
|
@ -49,7 +52,10 @@ export function getCITableColumns(data, attrList, width = 1600, height) {
|
|||
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
|
||||
const columns = []
|
||||
for (let attr of _attrList) {
|
||||
const editRender = { name: 'input' }
|
||||
const editRender = {
|
||||
name: 'input',
|
||||
enabled: !attr.is_computed && !attr.sys_computed
|
||||
}
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
editRender['props'] = { 'type': 'float' }
|
||||
|
@ -83,13 +89,35 @@ export function getCITableColumns(data, attrList, width = 1600, height) {
|
|||
delete editRender.props
|
||||
|
||||
}
|
||||
|
||||
let title = attr.alias || attr.name
|
||||
let sortable = !!attr.is_sortable
|
||||
let attr_id = attr.id
|
||||
|
||||
if ([CI_DEFAULT_ATTR.UPDATE_TIME, CI_DEFAULT_ATTR.UPDATE_USER].includes(attr.name)) {
|
||||
editRender.enabled = false
|
||||
attr_id = attr.name
|
||||
|
||||
switch (attr.name) {
|
||||
case CI_DEFAULT_ATTR.UPDATE_USER:
|
||||
title = i18n.t('cmdb.components.updater')
|
||||
break;
|
||||
case CI_DEFAULT_ATTR.UPDATE_TIME:
|
||||
title = i18n.t('cmdb.components.updateTime')
|
||||
sortable = true
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
columns.push({
|
||||
attr_id: attr.id,
|
||||
attr_id,
|
||||
editRender,
|
||||
title: attr.alias || attr.name,
|
||||
title,
|
||||
field: attr.name,
|
||||
value_type: attr.value_type,
|
||||
sortable: !!attr.is_sortable,
|
||||
sortable,
|
||||
filters: attr.is_choice ? attr.choice_value : null,
|
||||
choice_builtin: null,
|
||||
width: Math.min(Math.max(100, ...data.map(item => strLength(item[attr.name]))), 350),
|
||||
|
|
|
@ -259,7 +259,7 @@ export default {
|
|||
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
|
||||
await getCITypeGroupById(this.typeId).then((res1) => {
|
||||
const _attributesByGroup = res1.map((g) => {
|
||||
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
|
||||
g.attributes = g.attributes.filter((attr) => !attr.is_computed && !attr.sys_computed)
|
||||
return g
|
||||
})
|
||||
const attrHasGroupIds = []
|
||||
|
@ -268,7 +268,7 @@ export default {
|
|||
attrHasGroupIds.push(...id)
|
||||
})
|
||||
const otherGroupAttr = this.attributeList.filter(
|
||||
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
|
||||
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed && !attr.sys_computed
|
||||
)
|
||||
if (otherGroupAttr.length) {
|
||||
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<a v-if="!isEdit && !attr.is_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
|
||||
<a v-if="!isEdit && !attr.is_computed && !attr.sys_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
|
||||
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
||||
</span>
|
||||
</template>
|
||||
|
@ -267,7 +267,7 @@ export default {
|
|||
async handleCloseEdit() {
|
||||
const newData = this.form.getFieldValue(this.attr.name)
|
||||
if (!_.isEqual(this.ci[this.attr.name], newData)) {
|
||||
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData })
|
||||
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData ?? null })
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('updateSuccess'))
|
||||
this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
:disabled="!canEdit[parent.id]"
|
||||
@click="
|
||||
() => {
|
||||
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, parent.id, 'parents')
|
||||
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, parent, 'parents')
|
||||
}
|
||||
"
|
||||
><a-icon
|
||||
|
@ -76,7 +76,7 @@
|
|||
:disabled="!canEdit[child.id]"
|
||||
@click="
|
||||
() => {
|
||||
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, child.id, 'children')
|
||||
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, child, 'children')
|
||||
}
|
||||
"
|
||||
><a-icon
|
||||
|
|
|
@ -78,20 +78,24 @@ export default {
|
|||
},
|
||||
})
|
||||
this.canvas.setZoomable(true, true)
|
||||
|
||||
this.canvas.on('events', ({ type, data }) => {
|
||||
const sourceNode = data?.id || null
|
||||
if (type === 'custom:clickLeft') {
|
||||
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=1&count=10000`).then((res) => {
|
||||
this.redrawData(res, sourceNode, 'left')
|
||||
})
|
||||
this.debounceClick(sourceNode, 1)
|
||||
}
|
||||
if (type === 'custom:clickRight') {
|
||||
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=0&count=10000`).then((res) => {
|
||||
this.redrawData(res, sourceNode, 'right')
|
||||
})
|
||||
this.debounceClick(sourceNode, 0)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
debounceClick: _.debounce(function(sourceNode, reverse) {
|
||||
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=${reverse}&count=10000`).then((res) => {
|
||||
this.redrawData(res, sourceNode, reverse === 1 ? 'left' : 'right')
|
||||
})
|
||||
}, 300),
|
||||
|
||||
setTopoData(data) {
|
||||
const root = document.getElementById('ci-detail-relation-topo')
|
||||
if (root && root?.innerHTML) {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<AttributesTransfer
|
||||
:dataSource="attrList"
|
||||
:targetKeys="selectedAttrList"
|
||||
:showDefaultAttr="true"
|
||||
@setTargetKeys="setTargetKeys"
|
||||
@changeSingleItem="changeSingleItem"
|
||||
@handleSubmit="handleSubmit"
|
||||
|
@ -23,6 +24,8 @@
|
|||
import AttributesTransfer from '../../../components/attributesTransfer'
|
||||
import { subscribeCIType, getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
|
||||
export default {
|
||||
name: 'EditAttrsPopover',
|
||||
components: { AttributesTransfer },
|
||||
|
@ -48,8 +51,19 @@ export default {
|
|||
}
|
||||
},
|
||||
getAttrs() {
|
||||
const updatedByKey = CI_DEFAULT_ATTR.UPDATE_USER
|
||||
const updatedAtKey = CI_DEFAULT_ATTR.UPDATE_TIME
|
||||
|
||||
getCITypeAttributesByName(this.typeId).then((res) => {
|
||||
const attributes = res.attributes
|
||||
const attributes = res.attributes.filter((item) => ![updatedByKey, updatedAtKey].includes(item.name))
|
||||
;[updatedByKey, updatedAtKey].map((key) => {
|
||||
attributes.push({
|
||||
alias: key,
|
||||
name: key,
|
||||
id: key
|
||||
})
|
||||
})
|
||||
|
||||
getSubscribeAttributes(this.typeId).then((_res) => {
|
||||
const selectedAttrList = _res.attributes.map((item) => item.id.toString())
|
||||
|
||||
|
@ -71,12 +85,23 @@ export default {
|
|||
|
||||
handleSubmit() {
|
||||
if (this.selectedAttrList.length) {
|
||||
const customAttr = []
|
||||
const defaultAttr = []
|
||||
this.selectedAttrList.map((attr) => {
|
||||
if ([CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(attr)) {
|
||||
defaultAttr.push(attr)
|
||||
} else {
|
||||
customAttr.push(attr)
|
||||
}
|
||||
})
|
||||
const selectedAttrList = [...customAttr, ...defaultAttr]
|
||||
|
||||
subscribeCIType(
|
||||
this.typeId,
|
||||
this.selectedAttrList.map((item) => {
|
||||
selectedAttrList.map((item) => {
|
||||
return [item, !!this.fixedList.includes(item)]
|
||||
})
|
||||
).then((res) => {
|
||||
).then(() => {
|
||||
this.$message.success(this.$t('cmdb.components.subSuccess'))
|
||||
this.visible = false
|
||||
this.$emit('refresh')
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="$t(actionType === 'edit' ? 'cmdb.ipam.editCatalog' : 'cmdb.ipam.addCatalog')"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form-model
|
||||
ref="catelogFormRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 19 }"
|
||||
>
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ipam.catalogName')"
|
||||
prop="name"
|
||||
>
|
||||
<a-input
|
||||
:placeholder="$t('placeholder1')"
|
||||
v-model="form.name"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { postIPAMScope, putIPAMScope } from '@/modules/cmdb/api/ipam.js'
|
||||
|
||||
export default {
|
||||
name: 'CatalogForm',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
nodeId: null,
|
||||
actionType: 'create',
|
||||
form: {
|
||||
name: ''
|
||||
},
|
||||
formRules: {
|
||||
name: [
|
||||
{
|
||||
required: true, message: this.$t('placeholder1')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open({
|
||||
nodeId,
|
||||
type,
|
||||
name
|
||||
}) {
|
||||
this.nodeId = nodeId || null
|
||||
this.actionType = type || 'create'
|
||||
this.form.name = name || ''
|
||||
this.visible = true
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.visible = false
|
||||
this.form.name = ''
|
||||
this.actionType = 'create'
|
||||
this.nodeId = null
|
||||
|
||||
this.$refs.catelogFormRef.clearValidate()
|
||||
},
|
||||
|
||||
handleOk() {
|
||||
this.$refs.catelogFormRef.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.actionType === 'edit') {
|
||||
await putIPAMScope(this.nodeId, {
|
||||
name: this.form.name
|
||||
})
|
||||
this.$message.success(this.$t('editSuccess'))
|
||||
} else {
|
||||
await postIPAMScope({
|
||||
parent_id: this.nodeId,
|
||||
name: this.form.name
|
||||
})
|
||||
this.$message.success(this.$t('addSuccess'))
|
||||
}
|
||||
|
||||
this.$emit('ok')
|
||||
this.handleCancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -0,0 +1,317 @@
|
|||
<template>
|
||||
<div class="ipam-tree">
|
||||
<a-input
|
||||
v-model="searchValue"
|
||||
class="ipam-tree-search"
|
||||
:placeholder="$t('placeholder1')"
|
||||
/>
|
||||
|
||||
<div class="ipam-tree-main">
|
||||
<a-tree
|
||||
v-if="treeData.length"
|
||||
:treeData="filterTreeData"
|
||||
:selectedKeys="treeKey ? [treeKey] : []"
|
||||
:defaultExpandedKeys="treeKey ? [treeKey] : []"
|
||||
>
|
||||
<template #title="treeNodeData">
|
||||
<div
|
||||
class="ipam-tree-node"
|
||||
@click="clickTreeNode(treeNodeData)"
|
||||
>
|
||||
<ops-icon
|
||||
:type="treeNodeData.icon"
|
||||
class="ipam-tree-node-icon"
|
||||
:style="{ color: treeNodeData.iconColor }"
|
||||
/>
|
||||
<a-tooltip :title="treeNodeData.title">
|
||||
<span
|
||||
class="ipam-tree-node-title"
|
||||
:style="{
|
||||
color: treeKey === treeNodeData.key ? '#2F54EB' : ''
|
||||
}"
|
||||
>
|
||||
{{ treeNodeData.title }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<div class="ipam-tree-node-right">
|
||||
<span
|
||||
v-if="(treeNodeData.key === 'all' && treeNodeData.count) || (treeNodeData.key !== 'all' && treeNodeData.children && treeNodeData.children.length && treeNodeData.count)"
|
||||
class="ipam-tree-node-count"
|
||||
>
|
||||
{{ treeNodeData.count }}
|
||||
</span>
|
||||
|
||||
<a-dropdown :getPopupContainer="(trigger) => trigger">
|
||||
<a class="ipam-tree-node-action">
|
||||
<ops-icon type="veops-more" />
|
||||
</a>
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item
|
||||
v-if="treeNodeData.showCatalogBtn"
|
||||
@click="openCatalogForm(treeNodeData, 'create')"
|
||||
>
|
||||
<ops-icon type="veops-catalog" />
|
||||
{{ $t('cmdb.ipam.addCatalog') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
v-if="treeNodeData.showSubnetBtn"
|
||||
@click="openSubnetForm(treeNodeData, 'create')"
|
||||
>
|
||||
<ops-icon type="veops-increase" />
|
||||
{{ $t('cmdb.ipam.addSubnet') }}
|
||||
</a-menu-item>
|
||||
|
||||
<template v-if="treeNodeData.key !== 'all'">
|
||||
<a-menu-item
|
||||
v-if="!treeNodeData.isSubnet"
|
||||
@click="openCatalogForm(treeNodeData, 'edit')"
|
||||
>
|
||||
<ops-icon type="veops-edit" />
|
||||
{{ $t('cmdb.ipam.editName') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
v-if="treeNodeData.isSubnet"
|
||||
@click="openSubnetForm(treeNodeData, 'edit')"
|
||||
>
|
||||
<ops-icon type="veops-edit" />
|
||||
{{ $t('cmdb.ipam.editNode') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteNode(treeNodeData)">
|
||||
<ops-icon type="veops-delete" />
|
||||
{{ $t('cmdb.ipam.deleteNode') }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-tree>
|
||||
</div>
|
||||
|
||||
<SubnetForm
|
||||
ref="subnetFormRef"
|
||||
:subnetCIType="subnetCIType"
|
||||
@ok="refreshData"
|
||||
/>
|
||||
|
||||
<CatalogForm
|
||||
ref="catalogFormRef"
|
||||
@ok="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { deleteIPAMSubnet, deleteIPAMScope } from '@/modules/cmdb/api/ipam.js'
|
||||
|
||||
import SubnetForm from './subnetForm.vue'
|
||||
import CatalogForm from './catalogForm.vue'
|
||||
|
||||
export default {
|
||||
name: 'IPAMTree',
|
||||
components: {
|
||||
SubnetForm,
|
||||
CatalogForm
|
||||
},
|
||||
props: {
|
||||
treeData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
treeKey: {
|
||||
type: [String, Number],
|
||||
default: () => ''
|
||||
},
|
||||
subnetCIType: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchValue: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterTreeData() {
|
||||
if (this.searchValue) {
|
||||
let treeData = _.cloneDeep(this.treeData)
|
||||
treeData = treeData.filter((data) => {
|
||||
return this.handleTreeDataBySearch(data)
|
||||
})
|
||||
return treeData
|
||||
}
|
||||
|
||||
return this.treeData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTreeDataBySearch(data) {
|
||||
const isMatch = data?.title?.indexOf?.(this.searchValue) !== -1
|
||||
if (!data?.children?.length) {
|
||||
return isMatch ? data : null
|
||||
}
|
||||
|
||||
data.children = data.children.filter((data) => {
|
||||
return this.handleTreeDataBySearch(data)
|
||||
})
|
||||
return isMatch || data.children.length ? data : null
|
||||
},
|
||||
|
||||
openCatalogForm(node, type) {
|
||||
const nodeId = node?.key && node?.key !== 'all' ? node.key : null
|
||||
const name = type === 'edit' ? (node?.title || '') : ''
|
||||
|
||||
this.$refs.catalogFormRef.open({
|
||||
nodeId,
|
||||
type,
|
||||
name
|
||||
})
|
||||
},
|
||||
|
||||
openSubnetForm(node, type) {
|
||||
const nodeId = node?.key && node?.key !== 'all' ? node.key : null
|
||||
const parentId = node?.parentId || null
|
||||
|
||||
this.$refs.subnetFormRef.open(nodeId, type, parentId)
|
||||
},
|
||||
|
||||
deleteNode(node) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('confirmDelete'),
|
||||
onOk: async () => {
|
||||
if (node.isSubnet) {
|
||||
await deleteIPAMSubnet(node.key)
|
||||
} else {
|
||||
await deleteIPAMScope(node.key)
|
||||
}
|
||||
|
||||
if (node.key === this.treeKey) {
|
||||
this.$emit('updateTreeKey', 'all')
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.refreshData()
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
this.$emit('refreshData')
|
||||
},
|
||||
|
||||
clickTreeNode(node) {
|
||||
this.$emit('updateTreeKey', node.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ipam-tree {
|
||||
width: 100%;
|
||||
|
||||
&-search {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
&-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/deep/ .ant-tree {
|
||||
.ant-tree-node-content-wrapper {
|
||||
width: calc(100% - 24px);
|
||||
padding: 0px;
|
||||
display: inline-block;
|
||||
height: fit-content;
|
||||
|
||||
.ant-tree-title {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
& > li:first-child {
|
||||
.ant-tree-switcher {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-tree-node-content-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tree-switcher-icon {
|
||||
color: #CACDD9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-left: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-count {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: #A5A9BC;
|
||||
}
|
||||
|
||||
&-action {
|
||||
display: none;
|
||||
margin-left: 3px;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
/deep/ .ant-dropdown-menu {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/deep/ .ant-dropdown-menu-item {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ipam-tree-node-action {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,445 @@
|
|||
<template>
|
||||
<CustomDrawer
|
||||
width="800px"
|
||||
:title="$t(actionType === 'edit' ? 'cmdb.ipam.editSubnet' : 'cmdb.ipam.addSubnet')"
|
||||
:visible="visible"
|
||||
:bodyStyle="{ height: 'calc(-108px + 100vh)' }"
|
||||
@close="handleClose"
|
||||
>
|
||||
<a-form-model
|
||||
ref="subnetFormRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 17 }"
|
||||
>
|
||||
<div
|
||||
v-for="(group, groupIndex) in basicFormGroup"
|
||||
:key="groupIndex"
|
||||
>
|
||||
<div class="subnet-form-title">
|
||||
{{ group.name }}
|
||||
</div>
|
||||
|
||||
<a-form-model-item
|
||||
v-for="(attr) in group.attributes"
|
||||
:key="attr.name"
|
||||
:label="attr.alias || attr.name"
|
||||
:prop="attr.name"
|
||||
>
|
||||
<CIReferenceAttr
|
||||
v-if="attr.is_reference"
|
||||
:referenceTypeId="attr.reference_type_id"
|
||||
:isList="attr.is_list"
|
||||
:referenceShowAttrName="attr.showAttrName"
|
||||
:initSelectOption="getInitReferenceSelectOption(attr)"
|
||||
v-model="form[attr.name]"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="attr.is_choice"
|
||||
v-model="form[attr.name]"
|
||||
:mode="attr.is_list ? 'multiple' : 'default'"
|
||||
showSearch
|
||||
allowClear
|
||||
>
|
||||
<a-icon slot="suffixIcon" type="caret-down" />
|
||||
<a-select-option
|
||||
v-for="(choiceItem, choiceIndex) in attr.selectOption"
|
||||
:key="choiceIndex"
|
||||
:value="choiceItem.value"
|
||||
>
|
||||
{{ choiceItem.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-switch
|
||||
v-else-if="attr.is_bool"
|
||||
v-model="form[attr.name]"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
:placeholder="$t('placeholder1')"
|
||||
v-model="form[attr.name]"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
</div>
|
||||
|
||||
<div class="subnet-form-title">
|
||||
<a-row>
|
||||
<a-col :span="4">
|
||||
{{ $t('cmdb.ipam.scanRule') }}
|
||||
</a-col>
|
||||
<a-switch v-model="form.scan_enabled" />
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<template v-if="form.scan_enabled">
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ipam.adExecTarget')"
|
||||
>
|
||||
<CustomRadio
|
||||
v-model="agentType"
|
||||
:radioList="agentTypeRadioList"
|
||||
>
|
||||
<span
|
||||
v-show="agentType === 'master'"
|
||||
slot="extra_master"
|
||||
class="subnet-form-agent-tip"
|
||||
>
|
||||
{{ $t('cmdb.ipam.masterMachineTip') }}
|
||||
</span>
|
||||
<a-input
|
||||
v-show="agentType === 'agent_id'"
|
||||
slot="extra_agent_id"
|
||||
:style="{ width: '300px' }"
|
||||
:placeholder="$t('cmdb.ipam.oneagentIdTips')"
|
||||
v-model="agentId"
|
||||
/>
|
||||
</CustomRadio>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ipam.adInterval')"
|
||||
>
|
||||
<el-popover
|
||||
v-model="cronVisible"
|
||||
trigger="click"
|
||||
>
|
||||
<template slot>
|
||||
<Vcrontab
|
||||
ref="cronTab"
|
||||
:hideComponent="['second', 'year']"
|
||||
:expression="form.cron"
|
||||
:hasFooter="true"
|
||||
@fill="crontabFill"
|
||||
@hide="hideCron"
|
||||
></Vcrontab>
|
||||
</template>
|
||||
<a-input
|
||||
v-model="form.cron"
|
||||
slot="reference"
|
||||
:placeholder="$t('cmdb.ipam.cronTips')"
|
||||
/>
|
||||
</el-popover>
|
||||
</a-form-model-item>
|
||||
</template>
|
||||
</a-form-model>
|
||||
|
||||
<div class="custom-drawer-bottom-action">
|
||||
<a-button @click="handleClose">{{ $t('cancel') }}</a-button>
|
||||
<a-button @click="handleSubmit" type="primary">{{ $t('save') }}</a-button>
|
||||
</div>
|
||||
</CustomDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
postIPAMSubnet,
|
||||
getIPAMSubnetById,
|
||||
putIPAMSubnet
|
||||
} from '@/modules/cmdb/api/ipam.js'
|
||||
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
|
||||
import Vcrontab from '@/components/Crontab'
|
||||
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'SubnetForm',
|
||||
components: {
|
||||
Vcrontab,
|
||||
CIReferenceAttr
|
||||
},
|
||||
props: {
|
||||
subnetCIType: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nodeId: null,
|
||||
parentId: null,
|
||||
actionType: 'create',
|
||||
visible: false,
|
||||
form: {
|
||||
scan_enabled: true,
|
||||
cron: ''
|
||||
},
|
||||
basicFormGroup: [],
|
||||
formRules: {},
|
||||
|
||||
agentTypeRadioList: [
|
||||
{ value: 'master', label: this.$t('cmdb.ipam.masterMachine') },
|
||||
{ value: 'agent_id', label: this.$t('cmdb.ipam.specifyMachine') }
|
||||
],
|
||||
agentType: 'master',
|
||||
agentId: '',
|
||||
|
||||
cronVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open(nodeId, type, parentId) {
|
||||
this.visible = true
|
||||
this.actionType = type
|
||||
this.nodeId = nodeId
|
||||
this.parentId = parentId || null
|
||||
|
||||
let nodeData = {}
|
||||
if (type === 'edit') {
|
||||
nodeData = await getIPAMSubnetById(nodeId)
|
||||
this.form.scan_enabled = !!nodeData.scan_enabled
|
||||
|
||||
if (nodeData?.scan_enabled) {
|
||||
this.form.cron = nodeData.cron
|
||||
|
||||
if (nodeData.agent_id) {
|
||||
if (nodeData.agent_id === '0x0000') {
|
||||
this.agentType = 'master'
|
||||
} else {
|
||||
this.agentType = 'agent_id'
|
||||
this.agentId = nodeData.agent_id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const res = await getCITypeAttributesById(SUB_NET_CITYPE_NAME)
|
||||
// const attributes = res?.attributes || []
|
||||
|
||||
const groupAttr = await getCITypeGroupById(this.subnetCIType.id)
|
||||
const form = {
|
||||
...this.form
|
||||
}
|
||||
const formRules = {}
|
||||
|
||||
let basicFormGroup = []
|
||||
groupAttr.map((group) => {
|
||||
group.attributes = group?.attributes?.filter?.((attr) => !attr.sys_computed && !attr.is_computed) || []
|
||||
if (group.attributes.length) {
|
||||
group.attributes.forEach((attr) => {
|
||||
form[attr.name] = nodeData?.[attr.name] ?? undefined
|
||||
|
||||
if (attr?.is_choice) {
|
||||
let choice_value = attr?.choice_value || []
|
||||
if (attr.name === 'assign_status') {
|
||||
choice_value = choice_value.filter((item) => item?.[0] !== 1)
|
||||
}
|
||||
|
||||
attr.selectOption = choice_value.map((item) => {
|
||||
return {
|
||||
label: item?.[1]?.label || item?.[0] || '',
|
||||
value: item?.[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (attr.is_required) {
|
||||
formRules[attr.name] = [
|
||||
{
|
||||
required: true, message: this.$t('placeholder1')
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
basicFormGroup.push({
|
||||
name: group.name,
|
||||
attributes: group.attributes
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
basicFormGroup = await this.handleReferenceAttr(basicFormGroup, form)
|
||||
|
||||
this.form = form
|
||||
this.$set(this, 'basicFormGroup', basicFormGroup)
|
||||
this.formRules = formRules
|
||||
},
|
||||
|
||||
async handleReferenceAttr(basicFormGroup, ci) {
|
||||
const map = {}
|
||||
basicFormGroup.forEach((group) => {
|
||||
group.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id && ci[attr.name]) {
|
||||
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [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 basicFormGroup
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
basicFormGroup.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 = ci[attr.name];
|
||||
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
|
||||
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
|
||||
})
|
||||
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return basicFormGroup
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.form = {
|
||||
scan_enabled: true,
|
||||
cron: ''
|
||||
}
|
||||
this.basicFormGroup = []
|
||||
this.formRules = {}
|
||||
this.agentType = 'master'
|
||||
this.agentId = ''
|
||||
this.cronVisible = false
|
||||
this.nodeId = null
|
||||
this.parentId = null
|
||||
this.actionType = 'create'
|
||||
|
||||
this.$refs.subnetFormRef.clearValidate()
|
||||
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
handleSubmit() {
|
||||
this.$refs.subnetFormRef.validate(async (valid) => {
|
||||
if (!valid || !this.validateScan()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { cron, ...otherParams } = this.form
|
||||
const params = {
|
||||
...otherParams
|
||||
}
|
||||
|
||||
if (this.form.scan_enabled) {
|
||||
params.cron = cron
|
||||
|
||||
switch (this.agentType) {
|
||||
case 'master':
|
||||
params.agent_id = '0x0000'
|
||||
break
|
||||
case 'agent_id':
|
||||
params.agent_id = this.agentId
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (this.actionType === 'edit') {
|
||||
if (this.parentId) {
|
||||
params.parent_id = this.parentId
|
||||
}
|
||||
await putIPAMSubnet(this.nodeId, params)
|
||||
this.$message.success(this.$t('editSuccess'))
|
||||
} else {
|
||||
params.parent_id = this.nodeId
|
||||
await postIPAMSubnet(params)
|
||||
this.$message.success(this.$t('addSuccess'))
|
||||
}
|
||||
|
||||
this.$emit('ok')
|
||||
this.handleClose()
|
||||
})
|
||||
},
|
||||
|
||||
validateScan() {
|
||||
if (this.form.scan_enabled) {
|
||||
switch (this.agentType) {
|
||||
case 'agent_id':
|
||||
if (!this.agentId) {
|
||||
this.$message.error(this.$t('cmdb.ipam.specifyMachineTips'))
|
||||
return false
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!this.form.cron) {
|
||||
this.$message.error(this.$t('cmdb.ipam.cronRequiredTip'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
handleOpenCmdb() {
|
||||
this.$refs.cmdbDrawer.open()
|
||||
},
|
||||
|
||||
crontabFill(cron) {
|
||||
this.form.cron = cron
|
||||
},
|
||||
|
||||
hideCron() {
|
||||
this.cronVisible = false
|
||||
},
|
||||
|
||||
getInitReferenceSelectOption(attr) {
|
||||
const option = Object.keys(attr?.referenceShowAttrNameMap || {}).map((key) => {
|
||||
return {
|
||||
key: Number(key),
|
||||
title: attr?.referenceShowAttrNameMap?.[Number(key)] ?? ''
|
||||
}
|
||||
})
|
||||
return option
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.subnet-form-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.subnet-form-agent-tip {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
line-height: 14px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
export const SUB_NET_CITYPE_NAME = 'ipam_subnet'
|
||||
export const SCOPE_CITYPE_NAME = 'ipam_scope'
|
||||
export const ADDRESS_CITYPE_NAME = 'ipam_address'
|
|
@ -0,0 +1,296 @@
|
|||
<template>
|
||||
<TwoColumnLayout
|
||||
class="ipam"
|
||||
appName="cmdb-ipam"
|
||||
calcBasedParent
|
||||
>
|
||||
<template #one>
|
||||
<IPAMTree
|
||||
v-if="subnetCIType"
|
||||
:treeData="treeData"
|
||||
:treeKey="treeKey"
|
||||
:subnetCIType="subnetCIType"
|
||||
@refreshData="refreshData"
|
||||
@updateTreeKey="updateTreeKey"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #two>
|
||||
<a-tabs
|
||||
:activeKey="tabKey"
|
||||
@change="handleTabChange"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="(item) in tabs"
|
||||
:key="item.key"
|
||||
:tab="$t(item.title)"
|
||||
>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<Overview
|
||||
v-if="tabKey === 'overview'"
|
||||
ref="overviewRef"
|
||||
:nodeId="treeKey"
|
||||
/>
|
||||
<template v-if="addressCIType">
|
||||
<Address
|
||||
v-if="tabKey === 'address'"
|
||||
:nodeData="nodeData"
|
||||
:addressCIType="addressCIType"
|
||||
/>
|
||||
<IPSearch
|
||||
v-if="tabKey === 'ipSearch'"
|
||||
:addressCIType="addressCIType"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="subnetCIType">
|
||||
<SubnetList
|
||||
v-if="tabKey === 'subnet'"
|
||||
ref="subnetListRef"
|
||||
:subnetCIType="subnetCIType"
|
||||
@delete="getTreeData"
|
||||
/>
|
||||
</template>
|
||||
<HistoryLog
|
||||
v-if="tabKey === 'history'"
|
||||
ref="historyRef"
|
||||
/>
|
||||
</template>
|
||||
</TwoColumnLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getIPAMSubnet } from '@/modules/cmdb/api/ipam.js'
|
||||
import { getCIType } from '@/modules/cmdb/api/CIType.js'
|
||||
import { SUB_NET_CITYPE_NAME, ADDRESS_CITYPE_NAME, SCOPE_CITYPE_NAME } from './constants.js'
|
||||
|
||||
import TwoColumnLayout from '@/components/TwoColumnLayout'
|
||||
import IPAMTree from './components/ipamTree.vue'
|
||||
import Overview from './modules/overview/index.vue'
|
||||
import Address from './modules/address/index.vue'
|
||||
import IPSearch from './modules/ipSearch/index.vue'
|
||||
import SubnetList from './modules/subnetList/index.vue'
|
||||
import HistoryLog from './modules/history/index.vue'
|
||||
|
||||
const TAB_STORAGE_KEY = 'ops_ipam_tab_active'
|
||||
const TREE_STORAGE_KEY = 'ops_ipam_tree_active'
|
||||
|
||||
export default {
|
||||
name: 'IPAM',
|
||||
components: {
|
||||
TwoColumnLayout,
|
||||
IPAMTree,
|
||||
IPSearch,
|
||||
SubnetList,
|
||||
Address,
|
||||
HistoryLog,
|
||||
Overview
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabKey: localStorage.getItem(TAB_STORAGE_KEY) || 'overview',
|
||||
treeKey: localStorage.getItem(TREE_STORAGE_KEY) || 'all',
|
||||
|
||||
tabs: [
|
||||
{
|
||||
key: 'overview',
|
||||
title: 'cmdb.ipam.overview'
|
||||
},
|
||||
{
|
||||
key: 'address',
|
||||
title: 'cmdb.ipam.addressAssign'
|
||||
},
|
||||
{
|
||||
key: 'ipSearch',
|
||||
title: 'cmdb.ipam.ipSearch'
|
||||
},
|
||||
{
|
||||
key: 'subnet',
|
||||
title: 'cmdb.ipam.subnetList'
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
title: 'cmdb.ipam.history'
|
||||
}
|
||||
],
|
||||
treeData: [],
|
||||
subnetCIType: null,
|
||||
addressCIType: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
nodeData() {
|
||||
return this.findNodeById(this.treeData, this.treeKey)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tabKey: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(key) {
|
||||
switch (key) {
|
||||
case 'subnet':
|
||||
if (!this.subnetCITYpe) {
|
||||
this.getSubnetCIType()
|
||||
}
|
||||
break
|
||||
case 'address':
|
||||
case 'ipSearch':
|
||||
if (!this.addressCIType) {
|
||||
this.getAddressCIType()
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getSubnetCIType()
|
||||
this.getTreeData()
|
||||
},
|
||||
methods: {
|
||||
async getSubnetCIType() {
|
||||
const res = await getCIType(SUB_NET_CITYPE_NAME)
|
||||
this.subnetCIType = res?.ci_types?.[0] || {}
|
||||
},
|
||||
|
||||
async getAddressCIType() {
|
||||
const res = await getCIType(ADDRESS_CITYPE_NAME)
|
||||
this.addressCIType = res?.ci_types?.[0] || {}
|
||||
},
|
||||
|
||||
async getTreeData() {
|
||||
const res = await getIPAMSubnet()
|
||||
let treeData = []
|
||||
|
||||
if (res?.result?.length) {
|
||||
treeData = res.result.map((data) => {
|
||||
return this.handleTreeData(data, res.type2name)
|
||||
})
|
||||
}
|
||||
|
||||
const allCount = treeData.reduce((acc, cur) => acc + cur.count, 0)
|
||||
const rootShowSubnetBtn = treeData.every((item) => item.ci_type === SUB_NET_CITYPE_NAME)
|
||||
const rootShowCatalogBtn = treeData.every((item) => item.ci_type === SCOPE_CITYPE_NAME)
|
||||
|
||||
treeData.unshift({
|
||||
key: 'all',
|
||||
title: this.$t('all'),
|
||||
count: allCount,
|
||||
icon: 'veops-entire_network_',
|
||||
iconColor: '#2F54EB',
|
||||
showCatalogBtn: rootShowCatalogBtn,
|
||||
showSubnetBtn: rootShowSubnetBtn,
|
||||
parentId: '',
|
||||
})
|
||||
|
||||
this.treeData = treeData
|
||||
},
|
||||
|
||||
handleTreeData(data, type2name, parentId = '') {
|
||||
const title = data?.[type2name?.[data?._type]] || ''
|
||||
const isSubnet = data?.ci_type === SUB_NET_CITYPE_NAME
|
||||
const icon = isSubnet ? 'veops-subnet' : 'veops-folder'
|
||||
const iconColor = isSubnet ? '#CACDD9' : ''
|
||||
const key = String(data._id)
|
||||
|
||||
if (!data?.children?.length) {
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
count: isSubnet ? 1 : 0,
|
||||
icon,
|
||||
iconColor,
|
||||
showCatalogBtn: !isSubnet,
|
||||
showSubnetBtn: true,
|
||||
isSubnet,
|
||||
parentId,
|
||||
...data
|
||||
}
|
||||
}
|
||||
|
||||
const children = data.children.map((item) => {
|
||||
return this.handleTreeData(item, type2name, key)
|
||||
})
|
||||
|
||||
const showSubnetBtn = children.every((item) => item.ci_type === SUB_NET_CITYPE_NAME)
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
icon,
|
||||
iconColor,
|
||||
showCatalogBtn: !isSubnet && !showSubnetBtn,
|
||||
showSubnetBtn: showSubnetBtn,
|
||||
isSubnet,
|
||||
parentId,
|
||||
...data,
|
||||
children,
|
||||
count: children.reduce((acc, item) => {
|
||||
return acc + item.count
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
|
||||
handleTabChange(key) {
|
||||
if (key !== this.tabKey) {
|
||||
this.tabKey = key
|
||||
localStorage.setItem(TAB_STORAGE_KEY, key)
|
||||
}
|
||||
},
|
||||
|
||||
updateTreeKey(key) {
|
||||
this.treeKey = key
|
||||
localStorage.setItem(TREE_STORAGE_KEY, key)
|
||||
},
|
||||
|
||||
findNodeById(nodes, key) {
|
||||
for (const node of nodes) {
|
||||
if (node.key === key) {
|
||||
return node
|
||||
}
|
||||
if (node.children) {
|
||||
const foundNode = this.findNodeById(node.children, key)
|
||||
if (foundNode) {
|
||||
return foundNode
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
this.getTreeData()
|
||||
switch (this.tabKey) {
|
||||
case 'overview':
|
||||
if (this.$refs.overviewRef) {
|
||||
this.$refs.overviewRef.initData()
|
||||
}
|
||||
break
|
||||
case 'subnet':
|
||||
if (this.$refs.subnetListRef) {
|
||||
this.$refs.subnetListRef.getTableData()
|
||||
}
|
||||
break
|
||||
case 'history':
|
||||
if (this.$refs.historyRef) {
|
||||
this.$refs.historyRef.refreshData()
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ipam {
|
||||
/deep/ .ant-tabs {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,284 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:width="700"
|
||||
:title="$t('cmdb.ipam.addressAssign')"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form-model
|
||||
ref="assignFormRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
class="assign-form"
|
||||
>
|
||||
<a-form-model-item
|
||||
label="IP"
|
||||
>
|
||||
{{ ipData.ip }}
|
||||
</a-form-model-item>
|
||||
<a-form-model-item
|
||||
v-for="(item) in formList"
|
||||
:key="item.name"
|
||||
:label="item.alias || item.name"
|
||||
:prop="item.name"
|
||||
>
|
||||
<CIReferenceAttr
|
||||
v-if="item.is_reference"
|
||||
:referenceTypeId="item.reference_type_id"
|
||||
:isList="item.is_list"
|
||||
:referenceShowAttrName="item.showAttrName"
|
||||
:initSelectOption="getInitReferenceSelectOption(item)"
|
||||
v-model="form[item.name]"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="item.is_choice"
|
||||
:mode="item.is_list ? 'multiple' : 'default'"
|
||||
showSearch
|
||||
allowClear
|
||||
v-model="form[item.name]"
|
||||
>
|
||||
<a-icon slot="suffixIcon" type="caret-down" />
|
||||
<a-select-option
|
||||
v-for="(choiceItem, choiceIndex) in item.selectOption"
|
||||
:key="choiceIndex"
|
||||
:value="choiceItem.value"
|
||||
>
|
||||
{{ choiceItem.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-switch
|
||||
v-else-if="item.is_bool"
|
||||
v-model="form[item.name]"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
:placeholder="$t('placeholder1')"
|
||||
v-model="form[item.name]"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { postIPAMAddress } from '@/modules/cmdb/api/ipam.js'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
|
||||
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'AssignForm',
|
||||
components: {
|
||||
CIReferenceAttr
|
||||
},
|
||||
props: {
|
||||
attrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
subnetData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
ipData: {},
|
||||
nodeId: -1,
|
||||
formList: [],
|
||||
form: {},
|
||||
formRules: {},
|
||||
statusSelectOption: [
|
||||
{
|
||||
value: 0,
|
||||
label: 'cmdb.ipam.assigned'
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: 'cmdb.ipam.reserved'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open({
|
||||
ipData,
|
||||
nodeId,
|
||||
}) {
|
||||
this.ipData = ipData || {}
|
||||
this.nodeId = nodeId || -1
|
||||
this.visible = true
|
||||
|
||||
const form = {}
|
||||
const formRules = {}
|
||||
let formList = []
|
||||
let attrList = _.cloneDeep(this.attrList)
|
||||
attrList = attrList?.filter?.((attr) => !attr.sys_computed && !attr.is_computed) || []
|
||||
|
||||
if (ipData?.assign_status === 1) {
|
||||
ipData.assign_status = undefined
|
||||
}
|
||||
|
||||
if (attrList.length) {
|
||||
_.remove(attrList, (item) => {
|
||||
return ['subnet_mask', 'gateway', 'name', 'mac_address', 'is_used', 'ip'].includes(item.name)
|
||||
})
|
||||
|
||||
const assingStatusIndex = attrList.findIndex((attr) => attr.name === 'assign_status')
|
||||
if (assingStatusIndex > 0) {
|
||||
const assign_status = attrList.splice(assingStatusIndex, 1)
|
||||
attrList.unshift(...assign_status)
|
||||
}
|
||||
|
||||
attrList.forEach((attr) => {
|
||||
form[attr.name] = ipData?.[attr.name] ?? undefined
|
||||
|
||||
if (attr?.is_choice) {
|
||||
let choice_value = attr?.choice_value || []
|
||||
if (attr.name === 'assign_status') {
|
||||
choice_value = choice_value.filter((item) => item?.[0] !== 1)
|
||||
}
|
||||
|
||||
attr.selectOption = choice_value.map((item) => {
|
||||
return {
|
||||
label: item?.[1]?.label || item?.[0] || '',
|
||||
value: item?.[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
formList.push(attr)
|
||||
|
||||
if (attr.is_required) {
|
||||
formRules[attr.name] = [
|
||||
{
|
||||
required: true, message: attr?.is_choice ? this.$t('placeholder2') : this.$t('placeholder1')
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
formList = await this.handleReferenceAttr(formList, form)
|
||||
|
||||
this.form = form
|
||||
this.formList = formList
|
||||
this.formRules = formRules
|
||||
},
|
||||
|
||||
async handleReferenceAttr(formList, ci) {
|
||||
const map = {}
|
||||
formList.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id && ci[attr.name]) {
|
||||
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [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 formList
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
formList.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
|
||||
|
||||
const referenceShowAttrNameMap = {}
|
||||
const referenceCIIds = ci[attr.name];
|
||||
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
|
||||
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
|
||||
})
|
||||
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
|
||||
}
|
||||
})
|
||||
|
||||
return formList
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.visible = false
|
||||
this.ipData = {}
|
||||
this.nodeId = -1
|
||||
this.form = {}
|
||||
this.formRules = {}
|
||||
this.formList = []
|
||||
this.visible = false
|
||||
|
||||
this.$refs.assignFormRef.clearValidate()
|
||||
},
|
||||
|
||||
handleOk() {
|
||||
this.$refs.assignFormRef.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
await postIPAMAddress({
|
||||
ips: [this.ipData.ip],
|
||||
parent_id: this.nodeId,
|
||||
...this.form,
|
||||
subnet_mask: this?.ipData?.subnet_mask ?? undefined,
|
||||
gateway: this?.ipData?.gateway ?? undefined
|
||||
})
|
||||
|
||||
this.$emit('ok')
|
||||
this.handleCancel()
|
||||
})
|
||||
},
|
||||
|
||||
getInitReferenceSelectOption(attr) {
|
||||
const option = Object.keys(attr?.referenceShowAttrNameMap || {}).map((key) => {
|
||||
return {
|
||||
key: Number(key),
|
||||
title: attr?.referenceShowAttrNameMap?.[Number(key)] ?? ''
|
||||
}
|
||||
})
|
||||
return option
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.assign-form {
|
||||
padding-right: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
export const ADDRESS_STATUS = {
|
||||
ONLINE_ASSIGNED: '0',
|
||||
ONLINE_UNASSIGNED: '1',
|
||||
OFFLINE_ASSIGNED: '2',
|
||||
OFFLINE_UNASSIGNED: '3',
|
||||
}
|
||||
|
||||
export const STATUS_COLOR = {
|
||||
[ADDRESS_STATUS.ONLINE_ASSIGNED]: '#00B42A',
|
||||
[ADDRESS_STATUS.ONLINE_UNASSIGNED]: '#FF7D00',
|
||||
[ADDRESS_STATUS.OFFLINE_ASSIGNED]: '#2F54EB',
|
||||
[ADDRESS_STATUS.OFFLINE_UNASSIGNED]: '#A5A9BC'
|
||||
}
|
||||
|
||||
export const STATUS_LABEL = {
|
||||
[ADDRESS_STATUS.ONLINE_ASSIGNED]: 'cmdb.ipam.assignedOnline',
|
||||
[ADDRESS_STATUS.ONLINE_UNASSIGNED]: 'cmdb.ipam.unassignedOnline',
|
||||
[ADDRESS_STATUS.OFFLINE_ASSIGNED]: 'cmdb.ipam.assignedOffline',
|
||||
[ADDRESS_STATUS.OFFLINE_UNASSIGNED]: 'cmdb.ipam.unused'
|
||||
}
|
||||
|
||||
export const STATUS_OPTION = [
|
||||
{
|
||||
value: ADDRESS_STATUS.ONLINE_ASSIGNED,
|
||||
label: STATUS_LABEL[ADDRESS_STATUS.ONLINE_ASSIGNED],
|
||||
},
|
||||
{
|
||||
value: ADDRESS_STATUS.ONLINE_UNASSIGNED,
|
||||
label: STATUS_LABEL[ADDRESS_STATUS.ONLINE_UNASSIGNED],
|
||||
},
|
||||
{
|
||||
value: ADDRESS_STATUS.OFFLINE_ASSIGNED,
|
||||
label: STATUS_LABEL[ADDRESS_STATUS.OFFLINE_ASSIGNED],
|
||||
},
|
||||
{
|
||||
value: ADDRESS_STATUS.OFFLINE_UNASSIGNED,
|
||||
label: STATUS_LABEL[ADDRESS_STATUS.OFFLINE_UNASSIGNED],
|
||||
}
|
||||
]
|
|
@ -0,0 +1,399 @@
|
|||
<template>
|
||||
<div
|
||||
class="ip-grid"
|
||||
:style="{
|
||||
gap: gridGap + 'px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(item) in gridList"
|
||||
:key="item.ip"
|
||||
class="ip-grid-item"
|
||||
:style="{
|
||||
width: gridItemSize + 'px',
|
||||
height: gridItemSize + 'px',
|
||||
backgroundColor: `${STATUS_COLOR[item._ip_status]}22`,
|
||||
color: STATUS_COLOR[item._ip_status],
|
||||
borderColor: STATUS_COLOR[item._ip_status]
|
||||
}"
|
||||
@click="clickGridItem(item, $event)"
|
||||
>
|
||||
{{ item.gridTitle }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="infoCardVisible"
|
||||
class="info-card"
|
||||
:style="{
|
||||
top: infoCardY + 'px',
|
||||
left: infoCardX + 'px',
|
||||
width: infoCardWidth + 'px',
|
||||
height: infoCardHeight + 'px',
|
||||
}"
|
||||
>
|
||||
<div class="info-card-header">
|
||||
<div class="info-card-ip">
|
||||
{{ infoCardData.ip }}
|
||||
</div>
|
||||
<div
|
||||
class="info-card-status-dot"
|
||||
:style="{
|
||||
backgroundColor: `${STATUS_COLOR[infoCardData._ip_status]}22`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="info-card-status-dot-content"
|
||||
:style="{
|
||||
backgroundColor: STATUS_COLOR[infoCardData._ip_status]
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="info-card-status-text">
|
||||
{{ $t(STATUS_LABEL[infoCardData._ip_status]) }}
|
||||
</div>
|
||||
|
||||
<a-button
|
||||
type="primary"
|
||||
class="ops-button-ghost info-card-recycle"
|
||||
ghost
|
||||
@click="clickRecycle(infoCardData)"
|
||||
>
|
||||
<ops-icon type="veops-recycle" />
|
||||
{{ $t('cmdb.ipam.recycle') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="info-card-main">
|
||||
<div
|
||||
v-for="(col) in filterColumns"
|
||||
:key="col.field"
|
||||
class="info-card-main-row"
|
||||
>
|
||||
<div class="info-card-main-title">
|
||||
<a-tooltip :title="col.title">
|
||||
{{ col.title }}
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="info-card-main-value">
|
||||
<a-tooltip :title="infoCardTip[col.field]" placement="topLeft" >
|
||||
<template v-if="col.is_reference && infoCardData[col.field]" >
|
||||
<a
|
||||
v-for="(ciId) in (col.is_list ? infoCardData[col.field] : [infoCardData[col.field]])"
|
||||
:key="ciId"
|
||||
:href="`/cmdb/cidetail/${col.reference_type_id}/${ciId}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getReferenceAttrValue(ciId, col) }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="col.is_link && infoCardData[col.field]">
|
||||
<a
|
||||
v-for="(linkItem, linkIndex) in (col.is_list ? infoCardData[col.field] : [infoCardData[col.field]])"
|
||||
:key="linkIndex"
|
||||
:href="
|
||||
linkItem.startsWith('http') || linkItem.startsWith('https')
|
||||
? `${linkItem}`
|
||||
: `http://${linkItem}`
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getChoiceValueLabel(col, linkItem) || linkItem }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="col.is_choice && infoCardData[col.field]">
|
||||
<span
|
||||
v-for="value in (col.is_list ? infoCardData[col.field] : [infoCardData[col.field]])"
|
||||
:key="value"
|
||||
class="column-default-choice"
|
||||
>
|
||||
{{ getChoiceValueLabel(col, value) || value }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ infoCardData[col.field] !== undefined ? Array.isArray(infoCardData[col.field]) ? infoCardData[col.field].join(', ') : infoCardData[col.field] : '' }}
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STATUS_COLOR, STATUS_LABEL, ADDRESS_STATUS } from './constants.js'
|
||||
|
||||
export default {
|
||||
name: 'GridIP',
|
||||
props: {
|
||||
ipList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
referenceShowAttrNameMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
referenceCIIdMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
STATUS_COLOR,
|
||||
STATUS_LABEL,
|
||||
|
||||
gridItemSize: 52, //
|
||||
gridGap: 8,
|
||||
|
||||
infoCardX: 0,
|
||||
infoCardY: 0,
|
||||
infoCardWidth: 375,
|
||||
infoCardVisible: false,
|
||||
infoCardData: {},
|
||||
infoCardTip: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
gridList() {
|
||||
const list = this.ipList.map((item) => {
|
||||
const ipSplit = item?.ip?.split('.') || []
|
||||
const gridTitle = ipSplit?.[ipSplit.length - 1] || ''
|
||||
|
||||
return {
|
||||
...item,
|
||||
gridTitle
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
},
|
||||
filterColumns() {
|
||||
return this.columns.filter((col) => col.field !== '_ip_status') || []
|
||||
},
|
||||
infoCardHeight() {
|
||||
let infoCardHeight = 311
|
||||
if (this.filterColumns.length < 6) {
|
||||
infoCardHeight -= ((6 - this.filterColumns.length) * 36)
|
||||
}
|
||||
|
||||
return infoCardHeight
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('click', this.handleClick)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('click', this.handleClick)
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
const classStr = event?.target?.classList?.value
|
||||
if (classStr.indexOf('info-card') === -1 && classStr.indexOf('ip-grid-item') === -1) {
|
||||
this.infoCardVisible = false
|
||||
}
|
||||
},
|
||||
|
||||
clickGridItem(item, event) {
|
||||
if ([ADDRESS_STATUS.OFFLINE_UNASSIGNED, ADDRESS_STATUS.ONLINE_UNASSIGNED].includes(item?._ip_status)) {
|
||||
this.$emit('openAssign', item)
|
||||
} else {
|
||||
this.showInfoCard(item, event)
|
||||
}
|
||||
},
|
||||
|
||||
showInfoCard(item, event) {
|
||||
let infoCardX = event.clientX - event.offsetX
|
||||
let infoCardY = event.clientY - event.offsetY + this.gridItemSize + this.gridGap
|
||||
|
||||
// 右侧是否超出视口边界
|
||||
if (infoCardX + this.infoCardWidth > window.innerWidth) {
|
||||
infoCardX = infoCardX + this.gridItemSize - this.infoCardWidth
|
||||
}
|
||||
|
||||
// 底部是否超出视口边界
|
||||
if (infoCardY + this.infoCardHeight > window.innerHeight) {
|
||||
infoCardY = infoCardY - this.gridItemSize - this.gridGap * 2 - this.infoCardHeight
|
||||
}
|
||||
|
||||
const infoCardTip = {}
|
||||
this.filterColumns.forEach((col) => {
|
||||
const arrayValue = Array.isArray(item[col.field]) ? item[col.field] : [item[col.field]]
|
||||
infoCardTip[col.field] = arrayValue.map((value) => {
|
||||
if (value === undefined || value === null) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (col.is_reference) {
|
||||
return this.getReferenceAttrValue(value, col) || value
|
||||
}
|
||||
if (col.is_link || col.is_choice) {
|
||||
return this.getChoiceValueLabel(col, value) || value
|
||||
}
|
||||
return value
|
||||
}).join(', ')
|
||||
})
|
||||
|
||||
this.infoCardX = infoCardX
|
||||
this.infoCardY = infoCardY
|
||||
this.infoCardVisible = true
|
||||
this.infoCardData = item
|
||||
this.infoCardTip = infoCardTip
|
||||
},
|
||||
|
||||
clickRecycle(data) {
|
||||
this.$emit('recycle', data.ip)
|
||||
},
|
||||
|
||||
getReferenceAttrValue(id, col) {
|
||||
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
|
||||
if (!ci) {
|
||||
return id
|
||||
}
|
||||
|
||||
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
|
||||
return ci?.[attrName] || id
|
||||
},
|
||||
|
||||
getChoiceValueLabel(col, colValue) {
|
||||
const _find = col?.choice_value?.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find?.[1]?.label || ''
|
||||
}
|
||||
return ''
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ip-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
max-height: calc(100vh - 230px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: top 0.2s, left 0.2s;
|
||||
padding: 23px 18px;
|
||||
|
||||
border-radius: 2px;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: -2px 4px 12px 0px rgba(168, 191, 211, 0.25);
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-ip {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&-status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 14px;
|
||||
|
||||
&-content {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-status-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&-recycle {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&-main {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
border: solid 1px #F0F1F5;
|
||||
border-bottom-style: none;
|
||||
max-height: calc(100% - 47px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&-row {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-title {
|
||||
border-right: solid 1px #F0F1F5;
|
||||
background-color: #F7F8FA;
|
||||
padding-left: 17px;
|
||||
padding-right: 10px;
|
||||
width: 32%;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
border-bottom: solid 1px #E4E7ED;
|
||||
}
|
||||
|
||||
&-value {
|
||||
width: 68%;
|
||||
flex-shrink: 0;
|
||||
padding-left: 18px;
|
||||
padding-right: 10px;
|
||||
height: 100%;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
border-bottom: solid 1px #F0F1F5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,721 @@
|
|||
<template>
|
||||
<div
|
||||
ref="addressRef"
|
||||
class="address"
|
||||
>
|
||||
<div v-if="addressNullTip" class="address-null">
|
||||
<img class="address-null-img" :src="require(`@/modules/cmdb/assets/ipam_address_null.png`)"></img>
|
||||
<div class="address-null-tip">{{ $t('noData') }}</div>
|
||||
<div class="address-null-tip2">{{ $t(addressNullTip) }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loading" class="address-loading">
|
||||
<a-icon type="loading" class="address-loading-icon" />
|
||||
<span class="address-loading-text">{{ $t('loading') }}</span>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="address-header">
|
||||
<div class="address-header-left">
|
||||
<a-input-search
|
||||
v-model="searchValue"
|
||||
:placeholder="$t('placeholderSearch')"
|
||||
class="address-header-search"
|
||||
/>
|
||||
|
||||
<a-select
|
||||
class="address-header-filter"
|
||||
v-model="currentStatus"
|
||||
>
|
||||
<a-icon slot="suffixIcon" type="caret-down" />
|
||||
<a-select-option
|
||||
v-for="(item) in filterOption"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ $t(item.label) }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-select
|
||||
v-if="scopeSelectOption.length > 1"
|
||||
class="address-header-filter"
|
||||
v-model="currentSelectScope"
|
||||
showSearch
|
||||
>
|
||||
<a-icon slot="suffixIcon" type="caret-down" />
|
||||
<a-select-option
|
||||
v-for="(key) in scopeSelectOption"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ key }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<div
|
||||
v-if="currentLayout === 'grid'"
|
||||
class="address-header-status"
|
||||
>
|
||||
<div
|
||||
v-for="(item) in statusOption"
|
||||
:key="item.value"
|
||||
class="address-header-status-item"
|
||||
>
|
||||
<div
|
||||
class="address-header-status-dot"
|
||||
:style="{
|
||||
backgroundColor: `${STATUS_COLOR[item.value]}22`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="address-header-status-dot-content"
|
||||
:style="{
|
||||
backgroundColor: STATUS_COLOR[item.value]
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="address-header-status-text"
|
||||
>
|
||||
{{ $t(item.label) }}: {{ item.count }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address-header-right">
|
||||
<a-button
|
||||
type="primary"
|
||||
class="ops-button-ghost"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
<ops-icon type="veops-export" />
|
||||
{{ $t('export') }}
|
||||
</a-button>
|
||||
|
||||
<div class="address-header-layout">
|
||||
<div
|
||||
v-for="(item) in layoutList"
|
||||
:key="item.value"
|
||||
:class="['address-header-layout-item', currentLayout === item.value ?'address-header-layout-item-active' : '']"
|
||||
@click="currentLayout = item.value"
|
||||
>
|
||||
<ops-icon :type="item.icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address-main">
|
||||
<TableIP
|
||||
v-if="currentLayout === 'table'"
|
||||
ref="tableIPRef"
|
||||
:columns="columns"
|
||||
:allTableData="filterIPList"
|
||||
:referenceShowAttrNameMap="referenceShowAttrNameMap"
|
||||
:referenceCIIdMap="referenceCIIdMap"
|
||||
:columnWidth="columnWidth"
|
||||
@openAssign="openAssign"
|
||||
@recycle="handleRecycle"
|
||||
/>
|
||||
|
||||
<GridIP
|
||||
v-if="currentLayout === 'grid'"
|
||||
:ipList="filterIPList"
|
||||
:columns="columns"
|
||||
:referenceShowAttrNameMap="referenceShowAttrNameMap"
|
||||
:referenceCIIdMap="referenceCIIdMap"
|
||||
@openAssign="openAssign"
|
||||
@recycle="handleRecycle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<AssignForm
|
||||
ref="assignFormRef"
|
||||
:attrList="attrList"
|
||||
:subnetData="subnetData"
|
||||
@ok="getIPList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import { getIPAMAddress, getIPAMHosts, postIPAMAddress, getIPAMSubnetById } from '@/modules/cmdb/api/ipam.js'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { ADDRESS_STATUS, STATUS_COLOR, STATUS_OPTION, STATUS_LABEL } from './constants.js'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { strLength } from '@/modules/cmdb/utils/helper.js'
|
||||
|
||||
import TableIP from './tableIP.vue'
|
||||
import GridIP from './gridIP.vue'
|
||||
import AssignForm from './assignForm.vue'
|
||||
|
||||
export default {
|
||||
name: 'Address',
|
||||
components: {
|
||||
TableIP,
|
||||
GridIP,
|
||||
AssignForm
|
||||
},
|
||||
props: {
|
||||
nodeData: {
|
||||
type: [Object, null],
|
||||
default: null
|
||||
},
|
||||
addressCIType: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
STATUS_COLOR,
|
||||
searchValue: '',
|
||||
ipList: {},
|
||||
currentSelectScope: '',
|
||||
columns: [],
|
||||
attrList: [],
|
||||
subnetData: {},
|
||||
referenceShowAttrNameMap: {},
|
||||
referenceCIIdMap: {},
|
||||
columnWidth: {},
|
||||
loading: false,
|
||||
|
||||
currentStatus: 'all',
|
||||
filterOption: [
|
||||
{
|
||||
value: 'all',
|
||||
label: 'cmdb.ipam.allStatus',
|
||||
},
|
||||
...STATUS_OPTION,
|
||||
],
|
||||
|
||||
currentLayout: 'table',
|
||||
layoutList: [
|
||||
{
|
||||
value: 'table',
|
||||
icon: 'monitor-list_view'
|
||||
},
|
||||
{
|
||||
value: 'grid',
|
||||
icon: 'veops-map_view'
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
addressNullTip() {
|
||||
if (
|
||||
this?.nodeData?.isSubnet &&
|
||||
this?.nodeData?.cidr &&
|
||||
this?.nodeData?.children?.length === 0
|
||||
) {
|
||||
const cidrSplit = this.nodeData?.cidr?.split?.('/')
|
||||
const cidrNumber = cidrSplit[cidrSplit.length - 1]
|
||||
if (Number(cidrNumber) >= 16) {
|
||||
return ''
|
||||
} else {
|
||||
return 'cmdb.ipam.addressNullTip2'
|
||||
}
|
||||
}
|
||||
return 'cmdb.ipam.addressNullTip'
|
||||
},
|
||||
addressCITypeId() {
|
||||
return this.addressCIType?.id || null
|
||||
},
|
||||
filterIPList() {
|
||||
let ipList = this.ipList?.[this.currentSelectScope]
|
||||
|
||||
if (!ipList?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (this.searchValue) {
|
||||
ipList = ipList.filter((item) => item.ip.indexOf(this.searchValue) !== -1)
|
||||
}
|
||||
|
||||
if (this.currentStatus !== 'all') {
|
||||
ipList = ipList.filter((item) => item._ip_status === this.currentStatus)
|
||||
}
|
||||
|
||||
return ipList
|
||||
},
|
||||
scopeSelectOption() {
|
||||
if (typeof this.ipList === 'object') {
|
||||
return Object.keys(this.ipList)
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
statusOption() {
|
||||
const ipList = this.ipList?.[this.currentSelectScope] || []
|
||||
const statusCount = {
|
||||
[ADDRESS_STATUS.OFFLINE_ASSIGNED]: 0,
|
||||
[ADDRESS_STATUS.OFFLINE_UNASSIGNED]: 0,
|
||||
[ADDRESS_STATUS.ONLINE_ASSIGNED]: 0,
|
||||
[ADDRESS_STATUS.ONLINE_UNASSIGNED]: 0,
|
||||
}
|
||||
ipList.forEach((item) => {
|
||||
if (item._ip_status) {
|
||||
statusCount[item._ip_status]++
|
||||
}
|
||||
})
|
||||
return STATUS_OPTION.map((option) => ({
|
||||
...option,
|
||||
count: statusCount[option.value]
|
||||
}))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
nodeData: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(node, oldNode) {
|
||||
if (
|
||||
node &&
|
||||
node?.isSubnet &&
|
||||
node?.cidr &&
|
||||
node?.children?.length === 0 &&
|
||||
node?.key !== oldNode?.key
|
||||
) {
|
||||
const cidrSplit = node?.cidr?.split?.('/')
|
||||
const cidrNumber = cidrSplit[cidrSplit.length - 1]
|
||||
|
||||
if (Number(cidrNumber) >= 16) {
|
||||
this.initData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initData() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.getColumns()
|
||||
await this.handleReferenceShowAttrName()
|
||||
await this.getIPList(true)
|
||||
this.calcColumnWidth()
|
||||
} catch (error) {
|
||||
console.log('initData fail', error)
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async getColumns() {
|
||||
const getAttrRes = await getCITypeAttributesById(this.addressCITypeId)
|
||||
const attrList = getAttrRes.attributes
|
||||
this.attrList = _.cloneDeep(attrList)
|
||||
|
||||
const filterAttrList = _.remove(attrList, (item) => {
|
||||
return ['ip', 'subnet_mask', 'assign_status', 'is_used', '_updated_by', '_updated_at'].includes(item.name)
|
||||
})
|
||||
|
||||
const columns = []
|
||||
;['ip', 'subnet_mask'].forEach((key) => {
|
||||
const attr = filterAttrList.find((item) => item.name === key)
|
||||
if (attr) {
|
||||
columns.push({
|
||||
field: attr.name,
|
||||
title: attr.alias || attr.name || ''
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
columns.push({
|
||||
field: '_ip_status',
|
||||
title: this.$t('status')
|
||||
})
|
||||
|
||||
attrList.map((attr) => {
|
||||
columns.push({
|
||||
field: attr.name,
|
||||
title: attr.alias || attr.name || '',
|
||||
...attr
|
||||
})
|
||||
})
|
||||
this.columns = columns
|
||||
},
|
||||
|
||||
async getIPList(isInit = false) {
|
||||
const hostsList = await getIPAMHosts({
|
||||
cidr: this.nodeData.cidr
|
||||
})
|
||||
|
||||
const res = await getIPAMAddress({
|
||||
parent_id: this.nodeData.key
|
||||
})
|
||||
|
||||
const subnetData = await getIPAMSubnetById(this.nodeData.key)
|
||||
this.subnetData = subnetData
|
||||
|
||||
const addressMap = {}
|
||||
if (res?.result?.length) {
|
||||
res.result.forEach((item) => {
|
||||
addressMap[item.ip] = item
|
||||
})
|
||||
}
|
||||
|
||||
const ipList = {}
|
||||
let currentSelectScope = ''
|
||||
|
||||
hostsList.map((ip) => {
|
||||
let colData = {
|
||||
ip,
|
||||
_ip_status: ADDRESS_STATUS.OFFLINE_UNASSIGNED
|
||||
}
|
||||
if (addressMap[ip]) {
|
||||
const data = addressMap[ip]
|
||||
const assigned = data.assign_status === 0 || data.assign_status === 2
|
||||
|
||||
if (data.is_used) {
|
||||
colData._ip_status = assigned ? ADDRESS_STATUS.ONLINE_ASSIGNED : ADDRESS_STATUS.ONLINE_UNASSIGNED
|
||||
} else if (assigned) {
|
||||
colData._ip_status = ADDRESS_STATUS.OFFLINE_ASSIGNED
|
||||
}
|
||||
|
||||
colData = {
|
||||
...colData,
|
||||
...data
|
||||
}
|
||||
}
|
||||
|
||||
const itemData = {
|
||||
...colData,
|
||||
subnet_mask: colData?.subnet_mask ?? subnetData?.subnet_mask ?? undefined,
|
||||
gateway: colData?.gateway ?? subnetData?.gateway ?? undefined
|
||||
}
|
||||
|
||||
const key = ip.split(/\.(?=[^.]*$)/)?.[0]
|
||||
if (ipList[key]) {
|
||||
ipList[key].push(itemData)
|
||||
} else {
|
||||
if (!currentSelectScope) {
|
||||
currentSelectScope = key
|
||||
}
|
||||
ipList[key] = [itemData]
|
||||
}
|
||||
})
|
||||
this.ipList = ipList
|
||||
if (isInit) {
|
||||
this.currentSelectScope = currentSelectScope
|
||||
}
|
||||
this.handleReferenceCIIdMap()
|
||||
},
|
||||
|
||||
async handleReferenceShowAttrName() {
|
||||
const needRequiredCITypeIds = this.columns?.filter((col) => col?.is_reference && col?.reference_type_id).map((col) => col.reference_type_id) || []
|
||||
if (!needRequiredCITypeIds.length) {
|
||||
this.referenceShowAttrNameMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const res = await getCITypes({
|
||||
type_ids: needRequiredCITypeIds.join(',')
|
||||
})
|
||||
|
||||
const map = {}
|
||||
res.ci_types.forEach((ciType) => {
|
||||
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
this.referenceShowAttrNameMap = map
|
||||
},
|
||||
|
||||
async handleReferenceCIIdMap() {
|
||||
const referenceTypeCol = this.columns.filter((col) => col?.is_reference && col?.reference_type_id) || []
|
||||
if (!this.ipList?.[this.currentSelectScope]?.length || !referenceTypeCol?.length) {
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const map = {}
|
||||
this.ipList[this.currentSelectScope].forEach((row) => {
|
||||
referenceTypeCol.forEach((col) => {
|
||||
const ids = Array.isArray(row[col.field]) ? row[col.field] : row[col.field] ? [row[col.field]] : []
|
||||
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
|
||||
},
|
||||
|
||||
calcColumnWidth() {
|
||||
const columnWidth = {}
|
||||
this.columns.forEach((col) => {
|
||||
columnWidth[col.field] = Math.min(Math.max(100, ...this.ipList[this.currentSelectScope].map(item => strLength(item[col.field]))), 350)
|
||||
})
|
||||
|
||||
const wrapWidth = this.$refs.addressRef?.clientWidth
|
||||
const totalWidth = Object.values(columnWidth).reduce((acc, cur) => acc + cur, 0)
|
||||
|
||||
if (totalWidth < wrapWidth) {
|
||||
this.columnWidth = {}
|
||||
} else {
|
||||
this.columnWidth = columnWidth
|
||||
}
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
let tableData = []
|
||||
if (this.currentLayout === 'table') {
|
||||
tableData = this.$refs.tableIPRef.getCheckedTableData()
|
||||
} else {
|
||||
tableData = this.filterIPList
|
||||
}
|
||||
|
||||
if (!tableData.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const wb = new ExcelJS.Workbook()
|
||||
const ws = wb.addWorksheet(this.tabActive)
|
||||
|
||||
const columns = this.columns.map((col) => {
|
||||
return {
|
||||
header: col.title,
|
||||
key: col.field,
|
||||
width: 20
|
||||
}
|
||||
})
|
||||
ws.columns = columns
|
||||
|
||||
tableData.forEach((data) => {
|
||||
const row = {}
|
||||
columns.forEach(({ key }) => {
|
||||
let value = data?.[key] ?? null
|
||||
if (key === '_ip_status') {
|
||||
const text = STATUS_LABEL?.[data?.[key]]
|
||||
value = text ? this.$t(text) : null
|
||||
}
|
||||
row[key] = value
|
||||
})
|
||||
ws.addRow(row)
|
||||
})
|
||||
|
||||
wb.xlsx.writeBuffer().then((buffer) => {
|
||||
const fileName = `cmdb-${this.$t('cmdb.ipam.addressAssign')}-${moment().format('YYYYMMDDHHmmss')}.xlsx`
|
||||
const file = new Blob([buffer], {
|
||||
type: 'application/octet-stream',
|
||||
})
|
||||
FileSaver.saveAs(file, fileName)
|
||||
})
|
||||
},
|
||||
|
||||
openAssign(data) {
|
||||
this.$refs.assignFormRef.open({
|
||||
nodeId: this?.nodeData?._id,
|
||||
ipData: _.cloneDeep(data)
|
||||
})
|
||||
},
|
||||
|
||||
handleRecycle(ip) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('cmdb.ipam.recycleTip'),
|
||||
onOk: () => {
|
||||
postIPAMAddress({
|
||||
ips: [ip],
|
||||
parent_id: this.nodeData._id,
|
||||
assign_status: 1
|
||||
}).then(() => {
|
||||
this.$message.success(this.$t('cmdb.ipam.recycleSuccess', { ip }))
|
||||
this.getIPList()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.address {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
position: relative;
|
||||
|
||||
&-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 12px
|
||||
}
|
||||
|
||||
&-search {
|
||||
height: 32px;
|
||||
width: 246px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px
|
||||
}
|
||||
|
||||
&-filter {
|
||||
width: 150px;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
column-gap: 20px;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-content {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
column-gap: 24px;
|
||||
}
|
||||
|
||||
&-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
border: solid 1px #E4E7ED;
|
||||
|
||||
&-item {
|
||||
height: 100%;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: solid 1px #E4E7ED;
|
||||
}
|
||||
|
||||
&-active {
|
||||
color: #2F54EB;
|
||||
background-color: #F0F5FF;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
&-null {
|
||||
width: 100%;
|
||||
padding-top: 130px;
|
||||
text-align: center;
|
||||
|
||||
&-img {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
&-tip {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&-tip2 {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
|
||||
&-loading {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: #000000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
|
||||
&-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,346 @@
|
|||
<template>
|
||||
<div class="ip-table">
|
||||
<ops-table
|
||||
ref="xTable"
|
||||
size="small"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
:data="tableData"
|
||||
:row-config="{ useKey: true, keyField: 'ip' }"
|
||||
:column-config="{ resizable: true }"
|
||||
:checkbox-config="{ highlight: true, reserve: true, range: true }"
|
||||
:height="tableHeight"
|
||||
class="ops-unstripe-table checkbox-hover-table"
|
||||
@checkbox-change="onSelectChange"
|
||||
@checkbox-all="onSelectChange"
|
||||
@checkbox-range-end="onSelectChange"
|
||||
>
|
||||
<vxe-table-column
|
||||
align="center"
|
||||
type="checkbox"
|
||||
:width="60"
|
||||
>
|
||||
<template #default="{row}">
|
||||
{{ getRowSeq(row) }}
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
|
||||
<vxe-table-column
|
||||
v-for="(col) in columns"
|
||||
:key="col.field"
|
||||
:title="col.title"
|
||||
:field="col.field"
|
||||
:width="columnWidth[col.field] || undefined"
|
||||
>
|
||||
<template
|
||||
v-if="col.field === '_ip_status' || col.is_link || col.is_reference || col.is_choice"
|
||||
#default="{ row }"
|
||||
>
|
||||
<div v-if="col.field === '_ip_status'" class="ip-table-status">
|
||||
<div
|
||||
class="ip-table-status-dot"
|
||||
:style="{
|
||||
backgroundColor: `${STATUS_COLOR[row._ip_status]}22`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="ip-table-status-dot-content"
|
||||
:style="{
|
||||
backgroundColor: STATUS_COLOR[row._ip_status]
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="ip-table-status-text"
|
||||
>
|
||||
{{ $t(STATUS_LABEL[row._ip_status]) }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="col.is_reference && row[col.field]" >
|
||||
<a
|
||||
v-for="(ciId) in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="ciId"
|
||||
:href="`/cmdb/cidetail/${col.reference_type_id}/${ciId}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getReferenceAttrValue(ciId, col) }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="col.is_link && row[col.field]">
|
||||
<a
|
||||
v-for="(linkItem, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="linkIndex"
|
||||
:href="
|
||||
linkItem.startsWith('http') || linkItem.startsWith('https')
|
||||
? `${linkItem}`
|
||||
: `http://${linkItem}`
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getChoiceValueLabel(col, linkItem) || linkItem }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="col.is_choice && row[col.field]">
|
||||
<span
|
||||
v-for="value in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="value"
|
||||
class="column-default-choice"
|
||||
>
|
||||
{{ getChoiceValueLabel(col, value) || value }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
|
||||
<vxe-table-column
|
||||
:title="$t('operation')"
|
||||
:width="80"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="ip-table-operation">
|
||||
<template v-if="[ADDRESS_STATUS.ONLINE_ASSIGNED, ADDRESS_STATUS.OFFLINE_ASSIGNED].includes(row._ip_status)">
|
||||
<a-tooltip :title="$t('cmdb.ipam.editAssignAddress')">
|
||||
<a @click="assignAddress(row)"><ops-icon type="veops-edit" /></a>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('cmdb.ipam.recycle')">
|
||||
<a @click="clickRecycle(row)"><ops-icon type="veops-recycle" /></a>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tooltip v-else :title="$t('cmdb.ipam.assign')">
|
||||
<a @click="assignAddress(row)"><ops-icon type="monitor-add2" /></a>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
</ops-table>
|
||||
<div class="ip-table-pagination">
|
||||
<a-pagination
|
||||
:showSizeChanger="true"
|
||||
:current="page"
|
||||
size="small"
|
||||
:total="allTableData.length"
|
||||
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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import { STATUS_COLOR, STATUS_LABEL, ADDRESS_STATUS } from './constants.js'
|
||||
|
||||
export default {
|
||||
name: 'TableIP',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allTableData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
referenceShowAttrNameMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
referenceCIIdMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
columnWidth: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
STATUS_COLOR,
|
||||
STATUS_LABEL,
|
||||
ADDRESS_STATUS,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return `${this.windowHeight - 270}px`
|
||||
},
|
||||
tableData() {
|
||||
const start = (this.page - 1) * this.pageSize
|
||||
const end = start + this.pageSize
|
||||
const tableData = this.allTableData.slice(start, end)
|
||||
|
||||
return _.cloneDeep(tableData)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
allTableData: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.page = 1
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRowSeq(row) {
|
||||
const table = this.$refs?.['xTable']?.getVxetableRef?.() || null
|
||||
return table?.getRowSeq?.(row)
|
||||
},
|
||||
|
||||
handlePageSizeChange(_, pageSize) {
|
||||
this.pageSize = pageSize
|
||||
this.page = 1
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
this.page = page
|
||||
},
|
||||
|
||||
assignAddress(data) {
|
||||
this.$emit('openAssign', data)
|
||||
},
|
||||
|
||||
clickRecycle(data) {
|
||||
this.$emit('recycle', data.ip)
|
||||
},
|
||||
|
||||
getCheckedTableData(clearCheckbox = true) {
|
||||
const tableRef = this.$refs.xTable.getVxetableRef()
|
||||
let tableData = _.cloneDeep([
|
||||
...tableRef.getCheckboxReserveRecords(),
|
||||
...tableRef.getCheckboxRecords(true),
|
||||
])
|
||||
if (!tableData.length) {
|
||||
const { fullData } = tableRef.getTableData()
|
||||
tableData = _.cloneDeep(fullData)
|
||||
}
|
||||
|
||||
if (clearCheckbox) {
|
||||
tableRef.clearCheckboxRow()
|
||||
tableRef.clearCheckboxReserve()
|
||||
}
|
||||
|
||||
return tableData
|
||||
},
|
||||
|
||||
getReferenceAttrValue(id, col) {
|
||||
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
|
||||
if (!ci) {
|
||||
return id
|
||||
}
|
||||
|
||||
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
|
||||
return ci?.[attrName] || id
|
||||
},
|
||||
|
||||
getChoiceValueLabel(col, colValue) {
|
||||
const _find = col?.choice_value?.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find?.[1]?.label || ''
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
onSelectChange() {
|
||||
console.log('onSelectChange')
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ip-table {
|
||||
&-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-content {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
}
|
||||
|
||||
&-operation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
&-pagination {
|
||||
text-align: right;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div class="history">
|
||||
<div class="history-tab">
|
||||
<div
|
||||
v-for="(item) in tabs"
|
||||
:key="item.key"
|
||||
:class="['history-tab-item', activeKey === item.key ? 'history-tab-item-active' : '']"
|
||||
@click="activeKey = item.key"
|
||||
>
|
||||
{{ $t(item.title) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="history-main">
|
||||
<Operation
|
||||
v-if="activeKey === 'operation'"
|
||||
ref="operationRef"
|
||||
/>
|
||||
<Scan v-if="activeKey === 'scan'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Operation from './operation/index.vue'
|
||||
import Scan from './scan/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'HistoryLog',
|
||||
components: {
|
||||
Operation,
|
||||
Scan
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeKey: 'operation',
|
||||
tabs: [
|
||||
{
|
||||
key: 'operation',
|
||||
title: 'cmdb.ipam.operationLog'
|
||||
},
|
||||
{
|
||||
key: 'scan',
|
||||
title: 'cmdb.ipam.scanLog'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refreshData() {
|
||||
if (this.activeKey === 'operation' && this.$refs.operationRef) {
|
||||
this.$refs.operationRef.getTableData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.history {
|
||||
width: 100%;
|
||||
|
||||
&-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: solid 1px #E4E7ED;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
padding: 0 20px;
|
||||
background-color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&-active {
|
||||
background-color: #2F54EB;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,56 @@
|
|||
export const OPERATE_TYPE = {
|
||||
ADD_SCOPE: '0',
|
||||
UPDATE_SCOPE: '1',
|
||||
DELETE_SCOPE: '2',
|
||||
ADD_SUBNET: '3',
|
||||
UPDATE_SUBNET: '4',
|
||||
DELETE_SUBNET: '5',
|
||||
ASSIGN_ADDRESS: '6',
|
||||
REVOKE_ADDRESS: '7',
|
||||
}
|
||||
|
||||
export const OPERATE_TYPE_TEXT = {
|
||||
[OPERATE_TYPE.ADD_SCOPE]: 'cmdb.ipam.addCatalog',
|
||||
[OPERATE_TYPE.UPDATE_SCOPE]: 'cmdb.ipam.updateCatalog',
|
||||
[OPERATE_TYPE.DELETE_SCOPE]: 'cmdb.ipam.deleteCatalog',
|
||||
[OPERATE_TYPE.ADD_SUBNET]: 'cmdb.ipam.addSubnet',
|
||||
[OPERATE_TYPE.UPDATE_SUBNET]: 'cmdb.ipam.updateSubnet',
|
||||
[OPERATE_TYPE.DELETE_SUBNET]: 'cmdb.ipam.deleteSubnet',
|
||||
[OPERATE_TYPE.ASSIGN_ADDRESS]: 'cmdb.ipam.addressAssign',
|
||||
[OPERATE_TYPE.REVOKE_ADDRESS]: 'cmdb.ipam.revokeAddress',
|
||||
}
|
||||
|
||||
export const OPERATE_TYPE_COLOR = {
|
||||
[OPERATE_TYPE.ADD_SCOPE]: {
|
||||
color: '#2F54EB',
|
||||
backgroundColor: '#DCF5FF'
|
||||
},
|
||||
[OPERATE_TYPE.UPDATE_SCOPE]: {
|
||||
color: '#FF7D00',
|
||||
backgroundColor: '#FFECCF'
|
||||
},
|
||||
[OPERATE_TYPE.DELETE_SCOPE]: {
|
||||
color: '#FD4C6A',
|
||||
backgroundColor: '#FFECE8'
|
||||
},
|
||||
[OPERATE_TYPE.ADD_SUBNET]: {
|
||||
color: '#2F54EB',
|
||||
backgroundColor: '#DCF5FF'
|
||||
},
|
||||
[OPERATE_TYPE.UPDATE_SUBNET]: {
|
||||
color: '#FF7D00',
|
||||
backgroundColor: '#FFECCF'
|
||||
},
|
||||
[OPERATE_TYPE.DELETE_SUBNET]: {
|
||||
color: '#FD4C6A',
|
||||
backgroundColor: '#FFECE8'
|
||||
},
|
||||
[OPERATE_TYPE.ASSIGN_ADDRESS]: {
|
||||
color: '#00B42A',
|
||||
backgroundColor: '#F6FFED'
|
||||
},
|
||||
[OPERATE_TYPE.REVOKE_ADDRESS]: {
|
||||
color: '#0AA5A8',
|
||||
backgroundColor: '#E8FFFB'
|
||||
},
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
<template>
|
||||
<div class="operate">
|
||||
<a-input-search
|
||||
class="operate-search"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ops-table
|
||||
ref="xTable"
|
||||
size="small"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
:data="tableData"
|
||||
:height="tableHeight"
|
||||
class="ops-unstripe-table operate-table"
|
||||
:filter-config="{ remote: true }"
|
||||
:sort-config="{ remote: true, trigger: 'cell' }"
|
||||
:column-config="{ resizable: true }"
|
||||
@filter-change="handlefilterChange"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.operateTime')"
|
||||
sortable
|
||||
field="created_at"
|
||||
:width="150"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.operateUser')"
|
||||
field="uid"
|
||||
:filters="userFilters"
|
||||
:width="130"
|
||||
>
|
||||
<template #default="{row}">
|
||||
{{ row.nickname }}
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.operateType')"
|
||||
field="operate_type"
|
||||
:filters="operateTypeFilters"
|
||||
:width="150"
|
||||
>
|
||||
<template #default="{row}">
|
||||
<div
|
||||
v-if="row.operate_type"
|
||||
class="operate-table-type"
|
||||
:style="{
|
||||
backgroundColor: OPERATE_TYPE_COLOR[row.operate_type].backgroundColor,
|
||||
color: OPERATE_TYPE_COLOR[row.operate_type].color
|
||||
}"
|
||||
>
|
||||
{{ $t(OPERATE_TYPE_TEXT[row.operate_type]) }}
|
||||
</div>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
<vxe-table-column
|
||||
title="CIDR"
|
||||
field="cidr"
|
||||
:width="150"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.description')"
|
||||
field="description"
|
||||
></vxe-table-column>
|
||||
</ops-table>
|
||||
|
||||
<div class="operate-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,
|
||||
})
|
||||
"
|
||||
@change="handleChangePage"
|
||||
@showSizeChange="onShowSizeChange"
|
||||
>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
|
||||
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import { OPERATE_TYPE_TEXT, OPERATE_TYPE_COLOR, OPERATE_TYPE } from './constants.js'
|
||||
import { getIPAMHistoryOperate } from '@/modules/cmdb/api/ipam.js'
|
||||
|
||||
export default {
|
||||
name: 'Operate',
|
||||
data() {
|
||||
return {
|
||||
OPERATE_TYPE_TEXT,
|
||||
OPERATE_TYPE_COLOR,
|
||||
searchValue: '',
|
||||
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
tableData: [],
|
||||
totalNumber: 0,
|
||||
getTableDataParams: {
|
||||
reverse: 1
|
||||
},
|
||||
userFilters: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
allEmployees: (state) => state.user.allEmployees,
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return `${this.windowHeight - 308}px`
|
||||
},
|
||||
operateTypeFilters() {
|
||||
const filters = Object.values(OPERATE_TYPE).map((key) => {
|
||||
return {
|
||||
value: key,
|
||||
label: this.$t(OPERATE_TYPE_TEXT[key])
|
||||
}
|
||||
})
|
||||
return filters
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getTableData()
|
||||
},
|
||||
methods: {
|
||||
async getTableData() {
|
||||
const res = await getIPAMHistoryOperate({
|
||||
page: this.page,
|
||||
page_size: this.pageSize,
|
||||
...this.getTableDataParams
|
||||
})
|
||||
|
||||
const tableData = res?.result || []
|
||||
const userFilters = []
|
||||
const defaultUserChecked = this.getTableDataParams.uid ? this.getTableDataParams.uid.split(',') : []
|
||||
|
||||
tableData.forEach((item) => {
|
||||
const nickname = this.allEmployees?.find?.((user) => user?.acl_uid === item?.uid)?.nickname
|
||||
item.nickname = nickname
|
||||
userFilters.push({
|
||||
label: nickname,
|
||||
value: item.uid,
|
||||
checked: defaultUserChecked.includes(String(item.uid))
|
||||
})
|
||||
})
|
||||
|
||||
this.totalNumber = res?.numfound || 0
|
||||
this.tableData = tableData
|
||||
this.userFilters = _.uniqBy(userFilters, 'value')
|
||||
},
|
||||
handleSearch(v) {
|
||||
if (v) {
|
||||
this.getTableDataParams.cidr = `*${v}*`
|
||||
} else if (this.getTableDataParams.cidr) {
|
||||
delete this.getTableDataParams.cidr
|
||||
}
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
},
|
||||
handleChangePage(page) {
|
||||
this.page = page
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
onShowSizeChange(_, pageSize) {
|
||||
this.page = 1
|
||||
this.pageSize = pageSize
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
handlefilterChange({ field, values }) {
|
||||
this.page = 1
|
||||
const value = values.join(',')
|
||||
if (!value && this.getTableDataParams[field]) {
|
||||
delete this.getTableDataParams[field]
|
||||
} else {
|
||||
this.getTableDataParams[field] = values.join(',')
|
||||
}
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
handleSortChange(data) {
|
||||
if (data?.order === 'asc') {
|
||||
this.getTableDataParams.reverse = 0
|
||||
} else {
|
||||
this.getTableDataParams.reverse = 1
|
||||
}
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.operate {
|
||||
width: 100%;
|
||||
|
||||
&-search {
|
||||
width: 244px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
&-table {
|
||||
&-type {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
padding: 0 9px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&-pagination {
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,286 @@
|
|||
<template>
|
||||
<div class="scan">
|
||||
<a-input-search
|
||||
class="scan-search"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ops-table
|
||||
ref="xTable"
|
||||
size="small"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
:data="tableData"
|
||||
:height="tableHeight"
|
||||
:column-config="{ resizable: true }"
|
||||
class="ops-unstripe-table scan-table"
|
||||
>
|
||||
<vxe-table-column
|
||||
title="CIDR"
|
||||
field="cidr"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.ipNumber')"
|
||||
field="ip_num"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.startTime')"
|
||||
field="start_at"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.endTime')"
|
||||
field="end_at"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.scanningTime')"
|
||||
field="scanning_time"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.isSuccess')"
|
||||
field="status"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="scan-table-success" v-if="row.status === 0">
|
||||
<a-icon class="scan-table-success-icon" type="check-circle" theme="filled" />
|
||||
<div class="scan-table-success-text">{{ $t('success') }}</div>
|
||||
</div>
|
||||
<div class="scan-table-fail" v-else>
|
||||
<a-icon class="scan-table-fail-icon" type="close-circle" theme="filled" />
|
||||
<div class="scan-table-fail-text">{{ $t('fail') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.viewResult')"
|
||||
field="operation"
|
||||
:show-overflow="false"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<a-popover placement="left">
|
||||
<span class="scan-table-operation">
|
||||
{{ row.status === 0 ? row.ips ? row.ips.join(', ') : '' : row.stdout }}
|
||||
</span>
|
||||
<template slot="content">
|
||||
<div
|
||||
v-if="row.status === 0"
|
||||
class="scan-table-ip"
|
||||
>
|
||||
<div
|
||||
v-for="(ip, index) in row.ips"
|
||||
:key="index"
|
||||
class="scan-table-ip-item"
|
||||
>
|
||||
{{ ip }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="scan-table-error-log"
|
||||
>
|
||||
{{ row.stdout }}
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
</ops-table>
|
||||
|
||||
<div class="scan-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,
|
||||
})
|
||||
"
|
||||
@change="handleChangePage"
|
||||
@showSizeChange="onShowSizeChange"
|
||||
>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
|
||||
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import { mapState } from 'vuex'
|
||||
import { getIPAMHistoryScan } from '@/modules/cmdb/api/ipam.js'
|
||||
|
||||
export default {
|
||||
name: 'Scan',
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
tableData: [],
|
||||
totalNumber: 0,
|
||||
getTableDataParams: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return `${this.windowHeight - 308}px`
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getTableData()
|
||||
},
|
||||
methods: {
|
||||
async getTableData() {
|
||||
const res = await getIPAMHistoryScan({
|
||||
page: this.page,
|
||||
page_size: this.pageSize,
|
||||
reverse: 1,
|
||||
...this.getTableDataParams
|
||||
})
|
||||
|
||||
const tableData = res?.result || []
|
||||
|
||||
tableData.forEach((item) => {
|
||||
if (item.start_at && item.end_at) {
|
||||
const startAt = moment(item.start_at)
|
||||
const endAt = moment(item.end_at)
|
||||
item.scanning_time = `${endAt.diff(startAt, 'seconds')}s`
|
||||
}
|
||||
})
|
||||
|
||||
this.tableData = tableData
|
||||
this.totalNumber = res?.numfound || 0
|
||||
},
|
||||
|
||||
handleChangePage(page) {
|
||||
this.page = page
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
onShowSizeChange(_, pageSize) {
|
||||
this.page = 1
|
||||
this.pageSize = pageSize
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
handleSearch(v) {
|
||||
if (v) {
|
||||
this.getTableDataParams.cidr = `*${v}*`
|
||||
} else if (this.getTableDataParams.cidr) {
|
||||
delete this.getTableDataParams.cidr
|
||||
}
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.scan {
|
||||
width: 100%;
|
||||
|
||||
&-search {
|
||||
width: 244px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
&-table {
|
||||
&-success {
|
||||
padding: 4px 7px;
|
||||
border-radius: 1px;
|
||||
background-color: #DCF3E3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #00B42A;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #30AD2D;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&-fail {
|
||||
padding: 0px 7px;
|
||||
border-radius: 1px;
|
||||
background-color: #FFECE8;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #FD4C6A;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #FD4C6A;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&-operation {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-ip {
|
||||
width: 100%;
|
||||
max-height: 216px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border: solid 1px #F0F1F5;
|
||||
|
||||
&-item {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 1px #F0F1F5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-error-log {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&-pagination {
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,385 @@
|
|||
<template>
|
||||
<div ref="wrapRef">
|
||||
<div class="table-header">
|
||||
<SearchForm
|
||||
ref="search"
|
||||
:preferenceAttrList="preferenceAttrList"
|
||||
:typeId="addressCITypeId"
|
||||
@copyExpression="copyExpression"
|
||||
@refresh="handleSearch"
|
||||
/>
|
||||
|
||||
<div class="table-header-right">
|
||||
<EditAttrsPopover
|
||||
:typeId="addressCITypeId"
|
||||
@refresh="refreshAfterEditAttrs"
|
||||
>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
<ops-icon type="veops-configuration_table" />
|
||||
{{ $t('cmdb.configTable') }}
|
||||
</a-button>
|
||||
</EditAttrsPopover>
|
||||
<a-button
|
||||
v-if="instanceList && instanceList.length"
|
||||
type="primary"
|
||||
class="ops-button-ghost"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
<ops-icon type="veops-export" />
|
||||
{{ $t('export') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:loading="loading"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:data="instanceList"
|
||||
:height="tableHeight"
|
||||
@sort-change="handleSortCol"
|
||||
@openDetail="openDetail"
|
||||
@deleteCI="deleteCI"
|
||||
/>
|
||||
|
||||
<div class="table-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,
|
||||
})
|
||||
"
|
||||
@change="handleChangePage"
|
||||
@showSizeChange="onShowSizeChange"
|
||||
>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
|
||||
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
|
||||
<BatchDownload
|
||||
ref="batchDownload"
|
||||
:showFileTypeSelect="false"
|
||||
@batchDownload="batchDownload"
|
||||
/>
|
||||
|
||||
<CIDetailDrawer ref="detail" :typeId="addressCITypeId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import { searchCI, deleteCI } from '@/modules/cmdb/api/ci'
|
||||
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
|
||||
|
||||
import SearchForm from '@/modules/cmdb/components/searchForm/SearchForm.vue'
|
||||
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
|
||||
import BatchDownload from '@/modules/cmdb/components/batchDownload/batchDownload.vue'
|
||||
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
|
||||
import EditAttrsPopover from '@/modules/cmdb/views/ci/modules/editAttrsPopover.vue'
|
||||
|
||||
export default {
|
||||
name: 'IPSearch',
|
||||
components: {
|
||||
SearchForm,
|
||||
CITable,
|
||||
BatchDownload,
|
||||
CIDetailDrawer,
|
||||
EditAttrsPopover
|
||||
},
|
||||
props: {
|
||||
addressCIType: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
loading: false,
|
||||
sortByTable: undefined,
|
||||
|
||||
instanceList: [],
|
||||
totalNumber: 0,
|
||||
columns: [],
|
||||
preferenceAttrList: [],
|
||||
attrList: [],
|
||||
attributes: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return this.windowHeight - 260
|
||||
},
|
||||
addressCITypeId() {
|
||||
return this.addressCIType?.id || null
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
handleSearch: this.getTableData,
|
||||
attrList: () => {
|
||||
return this.attrList
|
||||
},
|
||||
attributes: () => {
|
||||
return this.attributes
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.$nextTick(async () => {
|
||||
if (this.addressCITypeId) {
|
||||
await this.getAttributeList()
|
||||
await this.getPreferenceAttrList()
|
||||
this.getTableData()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
async getAttributeList() {
|
||||
await getCITypeAttributesById(this.addressCITypeId).then((res) => {
|
||||
this.attrList = res.attributes
|
||||
this.attributes = res
|
||||
})
|
||||
},
|
||||
|
||||
async getPreferenceAttrList() {
|
||||
const subscribed = await getSubscribeAttributes(this.addressCITypeId)
|
||||
this.preferenceAttrList = subscribed.attributes
|
||||
},
|
||||
|
||||
async getTableData() {
|
||||
try {
|
||||
this.loading = true
|
||||
const fuzzySearch = this.$refs['search'].fuzzySearch
|
||||
const expression = this.$refs['search'].expression || ''
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const regSort = /(?<=sort=).+/g
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
|
||||
let sort
|
||||
if (this.sortByTable) {
|
||||
sort = this.sortByTable
|
||||
} else {
|
||||
sort = expression.match(regSort) ? expression.match(regSort)[0] : undefined
|
||||
}
|
||||
|
||||
const res = await searchCI({
|
||||
q: `_type:${this.addressCITypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
|
||||
count: this.pageSize,
|
||||
page: this.page,
|
||||
sort,
|
||||
})
|
||||
|
||||
this.totalNumber = res?.numfound
|
||||
const instanceList = res.result
|
||||
|
||||
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
|
||||
instanceList.forEach((item) => {
|
||||
jsonAttrList.forEach(
|
||||
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
|
||||
)
|
||||
})
|
||||
|
||||
this.getColumns(instanceList)
|
||||
this.instanceList = instanceList
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
getColumns(data) {
|
||||
const width = this.$refs.wrapRef.clientWidth - 50
|
||||
const columns = getCITableColumns(data, this.preferenceAttrList, width)
|
||||
columns.forEach((item) => {
|
||||
if (item.editRender) {
|
||||
item.editRender.enabled = false
|
||||
}
|
||||
})
|
||||
this.columns = columns
|
||||
},
|
||||
|
||||
copyExpression() {
|
||||
const expression = this.$refs['search'].expression || ''
|
||||
const fuzzySearch = this.$refs['search'].fuzzySearch
|
||||
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
const text = `q=_type:${this.addressCITypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`
|
||||
this.$copyText(text)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('copySuccess'))
|
||||
})
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
this.$refs.xTable.getVxetableRef().clearSort()
|
||||
this.$nextTick(() => {
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
})
|
||||
},
|
||||
|
||||
handleChangePage(page) {
|
||||
this.page = page
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
onShowSizeChange(_, pageSize) {
|
||||
this.page = 1
|
||||
this.pageSize = pageSize
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
handleSortCol({ property, order }) {
|
||||
let sortByTable
|
||||
if (order === 'asc') {
|
||||
sortByTable = property
|
||||
} else if (order === 'desc') {
|
||||
sortByTable = `-${property}`
|
||||
}
|
||||
|
||||
this.sortByTable = sortByTable
|
||||
this.$nextTick(() => {
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
})
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
this.$refs.batchDownload.open({
|
||||
preferenceAttrList: this.preferenceAttrList,
|
||||
ciTypeName: this.$t('cmdb.ipam.ipSearch') || '',
|
||||
})
|
||||
},
|
||||
|
||||
batchDownload({ checkedKeys, filename }) {
|
||||
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 columns = []
|
||||
|
||||
const attrMap = new Map()
|
||||
this.columns.filter((col) => checkedKeys.includes(col.field)).map((col) => {
|
||||
attrMap.set(col.field, col)
|
||||
|
||||
columns.push({
|
||||
header: col.title || '',
|
||||
key: col.field,
|
||||
width: 20,
|
||||
})
|
||||
})
|
||||
|
||||
ws.columns = columns
|
||||
|
||||
tableData.forEach((item) => {
|
||||
const row = {}
|
||||
|
||||
columns.forEach(({ key }) => {
|
||||
const value = item?.[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, `${filename}.xlsx`)
|
||||
})
|
||||
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
},
|
||||
|
||||
openDetail(id, activeTabKey, ciDetailRelationKey) {
|
||||
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
|
||||
},
|
||||
|
||||
async refreshAfterEditAttrs() {
|
||||
await this.getPreferenceAttrList()
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
deleteCI(record) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('confirmDelete'),
|
||||
onOk: () => {
|
||||
deleteCI(record.ci_id || record._id).then(() => {
|
||||
this.$message.success(this.$t('deleteSuccess'))
|
||||
this.getTableData()
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
}
|
||||
}
|
||||
.table-pagination {
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div class="overview">
|
||||
<Stats :statsData="statsData" />
|
||||
<SubnetTable :tableData="tableData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getIPAMStats } from '@/modules/cmdb/api/ipam.js'
|
||||
|
||||
import Stats from './stats.vue'
|
||||
import SubnetTable from './subnetTable.vue'
|
||||
|
||||
export default {
|
||||
name: 'Overview',
|
||||
components: {
|
||||
Stats,
|
||||
SubnetTable
|
||||
},
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statsData: {},
|
||||
tableData: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
nodeId: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
this.initData(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initData() {
|
||||
const res = await getIPAMStats({
|
||||
parent_id: this.nodeId === 'all' ? 0 : this.nodeId
|
||||
})
|
||||
const tableData = res?.subnets || []
|
||||
tableData.forEach((item) => {
|
||||
item.hosts_count = item?.hosts_count || 0
|
||||
item.used_ratio = item?.used_count && item?.hosts_count ? Math.round((item.used_count / item.hosts_count) * 100) : 0
|
||||
})
|
||||
|
||||
this.statsData = res
|
||||
this.tableData = tableData
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.overview {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,212 @@
|
|||
<template>
|
||||
<div class="stats">
|
||||
<div
|
||||
class="stats-card"
|
||||
v-for="(item, index) in statsListData"
|
||||
:key="index"
|
||||
>
|
||||
<div class="stats-card-left">
|
||||
<div class="stats-card-title">{{ $t(item.title) }}</div>
|
||||
|
||||
<div class="stats-card-row">
|
||||
<div
|
||||
v-for="(dataItem, dataIndex) in item.data"
|
||||
:key="dataIndex"
|
||||
class="stats-card-data"
|
||||
>
|
||||
<span class="stats-card-data-label">{{ $t(dataItem.label) }}</span>
|
||||
<span class="stats-card-data-value">{{ dataItem.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.logo"
|
||||
class="stats-card-logo"
|
||||
>
|
||||
<ops-icon
|
||||
:type="item.logo"
|
||||
class="stats-card-logo-icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StatsChart
|
||||
v-else
|
||||
:statsData="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StatsChart from './statsChart.vue'
|
||||
|
||||
export default {
|
||||
name: 'Statistics',
|
||||
components: {
|
||||
StatsChart
|
||||
},
|
||||
props: {
|
||||
statsData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statsListData() {
|
||||
const {
|
||||
subnet_num = 0,
|
||||
address_num = 0,
|
||||
address_free_num = 0,
|
||||
address_assign_num = 0,
|
||||
address_unassign_num = 0,
|
||||
address_used_num = 0,
|
||||
address_used_free_num = 0
|
||||
} = this.statsData || {}
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'cmdb.ipam.subnetStats',
|
||||
logo: 'caise-IPAM',
|
||||
data: [
|
||||
{
|
||||
label: 'cmdb.ipam.total',
|
||||
value: subnet_num
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'cmdb.ipam.addressStats',
|
||||
data: [
|
||||
{
|
||||
label: 'cmdb.ipam.total',
|
||||
value: address_num,
|
||||
chartValue: address_num - address_free_num,
|
||||
},
|
||||
{
|
||||
label: 'cmdb.ipam.free',
|
||||
value: address_free_num
|
||||
}
|
||||
],
|
||||
ratio: address_num && address_free_num ? Math.round((address_free_num / address_num) * 100) : 0,
|
||||
chartColor: ['#6EE3EB', '#6592FD']
|
||||
},
|
||||
{
|
||||
title: 'cmdb.ipam.assignStats',
|
||||
data: [
|
||||
{
|
||||
label: 'cmdb.ipam.assigned',
|
||||
value: address_assign_num
|
||||
},
|
||||
{
|
||||
label: 'cmdb.ipam.unassigned',
|
||||
value: address_unassign_num
|
||||
}
|
||||
],
|
||||
ratio: address_num && address_assign_num ? Math.round((address_assign_num / address_num) * 100) : 0,
|
||||
chartColor: ['#8C85ED', '#387BFD']
|
||||
},
|
||||
{
|
||||
title: 'cmdb.ipam.onlineStats',
|
||||
data: [
|
||||
{
|
||||
label: 'cmdb.ipam.online',
|
||||
value: address_used_num
|
||||
},
|
||||
{
|
||||
label: 'cmdb.ipam.offline',
|
||||
value: address_used_free_num
|
||||
}
|
||||
],
|
||||
ratio: address_num && address_used_num ? Math.round((address_used_num / address_num) * 100) : 0,
|
||||
chartColor: ['#009FA9', '#17D4B0']
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
statsData: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(data) {
|
||||
this.initData(data)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initData() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.stats {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
column-gap: 24px;
|
||||
row-gap: 12px;
|
||||
|
||||
&-card {
|
||||
padding: 14px 17px;
|
||||
background-color: #F7F8FA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 25px;
|
||||
}
|
||||
|
||||
&-data {
|
||||
display: flex;
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
}
|
||||
|
||||
&-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-logo {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 52px;
|
||||
background-color: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<div class="stats-chart">
|
||||
<div
|
||||
class="stats-chart-pie"
|
||||
ref="statsChartRef"
|
||||
></div>
|
||||
<div class="stats-chart-ratio">
|
||||
{{ statsData.ratio }}%
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'StatsChart',
|
||||
props: {
|
||||
statsData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
statsData: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(data) {
|
||||
this.updateChart(data)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart(data) {
|
||||
const option = {
|
||||
color: data?.chartColor || [],
|
||||
tooltip: {
|
||||
show: false
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['60%', '85%'],
|
||||
data: data?.data?.map((item) => {
|
||||
return {
|
||||
name: this.$t(item?.label),
|
||||
value: item?.chartValue ?? item.value
|
||||
}
|
||||
}) || [],
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (!this.chart) {
|
||||
const el = this.$refs.statsChartRef
|
||||
this.chart = echarts.init(el)
|
||||
}
|
||||
this.chart.setOption(option)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.stats-chart {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
|
||||
&-pie {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-ratio {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,207 @@
|
|||
<template>
|
||||
<div class="subnet-table">
|
||||
<div class="subnet-table-title">
|
||||
{{ $t('cmdb.ipam.onlineUsageStats') }}
|
||||
</div>
|
||||
|
||||
<ops-table
|
||||
ref="xTable"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
:data="tableData"
|
||||
size="small"
|
||||
:height="tableHeight"
|
||||
:column-config="{ resizable: true }"
|
||||
class="ops-unstripe-table"
|
||||
>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.subnetName')"
|
||||
:min-width="130"
|
||||
field="name"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
title="CIDR"
|
||||
field="cidr"
|
||||
:min-width="130"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.addressCount')"
|
||||
field="hosts_count"
|
||||
:min-width="70"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.onlineRatio')"
|
||||
field="onlineRatio"
|
||||
:min-width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="subnet-table-ratio">
|
||||
<div class="subnet-table-ratio-value">
|
||||
{{ row.used_ratio }}%
|
||||
</div>
|
||||
<div class="subnet-table-ratio-progress">
|
||||
<div
|
||||
class="subnet-table-ratio-progress-content"
|
||||
:style="{
|
||||
width: row.used_ratio + '%'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="subnet-table-ratio-count">
|
||||
{{ row.used_count }}/{{ row.hosts_count }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.assigned')"
|
||||
field="assign_count"
|
||||
:min-width="70"
|
||||
></vxe-table-column>
|
||||
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.free')"
|
||||
field="free_count"
|
||||
:min-width="50"
|
||||
></vxe-table-column>
|
||||
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.scanEnable')"
|
||||
field="scan_enabled"
|
||||
:min-width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="subnet-table-scan-yes" v-if="row.scan_enabled">
|
||||
<a-icon class="subnet-table-scan-yes-icon" type="check-circle" theme="filled" />
|
||||
<div class="subnet-table-scan-yes-text">{{ $t('yes') }}</div>
|
||||
</div>
|
||||
<div class="subnet-table-scan-no" v-else>
|
||||
<a-icon class="subnet-table-scan-no-icon" type="close-circle" theme="filled" />
|
||||
<div class="subnet-table-scan-no-text">{{ $t('no') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.ipam.lastScanTime')"
|
||||
field="last_scan_time"
|
||||
:min-width="100"
|
||||
></vxe-table-column>
|
||||
</ops-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'SubnetTable',
|
||||
props: {
|
||||
tableData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return `${this.windowHeight - 337}px`
|
||||
},
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.subnet-table {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
|
||||
&-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&-ratio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-value {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
&-progress {
|
||||
width: 84px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background-color: #EBEFF8;
|
||||
margin-left: 12px;
|
||||
|
||||
&-content {
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background-color: #7F97FA;
|
||||
}
|
||||
}
|
||||
|
||||
&-count {
|
||||
margin-left: 5px;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: #86909C;
|
||||
}
|
||||
}
|
||||
|
||||
&-scan-yes {
|
||||
padding: 4px 7px;
|
||||
border-radius: 1px;
|
||||
background-color: #DCF3E3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #00B42A;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #30AD2D;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&-scan-no {
|
||||
padding: 0px 7px;
|
||||
border-radius: 1px;
|
||||
background-color: #E4E7ED;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #A5A9BC;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,387 @@
|
|||
<template>
|
||||
<div ref="wrapRef">
|
||||
<div class="table-header">
|
||||
<SearchForm
|
||||
ref="search"
|
||||
:preferenceAttrList="preferenceAttrList"
|
||||
:typeId="subnetCITypeId"
|
||||
@copyExpression="copyExpression"
|
||||
@refresh="handleSearch"
|
||||
/>
|
||||
|
||||
<div class="table-header-right">
|
||||
<EditAttrsPopover
|
||||
:typeId="subnetCITypeId"
|
||||
@refresh="refreshAfterEditAttrs"
|
||||
>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
<ops-icon type="veops-configuration_table" />
|
||||
{{ $t('cmdb.configTable') }}
|
||||
</a-button>
|
||||
</EditAttrsPopover>
|
||||
<a-button
|
||||
v-if="instanceList && instanceList.length"
|
||||
type="primary"
|
||||
class="ops-button-ghost"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
<ops-icon type="veops-export" />
|
||||
{{ $t('export') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:loading="loading"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:data="instanceList"
|
||||
:height="tableHeight"
|
||||
@sort-change="handleSortCol"
|
||||
@openDetail="openDetail"
|
||||
@deleteCI="deleteCI"
|
||||
/>
|
||||
|
||||
<div class="table-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,
|
||||
})
|
||||
"
|
||||
@change="handleChangePage"
|
||||
@showSizeChange="onShowSizeChange"
|
||||
>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
|
||||
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
|
||||
<BatchDownload
|
||||
ref="batchDownload"
|
||||
:showFileTypeSelect="false"
|
||||
@batchDownload="batchDownload"
|
||||
/>
|
||||
|
||||
<CIDetailDrawer ref="detail" :typeId="subnetCITypeId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import { searchCI, deleteCI } from '@/modules/cmdb/api/ci'
|
||||
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
|
||||
|
||||
import SearchForm from '@/modules/cmdb/components/searchForm/SearchForm.vue'
|
||||
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
|
||||
import BatchDownload from '@/modules/cmdb/components/batchDownload/batchDownload.vue'
|
||||
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
|
||||
import EditAttrsPopover from '@/modules/cmdb/views/ci/modules/editAttrsPopover.vue'
|
||||
|
||||
export default {
|
||||
name: 'SubnetList',
|
||||
components: {
|
||||
SearchForm,
|
||||
CITable,
|
||||
BatchDownload,
|
||||
CIDetailDrawer,
|
||||
EditAttrsPopover
|
||||
},
|
||||
props: {
|
||||
subnetCIType: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
loading: false,
|
||||
sortByTable: undefined,
|
||||
|
||||
instanceList: [],
|
||||
totalNumber: 0,
|
||||
columns: [],
|
||||
preferenceAttrList: [],
|
||||
attrList: [],
|
||||
attributes: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return this.windowHeight - 260
|
||||
},
|
||||
subnetCITypeId() {
|
||||
return this?.subnetCIType?.id || null
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
handleSearch: this.getTableData,
|
||||
attrList: () => {
|
||||
return this.attrList
|
||||
},
|
||||
attributes: () => {
|
||||
return this.attributes
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.$nextTick(async () => {
|
||||
if (this.subnetCITypeId) {
|
||||
await this.getAttributeList()
|
||||
await this.getPreferenceAttrList()
|
||||
this.getTableData()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
async getAttributeList() {
|
||||
await getCITypeAttributesById(this.subnetCITypeId).then((res) => {
|
||||
this.attrList = res.attributes
|
||||
this.attributes = res
|
||||
})
|
||||
},
|
||||
|
||||
async getPreferenceAttrList() {
|
||||
const subscribed = await getSubscribeAttributes(this.subnetCITypeId)
|
||||
this.preferenceAttrList = subscribed.attributes
|
||||
},
|
||||
|
||||
async getTableData() {
|
||||
try {
|
||||
this.loading = true
|
||||
const fuzzySearch = this.$refs['search'].fuzzySearch
|
||||
const expression = this.$refs['search'].expression || ''
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const regSort = /(?<=sort=).+/g
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
|
||||
let sort
|
||||
if (this.sortByTable) {
|
||||
sort = this.sortByTable
|
||||
} else {
|
||||
sort = expression.match(regSort) ? expression.match(regSort)[0] : undefined
|
||||
}
|
||||
|
||||
const res = await searchCI({
|
||||
q: `_type:${this.subnetCITypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
|
||||
count: this.pageSize,
|
||||
page: this.page,
|
||||
sort,
|
||||
})
|
||||
|
||||
this.totalNumber = res?.numfound
|
||||
const instanceList = res.result
|
||||
|
||||
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
|
||||
instanceList.forEach((item) => {
|
||||
jsonAttrList.forEach(
|
||||
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
|
||||
)
|
||||
})
|
||||
|
||||
this.getColumns(instanceList)
|
||||
this.instanceList = instanceList
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
getColumns(data) {
|
||||
const width = this.$refs.wrapRef.clientWidth - 50
|
||||
const columns = getCITableColumns(data, this.preferenceAttrList, width)
|
||||
columns.forEach((item) => {
|
||||
if (item.editRender) {
|
||||
item.editRender.enabled = false
|
||||
}
|
||||
})
|
||||
this.columns = columns
|
||||
},
|
||||
|
||||
copyExpression() {
|
||||
const expression = this.$refs['search'].expression || ''
|
||||
const fuzzySearch = this.$refs['search'].fuzzySearch
|
||||
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
const text = `q=_type:${this.subnetCITypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`
|
||||
this.$copyText(text)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('copySuccess'))
|
||||
})
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
this.$refs.xTable.getVxetableRef().clearSort()
|
||||
this.$nextTick(() => {
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
})
|
||||
},
|
||||
|
||||
handleChangePage(page) {
|
||||
this.page = page
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
onShowSizeChange(_, pageSize) {
|
||||
this.page = 1
|
||||
this.pageSize = pageSize
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
handleSortCol({ property, order }) {
|
||||
let sortByTable
|
||||
if (order === 'asc') {
|
||||
sortByTable = property
|
||||
} else if (order === 'desc') {
|
||||
sortByTable = `-${property}`
|
||||
}
|
||||
|
||||
this.sortByTable = sortByTable
|
||||
this.$nextTick(() => {
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
})
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
this.$refs.batchDownload.open({
|
||||
preferenceAttrList: this.preferenceAttrList,
|
||||
ciTypeName: this.$t('cmdb.ipam.subnetList') || '',
|
||||
})
|
||||
},
|
||||
|
||||
batchDownload({ checkedKeys, filename }) {
|
||||
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 columns = []
|
||||
|
||||
const attrMap = new Map()
|
||||
this.columns.filter((col) => checkedKeys.includes(col.field)).map((col) => {
|
||||
attrMap.set(col.field, col)
|
||||
|
||||
columns.push({
|
||||
header: col.title || '',
|
||||
key: col.field,
|
||||
width: 20,
|
||||
})
|
||||
})
|
||||
|
||||
ws.columns = columns
|
||||
|
||||
tableData.forEach((item) => {
|
||||
const row = {}
|
||||
|
||||
columns.forEach(({ key }) => {
|
||||
const value = item?.[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, `${filename}.xlsx`)
|
||||
})
|
||||
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
},
|
||||
|
||||
openDetail(id, activeTabKey, ciDetailRelationKey) {
|
||||
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
|
||||
},
|
||||
|
||||
async refreshAfterEditAttrs() {
|
||||
await this.getPreferenceAttrList()
|
||||
this.getTableData()
|
||||
},
|
||||
|
||||
deleteCI(record) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('confirmDelete'),
|
||||
onOk: () => {
|
||||
deleteCI(record.ci_id || record._id).then(() => {
|
||||
this.$message.success(this.$t('deleteSuccess'))
|
||||
this.getTableData()
|
||||
this.$emit('delete')
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
|
@ -233,6 +233,7 @@ import CollapseTransition from '@/components/CollapseTransition'
|
|||
import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting'
|
||||
import { getCIAdcStatistics } from '../../api/ci'
|
||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||
import { SUB_NET_CITYPE_NAME, SCOPE_CITYPE_NAME, ADDRESS_CITYPE_NAME } from '../ipam/constants.js'
|
||||
|
||||
export default {
|
||||
name: 'Preference',
|
||||
|
@ -282,8 +283,16 @@ export default {
|
|||
getPreference2(true, true),
|
||||
getCIAdcStatistics(),
|
||||
])
|
||||
|
||||
const IPAM_CI = [
|
||||
SUB_NET_CITYPE_NAME,
|
||||
SCOPE_CITYPE_NAME,
|
||||
ADDRESS_CITYPE_NAME
|
||||
]
|
||||
|
||||
ciTypeGroup.forEach((group) => {
|
||||
if (group.ci_types && group.ci_types.length) {
|
||||
group.ci_types = group.ci_types.filter((type) => !IPAM_CI.includes(type.name))
|
||||
group.ci_types.forEach((type) => {
|
||||
const idx = pref.type_ids.findIndex((p) => p === type.id)
|
||||
if (idx > -1) {
|
||||
|
@ -304,10 +313,17 @@ export default {
|
|||
const { self, type_id2users } = pref2
|
||||
this.self = self
|
||||
this.type_id2users = type_id2users
|
||||
|
||||
const prefGroupTypes = pref.group_types.filter((group) => {
|
||||
group.ci_types = group?.ci_types?.filter((type) => !IPAM_CI.includes(type?.name)) || []
|
||||
return group?.ci_types?.length
|
||||
})
|
||||
const prefTreeTypes = pref?.tree_types?.filter((type) => !IPAM_CI.includes(type?.name)) || []
|
||||
|
||||
const _myPreferences = [
|
||||
{
|
||||
name: this.$t('cmdb.menu.ciTable'),
|
||||
groups: pref.group_types,
|
||||
groups: prefGroupTypes,
|
||||
icon: 'cmdb-ci',
|
||||
type: 'ci',
|
||||
},
|
||||
|
@ -315,7 +331,7 @@ export default {
|
|||
name: this.$t('cmdb.menu.ciTree'),
|
||||
groups: [
|
||||
{
|
||||
ci_types: pref.tree_types,
|
||||
ci_types: prefTreeTypes,
|
||||
name: null,
|
||||
}
|
||||
],
|
||||
|
@ -382,11 +398,13 @@ export default {
|
|||
})
|
||||
},
|
||||
resetRoute() {
|
||||
resetRouter()
|
||||
const roles = store.getters.roles
|
||||
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
|
||||
router.addRoutes(store.getters.appRoutes)
|
||||
this.getCITypes()
|
||||
resetRouter()
|
||||
this.$nextTick(() => {
|
||||
router.addRoutes(store.getters.appRoutes)
|
||||
this.getCITypes()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
@ -984,7 +984,28 @@ export default {
|
|||
this.batchTreeKey = []
|
||||
} else {
|
||||
const childTypeId = menuKey
|
||||
this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children', ancestor_ids)
|
||||
|
||||
let typeName = ''
|
||||
if (this?.relationViews?.id2type?.[childTypeId]) {
|
||||
typeName = this.relationViews.id2type[childTypeId]?.name || ''
|
||||
} else {
|
||||
const node2show_types = this?.relationViews?.views?.[this.viewName]?.node2show_types
|
||||
const typeId = _tempTree?.[1]
|
||||
if (node2show_types?.[typeId]?.length) {
|
||||
const findType = node2show_types[typeId].find((item) => item.id === childTypeId)
|
||||
typeName = findType?.name || ''
|
||||
}
|
||||
}
|
||||
this.$refs.addTableModal.openModal(
|
||||
firstCIObj,
|
||||
firstCIId,
|
||||
{
|
||||
id: childTypeId,
|
||||
name: typeName
|
||||
},
|
||||
'children',
|
||||
ancestor_ids
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
@refresh="handleSearch"
|
||||
>
|
||||
<a-button
|
||||
v-if="showCreateBtn"
|
||||
@click="
|
||||
() => {
|
||||
$refs.createInstanceForm.handleOpen(true, 'create')
|
||||
|
@ -116,6 +117,7 @@ import { getCITableColumns } from '../../../utils/helper'
|
|||
import SearchForm from '../../../components/searchForm/SearchForm.vue'
|
||||
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { SUB_NET_CITYPE_NAME, SCOPE_CITYPE_NAME, ADDRESS_CITYPE_NAME } from '@/modules/cmdb/views/ipam/constants.js'
|
||||
|
||||
export default {
|
||||
name: 'AddTableModal',
|
||||
|
@ -137,6 +139,7 @@ export default {
|
|||
preferenceAttrList: [],
|
||||
ancestor_ids: undefined,
|
||||
attrList1: [],
|
||||
showCreateBtn: true, // 是否展示新增按钮
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -159,18 +162,20 @@ export default {
|
|||
},
|
||||
watch: {},
|
||||
methods: {
|
||||
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
|
||||
console.log(ciObj, ciId, addTypeId, type)
|
||||
async openModal(ciObj, ciId, addType, type, ancestor_ids = undefined) {
|
||||
console.log(ciObj, ciId, addType, type)
|
||||
this.visible = true
|
||||
this.ciObj = ciObj
|
||||
this.ciId = ciId
|
||||
this.addTypeId = addTypeId
|
||||
this.addTypeId = addType.id
|
||||
this.type = type
|
||||
this.ancestor_ids = ancestor_ids
|
||||
await getSubscribeAttributes(addTypeId).then((res) => {
|
||||
this.showCreateBtn = ![SUB_NET_CITYPE_NAME, SCOPE_CITYPE_NAME, ADDRESS_CITYPE_NAME].includes(addType.name)
|
||||
|
||||
await getSubscribeAttributes(this.addTypeId).then((res) => {
|
||||
this.preferenceAttrList = res.attributes // 已经订阅的全部列
|
||||
})
|
||||
getCITypeAttributesById(addTypeId).then((res) => {
|
||||
getCITypeAttributesById(this.addTypeId).then((res) => {
|
||||
this.attrList = res.attributes
|
||||
})
|
||||
this.getTableData(true)
|
||||
|
@ -232,6 +237,7 @@ export default {
|
|||
this.expression = ''
|
||||
this.isFocusExpression = false
|
||||
this.visible = false
|
||||
this.showCreateBtn = true
|
||||
},
|
||||
async handleOk() {
|
||||
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
|
||||
|
|
|
@ -38,6 +38,9 @@
|
|||
:column-config="{ resizable: true }"
|
||||
:resizable-config="{ minWidth: 60 }"
|
||||
class="checkbox-hover-table"
|
||||
@checkbox-change="onSelectChange"
|
||||
@checkbox-all="onSelectChange"
|
||||
@checkbox-range-end="onSelectChange"
|
||||
>
|
||||
<vxe-table-column
|
||||
v-if="tableData.ciList && tableData.ciList.length"
|
||||
|
@ -137,7 +140,6 @@
|
|||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import { mapState } from 'vuex'
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
|
@ -266,8 +268,7 @@ export default {
|
|||
ciTypeName: this.tabActive || '',
|
||||
})
|
||||
},
|
||||
batchDownload({ checkedKeys }) {
|
||||
const excel_name = `cmdb-${this.tabActive}-${moment().format('YYYYMMDDHHmmss')}.xlsx`
|
||||
batchDownload({ checkedKeys, filename }) {
|
||||
const wb = new ExcelJS.Workbook()
|
||||
|
||||
const tableRef = this.$refs.xTable.getVxetableRef()
|
||||
|
@ -341,12 +342,16 @@ export default {
|
|||
const file = new Blob([buffer], {
|
||||
type: 'application/octet-stream',
|
||||
})
|
||||
FileSaver.saveAs(file, excel_name)
|
||||
FileSaver.saveAs(file, `${filename}.xlsx`)
|
||||
})
|
||||
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
}
|
||||
},
|
||||
|
||||
onSelectChange() {
|
||||
console.log('onSelectChange')
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue