feat: 首页根据设计图修改

develop
时启龙 2024-08-24 16:01:48 +08:00
parent a2d641dd31
commit 9f7bcf4e52
46 changed files with 2437 additions and 514 deletions

BIN
packages/common/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -16,6 +16,15 @@ export default [
},
component: () => import('views/configs/setting_dashboard/index.vue')
},
// 旧主页
{
path: '/Oldresource_dashboard',
meta: {
title: '资源概览',
noTag: true
},
component: () => import('views/configs/setting_dashboard/indexOld.vue')
},
{
name: 'ProfileMessage',
path: '/message',

View File

@ -0,0 +1,329 @@
import { request } from '@cmp/cmp-element'
import { wrapperParams } from 'utils'
// 指标列表
export function getMetrics(params) {
return request.get('/cms/v1/metrics', {
params: wrapperParams(params)
})
}
// 设置监控IP
export function setIps(params) {
return request.post(`/cms/v1/vms/${params.id}/ips`, wrapperParams(params))
}
export function geIps(id) {
return request.get(`/cms/v1/vms/${id}/ips`)
}
// 分发策略
// 列表
export function getDistributions(params) {
return request.get('/cms/v1/distributions', {
params: params
})
}
// 新增
export function createDistri(params) {
return request.post('/cms/v1/distributions', wrapperParams(params))
}
// 修改
export function modifyDistri(params) {
return request.put(`/cms/v1/distributions/${params.id}`, wrapperParams(params))
}
// 删除
export function removeDistri(id) {
return request.delete(`/cms/v1/distributions/${id}`)
}
// 详情
export function getDistriDetail(id) {
return request.get(`/cms/v1/distributions/${id}`)
}
// 告警模板
const tempUrl = '/cms/v1/templates'
// 列表
export function getTempList(params) {
return request.get(tempUrl, {
params: params
})
}
// 新增
export function createTemp(params) {
return request.post(tempUrl, wrapperParams(params))
}
// 修改
export function modifyTemp(params) {
return request.put(`${tempUrl}/${params.id}`, wrapperParams(params))
}
// 删除
export function removeTemp(id) {
return request.delete(`${tempUrl}/${id}`)
}
// 批量删除
export function batchRemoveTemp(params) {
return request.delete(tempUrl, {
data: params
})
}
// 详情
export function getTempDetail(id) {
return request.get(`${tempUrl}/${id}`)
}
// 告警列表
export function getAlarmList(params) {
return request.get('/cms/v1/alarms', {
params: params
})
}
export function getAlarmDetail(id) {
return request.get(`/cms/v1/alarms/${id}`)
}
// 告警确认
export function alarmConfirm(params) {
return request.patch('/cms/v1/alarms', {
action: 'confirm',
...wrapperParams(params)
})
}
// 告警解决
export function alarmSolve(params) {
return request.patch('/cms/v1/alarms', {
action: 'solve',
...wrapperParams(params)
})
}
export function getAlarmChart(params) {
return request.get('/cms/v1/alarms/chart', {
params
})
}
// vcenter主机资源概览
export function getVcHostOverview(id) {
return request.get(`/cms/v1/hosts/${id}`, {
params: wrapperParams({ type: 'VMWARE' })
})
}
// vcenter云主机资源概览
export function getVmOverview(id) {
return request.get(`/cms/v1/vms/${id}`)
}
// 云主机资源概览仪表盘
export function getHostDashboard(params) {
return request.get('/cms/v1/prometheus', {
params: wrapperParams(params)
})
}
// 主机云主机图表
export function getCharts(params) {
return request.get('/cms/v1/charts', {
params: wrapperParams(params)
})
}
// openstack主机详情
export function getOpenstackHost(id) {
return request.get(`/cms/v1/hosts/${id}`, {
params: wrapperParams({ type: 'OPENSTACK' })
})
}
// 主机CPU
export function getHostCpu(id) {
return request.get(`/cms/v1/hosts/${id}/metrics`, {
params: wrapperParams({ type: 'cpu' })
})
}
export function getHostMem(id) {
return request.get(`/cms/v1/hosts/${id}/metrics`, {
params: wrapperParams({ type: 'mem' })
})
}
export function getHostDisk(id) {
return request.get(`/cms/v1/hosts/${id}/metrics`, {
params: wrapperParams({ type: 'disk' })
})
}
// 告警策略主机列表
export function getPolicyHosts(params) {
return request.get('/cms/v1/hosts', {
params: params
})
}
// 云主机列表
export function getVms(params) {
return request.get('/cms/v1/vms', {
params: params
})
}
export function getDataStore(params) {
return request.get('/cms/v1/datastores', {
params: params
})
}
// 开启监控
export function openMonitor(params) {
return request.patch('/cms/v1/vendors', {
action: 'open',
...wrapperParams(params)
})
}
// 关闭监控
export function closeMonitor(params) {
return request.patch('/cms/v1/vendors', {
action: 'close',
...wrapperParams(params)
})
}
export function getRatio(params) {
return request.get('/cms/v1/vendors/ratio', {
params
})
}
export function ratioOk(params) {
return request.post('/cms/v1/vendors/ratio', wrapperParams(params))
}
// hmc主机分区列表
export function getServers(params) {
return request.get('/cms/v1/hmc/servers', {
params: params
})
}
export function getPartitions(params) {
return request.get('/cms/v1/hmc/partitions', {
params: params
})
}
// 运维认证
export function getAuthentications(vendorId) {
return request.get(`/cms/v1/vendors/${vendorId}/authentications`)
}
export function authenticationsOk(params) {
return request.post(`/cms/v1/vendors/${params.vendorId}/authentications`, wrapperParams(params))
}
export function getFusionHost(params) {
return request.get('/cms/v1/fusioncloud/hosts', {
params: params
})
}
export function getFusionHostDetail(id, type) {
return request.get(`/cmp/plugins/${type}/v1/hosts/${id}`)
}
// 安装agent
export function installTaskExporter(params) {
return request.post('/cms/v1/agent', wrapperParams(params))
}
export function getOpenstackVm(type, id) {
return request.get(`/cmp/plugins/${type}/v1/vms/${id}`)
}
export function getSecurityGroup(type, params) {
return request.get(`/cmp/plugins/${type}/v1/vms/${params.id}/sgroups`, {
params: params
})
}
export function getUsage(params) {
return request.get('/cms/v1/prometheus', {
params: wrapperParams(params)
})
}
export function getPoolDatas(params) {
return request.get('/cms/v1/prometheus/filter', {
params: wrapperParams(params)
})
}
export function getServices(params) {
return request.get('/cms/v1/services', {
params: params
})
}
// 资源利用率TOP5
export function getResTops(params) {
return request.get('/cms/v1/tops', {
params: wrapperParams(params)
})
}
// IPMI设置
export function getHosts(id) {
return request.get(`/cms/v1/hosts/ipmi/${id}`)
}
export function patchHosts(url, params) {
return request.patch(url, { ...wrapperParams(params) })
}
// 数据源配置
export function configDataSource(vendorId, params) {
return request.post(`/cms/v1/vendors/${vendorId}/monitor`, wrapperParams(params))
}
export function getfilters(params) {
return request.get('/cms/v1/prometheus/filter', {
params: wrapperParams(params)
})
}
// 告警策略
export function getRuleGroup(params) {
return request.get('/cms/v1/rulegroups', {
params: wrapperParams(params)
})
}
export function createRuleGroup(params) {
return request.post('/cms/v1/rulegroups', wrapperParams(params))
}
export function modifyRuleGroup(params) {
return request.put(`/cms/v1/rulegroups/${params.id}`, wrapperParams(params))
}
export function removeRuleGroup(id) {
return request.delete(`/cms/v1/rulegroups/${id}`)
}
export function batchRemoveRuleGroup(params) {
return request.delete('/cms/v1/rulegroups', {
data: wrapperParams(params)
})
}
export function getRuleGroupDetail(id) {
return request.get(`/cms/v1/rulegroups/${id}`)
}
export function ruleGroupEnable(params) {
return request.patch('/cms/v1/rulegroups/enable', { ...wrapperParams(params) })
}
export function rulegroupsBinding(params) {
return request.patch('/cms/v1/rulegroups/binding', { ...wrapperParams(params) })
}
export function rulegroupsUnBinding(params) {
return request.delete('/cms/v1/rulegroups/binding', {
data: wrapperParams(params)
})
}
export function getRuleGroupBind(id) {
return request.get(`/cms/v1/rulegroups/${id}/resources`)
}
export function modifyAlarmStatus(params) {
return request.post('/cms/v1/alarmstatus', wrapperParams(params))
}
export function deleteAlarmStatus(params) {
return request.delete('/cms/v1/alarmstatus', {
data: wrapperParams(params)
})
}
export function getVolumeByType(type1, params, type, projectName) {
return request.get(`/cmp/plugins/${type1}/v1/vendors/type/${type}/${projectName}/volumes`, {
params
})
}
// smart主机列表
export function getSmartHosts(params) {
return request.get('/cms/v1/smartx/hosts', {
params: params
})
}
// 华为云
export function getHuaweiResources(type, params) {
return request.get(`/cms/v1/hcso/${type}`, {
params: params
})
}
export function getStretch(vendorId) {
return request.get(`/cms/v1/hcso/as/${vendorId}`)
}
export function getMysqlRds(params) {
return request.get('/cms/v1/apsarastack/rds/mysql', { params })
}

View File

@ -0,0 +1,7 @@
import { request } from '@cmp/cmp-element'
const newsUrl = '/scms/v1/memorabilia'
export function mockHttp(params) {
return request.get(newsUrl, { params })
}

View File

@ -1,91 +1,45 @@
<template>
<el-container class="setting-wrapper" :class="{ full: !isSetting }">
<el-header class="setting-header" v-if="isSetting">
<span>自定义设置</span>
<div>
<cb-link @click="reset()"></cb-link>
<!-- <el-button>取消</el-button> -->
<el-button type="primary" @click="savePanels" :loading="loading">保存</el-button>
</div>
</el-header>
<el-container class="setting-wrapper">
<el-container class="setting-container">
<el-aside width="200px" class="setting-aside" v-if="isSetting">
<div class="aside-tool">
<span>隐藏已添加模块</span>
<el-switch class="pull-right" v-model="hideSelectedModule"></el-switch>
</div>
<el-scrollbar class="pool-scroll">
<draggable :list="poolList" v-bind="{ sort: false, group: { name: 'field', pull: 'clone', put: false } }" @end="drop">
<div class="pool-item" :class="forbidPoolCodes.includes(item.code) && 'forbid'" v-for="item in poolList" :key="item.id" :index="item.id" v-show="!(hideSelectedModule && forbidPoolCodes.includes(item.code))">
<span>{{ item.name }}</span>
<i class="icon" :class="forbidPoolCodes.includes(item.code) ? 'el-icon-success' : 'el-icon-plus'"></i>
</div>
</draggable>
</el-scrollbar>
</el-aside>
<el-main class="setting-main" id="setting-container">
<el-scrollbar style="height: 100%">
<div v-if="!isSetting" class="top-header">
<span>您好{{ userData.name }} 欢迎您</span>
<el-button class="pull-right" type="text" @click="goPage('/config/dashboard-setting')"></el-button>
</div>
<grid-layout v-if="layoutData && layoutData.length" :layout="layoutData" :col-num="12" :row-height="16" :is-draggable="isSetting" :is-resizable="isSetting" :vertical-compact="true" :margin="[16, 16]" :use-css-transforms="true">
<grid-item class="grid-item" v-for="(item, key) in layoutData" :key="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" @resize="resizeEvent" @resized="resizeEvent">
<div class="view-card">
<div class="card-title">
<span>{{ item.config.title }}</span>
<div class="card-operate" v-if="isSetting">
<el-dropdown>
<span class="operate-icon"><i class="el-icon-setting"></i></span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="handleEdit(item)">编辑</el-dropdown-item>
<el-dropdown-item @click.native="handleDelete(key)">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div class="card-body" v-if="item.ready">
<cg-loop-charts
v-if="item.config.type == 'LoopCharts'"
:setting="getChartConfig(item)"
ref="chartRef"
:id="item.i"
:data="item.data"
width="100%"
height="100%"
:theme="item.config.title"
:unit="item.config.unit"
:type="['TodayAlarmCount'].includes(item.config.code) ? 'half' : ''"
></cg-loop-charts>
<cg-line-charts v-else-if="item.config.type == 'LineCharts'" ref="chartRef" :id="item.i" :data="item.data" width="100%" height="100%" :unit="item.config.unit" :setting="getChartConfig(item)"></cg-line-charts>
<cg-bar-reverse-charts v-else-if="item.config.type == 'BarReverseCharts'" ref="chartRef" :id="item.i" :data="item.data" width="100%" height="100%" :unit="item.config.unit" :setting="topSetting"></cg-bar-reverse-charts>
<component v-else :is="getComponent(item.config)" :item-data="item" :is-setting="isSetting"></component>
</div>
</div>
</grid-item>
</grid-layout>
<el-dialog title="卡片配置" :close-on-click-modal="false" :visible.sync="dialogVisible" width="800px">
<el-form label-width="90px">
<template v-for="item in configData">
<el-form-item :label="item.name + ''" v-if="item.type === 'TEXT'" :key="item.name">
<el-input v-model="item.defaultValue"></el-input>
</el-form-item>
<select-vendor :itemData="item" :config-data="configData" v-else-if="item.type === 'SELECTVENDOR'" :key="item.name + 1"> </select-vendor>
</template>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="ghost" @click.native="dialogVisible = false">取消</el-button>
<el-button type="primary" @click.native="settingPanel">确定</el-button>
</div>
</template>
</el-dialog>
<cb-empty class="empty" v-if="layoutData && layoutData.length === 0">
<div class="text-center">
<div class="m-b-xs">暂无数据</div>
<div>请按照所纳管的平台进入<router-link class="detail-href" :to="{ name: 'SettingDashboard' }">管理中心系统配置首页配置</router-link></div>
</div>
</cb-empty>
<el-row :gutter="20">
<el-col :span="16">
<!-- 待办工单 -->
<TaskList class="m-b-md" height="918px" />
<!-- 平台容量统计 -->
<PlatformCapacity class="m-b-md" height="242px" />
<!-- 告警处理 -->
<AlarmHandling class="m-b-md" height="412px" />
</el-col>
<el-col :span="8">
<UserInfo class="m-b-md" />
<!-- 数据展示 -->
<StatisticsDisplay class="m-b-md" height="314px" />
<!-- 工单统计 -->
<OrderStatistics class="m-b-md" height="226px" />
<!-- 公告 -->
<NoticeList class="m-b-md" height="378px" />
<!-- 单日告警统计 -->
<DailyAlarmStatistics class="m-b-md" height="412px" />
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<!-- 应用 CPUTop5 -->
<CPUTop5 class="m-b-md" height="320px" />
</el-col>
<el-col :span="12">
<!-- 虚拟机 CPUTop5 -->
<VMCPUTop5 class="m-b-md" height="320px" />
</el-col>
</el-row>
<el-row>
<el-col>
<!-- 告警列表 -->
<WarningList />
</el-col>
</el-row>
</el-scrollbar>
</el-main>
</el-container>
@ -93,350 +47,36 @@
</template>
<script>
import { computed, nextTick, ref, unref, watch } from 'vue'
import VueGridLayout from 'vue-grid-layout'
import draggable from 'vuedraggable'
import CommonOperation from './CommonOperation.vue'
import AccessControl from './AccessControl.vue'
import DataCenterOverview from './DataCenterOverview.vue'
import CountCard from './CountCard.vue'
import DataView from './DataView.vue'
import ResUsed from './ResUsed.vue'
import AlarmCount from './AlarmCount.vue'
import TaskHistory from './TaskHistory.vue'
import SelectVendor from './SelectVendor.vue'
import { getPanel, getPool, savePanel, getConfig, resetPanel } from 'services/system/portal'
import { request } from '@cmp/cmp-element'
import { wrapperParams } from 'utils'
import { Message, MessageBox } from 'element-ui'
import { getCardData } from './utils'
import { topSetting } from './data'
import { useRouter, useRoute, useStore, useInstance } from '@cmp/cmp-core'
const GridLayout = VueGridLayout.GridLayout
const GridItem = VueGridLayout.GridItem
const colorMap = ['#1890FF', '#F84540', '#18BE6A', '#696BD8', '#FE9900', '#01b3eb']
const color = ['#E03B3B', '#F09C2B', '#049BD3', '#1E54DE']
import UserInfo from './new_dashboard_component/UserInfo.vue'
import TaskList from './new_dashboard_component/TaskList.vue'
import StatisticsDisplay from './new_dashboard_component/StatisticsDisplay.vue'
import OrderStatistics from './new_dashboard_component/OrderStatistics.vue'
import PlatformCapacity from './new_dashboard_component/PlatformCapacity.vue'
import NoticeList from './new_dashboard_component/NoticeList.vue'
import AlarmHandling from './new_dashboard_component/AlarmHandling.vue'
import DailyAlarmStatistics from './new_dashboard_component/DailyAlarmStatistics.vue'
import CPUTop5 from './new_dashboard_component/CPUTop5.vue'
import VMCPUTop5 from './new_dashboard_component/VMCPUTop5.vue'
import WarningList from './new_dashboard_component/WarningList.vue'
export default {
components: {
GridLayout,
GridItem,
draggable,
CommonOperation,
AccessControl,
DataCenterOverview,
CountCard,
DataView,
ResUsed,
AlarmCount,
TaskHistory,
SelectVendor
UserInfo,
TaskList,
StatisticsDisplay,
OrderStatistics,
PlatformCapacity,
NoticeList,
AlarmHandling,
DailyAlarmStatistics,
CPUTop5,
VMCPUTop5,
WarningList
},
setup(props, context) {
const loading = ref(false)
//
const isSetting = ref(true)
const route = useRoute()
const ins = useInstance()
function init() {
getPanelList()
if (ins.$route.path === '/resource_dashboard') {
//
isSetting.value = false
} else {
getPoolList()
isSetting.value = true
}
}
init()
watch(
() => ins.$route.path,
() => {
init()
}
)
//
const poolList = ref([])
const hideSelectedModule = ref(false)
const poolMap = computed(() => {
const map = {}
unref(poolList).forEach(item => {
map[item.id] = item
})
return map
})
async function getPoolList() {
const res = await getPool({ module: 'COS' })
if (res.success) {
poolList.value = res.data
}
}
//
const layoutData = ref([])
const forbidPoolCodes = computed(() => {
return unref(layoutData)
.map(item => item.config.code)
.filter(item => {
return !['PM', 'VM', 'DATAVIEW'].includes(item)
})
})
//
function getData(url, params, item) {
request
.get(url, {
params: wrapperParams(params)
})
.then(data => {
if (data.success) {
item.data = data.data
}
})
}
function handleListData(item) {
const config = (item.config = JSON.parse(item.config))
const location = item.location.split(',')
item.x = Number(location[0])
item.y = Number(location[1])
item.w = Number(location[2])
item.h = Number(location[3])
item.i = item.id.toString()
item.data = {}
item.ready = false
setTimeout(() => {
item.ready = true
})
getCardData(item)
config.url && getData(config.url, config.params, item)
return item
}
async function getPanelList() {
loading.value = true
const data = await getPanel({ module: 'COS' })
loading.value = false
if (data.success) {
const result = []
data.data.forEach(item => {
result.push(handleListData(item))
})
layoutData.value = result
}
}
// panel
function drop(e) {
const id = e.item.getAttribute('index')
const poolItem = poolMap.value[id]
if (forbidPoolCodes.value.includes(poolItem.code)) return
//
const colWidth = document.getElementById('setting-container').offsetWidth / 12
let x = Math.round((e.originalEvent.pageX - 300) / colWidth)
let y = Math.round((e.originalEvent.pageY - 92) / 80)
unref(layoutData).forEach(item => {
const xMax = item.x + item.w
const yMax = item.y + item.h
if (x >= item.x && x <= xMax && y >= item.y && y <= yMax) {
x = item.x
y = item.y
}
})
handleCreate(x, y, poolItem)
}
async function handleCreate(x, y, data) {
const { width, height, url, id, name, type, code, moreConfig } = data
if (x + width > 12) x = 12 - width //
const obj = {
x: x,
y: y,
w: width,
h: height,
ready: false,
i: new Date().getTime().toString(),
config: {
url,
id,
title: name,
type,
moreConfig,
code
},
data: {}
}
const params = { code: data.code }
data.properties.forEach(item => {
const { code, defaultValue } = item
defaultValue && (obj.config[code] = defaultValue)
if (item.paramsd) params[code] = defaultValue
// if (item.code === 'color') {
// const color = []
// JSON.parse(item.config).forEach((cell) => {
// color.push(cell.value)
// })
// obj.config.color = color
// }
})
obj.config.params = params
getCardData(obj)
data.url && getData(data.url, params, obj)
layoutData.value.unshift(obj)
await nextTick()
obj.ready = true
}
function getComponent(config) {
const map = {
// PM: 'ServerStatusOverview',
// VM: 'ServerStatusOverview'
}
const component = map[config.code] || config.type
const copComponentList = ['TaskCount', 'InspectCategoryCount', 'InspectHistory', 'Top5']
if (!copComponentList.includes(component)) return component
}
//
async function savePanels() {
const handleConfig = function () {
const config = []
layoutData.value.forEach(item => {
const obj = {
id: item.id,
module: 'CMC',
location: [item.x, item.y, item.w, item.h].join(','),
config: item.config
}
config.push(obj)
})
return config
}
loading.value = true
try {
const res = await savePanel({
module: 'COS',
config: handleConfig()
})
if (res.success) {
Message.success(res.message)
goPage('/resource_dashboard')
}
} catch (error) {}
loading.value = false
}
//
async function reset() {
MessageBox.confirm('您确定要恢复默认设置吗?', '提示', {
confirmButtonClass: 'el-button--danger',
type: 'warning'
})
.then(async () => {
const res = await resetPanel('COS')
if (res.success) {
Message.success(res.message)
goPage('/resource_dashboard')
}
})
.catch(e => {})
}
const store = useStore()
const userData = computed(() => store.getters.userData || {})
//
const currentPanel = ref({})
const configData = ref({})
function handleDelete(index) {
layoutData.value.splice(index, 1)
}
const dialogVisible = ref(false)
async function handleEdit(item) {
currentPanel.value = item
const config = item.config
configData.value = [
{
code: 'title',
defaultValue: config.title,
name: '卡片标题',
paramsd: false,
type: 'TEXT'
}
]
//
if (config.moreConfig) {
const res = await getConfig({
poolId: config.id
})
if (res.success) {
configData.value = res.data
}
}
unref(configData).forEach(item => {
item.defaultValue = config[item.code]
if (item.config) item.config = JSON.parse(item.config)
if (item.code === 'color' && config.color) {
item.config.forEach((cell, key) => {
cell.value = config.color[key]
})
}
})
dialogVisible.value = true
}
//
function settingPanel() {
const config = currentPanel.value.config
const params = { code: config.code }
unref(configData).forEach(item => {
config[item.code] = item.defaultValue
//
if (item.paramsd) params[item.code] = item.defaultValue
if (item.code === 'color') {
config.color = []
item.config.forEach(cell => {
config.color.push(cell.value)
})
}
})
dialogVisible.value = false
config.params = params
config.url && getData(config.url, params, currentPanel.value)
}
const chartRef = ref([])
//
function resizeEvent(i) {
const chartCell = unref(chartRef).find(item => item.id === i)
if (chartCell) chartCell.chart.resize()
}
function goPage(path) {
const router = useRouter()
router.push(path)
// router.push({
// name: 'Redirect',
// query: {
// path
// }
// })
}
function getChartConfig(item) {
const { chartConfig } = item.config
return typeof chartConfig === 'string' ? JSON.parse(chartConfig) : chartConfig
}
return {
topSetting,
loading,
userData,
poolList,
hideSelectedModule,
layoutData,
forbidPoolCodes,
drop,
getComponent,
isSetting,
savePanels,
reset,
getChartConfig,
//
dialogVisible,
currentPanel,
configData,
handleDelete,
handleEdit,
settingPanel,
chartRef,
resizeEvent,
color,
goPage
}
return {}
}
}
</script>
@ -445,108 +85,77 @@ export default {
height: calc(100vh - 50px);
overflow: hidden;
flex-direction: column;
&.full {
margin: 0 -16px;
}
font-size: 14px !important;
.setting-container {
height: calc(100% - 50px);
}
}
.setting-header {
background: #fff;
height: 50px !important;
display: flex;
align-items: center;
border-bottom: 1px solid #e4e4e4;
& > span {
font-weight: bold;
flex: 1;
::v-deep {
.el-table--group,
.el-table--border {
border: none;
}
}
.setting-aside {
background: #fff;
height: 100%;
padding: 16px;
.aside-tool {
margin-bottom: 18px;
.el-table th.is-leaf,
.el-table td {
border: none;
}
.pool-scroll {
height: calc(100% - 50px);
}
.pool-item {
display: flex;
align-items: center;
border-radius: 4px;
height: 36px;
padding: 0 12px;
font-size: 12px;
cursor: move;
border: 1px solid #e6e6e6;
margin-bottom: 10px;
&.forbid {
cursor: not-allowed;
}
& > span {
flex: 1;
}
.icon {
color: #1e54de;
font-size: 16px;
}
}
}
.view-card {
height: 100%;
box-sizing: border-box;
border-radius: 4px;
background: #ffffff;
padding: 20px;
position: relative;
overflow: hidden;
.card-title {
font-weight: bold;
color: #393b3e;
margin-bottom: 20px;
}
.card-body {
height: calc(100% - 40px);
display: flex;
flex-direction: column;
justify-content: center;
}
.card-operate {
.el-table::before,
.el-table--group::after,
.el-table--border::after {
content: '';
position: absolute;
right: -32.5px;
top: -32.5px;
display: flex;
z-index: 2;
align-items: center;
justify-content: center;
width: 65px;
height: 65px;
border-radius: 50%;
background: rgba(30, 84, 222, 0.25);
.operate-icon {
color: #fff;
background-color: #fff;
z-index: 1;
}
.table-container .table-header {
background: linear-gradient(180deg, #f0f3ff 0%, #fafbff 100%);
color: #393b3e;
}
//
.el-button--primary.is-plain {
color: rgba(72, 144, 253, 1);
background: rgba(72, 144, 253, 0.1);
border: none;
}
.el-button--primary.is-plain:hover,
.el-button--primary.is-plain:focus {
background: rgba(72, 144, 253, 1);
border-color: rgba(72, 144, 253, 1);
color: #ffffff;
}
.el-button--text {
color: #4890fd;
}
.el-button--text:hover,
.el-button--text:focus {
color: #4b76dd;
border-color: transparent;
background-color: transparent;
}
//
.pagination-container {
position: relative;
.el-pagination__total {
position: absolute;
bottom: -24px;
left: -18px;
cursor: pointer;
left: 0;
}
.el-pagination__jump {
display: none;
}
.el-pagination__sizes {
margin-right: 0;
}
.el-pagination.is-background .el-pager li:not(.disabled).active {
background: rgba(0, 144, 255, 0.1);
color: #1890ff;
}
.el-pagination.is-background .btn-prev,
.el-pagination.is-background .btn-next,
.el-pagination.is-background .el-pager li {
background-color: transparent;
}
}
}
.grid-item {
touch-action: none;
box-sizing: border-box;
}
.setting-main {
padding: 0;
height: 100%;
.top-header {
padding: 17px 17px 0;
}
}
.full-height {
height: 100%;
}
</style>

View File

@ -0,0 +1,552 @@
<template>
<el-container class="setting-wrapper" :class="{ full: !isSetting }">
<el-header class="setting-header" v-if="isSetting">
<span>自定义设置</span>
<div>
<cb-link @click="reset()"></cb-link>
<!-- <el-button>取消</el-button> -->
<el-button type="primary" @click="savePanels" :loading="loading">保存</el-button>
</div>
</el-header>
<el-container class="setting-container">
<el-aside width="200px" class="setting-aside" v-if="isSetting">
<div class="aside-tool">
<span>隐藏已添加模块</span>
<el-switch class="pull-right" v-model="hideSelectedModule"></el-switch>
</div>
<el-scrollbar class="pool-scroll">
<draggable :list="poolList" v-bind="{ sort: false, group: { name: 'field', pull: 'clone', put: false } }" @end="drop">
<div class="pool-item" :class="forbidPoolCodes.includes(item.code) && 'forbid'" v-for="item in poolList" :key="item.id" :index="item.id" v-show="!(hideSelectedModule && forbidPoolCodes.includes(item.code))">
<span>{{ item.name }}</span>
<i class="icon" :class="forbidPoolCodes.includes(item.code) ? 'el-icon-success' : 'el-icon-plus'"></i>
</div>
</draggable>
</el-scrollbar>
</el-aside>
<el-main class="setting-main" id="setting-container">
<el-scrollbar style="height: 100%">
<div v-if="!isSetting" class="top-header">
<span>您好{{ userData.name }} 欢迎您</span>
<el-button class="pull-right" type="text" @click="goPage('/config/dashboard-setting')"></el-button>
</div>
<grid-layout v-if="layoutData && layoutData.length" :layout="layoutData" :col-num="12" :row-height="16" :is-draggable="isSetting" :is-resizable="isSetting" :vertical-compact="true" :margin="[16, 16]" :use-css-transforms="true">
<grid-item class="grid-item" v-for="(item, key) in layoutData" :key="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" @resize="resizeEvent" @resized="resizeEvent">
<div class="view-card">
<div class="card-title">
<span>{{ item.config.title }}</span>
<div class="card-operate" v-if="isSetting">
<el-dropdown>
<span class="operate-icon"><i class="el-icon-setting"></i></span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="handleEdit(item)">编辑</el-dropdown-item>
<el-dropdown-item @click.native="handleDelete(key)">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div class="card-body" v-if="item.ready">
<cg-loop-charts
v-if="item.config.type == 'LoopCharts'"
:setting="getChartConfig(item)"
ref="chartRef"
:id="item.i"
:data="item.data"
width="100%"
height="100%"
:theme="item.config.title"
:unit="item.config.unit"
:type="['TodayAlarmCount'].includes(item.config.code) ? 'half' : ''"
></cg-loop-charts>
<cg-line-charts v-else-if="item.config.type == 'LineCharts'" ref="chartRef" :id="item.i" :data="item.data" width="100%" height="100%" :unit="item.config.unit" :setting="getChartConfig(item)"></cg-line-charts>
<cg-bar-reverse-charts v-else-if="item.config.type == 'BarReverseCharts'" ref="chartRef" :id="item.i" :data="item.data" width="100%" height="100%" :unit="item.config.unit" :setting="topSetting"></cg-bar-reverse-charts>
<component v-else :is="getComponent(item.config)" :item-data="item" :is-setting="isSetting"></component>
</div>
</div>
</grid-item>
</grid-layout>
<el-dialog title="卡片配置" :close-on-click-modal="false" :visible.sync="dialogVisible" width="800px">
<el-form label-width="90px">
<template v-for="item in configData">
<el-form-item :label="item.name + ''" v-if="item.type === 'TEXT'" :key="item.name">
<el-input v-model="item.defaultValue"></el-input>
</el-form-item>
<select-vendor :itemData="item" :config-data="configData" v-else-if="item.type === 'SELECTVENDOR'" :key="item.name + 1"> </select-vendor>
</template>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="ghost" @click.native="dialogVisible = false">取消</el-button>
<el-button type="primary" @click.native="settingPanel">确定</el-button>
</div>
</template>
</el-dialog>
<cb-empty class="empty" v-if="layoutData && layoutData.length === 0">
<div class="text-center">
<div class="m-b-xs">暂无数据</div>
<div>请按照所纳管的平台进入<router-link class="detail-href" :to="{ name: 'SettingDashboard' }">管理中心系统配置首页配置</router-link></div>
</div>
</cb-empty>
</el-scrollbar>
</el-main>
</el-container>
</el-container>
</template>
<script>
import { computed, nextTick, ref, unref, watch } from 'vue'
import VueGridLayout from 'vue-grid-layout'
import draggable from 'vuedraggable'
import CommonOperation from './CommonOperation.vue'
import AccessControl from './AccessControl.vue'
import DataCenterOverview from './DataCenterOverview.vue'
import CountCard from './CountCard.vue'
import DataView from './DataView.vue'
import ResUsed from './ResUsed.vue'
import AlarmCount from './AlarmCount.vue'
import TaskHistory from './TaskHistory.vue'
import SelectVendor from './SelectVendor.vue'
import { getPanel, getPool, savePanel, getConfig, resetPanel } from 'services/system/portal'
import { request } from '@cmp/cmp-element'
import { wrapperParams } from 'utils'
import { Message, MessageBox } from 'element-ui'
import { getCardData } from './utils'
import { topSetting } from './data'
import { useRouter, useRoute, useStore, useInstance } from '@cmp/cmp-core'
const GridLayout = VueGridLayout.GridLayout
const GridItem = VueGridLayout.GridItem
const colorMap = ['#1890FF', '#F84540', '#18BE6A', '#696BD8', '#FE9900', '#01b3eb']
const color = ['#E03B3B', '#F09C2B', '#049BD3', '#1E54DE']
export default {
components: {
GridLayout,
GridItem,
draggable,
CommonOperation,
AccessControl,
DataCenterOverview,
CountCard,
DataView,
ResUsed,
AlarmCount,
TaskHistory,
SelectVendor
},
setup(props, context) {
const loading = ref(false)
//
const isSetting = ref(true)
const route = useRoute()
const ins = useInstance()
function init() {
getPanelList()
if (ins.$route.path === '/resource_dashboard') {
//
isSetting.value = false
} else {
getPoolList()
isSetting.value = true
}
}
init()
watch(
() => ins.$route.path,
() => {
init()
}
)
//
const poolList = ref([])
const hideSelectedModule = ref(false)
const poolMap = computed(() => {
const map = {}
unref(poolList).forEach(item => {
map[item.id] = item
})
return map
})
async function getPoolList() {
const res = await getPool({ module: 'COS' })
if (res.success) {
poolList.value = res.data
}
}
//
const layoutData = ref([])
const forbidPoolCodes = computed(() => {
return unref(layoutData)
.map(item => item.config.code)
.filter(item => {
return !['PM', 'VM', 'DATAVIEW'].includes(item)
})
})
//
function getData(url, params, item) {
request
.get(url, {
params: wrapperParams(params)
})
.then(data => {
if (data.success) {
item.data = data.data
}
})
}
function handleListData(item) {
const config = (item.config = JSON.parse(item.config))
const location = item.location.split(',')
item.x = Number(location[0])
item.y = Number(location[1])
item.w = Number(location[2])
item.h = Number(location[3])
item.i = item.id.toString()
item.data = {}
item.ready = false
setTimeout(() => {
item.ready = true
})
getCardData(item)
config.url && getData(config.url, config.params, item)
return item
}
async function getPanelList() {
loading.value = true
const data = await getPanel({ module: 'COS' })
loading.value = false
if (data.success) {
const result = []
data.data.forEach(item => {
result.push(handleListData(item))
})
layoutData.value = result
}
}
// panel
function drop(e) {
const id = e.item.getAttribute('index')
const poolItem = poolMap.value[id]
if (forbidPoolCodes.value.includes(poolItem.code)) return
//
const colWidth = document.getElementById('setting-container').offsetWidth / 12
let x = Math.round((e.originalEvent.pageX - 300) / colWidth)
let y = Math.round((e.originalEvent.pageY - 92) / 80)
unref(layoutData).forEach(item => {
const xMax = item.x + item.w
const yMax = item.y + item.h
if (x >= item.x && x <= xMax && y >= item.y && y <= yMax) {
x = item.x
y = item.y
}
})
handleCreate(x, y, poolItem)
}
async function handleCreate(x, y, data) {
const { width, height, url, id, name, type, code, moreConfig } = data
if (x + width > 12) x = 12 - width //
const obj = {
x: x,
y: y,
w: width,
h: height,
ready: false,
i: new Date().getTime().toString(),
config: {
url,
id,
title: name,
type,
moreConfig,
code
},
data: {}
}
const params = { code: data.code }
data.properties.forEach(item => {
const { code, defaultValue } = item
defaultValue && (obj.config[code] = defaultValue)
if (item.paramsd) params[code] = defaultValue
// if (item.code === 'color') {
// const color = []
// JSON.parse(item.config).forEach((cell) => {
// color.push(cell.value)
// })
// obj.config.color = color
// }
})
obj.config.params = params
getCardData(obj)
data.url && getData(data.url, params, obj)
layoutData.value.unshift(obj)
await nextTick()
obj.ready = true
}
function getComponent(config) {
const map = {
// PM: 'ServerStatusOverview',
// VM: 'ServerStatusOverview'
}
const component = map[config.code] || config.type
const copComponentList = ['TaskCount', 'InspectCategoryCount', 'InspectHistory', 'Top5']
if (!copComponentList.includes(component)) return component
}
//
async function savePanels() {
const handleConfig = function () {
const config = []
layoutData.value.forEach(item => {
const obj = {
id: item.id,
module: 'CMC',
location: [item.x, item.y, item.w, item.h].join(','),
config: item.config
}
config.push(obj)
})
return config
}
loading.value = true
try {
const res = await savePanel({
module: 'COS',
config: handleConfig()
})
if (res.success) {
Message.success(res.message)
goPage('/resource_dashboard')
}
} catch (error) {}
loading.value = false
}
//
async function reset() {
MessageBox.confirm('您确定要恢复默认设置吗?', '提示', {
confirmButtonClass: 'el-button--danger',
type: 'warning'
})
.then(async () => {
const res = await resetPanel('COS')
if (res.success) {
Message.success(res.message)
goPage('/resource_dashboard')
}
})
.catch(e => {})
}
const store = useStore()
const userData = computed(() => store.getters.userData || {})
//
const currentPanel = ref({})
const configData = ref({})
function handleDelete(index) {
layoutData.value.splice(index, 1)
}
const dialogVisible = ref(false)
async function handleEdit(item) {
currentPanel.value = item
const config = item.config
configData.value = [
{
code: 'title',
defaultValue: config.title,
name: '卡片标题',
paramsd: false,
type: 'TEXT'
}
]
//
if (config.moreConfig) {
const res = await getConfig({
poolId: config.id
})
if (res.success) {
configData.value = res.data
}
}
unref(configData).forEach(item => {
item.defaultValue = config[item.code]
if (item.config) item.config = JSON.parse(item.config)
if (item.code === 'color' && config.color) {
item.config.forEach((cell, key) => {
cell.value = config.color[key]
})
}
})
dialogVisible.value = true
}
//
function settingPanel() {
const config = currentPanel.value.config
const params = { code: config.code }
unref(configData).forEach(item => {
config[item.code] = item.defaultValue
//
if (item.paramsd) params[item.code] = item.defaultValue
if (item.code === 'color') {
config.color = []
item.config.forEach(cell => {
config.color.push(cell.value)
})
}
})
dialogVisible.value = false
config.params = params
config.url && getData(config.url, params, currentPanel.value)
}
const chartRef = ref([])
//
function resizeEvent(i) {
const chartCell = unref(chartRef).find(item => item.id === i)
if (chartCell) chartCell.chart.resize()
}
function goPage(path) {
const router = useRouter()
router.push(path)
// router.push({
// name: 'Redirect',
// query: {
// path
// }
// })
}
function getChartConfig(item) {
const { chartConfig } = item.config
return typeof chartConfig === 'string' ? JSON.parse(chartConfig) : chartConfig
}
return {
topSetting,
loading,
userData,
poolList,
hideSelectedModule,
layoutData,
forbidPoolCodes,
drop,
getComponent,
isSetting,
savePanels,
reset,
getChartConfig,
//
dialogVisible,
currentPanel,
configData,
handleDelete,
handleEdit,
settingPanel,
chartRef,
resizeEvent,
color,
goPage
}
}
}
</script>
<style lang="scss" scoped>
.setting-wrapper {
height: calc(100vh - 50px);
overflow: hidden;
flex-direction: column;
&.full {
margin: 0 -16px;
}
.setting-container {
height: calc(100% - 50px);
}
}
.setting-header {
background: #fff;
height: 50px !important;
display: flex;
align-items: center;
border-bottom: 1px solid #e4e4e4;
& > span {
font-weight: bold;
flex: 1;
}
}
.setting-aside {
background: #fff;
height: 100%;
padding: 16px;
.aside-tool {
margin-bottom: 18px;
}
.pool-scroll {
height: calc(100% - 50px);
}
.pool-item {
display: flex;
align-items: center;
border-radius: 4px;
height: 36px;
padding: 0 12px;
font-size: 12px;
cursor: move;
border: 1px solid #e6e6e6;
margin-bottom: 10px;
&.forbid {
cursor: not-allowed;
}
& > span {
flex: 1;
}
.icon {
color: #1e54de;
font-size: 16px;
}
}
}
.view-card {
height: 100%;
box-sizing: border-box;
border-radius: 4px;
background: #ffffff;
padding: 20px;
position: relative;
overflow: hidden;
.card-title {
font-weight: bold;
color: #393b3e;
margin-bottom: 20px;
}
.card-body {
height: calc(100% - 40px);
display: flex;
flex-direction: column;
justify-content: center;
}
.card-operate {
position: absolute;
right: -32.5px;
top: -32.5px;
display: flex;
z-index: 2;
align-items: center;
justify-content: center;
width: 65px;
height: 65px;
border-radius: 50%;
background: rgba(30, 84, 222, 0.25);
.operate-icon {
color: #fff;
position: absolute;
bottom: -24px;
left: -18px;
cursor: pointer;
}
}
}
.grid-item {
touch-action: none;
box-sizing: border-box;
}
.setting-main {
padding: 0;
height: 100%;
.top-header {
padding: 17px 17px 0;
}
}
.full-height {
height: 100%;
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<ItemCard title="告警处理" v-bind="$attrs">
<el-col :span="8">
<div class="cell" v-for="(item, index) in countData" :key="item.name">
<div class="mark" :style="{ background: colorMap[index] }"></div>
<span class="title">{{ item.name }}</span>
<span class="value">{{ item.value }}</span>
</div>
</el-col>
<el-col :span="16" class="full-height">
<span class="chart-title">近七日告警趋势统计</span>
<div style="height: calc(100% - 50px)">
<LineCharts v-if="lineData" :data="lineData" width="100%" height="100%" :setting="chartSetting"></LineCharts>
</div>
</el-col>
</ItemCard>
</template>
<script>
import { ref } from 'vue'
import LineCharts from './echarts/LineCharts.vue'
import ItemCard from './ItemCard.vue'
import { getAlarmChart } from 'services/monitor/index'
import * as echarts from 'echarts/core'
export const colorMap = ['rgba(255, 0, 0, 1)', 'rgba(245, 167, 45, 1)', 'rgba(18, 185, 242, 1)', 'rgba(24, 144, 255, 1)']
const chartSetting = {
color: colorMap,
legend: {
right: 0,
textStyle: {
color: 'rgba(140, 140, 140, 1)'
}
}
}
export default {
components: { ItemCard, LineCharts },
setup(props, context) {
const countData = ref([
{
name: '紧急告警',
value: 0
},
{
name: '重要告警',
value: 0
},
{
name: '次要告警',
value: 0
},
{
name: '提示告警',
value: 0
}
])
;(async function () {
const res = await getAlarmChart({ action: 'pieChart' })
countData.value = res.data
})()
const lineData = ref({
keys: [],
values: []
})
;(async function () {
const end = new Date().setHours(0, 0, 0, 0) / 1000
// 86400
const res = await getAlarmChart({ action: 'barChart', start: end - 86400 * 7, end })
res.data.values = res.data.values.map((item, index) => {
item.series = {
areaStyle: {
opacity: 0.1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: colorMap[index]
},
{
offset: 1,
color: 'rgba(255,255,255, 0)'
}
])
}
}
return item
})
lineData.value = res.data
})()
return {
colorMap,
chartSetting,
countData,
lineData
}
}
}
</script>
<style lang="scss" scoped>
.cell {
position: relative;
display: flex;
align-items: center;
margin-bottom: 18px;
margin-left: 8px;
height: 56px;
background: linear-gradient(180deg, rgba(240, 243, 255, 0.5) 0%, rgba(250, 251, 255, 0.5) 100%);
border-radius: 4px;
.title {
margin-left: 23px;
}
.value {
position: absolute;
left: 70%;
font-size: 24px;
font-weight: 600;
}
.mark {
margin-left: 10px;
width: 4px;
height: 36px;
background: #ff0000;
border-radius: 4px;
}
}
.chart-title {
margin-left: 15px;
font-weight: 600;
color: #092943;
}
.full-height {
height: 100%;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<ItemCard title="应用CPU TOP5" v-bind="$attrs">
<BarReverseCharts width="100%" height="100%" :data="barData" unit="%" v-if="barData" :setting="chartSetting"></BarReverseCharts>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import BarReverseCharts from './echarts/BarReverseCharts.vue'
import { getResTops } from 'services/monitor/index'
const chartSetting = {
barColor: ['rgba(255, 204, 77, 1)', 'rgba(59, 228, 218, 1)', 'rgba(93, 148, 255, 1)', 'rgba(93, 148, 255, 1)', 'rgba(93, 148, 255, 1)']
}
export default {
components: { ItemCard, BarReverseCharts },
data() {
return {
barData: null,
chartSetting
}
},
created() {
this.getData()
},
methods: {
getData() {
getResTops({
vendorId: 1,
type: 'vmMem',
limit: 5
}).then(data => {
this.barData = data.data
})
}
}
}
</script>

View File

@ -0,0 +1,39 @@
<template>
<ItemCard title="单日告警统计" v-bind="$attrs">
<LoopCharts class="full-height" id="alarm1" v-if="todayAlarmData" :data="todayAlarmData" width="100%" type="half" :setting="chartSetting"></LoopCharts>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import { getAlarmChart } from 'services/monitor/index.js'
import LoopCharts from './echarts/LoopCharts.vue'
import { colorMap } from './AlarmHandling.vue'
const chartSetting = {
color: colorMap
}
export default {
components: { ItemCard, LoopCharts },
data() {
return {
todayAlarmData: null,
chartSetting
}
},
created() {
this, this.getTodayAlarm()
},
methods: {
getTodayAlarm() {
getAlarmChart({
action: 'pieChart',
time: 'TODAY'
}).then(data => {
if (data.success) {
this.todayAlarmData = data.data
}
})
}
}
}
</script>

View File

@ -0,0 +1,52 @@
<template>
<div class="view-card" :style="{ height }">
<div class="card-title">
<span>{{ title }}</span>
<div class="card-operate">
<slot name="operate"></slot>
</div>
</div>
<div class="card-body">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true
},
height: {
type: String,
default: '100%'
}
}
}
</script>
<style lang="scss" scoped>
.view-card {
height: 100%;
box-sizing: border-box;
border-radius: 4px;
background: #ffffff;
padding: 20px;
position: relative;
overflow: hidden;
.card-title {
font-weight: bold;
font-size: 16px;
color: #393b3e;
margin-bottom: 18px;
}
.card-body {
height: calc(100% - 40px);
}
.card-operate {
position: absolute;
right: 15px;
top: 15px;
}
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<ItemCard title="公告列表" v-bind="$attrs">
<cb-table
:data="tableList"
:params="params"
:get-list="getItemList"
:total="total"
:otherProps="{
border: false
}"
>
<el-table-column show-overflow-tooltip label="标题" prop="title">
<template slot-scope="{ row }">
<div class="flex">
<img :src="imgMap[row.type]" alt="" />
<span>{{ row.title }}</span>
</div>
</template>
</el-table-column>
<el-table-column show-overflow-tooltip label="内容" prop="content"></el-table-column>
<el-table-column show-overflow-tooltip label="时间" prop="date"></el-table-column>
<span slot="pagination"></span>
</cb-table>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import { mockHttp } from 'services/system/dashboard'
import from './images/通知-红.png'
import from './images/通知-黄.png'
import from './images/通知-蓝.png'
export default {
components: { ItemCard },
data() {
return {
imgMap: {
,
,
},
tableList: [
{
title: '测试标题',
date: '2020-01-01',
content: '测试内容',
type: '红'
}
],
params: {
page: 1,
rows: 10
},
total: 0
}
},
methods: {
getItemList() {
mockHttp(this.params).then(data => {
if (data.success) {
this.tableList = data.data.rows
this.total = data.data.total
}
})
}
}
}
</script>
<style lang="scss" scoped>
.flex {
display: flex;
align-items: center;
gap: 13px;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<ItemCard title="工单统计" v-bind="$attrs">
<el-row :gutter="20">
<el-col :span="8" v-for="item in numList" :key="item.label">
<div class="num-item" :style="{ backgroundColor: item.background, backgroundImage: `url(${item.bgImg})` }">
<div class="value">{{ item.value }}</div>
<div class="title">{{ item.label }}</div>
</div>
</el-col>
</el-row>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import 待办工单 from './images/icon-待办工单.png'
import 已办工单 from './images/icon-已办工单.png'
import 总数 from './images/icon-总数.png'
export default {
components: { ItemCard },
data() {
return {
numList: [
{ label: '待办工单', value: 36, bgImg: 待办工单, background: 'rgba(252, 182, 98, 0.1)' },
{ label: '已办工单', value: 20, bgImg: 已办工单, background: 'rgba(70, 216, 171, 0.1)' },
{ label: '总数', value: 20, bgImg: 总数, background: 'rgba(93, 148, 255, 0.1)' }
]
}
}
}
</script>
<style scoped lang="scss">
::v-deep {
.num-item {
color: #fff;
height: 110px;
padding: 30px 0 0 25px;
border-radius: 5px;
background-position: right bottom;
background-repeat: no-repeat;
.value {
color: #000;
font-size: 34px;
font-weight: 600;
}
.title {
color: #8c8da3;
}
}
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<ItemCard title="平台容量统计" v-bind="$attrs">
<el-row :gutter="20">
<el-col :span="6" v-for="item in numList" :key="item.label">
<div class="num-item" :style="{ backgroundColor: item.background, backgroundImage: `url(${item.bgImg})` }">
<div class="title">{{ item.label }}</div>
<div class="value">{{ item.value }}</div>
</div>
</el-col>
</el-row>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import 服务器 from './images/服务器(台).png'
import 虚拟机 from './images/虚拟机(台).png'
import 网络设备 from './images/网络设备(台).png'
import 安全设备 from './images/安全设备(台).png'
export default {
components: { ItemCard },
data() {
return {
numList: [
{ label: '服务器(台)', value: 36, bgImg: 服务器, background: 'rgba(231, 242, 252, 1)' },
{ label: '虚拟机(台)', value: 20, bgImg: 虚拟机, background: 'rgba(250, 243, 233, 1)' },
{ label: '网络设备(台)', value: 20, bgImg: 网络设备, background: 'rgba(227, 245, 252, 1)' },
{ label: '安全设备(台)', value: 20, bgImg: 安全设备, background: 'rgba(235, 246, 239, 1)' }
]
}
}
}
</script>
<style scoped lang="scss">
::v-deep {
.num-item {
color: #fff;
height: 110px;
padding: 30px 0 0;
padding-left: 40%;
border-radius: 5px;
background-position: 10% 40%;
background-size: 56px;
background-repeat: no-repeat;
.value {
color: #000;
font-size: 28px;
font-weight: 600;
}
.title {
color: #8c8da3;
}
}
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<ItemCard title="数据展示" v-bind="$attrs">
<el-row :gutter="20">
<el-col :span="12" v-for="item in numList" :key="item.label">
<div class="num-item" :style="{ backgroundImage: `url(${item.bgImg})` }">
<div class="value">{{ item.value }}</div>
<div>{{ item.label }}</div>
</div>
</el-col>
</el-row>
<el-row :gutter="20" type="flex" justify="space-around">
<el-col :span="7" v-for="item in statistics" :key="item.label">
<div class="statistic-item">
<div class="value">{{ item.value }}</div>
<div class="title">{{ item.label }}</div>
</div>
</el-col>
</el-row>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import 当前用户授权应用数 from './images/icon-当前用户授权应用数.png'
import 数据权限申请数量 from './images/icon-数据权限申请数量.png'
export default {
components: { ItemCard },
data() {
return {
numList: [
{ label: '当前用户授权应用数', value: 36, bgImg: 当前用户授权应用数 },
{ label: '数据权限申请数量', value: 20, bgImg: 数据权限申请数量 }
],
statistics: [
{ label: '数据调度任务量', value: 36 },
{ label: '运维任务申请表', value: 17 },
{ label: '日志审计任务量', value: 20 }
]
}
}
}
</script>
<style scoped lang="scss">
::v-deep {
.num-item {
color: #fff;
height: 95px;
padding: 30px 0 0 25px;
background: #5d94ff;
border-radius: 5px;
background-position: right;
background-repeat: no-repeat;
.value {
font-size: 34px;
font-weight: 600;
}
}
.statistic-item {
margin-top: 25px;
.value {
font-size: 30px;
color: #002c46;
font-weight: 600;
}
.title {
color: #8c8da3;
}
}
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<ItemCard title="待办工单" v-bind="$attrs">
<el-button slot="operate" type="primary" plain>查看更多</el-button>
<cb-table :data="tableList" :params="params" :get-list="getItemList" :total="total">
<el-table-column type="index" width="50" label="序号"> </el-table-column>
<el-table-column show-overflow-tooltip label="标题" prop="name">
<template slot-scope="scope">
<span class="name">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column show-overflow-tooltip label="状态" prop="status"></el-table-column>
<el-table-column show-overflow-tooltip label="发起人" prop="progress"></el-table-column>
<el-table-column show-overflow-tooltip label="时间" prop="time"></el-table-column>
<el-table-column show-overflow-tooltip label="操作" width="60px">
<template slot-scope="scope">
<cb-link @click="handleOperation(scope.row)"></cb-link>
</template>
</el-table-column>
</cb-table>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import { mockHttp } from 'services/system/dashboard'
export default {
components: { ItemCard },
data() {
return {
tableList: [
{
name: '测试标题',
status: '待处理',
progress: '张三',
time: '2020-01-01'
}
],
params: {
page: 1,
rows: 10
},
total: 0
}
},
methods: {
getItemList() {
mockHttp(this.params).then(data => {
if (data.success) {
this.tableList = data.data.rows
this.total = data.data.total
}
})
},
handleOperation(row) {
console.log('操作任务', row)
}
}
}
</script>
<style scoped lang="scss">
.name {
font-weight: 600;
color: #344767;
}
v-deep {
.el-table--small {
font-size: 14px !important;
}
}
</style>

View File

@ -0,0 +1,89 @@
<template>
<div class="user-info">
<div class="user-info__avatar">
<img :src="userData.portrait" alt="用户头像" />
</div>
<div class="user-info__details">
<div class="user-info__greeting">{{ greetingMessage }}</div>
<div>
<div class="user-info__role" v-for="role in userRole" :key="role">{{ role }}</div>
</div>
<div class="user-info__login-time">登录时间{{ loginTime }}</div>
</div>
</div>
</template>
<script>
import { useRouter, useRoute, useStore, useInstance } from '@cmp/cmp-core'
import { computed, nextTick, ref, unref, watch } from 'vue'
export default {
setup() {
const store = useStore()
const userData = computed(() => store.getters.userData || {})
const greetingMessage = computed(() => (userData.value.name ? userData.value.name + ', 您好' : '--'))
const userRole = computed(() => userData.value.roleNames || [])
const loginTime = computed(() => userData.value.lastLoginDate)
return {
userData,
greetingMessage,
userRole,
loginTime
}
}
}
</script>
<style scoped>
.user-info {
background-image: url('./images/个人信息bg.png');
background-repeat: no-repeat;
background-size: cover;
display: flex;
align-items: center;
padding-left: 40px;
background-color: #f5f7fa;
border-radius: 8px;
height: 204px;
}
.user-info__avatar {
margin-right: 30px;
border-radius: 12px;
overflow: hidden;
width: 100px;
height: 100px;
}
.user-info__avatar img {
width: 100%;
height: auto;
}
.user-info__details {
display: flex;
flex-direction: column;
justify-content: center;
}
.user-info__greeting {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.user-info__role {
display: inline-block;
color: #fff;
margin-bottom: 8px;
line-height: 25px;
margin-right: 5px;
padding: 0 10px;
height: 25px;
background: #5d94ff;
border-radius: 4px;
}
.user-info__login-time {
color: #666;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<ItemCard title="虚拟机CPU利用率 TOP5" v-bind="$attrs">
<BarReverseCharts width="100%" height="100%" :data="barData" unit="%" v-if="barData" :setting="chartSetting"></BarReverseCharts>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import BarReverseCharts from './echarts/BarReverseCharts.vue'
import { getResTops } from 'services/monitor/index'
const chartSetting = {
barColor: ['rgba(255, 204, 77, 1)', 'rgba(59, 228, 218, 1)', 'rgba(93, 148, 255, 1)', 'rgba(93, 148, 255, 1)', 'rgba(93, 148, 255, 1)']
}
export default {
components: { ItemCard, BarReverseCharts },
data() {
return {
barData: null,
chartSetting
}
},
created() {
this.getData()
},
methods: {
getData() {
getResTops({
vendorId: 1,
type: 'hostCpu',
limit: 5
}).then(data => {
this.barData = data.data
})
}
}
}
</script>

View File

@ -0,0 +1,49 @@
<template>
<ItemCard title="告警列表" v-bind="$attrs">
<cb-table :data="tableList" :params="params" :get-list="getItemList" :total="total">
<el-table-column type="index" width="50" label="序号"> </el-table-column>
<el-table-column show-overflow-tooltip label="标题" prop="name"></el-table-column>
<el-table-column show-overflow-tooltip label="级别" prop="level"></el-table-column>
<el-table-column show-overflow-tooltip label="宫颈类型" prop="type"></el-table-column>
<el-table-column show-overflow-tooltip label="告警来源" prop="time"></el-table-column>
<el-table-column show-overflow-tooltip label="时间" prop="time"></el-table-column>
<el-table-column show-overflow-tooltip label="操作" width="50">
<template slot-scope="scope">
<cb-link @click="handleWarningOperation(scope.row)"></cb-link>
</template>
</el-table-column>
</cb-table>
</ItemCard>
</template>
<script>
import ItemCard from './ItemCard.vue'
import { mockHttp } from 'services/system/dashboard'
export default {
components: { ItemCard },
data() {
return {
tableList: [],
params: {
page: 1,
rows: 10
},
total: 0
}
},
methods: {
getItemList() {
mockHttp(this.params).then(data => {
if (data.success) {
this.tableList = data.data.rows
this.total = data.data.total
}
})
},
handleWarningOperation(row) {
console.log('处理告警', row)
}
}
}
</script>

View File

@ -0,0 +1,272 @@
<template>
<div :style="{ height, width }" class="chart-container">
<div class="chart" :class="{ hide: isNoData }" :id="id" style="width: 100%; height: 100%"></div>
<cb-empty class="chart-no-data" v-show="isNoData"></cb-empty>
</div>
</template>
<script>
import { computed } from 'vue'
import useEchart, { commonProps } from '@cmp/cmp-echarts/hooks/useChart'
import { merge } from 'lodash-es'
import * as echarts from 'echarts/core'
import 排名1 from '../images/排名 1.png'
import 排名2 from '../images/排名 2.png'
import 排名3 from '../images/排名 3.png'
import 排名4 from '../images/排名 4.png'
import 排名5 from '../images/排名 5.png'
const commonLinerColor = [
['#8699FF', '#4B66FF'],
['#23A3B0', '#1BB879'],
['#888B79', '#DC931F']
]
function getLinerColor(startColor, endColor) {
//
if (!endColor) return startColor
return new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{
offset: 0,
color: startColor
},
{
offset: 1,
color: endColor
}
])
}
export default {
props: {
...commonProps
},
setup(props) {
//
function handleData(data) {
const { legendLength = 60, series: seriesConfig = {}, colorMap = {} } = props.setting
const legends = []
const series = []
const { values = [], keys = [] } = data
values.forEach(item => {
if (!item.data) return
const { name } = item
const resName = name.substring(0, legendLength)
legends.push(name)
const data = item.data.map((cell, index) => {
return {
value: cell,
itemStyle: {
color: props.setting.barColor[index]
}
}
})
series.push({
name: resName,
type: 'bar',
smooth: true,
barMaxWidth: 11,
showBackground: true,
backgroundStyle: {
color: 'rgba(245, 247, 250, 1)',
borderRadius: 50
},
itemStyle: {
borderRadius: 50
},
...seriesConfig,
data
})
})
return {
legends,
series,
keys
}
}
function updateChart() {
var _props$data
if (!((_props$data = props.data) !== null && _props$data !== void 0 && _props$data.values)) return
const { linerColor = commonLinerColor, color: scolor, legend = {}, xAxis = {}, yAxis = {}, grid = {} } = props.setting
const { legends, series, keys } = handleData(props.data)
const color =
scolor ||
(linerColor &&
linerColor.map(item => {
return getLinerColor(item[0], item[1])
}))
const options = {
color: color || commonColor,
legend: {
show: false,
data: legends,
...legend
},
grid: {
top: 10,
left: '-150',
right: '2%',
bottom: 1,
containLabel: true,
...grid
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: [
{
type: 'value',
position: 'bottom',
splitLine: {
lineStyle: {
type: 'dashed'
}
},
axisLine: {
show: false
},
axisLabel: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: false
},
...xAxis
}
],
yAxis: [
{
type: 'category',
inverse: true,
// ...yAxis,
data: keys,
axisLine: {
show: false
},
position: 'left',
axisLabel: {
show: true,
margin: 150,
textStyle: {
align: 'left'
},
color: 'rgba(52, 71, 103, 1)',
formatter: function (value, index) {
return `{rang${index + 1}|}` + ' ' + value
},
rich: {
rang1: {
width: 24,
height: 28,
backgroundColor: {
image: 排名1
}
},
rang2: {
width: 24,
height: 28,
backgroundColor: {
image: 排名2
}
},
rang3: {
width: 24,
height: 28,
backgroundColor: {
image: 排名3
}
},
rang4: {
width: 24,
height: 28,
backgroundColor: {
image: 排名4
}
},
rang5: {
width: 24,
height: 28,
backgroundColor: {
image: 排名5
}
}
}
},
axisTick: {
show: false
},
splitLine: {
show: false
},
...yAxis
},
{
type: 'category',
inverse: true,
// ...yAxis,
data: keys,
axisLine: {
show: false
},
axisLabel: {
show: true,
formatter: function (value, index) {
return props.data.values[0].data[index] + '%'
},
margin: 40,
width: 10,
fontSize: 14,
fontWeight: 600
},
axisTick: {
show: false
},
splitLine: {
show: false
},
...yAxis
}
],
series
}
chart.value && chart.value.setOption(merge(options, props.options), true)
}
const { chart } = useEchart(props, updateChart)
const isNoData = computed(() => !props.data?.keys?.length)
return {
updateChart,
chart,
isNoData
}
}
}
</script>
<style scoped lang="scss">
.chart-container {
position: relative;
width: 100%;
height: 100%;
.chart-no-data {
position: absolute;
top: 0;
width: 100%;
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
justify-content: center;
align-items: center;
}
.chart {
width: 100%;
height: 100%;
&.hide {
visibility: hidden;
}
}
}
</style>

View File

@ -0,0 +1,159 @@
<template>
<div :style="{ height, width }" class="chart-container">
<div class="chart" :class="{ hide: isNoData }" :id="id" style="width: 100%; height: 100%"></div>
<cb-empty class="chart-no-data" v-show="isNoData"></cb-empty>
</div>
</template>
<script>
import { computed } from 'vue'
import useEchart, { commonProps } from '@cmp/cmp-echarts/hooks/useChart'
import { merge } from 'lodash-es'
export default {
props: {
...commonProps
},
setup(props, context) {
const commonYAxis = {
type: 'value',
nameGap: 5,
min: 0,
splitLine: {
lineStyle: {
color: ['#ccc'],
type: 'solid'
}
},
axisLine: {
lineStyle: {
color: ['#d9d9d9']
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#333'
},
nameTextStyle: {
color: '#333'
}
}
const commonGrid = {
left: 20,
right: 1,
top: 30,
bottom: 1,
containLabel: true
}
//
function handleData(data) {
const { legendLength = 60 } = props.setting
const legends = []
const series = []
const { values = [], keys = [] } = data
values.forEach(item => {
const { name, data, series: seriesConfig = {} } = item
const resName = name?.substring(0, legendLength)
legends.push(name)
series.push({
name: resName,
type: 'line',
smooth: true,
showSymbol: false,
...seriesConfig,
data
})
})
return {
legends,
series,
keys
}
}
function updateChart() {
if (!props.data?.values) return
const { legends, series, keys } = handleData(props.data)
const { legend = {}, grid = {}, xAxis = {}, yAxis = {}, color = [] } = props.setting
const options = {
color,
legend: {
type: 'scroll',
data: legends,
...legend
},
tooltip: {
trigger: 'axis',
confine: true
},
xAxis: [
{
axisTick: {
show: false
},
type: 'category',
data: keys.map(item => {
return item.replace(' ', '\n')
}),
axisLabel: {
color: 'rgba(140, 140, 140, 1)'
},
...xAxis
}
],
yAxis: [
{
...commonYAxis,
name: props.unit,
axisLabel: {
color: 'rgba(140, 140, 140, 1)'
},
...yAxis
}
],
grid: {
...commonGrid,
...grid
},
series
}
chart.value && chart.value.setOption(merge(options, props.options), true)
}
const { chart } = useEchart(props, updateChart)
const isNoData = computed(() => !props.data?.keys?.length)
return {
chart,
updateChart,
isNoData
}
}
}
</script>
<style scoped lang="scss">
.chart-container {
position: relative;
width: 100%;
height: 100%;
.chart-no-data {
position: absolute;
top: 0;
width: 100%;
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
justify-content: center;
align-items: center;
}
.chart {
width: 100%;
height: 100%;
&.hide {
visibility: hidden;
}
}
}
</style>

View File

@ -0,0 +1,221 @@
<template>
<div :style="{ height, width }" class="chart-container">
<div class="chart" :class="{ hide: isNoData }" :id="id" style="width: 100%; height: 100%"></div>
<cb-empty class="chart-no-data" v-show="isNoData"></cb-empty>
</div>
</template>
<script>
import { computed } from 'vue'
import useEchart, { commonProps } from '@cmp/cmp-echarts/hooks/useChart'
import { merge } from 'lodash-es'
export default {
props: {
...commonProps,
theme: {
type: String,
default: '数据统计'
},
type: {
type: String
}
},
computed: {
isNoData() {
var _this$data
return !((_this$data = this.data) !== null && _this$data !== void 0 && _this$data.length)
}
},
setup(props, context) {
function getLegendOptions({ countMap, legend }) {
return {
icon: 'circle',
top: '80%',
left: 0,
type: 'scroll',
formatter: name => {
const { legendLength = 10 } = props.setting
const resultName = `${name.substr(0, legendLength)}${name.length > legendLength ? '...' : ''}`
return `{count|${countMap[name]}${props.unit || ''}}\n{name|${resultName}}`
},
textStyle: {
rich: {
name: {
color: 'rgba(140, 141, 163, 1)',
width: 'auto'
},
count: {
verticalAlign: 'middle',
lineHeight: 50,
color: '#000',
width: 70,
fontWeight: 800,
fontSize: '24px'
}
}
},
...legend
}
}
//
function handleData(data) {
let total = 0
const countMap = {}
//
const leftLegends = []
data.forEach((item, index) => {
const { name, value } = item
leftLegends.push(name)
total += value / 1
countMap[name] = value >= 0 ? value : 0
})
const { fixed = 0 } = props.setting
return {
total: total.toFixed(fixed) / 1,
leftLegends,
countMap
}
}
function getSeries(total) {
if (props.type === 'half') {
const data = [...props.data]
if (total === 0) {
data.push({
name: '',
value: 1,
tooltip: {
show: false
},
itemStyle: {
color: 'rgba(53, 114, 248, 1)',
borderColor: '#ecf0fc',
borderWidth: 15,
borderRadius: 0
}
})
}
data.push({
name: '',
value: total || 1,
tooltip: {
show: false
},
itemStyle: {
borderWidth: 0,
color: 'transparent'
}
})
return {
radius: props.setting.radius || ['80%', '100%'],
center: props.setting.center || ['50%', '55%'],
data
}
}
return {
radius: props.setting.radius || ['45%', '60%'],
center: props.setting.center || ['50%', '32%'],
data: props.data
}
}
function updateChart() {
var _props$data
if (!((_props$data = props.data) !== null && _props$data !== void 0 && _props$data.length)) return
const { leftLegends, total, countMap } = handleData(props.data)
const { legend = {}, color = [], series = {} } = props.setting
const legendOptions = getLegendOptions({
countMap,
legend
})
const options = {
color,
title: {
text: `{name|总数}\n{value|${total}}`,
top: props.type === 'half' ? '30%' : '25%',
left: 'center',
textStyle: {
rich: {
name: {
fontSize: '14px',
fontWeight: 'normal',
color: '#002C46'
},
value: {
fontSize: '26px',
lineHeight: 50,
fontWeight: 800,
color: '#002C46'
}
}
}
},
legend: [
{
...legendOptions,
data: leftLegends,
left: '10%'
}
],
tooltip: {
trigger: 'item',
formatter: function (params) {
const str = params.seriesName + '</br>' + params.name + ':' + (params.data.value >= 0 ? params.data.value : 0) + (props.unit ? props.unit : '') + '(' + (params.percent >= 0 ? params.percent : 0) + '%)'
return str
}
},
series: [
{
name: props.theme,
type: 'pie',
startAngle: props.type === 'half' ? 180 : 90,
avoidLabelOverlap: false,
label: {
show: false,
formatter: '{b} {c}',
...series.label
},
itemStyle: {
borderRadius: 5,
borderColor: '#fff',
borderWidth: 1
},
...series,
...getSeries(total)
}
]
}
chart.value && chart.value.setOption(merge(options, props.options), true)
}
const { chart } = useEchart(props, updateChart)
return {
updateChart,
chart
}
}
}
</script>
<style scoped lang="scss">
.chart-container {
position: relative;
width: 100%;
height: 100%;
.chart-no-data {
position: absolute;
top: 0;
width: 100%;
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
justify-content: center;
align-items: center;
}
.chart {
width: 100%;
height: 100%;
&.hide {
visibility: hidden;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B