dashboard ui update

This commit is contained in:
pycook 2023-09-15 17:36:10 +08:00
parent 737b29f7d6
commit 47dbe5ba18
11 changed files with 1271 additions and 176 deletions

View File

@ -68,7 +68,8 @@ export default {
},
methods: {
visibleChange(open) {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
@ -151,15 +152,20 @@ export default {
})
this.ruleList = [...expArray]
} else if (open) {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
? this.canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {

View File

@ -77,6 +77,14 @@ export function getCITypeAttributesByTypeIds(params) {
})
}
export function getCITypeCommonAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/common_attributes`,
method: 'get',
params: params
})
}
/**
* 删除属性
* @param attrId

View File

@ -61,3 +61,10 @@ export function revokeTypeRelation(first_type_id, second_type_id, rid, data) {
data
})
}
export function getRecursive_level2children(type_id) {
return axios({
url: `/v0.1/ci_type_relations/${type_id}/recursive_level2children`,
method: 'GET'
})
}

View File

@ -37,3 +37,11 @@ export function batchUpdateCustomDashboard(data) {
data
})
}
export function postCustomDashboardPreview(data) {
return axios({
url: '/v0.1/custom_dashboard/preview',
method: 'post',
data
})
}

View File

@ -1,11 +1,55 @@
<template>
<div :style="{ width: '100%', height: 'calc(100% - 2.2vw)' }">
<div v-if="category === 0" class="cmdb-dashboard-grid-item-chart">
<div
:id="`cmdb-dashboard-${chartId}-${editable}-${isPreview}`"
:style="{ width: '100%', height: 'calc(100% - 2.2vw)' }"
>
<div
v-if="options.chartType === 'count'"
:style="{ color: options.fontColor || '#fff' }"
class="cmdb-dashboard-grid-item-chart"
>
<div class="cmdb-dashboard-grid-item-chart-icon" v-if="options.showIcon && ciType">
<template v-if="ciType.icon">
<img v-if="ciType.icon.split('$$')[2]" :src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`" />
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</div>
<span :style="{ ...options.fontConfig }">{{ toThousands(data) }}</span>
</div>
<vxe-table
:max-height="tableHeight"
:data="tableData"
:stripe="!!options.ret"
size="mini"
class="ops-stripe-table"
v-if="options.chartType === 'table'"
:span-method="mergeRowMethod"
:border="!options.ret"
:show-header="!!options.ret"
>
<template v-if="options.ret">
<vxe-column v-for="col in columns" :key="col" :title="col" :field="col"></vxe-column>
</template>
<template v-else>
<vxe-column
v-for="(key, index) in Array(keyLength)"
:key="`key${index}`"
:title="`key${index}`"
:field="`key${index}`"
></vxe-column>
<vxe-column field="value" title="value"></vxe-column>
</template>
</vxe-table>
<div
:id="`cmdb-dashboard-${chartId}-${editable}`"
v-if="category === 1 || category === 2"
v-else-if="category === 1 || category === 2"
class="cmdb-dashboard-grid-item-chart"
></div>
</div>
@ -15,17 +59,27 @@
import * as echarts from 'echarts'
import { mixin } from '@/utils/mixin'
import { toThousands } from '../../utils/helper'
import { category_1_bar_options, category_1_pie_options, category_2_bar_options } from './chartOptions'
import {
category_1_bar_options,
category_1_line_options,
category_1_pie_options,
category_2_bar_options,
category_2_pie_options,
} from './chartOptions'
export default {
name: 'Chart',
mixins: [mixin],
props: {
ci_types: {
type: Array,
default: () => [],
},
chartId: {
type: Number,
default: 0,
},
data: {
type: [Number, Object],
type: [Number, Object, Array],
default: 0,
},
category: {
@ -40,20 +94,65 @@ export default {
type: Boolean,
default: false,
},
type_id: {
type: [Number, Array],
default: null,
},
isPreview: {
type: Boolean,
default: false,
},
},
data() {
return {
chart: null,
columns: [],
tableHeight: '',
tableData: [],
keyLength: 0,
}
},
computed: {
ciType() {
if (this.type_id || this.options?.type_ids) {
const _find = this.ci_types.find((item) => item.id === this.type_id || item.id === this.options?.type_ids[0])
return _find || null
}
return null
},
},
watch: {
data: {
immediate: true,
deep: true,
handler(newValue, oldValue) {
if (this.category === 1 || this.category === 2) {
if (Object.prototype.toString.call(newValue) === '[object Object]') {
this.setChart()
if (this.options.chartType !== 'table' && Object.prototype.toString.call(newValue) === '[object Object]') {
if (this.isPreview) {
this.$nextTick(() => {
this.setChart()
})
} else {
this.setChart()
}
}
}
if (this.options.chartType === 'table') {
this.$nextTick(() => {
const dom = document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}-${this.isPreview}`)
this.tableHeight = dom.offsetHeight
})
if (this.options.ret) {
const excludeKeys = ['_X_ROW_KEY', 'ci_type', 'ci_type_alias', 'unique', 'unique_alias', '_id', '_type']
if (newValue && newValue.length) {
this.columns = Object.keys(newValue[0]).filter((keys) => !excludeKeys.includes(keys))
this.tableData = newValue
}
} else {
const _data = []
this.keyLength = this.options?.attr_ids?.length ?? 0
this.formatTableData(_data, this.data, {})
this.tableData = _data
}
}
},
@ -81,13 +180,19 @@ export default {
this.chart = echarts.init(document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}`))
}
if (this.category === 1 && this.options.chartType === 'bar') {
this.chart.setOption(category_1_bar_options(this.data), true)
this.chart.setOption(category_1_bar_options(this.data, this.options), true)
}
if (this.category === 1 && this.options.chartType === 'line') {
this.chart.setOption(category_1_line_options(this.data, this.options), true)
}
if (this.category === 1 && this.options.chartType === 'pie') {
this.chart.setOption(category_1_pie_options(this.data), true)
this.chart.setOption(category_1_pie_options(this.data, this.options), true)
}
if (this.category === 2) {
this.chart.setOption(category_2_bar_options(this.data), true)
if (this.category === 2 && ['bar', 'line'].includes(this.options.chartType)) {
this.chart.setOption(category_2_bar_options(this.data, this.options, this.options.chartType), true)
}
if (this.category === 2 && this.options.chartType === 'pie') {
this.chart.setOption(category_2_pie_options(this.data, this.options), true)
}
},
resizeChart() {
@ -97,6 +202,34 @@ export default {
}
})
},
formatTableData(_data, data, obj) {
Object.keys(data).forEach((k) => {
if (typeof data[k] === 'number') {
_data.push({ ...obj, [`key${Object.keys(obj).length}`]: k, value: data[k] })
} else {
this.formatTableData(_data, data[k], { ...obj, [`key${Object.keys(obj).length}`]: k })
}
})
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['key0', 'key1', 'key2']
const cellValue = row[column.field]
if (cellValue && fields.includes(column.field)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow[column.field] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.field] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
},
}
</script>
@ -106,14 +239,28 @@ export default {
width: 100%;
height: 100%;
position: relative;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
> span {
font-size: 50px;
font-weight: 700;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.cmdb-dashboard-grid-item-chart-icon {
> i {
font-size: 4vw;
}
> img {
width: 4vw;
}
> span {
display: inline-block;
width: 4vw;
height: 4vw;
font-size: 50px;
text-align: center;
line-height: 50px;
}
}
}
</style>

View File

@ -1,65 +1,307 @@
<template>
<a-modal :title="`${type === 'add' ? '新增' : '编辑'}图表`" :visible="visible" @cancel="handleclose" @ok="handleok">
<a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
<a-form-model-item label="类型" prop="category">
<a-select v-model="form.category" @change="changeDashboardCategory">
<a-select-option v-for="cate in Object.keys(dashboardCategory)" :key="cate" :value="Number(cate)">{{
dashboardCategory[cate].label
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-if="form.category !== 0" label="名称" prop="name">
<a-input v-model="form.name" placeholder="请输入图表名称"></a-input>
</a-form-model-item>
<a-form-model-item label="模型" prop="type_id">
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_id"
placeholder="请选择模型"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-if="form.category === 1" label="模型属性" prop="attr_id">
<a-select show-search optionFilterProp="children" v-model="form.attr_id" placeholder="请选择模型属性">
<a-select-option v-for="attr in attributes" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-if="form.category === 1" label="图表类型" prop="chartType">
<a-radio-group v-model="chartType">
<a-radio value="bar">
柱状图
</a-radio>
<a-radio value="pie">
饼图
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item v-if="form.category === 2" label="关系层级" prop="level">
<a-input v-model="form.level" placeholder="请输入关系层级"></a-input>
</a-form-model-item>
<a-form-model-item v-if="form.category === 0" label="字体">
<FontConfig ref="fontConfig" />
</a-form-model-item>
</a-form-model>
<a-modal
width="1100px"
:title="`${type === 'add' ? '新增' : '编辑'}图表`"
:visible="visible"
@cancel="handleclose"
@ok="handleok"
:bodyStyle="{ paddingTop: 0 }"
>
<div class="chart-wrapper">
<div class="chart-left">
<a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
<a-form-model-item label="标题" prop="name">
<a-input v-model="form.name" placeholder="请输入图表标题"></a-input>
</a-form-model-item>
<a-form-model-item label="类型" prop="category" v-if="chartType !== 'count' && chartType !== 'table'">
<a-radio-group
@change="
() => {
resetForm()
}
"
:default-value="1"
v-model="form.category"
>
<a-radio-button :value="Number(key)" :key="key" v-for="key in Object.keys(dashboardCategory)">
{{ dashboardCategory[key].label }}
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="类型" prop="tableCategory" v-if="chartType === 'table'">
<a-radio-group
@change="
() => {
resetForm()
}
"
:default-value="1"
v-model="form.tableCategory"
>
<a-radio-button :value="1">
计算指标
</a-radio-button>
<a-radio-button :value="2">
资源数据
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item
v-if="(chartType !== 'table' && form.category !== 2) || (chartType === 'table' && form.tableCategory === 1)"
label="模型"
prop="type_ids"
>
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_ids"
placeholder="请选择模型"
mode="multiple"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-else label="模型" prop="type_id">
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_id"
placeholder="请选择模型"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
label="维度"
prop="attr_ids"
v-if="(['bar', 'line', 'pie'].includes(chartType) && form.category === 1) || chartType === 'table'"
>
<a-select @change="changeAttr" v-model="form.attr_ids" placeholder="请选择维度" mode="multiple" show-search>
<a-select-option v-for="attr in commonAttributes" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
prop="type_ids"
label="关系模型"
v-if="['bar', 'line', 'pie'].includes(chartType) && form.category === 2"
>
<a-select
show-search
optionFilterProp="children"
mode="multiple"
v-model="form.type_ids"
placeholder="请选择模型"
>
<a-select-opt-group
v-for="(key, index) in Object.keys(level2children)"
:key="key"
:label="`层级${index + 1}`"
>
<a-select-option
@click="(e) => clickLevel2children(e, citype, index + 1)"
v-for="citype in level2children[key]"
:key="citype.id"
:value="citype.id"
>
{{ citype.alias || citype.name }}
</a-select-option>
</a-select-opt-group>
</a-select>
</a-form-model-item>
<div class="chart-left-preview">
<span class="chart-left-preview-operation" @click="showPreview"><a-icon type="play-circle" /> 预览</span>
<template v-if="isShowPreview">
<div v-if="chartType !== 'count'" class="cmdb-dashboard-grid-item-title">
<template v-if="form.showIcon && ciType">
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</template>
<span :style="{ color: '#000' }"> {{ form.name }}</span>
</div>
<div
class="chart-left-preview-box"
:style="{
height: chartType === 'count' ? '120px' : '',
marginTop: chartType === 'count' ? '80px' : '',
background:
chartType === 'count'
? Array.isArray(bgColor)
? `linear-gradient(to bottom, ${bgColor[0]} 0%, ${bgColor[1]} 100%)`
: bgColor
: '#fafafa',
}"
>
<div :style="{ color: fontColor }">{{ form.name }}</div>
<Chart
:ref="`chart_${item.id}`"
:chartId="item.id"
:data="previewData"
:category="form.category"
:options="{
...item.options,
name: form.name,
fontColor: fontColor,
bgColor: bgColor,
chartType: chartType,
showIcon: form.showIcon,
barDirection: barDirection,
barStack: barStack,
chartColor: chartColor,
type_ids: form.type_ids,
attr_ids: form.attr_ids,
isShadow: isShadow,
}"
:editable="false"
:ci_types="ci_types"
:type_id="form.type_id || form.type_ids"
isPreview
/>
</div>
</template>
</div>
<a-form-model-item label="是否显示icon" prop="showIcon" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-switch v-model="form.showIcon"></a-switch>
</a-form-model-item>
</a-form-model>
</div>
<div class="chart-right">
<h4>图表类型</h4>
<div class="chart-right-type">
<div
:class="{ 'chart-right-type-box': true, 'chart-right-type-box-selected': chartType === t.value }"
v-for="t in chartTypeList"
:key="t.value"
@click="changeChartType(t)"
>
<ops-icon :type="`cmdb-${t.value}`" />
<span>{{ t.label }}</span>
</div>
</div>
<h4>数据筛选</h4>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attributes"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
<h4>格式</h4>
<a-form-model :colon="false" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-model-item label="字体颜色" v-if="chartType === 'count'">
<ColorPicker
v-model="fontColor"
:colorList="[
'#1D2129',
'#4E5969',
'#103C93',
'#86909C',
'#ffffff',
'#C9F2FF',
'#FFEAC0',
'#D6FFE6',
'#F2DEFF',
]"
/>
</a-form-model-item>
<a-form-model-item label="背景颜色" v-if="chartType === 'count'">
<ColorPicker
v-model="bgColor"
:colorList="[
['#6ABFFE', '#5375EB'],
['#C69EFF', '#A377F9'],
['#85EBC9', '#4AB8D8'],
['#FEB58B', '#DF6463'],
'#ffffff',
'#FFFBF0',
'#FFF1EC',
'#E5FFFE',
'#E5E7FF',
]"
/>
</a-form-model-item>
<a-form-model-item label="图表颜色" v-else-if="chartType !== 'table'">
<ColorListPicker v-model="chartColor" />
</a-form-model-item>
<a-form-model-item label="图表长度(%)">
<a-radio-group class="chart-width" style="width:100%;" v-model="width">
<a-radio-button :value="3">
25
</a-radio-button>
<a-radio-button :value="6">
50
</a-radio-button>
<a-radio-button :value="9">
75
</a-radio-button>
<a-radio-button :value="12">
100
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="柱状图类型" v-if="chartType === 'bar'">
<a-radio-group v-model="barStack">
<a-radio value="total">
堆积柱状图
</a-radio>
<a-radio value="">
多系列柱状图
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="方向" v-if="chartType === 'bar'">
<a-radio-group v-model="barDirection">
<a-radio value="x">
X轴
</a-radio>
<a-radio value="y">
y轴
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="下方阴影" v-if="chartType === 'line'">
<a-switch v-model="isShadow" />
</a-form-model-item>
</a-form-model>
</div>
</div>
</a-modal>
</template>
<script>
import Chart from './chart.vue'
import { dashboardCategory } from './constant'
import { postCustomDashboard, putCustomDashboard } from '../../api/customDashboard'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '../../api/customDashboard'
import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import { getRecursive_level2children } from '../../api/CITypeRelation'
import { getLastLayout } from '../../utils/helper'
import FontConfig from './fontConfig.vue'
import FilterComp from '@/components/CMDBFilterComp'
import ColorPicker from './colorPicker.vue'
import ColorListPicker from './colorListPicker.vue'
export default {
name: 'ChartForm',
components: { FontConfig },
components: { Chart, FilterComp, ColorPicker, ColorListPicker },
props: {
ci_types: {
type: Array,
@ -67,100 +309,226 @@ export default {
},
},
data() {
const chartTypeList = [
{
value: 'count',
label: '指标',
},
{
value: 'bar',
label: '柱状图',
},
{
value: 'line',
label: '折线图',
},
{
value: 'pie',
label: '饼状图',
},
{
value: 'table',
label: '表格',
},
]
return {
dashboardCategory,
chartTypeList,
visible: false,
attributes: [],
type: 'add',
form: {
category: 0,
tableCategory: 1,
name: undefined,
type_id: undefined,
attr_id: undefined,
type_ids: undefined,
attr_ids: undefined,
level: undefined,
showIcon: false,
},
rules: {
category: [{ required: true, trigger: 'change' }],
name: [{ required: true, message: '请输入图表名称' }],
type_id: [{ required: true, message: '请选择模型', trigger: 'change' }],
attr_id: [{ required: true, message: '请选择模型属性', trigger: 'change' }],
type_ids: [{ required: true, message: '请选择模型', trigger: 'change' }],
attr_ids: [{ required: true, message: '请选择模型属性', trigger: 'change' }],
level: [{ required: true, message: '请输入关系层级' }],
showIcon: [{ required: false }],
},
item: {},
chartType: 'bar',
chartType: 'count', // table,bar,line,pie,count
width: 3,
fontColor: '#ffffff',
bgColor: ['#6ABFFE', '#5375EB'],
chartColor: '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD', // 图表颜色
isShowPreview: false,
filterExp: undefined,
previewData: null,
barStack: 'total',
barDirection: 'y',
commonAttributes: [],
level2children: {},
isShadow: false,
}
},
computed: {
ciType() {
if (this.form.type_id || this.form.type_ids) {
const _find = this.ci_types.find((item) => item.id === this.form.type_id || item.id === this.form.type_ids[0])
return _find || null
}
return null
},
},
inject: ['layout'],
methods: {
open(type, item = {}) {
async open(type, item = {}) {
this.visible = true
this.type = type
this.item = item
const { category = 0, name, type_id, attr_id, level } = item
const chartType = (item.options || {}).chartType || 'bar'
const chartType = (item.options || {}).chartType || 'count'
const fontColor = (item.options || {}).fontColor || '#ffffff'
const bgColor = (item.options || {}).bgColor || ['#6ABFFE', '#5375EB']
const width = (item.options || {}).w
const showIcon = (item.options || {}).showIcon
const type_ids = item?.options?.type_ids || []
const attr_ids = item?.options?.attr_ids || []
const ret = item?.options?.ret || ''
this.width = width
this.chartType = chartType
if (type_id && attr_id) {
getCITypeAttributesById(type_id).then((res) => {
this.filterExp = item?.options?.filter ?? ''
this.chartColor = item?.options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD'
this.isShadow = item?.options?.isShadow ?? false
if (chartType === 'count') {
this.fontColor = fontColor
this.bgColor = bgColor
}
if (type_ids && type_ids.length) {
await getCITypeAttributesByTypeIds({ type_ids: type_ids.join(',') }).then((res) => {
this.attributes = res.attributes
})
if ((['bar', 'line', 'pie'].includes(chartType) && category === 1) || chartType === 'table') {
this.barDirection = item?.options?.barDirection ?? 'y'
this.barStack = item?.options?.barStack ?? 'total'
await getCITypeCommonAttributesByTypeIds({
type_ids: type_ids.join(','),
}).then((res) => {
this.commonAttributes = res.attributes
})
}
}
if (type_id) {
getRecursive_level2children(type_id).then((res) => {
this.level2children = res
})
await getCITypeCommonAttributesByTypeIds({
type_ids: type_id,
}).then((res) => {
this.commonAttributes = res.attributes
})
}
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
})
const default_form = {
category: 0,
name: undefined,
type_id: undefined,
attr_id: undefined,
type_ids: undefined,
attr_ids: undefined,
level: undefined,
showIcon: false,
tableCategory: 1,
}
this.form = {
...default_form,
category,
name,
type_id,
attr_id,
type_ids,
attr_ids,
level,
}
if (category === 0) {
this.$nextTick(() => {
this.$refs.fontConfig.setConfig((item.options || {}).fontConfig)
})
showIcon,
tableCategory: ret === 'cis' ? 2 : 1,
}
},
handleclose() {
this.attributes = []
this.$refs.chartForm.clearValidate()
this.isShowPreview = false
this.visible = false
},
changeCIType(value) {
getCITypeAttributesById(value).then((res) => {
this.form.attr_ids = []
this.commonAttributes = []
getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.attributes = res.attributes
this.form = {
...this.form,
attr_id: undefined,
}
})
if (!Array.isArray(value)) {
getRecursive_level2children(value).then((res) => {
this.level2children = res
})
}
if ((['bar', 'line', 'pie'].includes(this.chartType) && this.form.category === 1) || this.chartType === 'table') {
getCITypeCommonAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.commonAttributes = res.attributes
})
}
},
handleok() {
this.$refs.chartForm.validate(async (valid) => {
if (valid) {
const fontConfig = this.form.category === 0 ? this.$refs.fontConfig.getConfig() : undefined
const _find = this.ci_types.find((attr) => attr.id === this.form.type_id)
const name = this.form.name || (_find || {}).alias || (_find || {}).name
const name = this.form.name
const { chartType, fontColor, bgColor } = this
this.$refs.filterComp.handleSubmit()
if (this.item.id) {
await putCustomDashboard(this.item.id, {
const params = {
...this.form,
options: {
...this.item.options,
name,
fontConfig,
w: this.width,
chartType: this.chartType,
showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
},
})
}
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
await putCustomDashboard(this.item.id, params)
} else {
const { xLast, yLast, wLast } = getLastLayout(this.layout())
const w = 3
const w = this.width
const x = xLast + wLast + w > 12 ? 0 : xLast + wLast
const y = xLast + wLast + w > 12 ? yLast + 1 : yLast
await postCustomDashboard({
const params = {
...this.form,
options: {
x,
@ -169,23 +537,216 @@ export default {
h: this.form.category === 0 ? 3 : 5,
name,
chartType: this.chartType,
fontConfig,
showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
},
})
}
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
await postCustomDashboard(params)
}
this.handleclose()
this.$emit('refresh')
}
})
},
changeDashboardCategory(value) {
this.$refs.chartForm.clearValidate()
if (value === 1 && this.form.type_id) {
this.changeCIType(this.form.type_id)
// changeDashboardCategory(value) {
// this.$refs.chartForm.clearValidate()
// if (value === 1 && this.form.type_id) {
// this.changeCIType(this.form.type_id)
// }
// },
changeChartType(t) {
this.chartType = t.value
this.isShowPreview = false
if (t.value === 'count') {
this.form.category = 0
} else {
this.form.category = 1
}
this.resetForm()
},
showPreview() {
this.$refs.chartForm.validate(async (valid) => {
if (valid) {
this.isShowPreview = false
const name = this.form.name
const { chartType, fontColor, bgColor } = this
this.$refs.filterComp.handleSubmit()
const params = {
...this.form,
options: {
name,
chartType,
showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
},
}
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
postCustomDashboardPreview(params).then((res) => {
this.isShowPreview = true
this.previewData = res.counter
})
}
})
},
setExpFromFilter(filterExp) {
if (filterExp) {
this.filterExp = `${filterExp}`
} else {
this.filterExp = undefined
}
},
resetForm() {
this.form.type_id = undefined
this.form.type_ids = []
this.form.attr_ids = []
this.$refs.chartForm.clearValidate()
},
changeAttr(value) {
if (value && value.length) {
if (['line', 'pie'].includes(this.chartType)) {
this.form.attr_ids = [value[value.length - 1]]
}
if (['bar'].includes(this.chartType) && value.length > 2) {
this.form.attr_ids = value.slice(value.length - 2, value.length)
}
if (['table'].includes(this.chartType) && value.length > 3) {
this.form.attr_ids = value.slice(value.length - 3, value.length)
}
}
},
clickLevel2children(e, citype, level) {
if (this.form.level !== level) {
this.$nextTick(() => {
this.form.type_ids = [citype.id]
})
}
this.form.level = level
},
},
}
</script>
<style></style>
<style lang="less" scoped>
.chart-wrapper {
display: flex;
.chart-left {
width: 50%;
.chart-left-preview {
border: 1px solid #e4e7ed;
border-radius: 2px;
height: 280px;
width: 92%;
position: relative;
padding: 12px;
.chart-left-preview-operation {
color: #86909c;
position: absolute;
top: 12px;
right: 12px;
cursor: pointer;
}
.chart-left-preview-box {
padding: 6px 12px;
height: 250px;
border-radius: 8px;
}
}
}
.chart-right {
width: 50%;
h4 {
font-weight: 700;
color: #000;
}
.chart-right-type {
display: flex;
justify-content: space-between;
background-color: #f0f5ff;
padding: 6px 12px;
.chart-right-type-box {
cursor: pointer;
width: 70px;
height: 60px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
> i {
font-size: 32px;
}
> span {
font-size: 12px;
}
}
.chart-right-type-box-selected {
background-color: #e5f1ff;
}
}
.chart-width {
width: 100%;
> label {
width: 25%;
text-align: center;
}
}
}
}
</style>
<style lang="less">
.chart-wrapper {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

View File

@ -1,23 +1,61 @@
export const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']
export const category_1_bar_options = (data) => {
export const category_1_bar_options = (data, options) => {
// 计算一级分类
const xData = Object.keys(data)
// 计算共有多少二级分类
const secondCategory = {}
Object.keys(data).forEach(key => {
if (Object.prototype.toString.call(data[key]) === '[object Object]') {
Object.keys(data[key]).forEach(key1 => {
secondCategory[key1] = Array.from({ length: xData.length }).fill(0)
})
} else {
secondCategory['其他'] = Array.from({ length: xData.length }).fill(0)
}
})
Object.keys(secondCategory).forEach(key => {
xData.forEach((x, idx) => {
if (data[x][key]) {
secondCategory[key][idx] = data[x][key]
}
if (typeof data[x] === 'number') {
secondCategory['其他'][idx] = data[x]
}
})
})
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 0,
bottom: 0,
right: 10,
bottom: 20,
containLabel: true,
},
xAxis: {
type: 'category',
data: Object.keys(data)
legend: {
data: Object.keys(secondCategory),
bottom: 0,
type: 'scroll',
},
yAxis: {
xAxis: options.barDirection === 'y' ? {
type: 'category',
axisTick: { show: false },
data: xData
}
: {
type: 'value',
splitLine: {
show: false
}
},
yAxis: options.barDirection === 'y' ? {
type: 'value',
splitLine: {
show: false
}
} : {
type: 'category',
axisTick: { show: false },
data: xData
},
tooltip: {
trigger: 'axis',
@ -25,34 +63,76 @@ export const category_1_bar_options = (data) => {
type: 'shadow'
}
},
series: Object.keys(secondCategory).map(key => {
return {
name: key,
type: 'bar',
stack: options?.barStack ?? 'total',
barGap: 0,
emphasis: {
focus: 'series'
},
data: secondCategory[key]
}
})
}
}
export const category_1_line_options = (data, options) => {
const xData = Object.keys(data)
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 10,
bottom: 20,
containLabel: true,
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
series: [
{
data: Object.keys(data).map((key, index) => {
return {
value: data[key],
itemStyle: { color: colorList[0] }
data: xData.map(item => data[item]),
type: 'line',
smooth: true,
showSymbol: false,
areaStyle: options?.isShadow ? {
opacity: 0.5,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: options.chartColor.split(',')[0] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
global: false // 缺省为 false
}
}),
type: 'bar',
label: {
show: true,
position: 'top',
fontSize: 10,
formatter(data) {
return `${data.value || ''}`
}
},
} : null
}
]
}
}
export const category_1_pie_options = (data) => {
export const category_1_pie_options = (data, options) => {
return {
color: options.chartColor.split(','),
grid: {
top: 10,
left: 'left',
right: 0,
right: 10,
bottom: 0,
containLabel: true,
},
@ -89,7 +169,7 @@ export const category_1_pie_options = (data) => {
}
}
export const category_2_bar_options = (data) => {
export const category_2_bar_options = (data, options, chartType) => {
const xAxisData = Object.keys(data.detail)
const _legend = []
xAxisData.forEach(key => {
@ -97,10 +177,11 @@ export const category_2_bar_options = (data) => {
})
const legend = [...new Set(_legend)]
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 0,
right: 10,
bottom: 20,
containLabel: true,
},
@ -116,41 +197,110 @@ export const category_2_bar_options = (data) => {
type: 'scroll',
data: legend
},
xAxis: [
{
type: 'category',
axisTick: { show: false },
data: xAxisData
}
],
yAxis: [
{
xAxis: options.barDirection === 'y' || chartType === 'line' ? {
type: 'category',
axisTick: { show: false },
data: xAxisData
}
: {
type: 'value',
splitLine: {
show: false
}
},
yAxis: options.barDirection === 'y' || chartType === 'line' ? {
type: 'value',
splitLine: {
show: false
}
],
series: legend.map(le => {
} : {
type: 'category',
axisTick: { show: false },
data: xAxisData
},
series: legend.map((le, index) => {
return {
name: le,
type: 'bar',
type: chartType,
barGap: 0,
emphasis: {
focus: 'series'
},
stack: chartType === 'line' ? '' : options?.barStack ?? 'total',
data: xAxisData.map(x => {
return data.detail[x][le] || 0
}),
smooth: true,
showSymbol: false,
label: {
show: true,
position: 'top',
fontSize: 10,
formatter(data) {
return `${data.value || ''}`
}
show: false,
},
areaStyle: chartType === 'line' && options?.isShadow ? {
opacity: 0.5,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: options.chartColor.split(',')[index % 8] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
global: false // 缺省为 false
}
} : null
}
})
}
}
export const category_2_pie_options = (data, options) => {
console.log(1111, options)
const _legend = []
Object.keys(data.detail).forEach(key => {
Object.keys(data.detail[key]).forEach(key2 => {
_legend.push({ value: data.detail[key][key2], name: `${key}-${key2}` })
})
})
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 10,
bottom: 20,
containLabel: true,
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left',
type: 'scroll',
formatter: function (name) {
const _find = _legend.find(item => item.name === name)
return `${name}${_find.value}`
}
},
series: [
{
type: 'pie',
radius: '90%',
data: _legend,
label: {
show: false,
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
}

View File

@ -0,0 +1,54 @@
<template>
<a-select v-model="currenColor">
<a-select-option v-for="i in list" :value="i" :key="i">
<div>
<span :style="{ backgroundColor: color }" class="color-box" v-for="color in i.split(',')" :key="color"></span>
</div>
</a-select-option>
</a-select>
</template>
<script>
export default {
name: 'ColorListPicker',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array],
default: null,
},
},
data() {
return {
list: [
'#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD',
'#C1A9DC,#E2B5CD,#EE8EBC,#8483C3,#4D66BD,#213764,#D9B6E9,#DD88EB',
'#6FC4DF,#9FE8CE,#16B4BE,#86E6FB,#1871A3,#E1BF8D,#ED8D8D,#DD88EB',
'#F8B751,#FC9054,#FFE380,#DF963F,#AB5200,#EA9387,#FFBB7C,#D27467',
],
}
},
computed: {
currenColor: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
}
</script>
<style lang="less" scoped>
.color-box {
display: inline-block;
width: 40px;
height: 10px;
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="color-picker">
<div
:style="{
background: Array.isArray(item) ? `linear-gradient(to bottom, ${item[0]} 0%, ${item[1]} 100%)` : item,
}"
:class="{ 'color-picker-box': true, 'color-picker-box-selected': isEqual(currenColor, item) }"
v-for="item in colorList"
:key="Array.isArray(item) ? item.join() : item"
@click="changeColor(item)"
></div>
</div>
</template>
<script>
import _ from 'lodash'
export default {
name: 'ColorPicker',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array],
default: null,
},
colorList: {
type: Array,
default: () => [],
},
},
computed: {
currenColor: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
methods: {
isEqual: _.isEqual,
changeColor(item) {
this.$emit('change', item)
},
},
}
</script>
<style lang="less" scoped>
.color-picker {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
.color-picker-box {
width: 19px;
height: 19px;
border: 1px solid #dae2e7;
border-radius: 1px;
cursor: pointer;
}
.color-picker-box-selected {
position: relative;
&:after {
content: '';
position: absolute;
width: 24px;
height: 24px;
border: 1px solid #43bbff;
top: -3px;
left: -3px;
}
}
}
</style>

View File

@ -1,5 +1,4 @@
export const dashboardCategory = {
0: { label: 'CI数统计' },
1: { label: '按属性值分类统计' },
2: { label: '关系统计' }
1: { label: '默认' },
2: { label: '关系' }
}

View File

@ -11,8 +11,8 @@
<template v-if="layout && layout.length">
<div v-if="editable">
<a-button
:style="{ marginLeft: '10px' }"
@click="openChartForm('add', {})"
:style="{ marginLeft: '22px', marginTop: '20px' }"
@click="openChartForm('add', { options: { w: 3 } })"
ghost
type="primary"
size="small"
@ -39,11 +39,44 @@
:h="item.h"
:i="item.i"
:key="item.i"
:style="{ backgroundColor: '#fafafa' }"
:style="{
background:
item.options.chartType === 'count'
? Array.isArray(item.options.bgColor)
? `linear-gradient(to bottom, ${item.options.bgColor[0]} 0%, ${item.options.bgColor[1]} 100%)`
: item.options.bgColor
: '#fafafa',
}"
>
<CardTitle>{{ item.options.name }}</CardTitle>
<div class="cmdb-dashboard-grid-item-title">
<template v-if="item.options.chartType !== 'count' && item.options.showIcon && getCiType(item)">
<template v-if="getCiType(item).icon">
<img
v-if="getCiType(item).icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${getCiType(item).icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: getCiType(item).icon.split('$$')[1],
}"
:type="getCiType(item).icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ getCiType(item).name[0].toUpperCase() }}</span>
</template>
<span :style="{ color: item.options.chartType === 'count' ? item.options.fontColor : '#000' }">{{
item.options.name
}}</span>
</div>
<a-dropdown v-if="editable">
<a class="cmdb-dashboard-grid-item-operation"><a-icon type="menu"></a-icon></a>
<a
class="cmdb-dashboard-grid-item-operation"
:style="{
color: item.options.chartType === 'count' ? item.options.fontColor : '',
}"
><a-icon type="menu"></a-icon
></a>
<a-menu slot="overlay">
<a-menu-item>
<a @click="() => openChartForm('edit', item)"><a-icon style="margin-right:5px" type="edit" />编辑</a>
@ -53,13 +86,13 @@
</a-menu-item>
</a-menu>
</a-dropdown>
<a
<!-- <a
v-if="editable && item.category === 1"
class="cmdb-dashboard-grid-item-chart-type"
@click="changeChartType(item)"
><a-icon
:type="item.options.chartType === 'bar' ? 'bar-chart' : 'pie-chart'"
/></a>
/></a> -->
<Chart
:ref="`chart_${item.id}`"
:chartId="item.id"
@ -67,18 +100,26 @@
:category="item.category"
:options="item.options"
:editable="editable"
:ci_types="ci_types"
:type_id="item.type_id"
/>
</GridItem>
</GridLayout>
</template>
<div v-else class="dashboard-empty">
<a-empty :image="emptyImage" description=""></a-empty>
<a-button @click="openChartForm('add', {})" v-if="editable" size="small" type="primary" icon="plus">
<a-button
@click="openChartForm('add', { options: { w: 3 } })"
v-if="editable"
size="small"
type="primary"
icon="plus"
>
定制仪表盘
</a-button>
<span v-else>管理员暂未定制仪表盘</span>
</div>
<ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" />
<ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" :totalData="totalData" />
</div>
</template>
@ -127,12 +168,14 @@ export default {
},
}
},
mounted() {
this.getLayout()
created() {
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
},
mounted() {
this.getLayout()
},
methods: {
async getLayout() {
const res = await getCustomDashboard()
@ -196,6 +239,13 @@ export default {
})
}
},
getCiType(item) {
if (item.type_id || item.options?.type_ids) {
const _find = this.ci_types.find((type) => type.id === item.type_id || type.id === item.options?.type_ids[0])
return _find || null
}
return null
},
},
}
</script>
@ -206,15 +256,18 @@ export default {
text-align: center;
}
.cmdb-dashboard-grid-item {
border-radius: 15px;
border-radius: 8px;
padding: 6px 12px;
.cmdb-dashboard-grid-item-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700;
padding-left: 6px;
color: #000000bd;
color: #000000;
}
.cmdb-dashboard-grid-item-operation {
position: absolute;
right: 6px;
right: 12px;
top: 6px;
}
.cmdb-dashboard-grid-item-chart-type {
@ -224,3 +277,26 @@ export default {
}
}
</style>
<style lang="less">
.cmdb-dashboard-grid-item-title {
display: flex;
align-items: center;
> i {
font-size: 16px;
margin-right: 5px;
}
> img {
width: 16px;
margin-right: 5px;
}
> span:not(:last-child) {
display: inline-block;
width: 16px;
height: 16px;
font-size: 16px;
text-align: center;
margin-right: 5px;
}
}
</style>