Browse Source

首页页面实现

dev-V1.0.6
wangmingyong@163.com 2 weeks ago
parent
commit
78221c86c7
  1. 1
      zdxt-web-client/zdxt-admin-plus-ui/index.html
  2. 8
      zdxt-web-client/zdxt-admin-plus-ui/src/layout/components/AppMain.vue
  3. 508
      zdxt-web-client/zdxt-admin-plus-ui/src/layout/components/Navbar.vue
  4. 70
      zdxt-web-client/zdxt-admin-plus-ui/src/layout/index.vue
  5. 1920
      zdxt-web-client/zdxt-admin-plus-ui/src/views/home/index.vue
  6. 23
      zdxt-web-client/zdxt-admin-plus-ui/src/views/supervision/statistical/utils/chartOptions.ts

1
zdxt-web-client/zdxt-admin-plus-ui/index.html

@ -6,6 +6,7 @@
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/statistical/font-awesome/css/font-awesome.min.css" />
<title>%VITE_APP_TITLE%</title>
<!--[if lt IE 11
]><script>

8
zdxt-web-client/zdxt-admin-plus-ui/src/layout/components/AppMain.vue

@ -53,26 +53,22 @@ function addIframe() {
<style lang="scss" scoped>
.app-main {
/* navbar 50px 已由 main-container 的 padding-top 处理 */
min-height: calc(100vh - 50px);
height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header + .app-main {
/* fixed-header 现仅含 tags-view,无 tags-view 时为空,无需额外 padding */
padding-top: 0;
}
.hasTagsView {
.app-main {
/* 84 = navbar 50 + tags-view 34 */
min-height: calc(100vh - 84px);
height: calc(100vh - 84px);
}
.fixed-header + .app-main {
/* tags-view 34px 固定在顶部,需让出空间 */
padding-top: 34px;
}
}

508
zdxt-web-client/zdxt-admin-plus-ui/src/layout/components/Navbar.vue

@ -1,103 +1,45 @@
<template>
<div class="navbar">
<div class="flex align-center">
<div class="logo">执法监督码 执法监督</div>
<hamburger v-if="!permissionStore.iframeMode" id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container"
@toggle-click="toggleSideBar" />
<div class="dashboard-navbar">
<div class="navbar-left">
<div class="hamburger-btn" @click="toggleSideBar">
<i class="fa fa-bars"></i>
</div>
<div class="system-title">
<span class="title-main">执法监督码</span>
<span class="title-dot">·</span>
<span class="title-sub">执法监督</span>
</div>
</div>
<!-- <breadcrumb v-if="!settingsStore.topNav && !permissionStore.iframeMode" id="breadcrumb-container" class="breadcrumb-container" /> -->
<top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
<div class="right-menu flex align-center">
<template v-if="appStore.device !== 'mobile'">
<div class="system-tabs">
<div
v-for="item in permissionStore.visibleSystemCodeOptions"
:key="item.value"
class="system-tab"
:class="{ 'is-active': item.value === currentSystemCode }"
@click="handleSystemCodeChange(item.value)"
>
<div class="navbar-right">
<div class="system-tabs">
<div
v-for="item in permissionStore.visibleSystemCodeOptions"
:key="item.value"
class="system-tab"
:class="{ 'is-active': item.value === currentSystemCode }"
@click="handleSystemCodeChange(item.value)"
>
<div class="nav-icon">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</div>
<div class="nav-label">{{ item.label }}</div>
</div>
</div>
<!-- <el-select
v-if="userId === 1 && tenantEnabled"
v-model="companyName"
class="min-w-244px"
clearable
filterable
reserve-keyword
:placeholder="proxy.$t('navbar.selectTenant')"
@change="dynamicTenantEvent"
@clear="dynamicClearEvent"
>
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
</el-select>-->
<!-- <search-menu ref="searchMenuRef" />
<el-tooltip content="搜索" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect" @click="openSearchMenu">
<svg-icon class-name="search-icon" icon-class="search" />
</div>
</el-tooltip>-->
<!-- 消息 -->
<!-- <el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">
<div>
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :value="newNotice > 0 ? newNotice : ''" :max="99">
<div class="right-menu-item hover-effect" style="display: block"><svg-icon icon-class="message" /></div>
</el-badge>
</template>
<template #default>
<notice></notice>
</template>
</el-popover>
</div>
</el-tooltip>
<el-tooltip content="Github" effect="dark" placement="bottom">
<ruo-yi-git id="zdxt-git" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.document')" effect="dark" placement="bottom">
<ruo-yi-doc id="zdxt-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">
<screenfull id="screenfull" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.language')" effect="dark" placement="bottom">
<lang-select id="lang-select" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>-->
</template>
<div class="avatar-container">
<el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
<div class="avatar-wrapper">
<img :src="userStore.avatar" class="user-avatar" />
<el-icon><caret-bottom /></el-icon>
</div>
<div class="user-info">
<div class="user-avatar">
<img :src="userStore.avatar" alt="avatar" />
</div>
<div class="user-detail">
<div class="username">{{ userStore.nickname || 'admin' }}</div>
<div class="user-org">深圳市司法局</div>
</div>
<el-dropdown trigger="click" @command="handleCommand">
<i class="fa fa-caret-down user-caret"></i>
<template #dropdown>
<el-dropdown-menu>
<router-link v-if="!dynamic" to="/user/profile">
<el-dropdown-item>{{ proxy.$t('navbar.personalCenter') }}</el-dropdown-item>
</router-link>
<el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">
<span>{{ proxy.$t('navbar.layoutSetting') }}</span>
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<span>{{ proxy.$t('navbar.logout') }}</span>
</el-dropdown-item>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@ -107,135 +49,119 @@
</template>
<script setup lang="ts">
import SearchMenu from './TopBar/search.vue';
import { usePermissionStore } from '@/store/modules/permission';
import { ref, watch } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { useUserStore } from '@/store/modules/user';
import { useSettingsStore } from '@/store/modules/settings';
import { useNoticeStore } from '@/store/modules/notice';
import { getTenantList } from '@/api/login';
import { dynamicClear, dynamicTenant } from '@/api/system/tenant';
import { TenantVO } from '@/api/types';
import notice from './notice/index.vue';
import { usePermissionStore } from '@/store/modules/permission';
import { ElMessageBox } from 'element-plus';
import type { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
import router from '@/router';
import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const noticeStore = storeToRefs(useNoticeStore());
const newNotice = ref(<number>0);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userId = ref(userStore.userId);
const companyName = ref(undefined);
const tenantList = ref<TenantVO[]>([]);
//
const dynamic = ref(false);
//
const tenantEnabled = ref(true);
//
const searchMenuRef = ref<InstanceType<typeof SearchMenu>>();
//
const currentSystemCode = ref(permissionStore.currentSystemCode);
watch(() => permissionStore.currentSystemCode, (val) => {
currentSystemCode.value = val;
});
watch(
() => permissionStore.currentSystemCode,
(val) => {
currentSystemCode.value = val;
}
);
const toggleSideBar = () => {
appStore.toggleSideBar(false);
};
/** 切换菜单组 */
const handleSystemCodeChange = async (systemCode: string) => {
currentSystemCode.value = systemCode;
await permissionStore.switchSystemCode(systemCode);
};
const openSearchMenu = () => {
searchMenuRef.value?.openSearch();
};
//
const dynamicTenantEvent = async (tenantId: string) => {
if (companyName.value != null && companyName.value !== '') {
await dynamicTenant(tenantId);
dynamic.value = true;
await proxy?.$router.push('/');
await proxy?.$tab.closeAllPage();
await proxy?.$tab.refreshPage();
const handleCommand = async (command: string) => {
if (command === 'logout') {
await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
} as ElMessageBoxOptions);
await userStore.logout();
router.replace({ path: '/login' });
}
};
</script>
const dynamicClearEvent = async () => {
await dynamicClear();
dynamic.value = false;
await proxy?.$router.push('/');
await proxy?.$tab.closeAllPage();
await proxy?.$tab.refreshPage();
};
<style lang="scss" scoped>
.dashboard-navbar {
height: 50px;
background: linear-gradient(180deg, #0c3d8f 0%, #0a2e72 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 1000;
}
.navbar-left {
display: flex;
align-items: center;
gap: 20px;
}
/** 租户列表 */
const initTenantList = async () => {
const { data } = await getTenantList(true);
tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
if (tenantEnabled.value) {
tenantList.value = data.voList;
.hamburger-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
i {
color: rgba(255, 255, 255, 0.9);
font-size: 18px;
}
};
defineExpose({
initTenantList
});
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
const toggleSideBar = () => {
appStore.toggleSideBar(false);
};
.system-title {
display: flex;
align-items: baseline;
gap: 2px;
}
const logout = async () => {
await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
} as ElMessageBoxOptions);
await userStore.logout();
proxy?.$tab.closeAllPage();
router.replace({
path: '/login',
query: {
redirect: encodeURIComponent(router.currentRoute.value.fullPath || '/')
}
});
};
.title-main {
font-size: 20px;
font-weight: 600;
color: #fff;
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(64, 180, 255, 0.5);
}
const emits = defineEmits(['setLayout']);
const setLayout = () => {
emits('setLayout');
};
// Command key
const commandMap: { [key: string]: any } = {
setLayout,
logout
};
const handleCommand = (command: string) => {
//
if (commandMap[command]) {
commandMap[command]();
}
};
//
watch(
() => noticeStore.state.value.notices,
(newVal) => {
newNotice.value = newVal.filter((item: any) => !item.read).length;
},
{ deep: true }
);
</script>
.title-dot {
font-size: 18px;
color: rgba(255, 255, 255, 0.7);
margin: 0 2px;
}
<style lang="scss" scoped>
:deep(.el-badge__content.is-fixed) {
top: 12px;
.title-sub {
font-size: 18px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 2px;
}
.navbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.system-tabs {
@ -247,139 +173,117 @@ watch(
.system-tab {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
height: 32px;
padding: 0 14px;
border-radius: 4px;
font-size: 13px;
color: #5a5e66;
justify-content: center;
width: 64px;
height: 44px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
transition: all 0.25s ease;
position: relative;
&:hover {
color: #1e6bb8;
background: rgba(58, 174, 251, 0.08);
background: rgba(255, 255, 255, 0.12);
.nav-icon .el-icon {
color: rgba(255, 255, 255, 0.9);
}
.nav-label {
color: rgba(255, 255, 255, 0.9);
}
}
&.is-active {
background: rgba(58, 174, 251, 0.12);
color: #1e6bb8;
background: rgba(64, 160, 255, 0.25);
box-shadow: inset 0 -2px 0 #40a0ff;
.el-icon {
color: #1e6bb8;
.nav-icon .el-icon {
color: #fff;
}
}
.el-icon {
font-size: 16px;
color: #5a5e66;
transition: color 0.2s;
.nav-label {
color: #fff;
}
}
}
.flex {
.nav-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1px;
.el-icon {
font-size: 16px;
color: rgba(255, 255, 255, 0.75);
transition: color 0.25s;
}
}
.align-center {
align-items: center;
.nav-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
white-space: nowrap;
transition: color 0.25s;
}
.navbar {
height: 50px;
overflow: hidden;
position: relative;
//background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.user-info {
display: flex;
justify-content: space-between;
.logo {
font-size: 18px;
color: #5a5e66;
font-weight: bold;
margin-left: 20px;
}
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
align-items: center;
gap: 10px;
margin-left: 16px;
padding: 4px 12px 4px 4px;
border-radius: 24px;
background: rgba(255, 255, 255, 0.08);
cursor: pointer;
transition: background 0.2s;
.breadcrumb-container {
float: left;
&:hover {
background: rgba(255, 255, 255, 0.15);
}
}
.topmenu-container {
position: absolute;
left: 50px;
}
.user-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
overflow: hidden;
border: 2px solid rgba(64, 160, 255, 0.6);
box-shadow: 0 0 8px rgba(64, 160, 255, 0.3);
.errLog-container {
display: inline-block;
vertical-align: top;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
display: flex;
.user-detail {
display: flex;
flex-direction: column;
line-height: 1.2;
}
&:focus {
outline: none;
}
.username {
font-size: 14px;
font-weight: 500;
color: #fff;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
.user-org {
font-size: 11px;
color: rgba(255, 255, 255, 0.65);
margin-top: 2px;
}
.avatar-container {
margin-right: 40px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
margin-top: 10px;
}
i {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
.user-caret {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
margin-left: 4px;
}
</style>

70
zdxt-web-client/zdxt-admin-plus-ui/src/layout/index.vue

@ -3,11 +3,11 @@
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<!-- iframe 全屏模式隐藏侧边栏标签栏路由内容只保留导航栏 + iframe -->
<template v-if="permissionStore.iframeMode">
<div class="main-container iframe-mode" style="margin-left: 0;">
<div class="main-container iframe-mode" :class="{ 'dashboard-mode': isDashboardRoute }" style="margin-left: 0;">
<div :class="{ 'fixed-header': fixedHeader }" style="width: 100%;">
<navbar ref="navbarRef" @set-layout="setLayout" />
<navbar ref="navbarRef" />
</div>
<section class="iframe-main">
<section class="iframe-main" :class="{ 'iframe-main-full': isDashboardRoute }">
<iframe v-if="permissionStore.iframeUrl" :src="permissionStore.iframeUrl" class="iframe-full" frameborder="0" />
<router-view v-else class="iframe-full-page" />
</section>
@ -18,7 +18,7 @@
<template v-else>
<!-- 顶部头部整行 -->
<div class="top-header">
<navbar ref="navbarRef" @set-layout="setLayout" />
<navbar ref="navbarRef" />
</div>
<!-- 下方左侧边栏 + 右内容 -->
<side-bar v-if="!sidebar.hide" class="sidebar-container" />
@ -76,9 +76,12 @@ watchEffect(() => {
const navbarRef = ref<InstanceType<typeof Navbar>>();
const settingRef = ref<InstanceType<typeof Settings>>();
const isDashboardRoute = computed(() => {
return permissionStore.fullscreenRoute === router.currentRoute.value.path && !permissionStore.iframeUrl;
});
onMounted(() => {
nextTick(() => {
navbarRef.value?.initTenantList();
if (permissionStore.iframeMode && permissionStore.fullscreenRoute && router.currentRoute.value.path !== permissionStore.fullscreenRoute) {
router.replace(permissionStore.fullscreenRoute);
}
@ -175,12 +178,23 @@ const setLayout = () => {
}
}
.iframe-mode.dashboard-mode {
}
.iframe-mode.dashboard-mode .iframe-main {
}
.iframe-main {
width: 100%;
position: relative;
overflow: hidden;
}
.iframe-main-full {
height: calc(100vh - 50px);
overflow: hidden;
}
.fixed-header + .iframe-main {
padding-top: 50px;
}
@ -198,47 +212,17 @@ const setLayout = () => {
overflow: auto;
}
.iframe-mode .navbar {
background: linear-gradient(180deg, #0d1d3a 0%, #0a1628 100%);
border-bottom: 1px solid rgba(58, 174, 251, 0.2);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.iframe-mode .navbar .right-menu-item,
.iframe-mode .navbar .breadcrumb-container,
.iframe-mode .navbar .topmenu-container {
color: rgba(255, 255, 255, 0.85);
}
.iframe-mode .navbar .right-menu-item:hover,
.iframe-mode .navbar .avatar-wrapper:hover {
background: rgba(58, 174, 251, 0.15);
.iframe-mode .dashboard-navbar {
background: linear-gradient(180deg, #0c3d8f 0%, #0a2e72 100%);
border-bottom: 1px solid rgba(64, 160, 255, 0.2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.iframe-mode .navbar .user-avatar {
border: 2px solid rgba(58, 174, 251, 0.4);
.iframe-mode .dashboard-navbar .nav-item:hover {
background: rgba(255, 255, 255, 0.12);
}
.iframe-mode .navbar .system-tab {
color: rgba(255, 255, 255, 0.75);
.el-icon {
color: rgba(255, 255, 255, 0.6);
}
&:hover {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
&.is-active {
background: rgba(58, 174, 251, 0.25);
color: #fff;
box-shadow: 0 0 12px rgba(58, 174, 251, 0.3);
.el-icon {
color: #3aaefb;
}
}
.iframe-mode .dashboard-navbar .user-info:hover {
background: rgba(255, 255, 255, 0.15);
}
</style>

1920
zdxt-web-client/zdxt-admin-plus-ui/src/views/home/index.vue

File diff suppressed because it is too large

23
zdxt-web-client/zdxt-admin-plus-ui/src/views/supervision/statistical/utils/chartOptions.ts

@ -15,7 +15,14 @@ export function getSmartMaxAndInterval(data1: number[], data2: number[], maxSpli
return { max: interval * maxSplits, interval };
}
const ADMIN_PIE_COLORS = ['#2d6fcf', '#4fa0a8', '#5e9acc', '#b29f60', '#af5172', '#8a61d2', '#6f2c9b', '#9d7173', '#887456', '#9eac71'];
const NON_ADMIN_PIE_COLORS = ['#7bb06c', '#489d61', '#4e97a6', '#3953d4', '#4e3ec5', '#6f2c9b', '#9d7173', '#887456', '#9eac71', '#8e5aac'];
export function buildAdminPieOption(data: { value: number; name: string }[]): EChartsOption {
const coloredData = data.map((item, index) => ({
...item,
itemStyle: { color: ADMIN_PIE_COLORS[index % ADMIN_PIE_COLORS.length] }
}));
return {
tooltip: {
trigger: 'item',
@ -32,14 +39,13 @@ export function buildAdminPieOption(data: { value: number; name: string }[]): EC
itemHeight: 6.5,
itemGap: 15
},
color: ADMIN_PIE_COLORS,
series: [
{
name: '行政执法类别数量及占比',
type: 'pie',
color: ['#2d6fcf', '#4fa0a8', '#5e9acc', '#b29f60', '#af5172', '#8a61d2', '#6f2c9b', '#9d7173', '#887456', '#9eac71'],
radius: [52, 72],
center: ['50%', '50%'],
padAngle: 2,
itemStyle: { borderRadius: 1 },
label: {
show: true,
@ -51,13 +57,17 @@ export function buildAdminPieOption(data: { value: number; name: string }[]): EC
}
},
labelLine: { show: true, length: 20, length2: 5, smooth: 0 },
data
data: coloredData
}
]
};
}
export function buildNonAdminPieOption(data: { value: number; name: string }[]): EChartsOption {
const coloredData = data.map((item, index) => ({
...item,
itemStyle: { color: NON_ADMIN_PIE_COLORS[index % NON_ADMIN_PIE_COLORS.length] }
}));
return {
tooltip: {
trigger: 'item',
@ -74,14 +84,13 @@ export function buildNonAdminPieOption(data: { value: number; name: string }[]):
itemHeight: 6.5,
itemGap: 15
},
color: NON_ADMIN_PIE_COLORS,
series: [
{
name: '非行政执法类别数量及占比',
type: 'pie',
color: ['#7bb06c', '#489d61', '#4e97a6', '#3953d4', '#4e3ec5', '#6f2c9b', '#9d7173', '#887456', '#9eac71', '#8e5aac'],
radius: [52, 72],
center: ['50%', '40%'],
padAngle: 2,
itemStyle: { borderRadius: 1 },
label: {
show: true,
@ -92,8 +101,8 @@ export function buildNonAdminPieOption(data: { value: number; name: string }[]):
d: { color: '#3aaefb', fontSize: 12 }
}
},
labelLine: { show: true, length: 20, length2: 5, smooth: 0 },
data
labelLine: { show: true, length: 20, length2: 5, smooth: 0 ,maxSurfaceAngle: 80 },
data: coloredData
}
]
};

Loading…
Cancel
Save