mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-29 14:46:41 +08:00
feat: add vchart hook
This commit is contained in:
parent
2486d9d296
commit
0e105f32e4
@ -59,6 +59,7 @@
|
|||||||
"@sa/materials": "workspace:*",
|
"@sa/materials": "workspace:*",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"@visactor/vchart": "1.12.8",
|
"@visactor/vchart": "1.12.8",
|
||||||
|
"@visactor/vchart-theme": "1.12.1",
|
||||||
"@visactor/vue-vtable": "1.10.1",
|
"@visactor/vue-vtable": "1.10.1",
|
||||||
"@vueuse/components": "^11.1.0",
|
"@vueuse/components": "^11.1.0",
|
||||||
"@vueuse/core": "11.1.0",
|
"@vueuse/core": "11.1.0",
|
||||||
|
@ -44,6 +44,9 @@ importers:
|
|||||||
'@visactor/vchart':
|
'@visactor/vchart':
|
||||||
specifier: 1.12.8
|
specifier: 1.12.8
|
||||||
version: 1.12.8
|
version: 1.12.8
|
||||||
|
'@visactor/vchart-theme':
|
||||||
|
specifier: 1.12.1
|
||||||
|
version: 1.12.1(@visactor/vchart@1.12.8)
|
||||||
'@visactor/vue-vtable':
|
'@visactor/vue-vtable':
|
||||||
specifier: 1.10.1
|
specifier: 1.10.1
|
||||||
version: 1.10.1
|
version: 1.10.1
|
||||||
@ -2614,6 +2617,14 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@visactor/vchart-theme@1.12.1(@visactor/vchart@1.12.8):
|
||||||
|
resolution: {integrity: sha512-8994bN0+dnr/hrWH5nIBCFCl29MI7T2O5Q8/ZHDJKtgtt9zjzern11wAmP1sUPooZwT59feveGB3iWEonLCVuQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@visactor/vchart': '>=1.10.4'
|
||||||
|
dependencies:
|
||||||
|
'@visactor/vchart': 1.12.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@visactor/vchart@1.12.8:
|
/@visactor/vchart@1.12.8:
|
||||||
resolution: {integrity: sha512-QG8KY/KMcr78AL1DPOMNBVi1pDK2AyRlaP7uQueu0vgBmqCbJfFNsAy7RUSNvufw3cDk85wfBqmsckqOiEeiow==}
|
resolution: {integrity: sha512-QG8KY/KMcr78AL1DPOMNBVi1pDK2AyRlaP7uQueu0vgBmqCbJfFNsAy7RUSNvufw3cDk85wfBqmsckqOiEeiow==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -0,0 +1,157 @@
|
|||||||
|
import { computed, effectScope, onScopeDispose, ref, watch } from 'vue';
|
||||||
|
import VChart, { registerLiquidChart } from '@visactor/vchart';
|
||||||
|
import type { ISpec } from '@visactor/vchart';
|
||||||
|
import light from '@visactor/vchart-theme/public/light.json';
|
||||||
|
import dark from '@visactor/vchart-theme/public/dark.json';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
registerLiquidChart();
|
||||||
|
|
||||||
|
// register the theme
|
||||||
|
VChart.ThemeManager.registerTheme('light', light as any);
|
||||||
|
VChart.ThemeManager.registerTheme('dark', dark as any);
|
||||||
|
|
||||||
|
interface ChartHooks {
|
||||||
|
onRender?: (chart: VChart) => void | Promise<void>;
|
||||||
|
onUpdated?: (chart: VChart) => void | Promise<void>;
|
||||||
|
onDestroy?: (chart: VChart) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVChart<T extends ISpec>(specFactory: () => T, hooks: ChartHooks = {}) {
|
||||||
|
const scope = effectScope();
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
const darkMode = computed(() => themeStore.darkMode);
|
||||||
|
|
||||||
|
const domRef = ref<HTMLElement | null>(null);
|
||||||
|
const initialSize = { width: 0, height: 0 };
|
||||||
|
const { width, height } = useElementSize(domRef, initialSize);
|
||||||
|
|
||||||
|
let chart: VChart | null = null;
|
||||||
|
const spec: T = specFactory();
|
||||||
|
|
||||||
|
const { onRender, onUpdated, onDestroy } = hooks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether can render chart
|
||||||
|
*
|
||||||
|
* when domRef is ready and initialSize is valid
|
||||||
|
*/
|
||||||
|
function canRender() {
|
||||||
|
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** is chart rendered */
|
||||||
|
function isRendered() {
|
||||||
|
return Boolean(domRef.value && chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update chart spec
|
||||||
|
*
|
||||||
|
* @param callback callback function
|
||||||
|
*/
|
||||||
|
async function updateSpec(callback: (opts: T, optsFactory: () => T) => ISpec = () => spec) {
|
||||||
|
if (!isRendered()) return;
|
||||||
|
|
||||||
|
const updatedOpts = callback(spec, specFactory);
|
||||||
|
|
||||||
|
Object.assign(spec, updatedOpts);
|
||||||
|
|
||||||
|
if (isRendered()) {
|
||||||
|
chart?.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
chart?.updateSpec({ ...updatedOpts }, true);
|
||||||
|
|
||||||
|
await onUpdated?.(chart!);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSpec(newSpec: T) {
|
||||||
|
chart?.updateSpec(newSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** render chart */
|
||||||
|
async function render() {
|
||||||
|
if (!isRendered()) {
|
||||||
|
// apply the theme
|
||||||
|
if (darkMode.value) {
|
||||||
|
VChart.ThemeManager.setCurrentTheme('dark');
|
||||||
|
} else {
|
||||||
|
VChart.ThemeManager.setCurrentTheme('light');
|
||||||
|
}
|
||||||
|
|
||||||
|
chart = new VChart(spec, { dom: domRef.value as HTMLElement });
|
||||||
|
chart.renderSync();
|
||||||
|
|
||||||
|
await onRender?.(chart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** resize chart */
|
||||||
|
function resize() {
|
||||||
|
// chart?.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** destroy chart */
|
||||||
|
async function destroy() {
|
||||||
|
if (!chart) return;
|
||||||
|
|
||||||
|
await onDestroy?.(chart);
|
||||||
|
chart?.release();
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** change chart theme */
|
||||||
|
async function changeTheme() {
|
||||||
|
await destroy();
|
||||||
|
await render();
|
||||||
|
await onUpdated?.(chart!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* render chart by size
|
||||||
|
*
|
||||||
|
* @param w width
|
||||||
|
* @param h height
|
||||||
|
*/
|
||||||
|
async function renderChartBySize(w: number, h: number) {
|
||||||
|
initialSize.width = w;
|
||||||
|
initialSize.height = h;
|
||||||
|
|
||||||
|
// size is abnormal, destroy chart
|
||||||
|
if (!canRender()) {
|
||||||
|
await destroy();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resize chart
|
||||||
|
if (isRendered()) {
|
||||||
|
resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// render chart
|
||||||
|
await render();
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
watch([width, height], ([newWidth, newHeight]) => {
|
||||||
|
renderChartBySize(newWidth, newHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(darkMode, () => {
|
||||||
|
changeTheme();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onScopeDispose(() => {
|
||||||
|
destroy();
|
||||||
|
scope.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
domRef,
|
||||||
|
updateSpec,
|
||||||
|
setSpec
|
||||||
|
};
|
||||||
|
}
|
861
src/views/plugin/charts/vchart/data.ts
Normal file
861
src/views/plugin/charts/vchart/data.ts
Normal file
@ -0,0 +1,861 @@
|
|||||||
|
export const shapeWordCloudSpec = {
|
||||||
|
type: 'wordCloud',
|
||||||
|
maskShape: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/log.jpeg',
|
||||||
|
nameField: 'challenge_name',
|
||||||
|
valueField: 'sum_count',
|
||||||
|
seriesField: 'challenge_name',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: 'data',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
challenge_name: '刘浩存',
|
||||||
|
sum_count: 957
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '刘昊然',
|
||||||
|
sum_count: 942
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '喜欢',
|
||||||
|
sum_count: 842
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '真的',
|
||||||
|
sum_count: 828
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '四海',
|
||||||
|
sum_count: 665
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '好看',
|
||||||
|
sum_count: 627
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '评论',
|
||||||
|
sum_count: 574
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '好像',
|
||||||
|
sum_count: 564
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '沈腾',
|
||||||
|
sum_count: 554
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '不像',
|
||||||
|
sum_count: 540
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '多少钱',
|
||||||
|
sum_count: 513
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '韩寒',
|
||||||
|
sum_count: 513
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '不知道',
|
||||||
|
sum_count: 499
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '感觉',
|
||||||
|
sum_count: 499
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '尹正',
|
||||||
|
sum_count: 495
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '不看',
|
||||||
|
sum_count: 487
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '奥特之父',
|
||||||
|
sum_count: 484
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '阿姨',
|
||||||
|
sum_count: 482
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '支持',
|
||||||
|
sum_count: 482
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '父母',
|
||||||
|
sum_count: 479
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '一条',
|
||||||
|
sum_count: 462
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '女主',
|
||||||
|
sum_count: 456
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '确实',
|
||||||
|
sum_count: 456
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '票房',
|
||||||
|
sum_count: 456
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '无语',
|
||||||
|
sum_count: 443
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '干干净净',
|
||||||
|
sum_count: 443
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '为啥',
|
||||||
|
sum_count: 426
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '爱情',
|
||||||
|
sum_count: 425
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '喜剧',
|
||||||
|
sum_count: 422
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '春节',
|
||||||
|
sum_count: 414
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '剧情',
|
||||||
|
sum_count: 414
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '人生',
|
||||||
|
sum_count: 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '风格',
|
||||||
|
sum_count: 408
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '演员',
|
||||||
|
sum_count: 403
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '成长',
|
||||||
|
sum_count: 403
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '玩意',
|
||||||
|
sum_count: 402
|
||||||
|
},
|
||||||
|
{
|
||||||
|
challenge_name: '文学',
|
||||||
|
sum_count: 397
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const circularProgressTickSpec = {
|
||||||
|
type: 'circularProgress',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'id0',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
type: 'Tradition Industries',
|
||||||
|
value: 0.795,
|
||||||
|
text: '79.5%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Business Companies',
|
||||||
|
value: 0.5,
|
||||||
|
text: '50%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Customer-facing Companies',
|
||||||
|
value: 0.25,
|
||||||
|
text: '25%'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
color: ['rgb(255, 222, 0)', 'rgb(171, 205, 5)', 'rgb(0, 154, 68)'],
|
||||||
|
valueField: 'value',
|
||||||
|
categoryField: 'type',
|
||||||
|
seriesField: 'type',
|
||||||
|
radius: 0.8,
|
||||||
|
innerRadius: 0.4,
|
||||||
|
tickMask: {
|
||||||
|
visible: true,
|
||||||
|
angle: 10,
|
||||||
|
offsetAngle: 0,
|
||||||
|
forceAlign: true,
|
||||||
|
style: {
|
||||||
|
cornerRadius: 15
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
visible: false,
|
||||||
|
type: 'linear',
|
||||||
|
orient: 'angle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
visible: false,
|
||||||
|
type: 'band',
|
||||||
|
orient: 'radius'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
indicator: {
|
||||||
|
visible: true,
|
||||||
|
trigger: 'hover',
|
||||||
|
title: {
|
||||||
|
visible: true,
|
||||||
|
field: 'type',
|
||||||
|
autoLimit: true,
|
||||||
|
style: {
|
||||||
|
fontSize: 20,
|
||||||
|
fill: 'black'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
visible: true,
|
||||||
|
field: 'text',
|
||||||
|
style: {
|
||||||
|
fontSize: 16,
|
||||||
|
fill: 'gray'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
legends: {
|
||||||
|
visible: true,
|
||||||
|
orient: 'bottom',
|
||||||
|
title: {
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const liquidChartSmartInvertSpec = {
|
||||||
|
type: 'liquid',
|
||||||
|
valueField: 'value',
|
||||||
|
data: {
|
||||||
|
id: 'data',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
value: 0.8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
maskShape: 'drop', // 水滴
|
||||||
|
// maskShape: 'circle',
|
||||||
|
// maskShape: 'star',
|
||||||
|
indicatorSmartInvert: true,
|
||||||
|
indicator: {
|
||||||
|
visible: true,
|
||||||
|
title: {
|
||||||
|
visible: true,
|
||||||
|
style: {
|
||||||
|
text: '进度'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
visible: true,
|
||||||
|
style: {
|
||||||
|
fill: 'black',
|
||||||
|
text: '80%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
liquidBackground: {
|
||||||
|
style: {
|
||||||
|
fill: 'blue'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goldenMedals: Record<number, any[]> = {
|
||||||
|
2000: [
|
||||||
|
{ country: 'USA', value: 37 },
|
||||||
|
{ country: 'Russia', value: 32 },
|
||||||
|
{ country: 'China', value: 28 },
|
||||||
|
{ country: 'Australia', value: 16 },
|
||||||
|
{ country: 'Germany', value: 13 },
|
||||||
|
{ country: 'France', value: 13 },
|
||||||
|
{ country: 'Italy', value: 13 },
|
||||||
|
{ country: 'Netherlands', value: 12 },
|
||||||
|
{ country: 'Cuba', value: 11 },
|
||||||
|
{ country: 'U.K.', value: 11 }
|
||||||
|
],
|
||||||
|
2004: [
|
||||||
|
{ country: 'USA', value: 36 },
|
||||||
|
{ country: 'China', value: 32 },
|
||||||
|
{ country: 'Russia', value: 28 },
|
||||||
|
{ country: 'Australia', value: 17 },
|
||||||
|
{ country: 'Japan', value: 16 },
|
||||||
|
{ country: 'Germany', value: 13 },
|
||||||
|
{ country: 'France', value: 11 },
|
||||||
|
{ country: 'Italy', value: 10 },
|
||||||
|
{ country: 'South Korea', value: 9 },
|
||||||
|
{ country: 'U.K.', value: 9 }
|
||||||
|
],
|
||||||
|
2008: [
|
||||||
|
{ country: 'China', value: 48 },
|
||||||
|
{ country: 'USA', value: 36 },
|
||||||
|
{ country: 'Russia', value: 24 },
|
||||||
|
{ country: 'U.K.', value: 19 },
|
||||||
|
{ country: 'Germany', value: 16 },
|
||||||
|
{ country: 'Australia', value: 14 },
|
||||||
|
{ country: 'South Korea', value: 13 },
|
||||||
|
{ country: 'Japan', value: 9 },
|
||||||
|
{ country: 'Italy', value: 8 },
|
||||||
|
{ country: 'France', value: 7 }
|
||||||
|
],
|
||||||
|
2012: [
|
||||||
|
{ country: 'USA', value: 46 },
|
||||||
|
{ country: 'China', value: 39 },
|
||||||
|
{ country: 'U.K.', value: 29 },
|
||||||
|
{ country: 'Russia', value: 19 },
|
||||||
|
{ country: 'South Korea', value: 13 },
|
||||||
|
{ country: 'Germany', value: 11 },
|
||||||
|
{ country: 'France', value: 11 },
|
||||||
|
{ country: 'Australia', value: 8 },
|
||||||
|
{ country: 'Italy', value: 8 },
|
||||||
|
{ country: 'Hungary', value: 8 }
|
||||||
|
],
|
||||||
|
2016: [
|
||||||
|
{ country: 'USA', value: 46 },
|
||||||
|
{ country: 'U.K.', value: 27 },
|
||||||
|
{ country: 'China', value: 26 },
|
||||||
|
{ country: 'Russia', value: 19 },
|
||||||
|
{ country: 'Germany', value: 17 },
|
||||||
|
{ country: 'Japan', value: 12 },
|
||||||
|
{ country: 'France', value: 10 },
|
||||||
|
{ country: 'South Korea', value: 9 },
|
||||||
|
{ country: 'Italy', value: 8 },
|
||||||
|
{ country: 'Australia', value: 8 }
|
||||||
|
],
|
||||||
|
2020: [
|
||||||
|
{ country: 'USA', value: 39 },
|
||||||
|
{ country: 'China', value: 38 },
|
||||||
|
{ country: 'Japan', value: 27 },
|
||||||
|
{ country: 'U.K.', value: 22 },
|
||||||
|
{ country: 'Russian Olympic Committee', value: 20 },
|
||||||
|
{ country: 'Australia', value: 17 },
|
||||||
|
{ country: 'Netherlands', value: 10 },
|
||||||
|
{ country: 'France', value: 10 },
|
||||||
|
{ country: 'Germany', value: 10 },
|
||||||
|
{ country: 'Italy', value: 10 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
China: '#d62728',
|
||||||
|
USA: '#1664FF',
|
||||||
|
Russia: '#B2CFFF',
|
||||||
|
'U.K.': '#1AC6FF',
|
||||||
|
Australia: '#94EFFF',
|
||||||
|
Japan: '#FF8A00',
|
||||||
|
Cuba: '#FFCE7A',
|
||||||
|
Germany: '#3CC780',
|
||||||
|
France: '#B9EDCD',
|
||||||
|
Italy: '#7442D4',
|
||||||
|
'South Korea': '#DDC5FA',
|
||||||
|
'Russian Olympic Committee': '#B2CFFF',
|
||||||
|
Netherlands: '#FFC400',
|
||||||
|
Hungary: '#FAE878'
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataSpecs = Object.keys(goldenMedals).map(year => {
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
values: (goldenMedals[year as unknown as number] as any)
|
||||||
|
.sort((a: any, b: any) => b.value - a.value)
|
||||||
|
.map((v: any) => {
|
||||||
|
return { ...v, fill: (colors as any)[v.country] };
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'year',
|
||||||
|
values: [{ year }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const duration = 1000;
|
||||||
|
const exchangeDuration = 600;
|
||||||
|
|
||||||
|
export const rankingBarSpec = {
|
||||||
|
type: 'bar',
|
||||||
|
padding: {
|
||||||
|
top: 12,
|
||||||
|
right: 100,
|
||||||
|
bottom: 12
|
||||||
|
},
|
||||||
|
data: dataSpecs[0].data,
|
||||||
|
direction: 'horizontal',
|
||||||
|
yField: 'country',
|
||||||
|
xField: 'value',
|
||||||
|
seriesField: 'country',
|
||||||
|
bar: {
|
||||||
|
style: {
|
||||||
|
fill: (datum: any) => datum.fill
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
animation: true,
|
||||||
|
orient: 'bottom',
|
||||||
|
type: 'linear',
|
||||||
|
visible: true,
|
||||||
|
max: 50,
|
||||||
|
grid: {
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
animation: true,
|
||||||
|
id: 'axis-left',
|
||||||
|
orient: 'left',
|
||||||
|
width: 130,
|
||||||
|
tick: { visible: false },
|
||||||
|
label: { visible: true },
|
||||||
|
type: 'band'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: {
|
||||||
|
visible: true,
|
||||||
|
text: 'Top 10 Olympic Gold Medals by Country Since 2000'
|
||||||
|
},
|
||||||
|
animationUpdate: {
|
||||||
|
bar: [
|
||||||
|
{
|
||||||
|
type: 'update',
|
||||||
|
options: { excludeChannels: ['y'] },
|
||||||
|
easing: 'linear',
|
||||||
|
duration
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: ['y'],
|
||||||
|
easing: 'circInOut',
|
||||||
|
duration: exchangeDuration
|
||||||
|
}
|
||||||
|
],
|
||||||
|
axis: {
|
||||||
|
duration: exchangeDuration,
|
||||||
|
easing: 'circInOut'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animationEnter: {
|
||||||
|
bar: [
|
||||||
|
{
|
||||||
|
type: 'moveIn',
|
||||||
|
duration: exchangeDuration,
|
||||||
|
easing: 'circInOut',
|
||||||
|
options: {
|
||||||
|
direction: 'y',
|
||||||
|
orient: 'negative'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
animationExit: {
|
||||||
|
bar: [
|
||||||
|
{
|
||||||
|
type: 'fadeOut',
|
||||||
|
duration: exchangeDuration
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
customMark: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
dataId: 'year',
|
||||||
|
style: {
|
||||||
|
textBaseline: 'bottom',
|
||||||
|
fontSize: 200,
|
||||||
|
textAlign: 'right',
|
||||||
|
fontFamily: 'PingFang SC',
|
||||||
|
fontWeight: 600,
|
||||||
|
text: (datum: any) => datum.year,
|
||||||
|
x: (_datum: any, ctx: any) => {
|
||||||
|
return ctx.vchart.getChart().getCanvasRect()?.width - 50;
|
||||||
|
},
|
||||||
|
y: (_datum: any, ctx: any) => {
|
||||||
|
return ctx.vchart.getChart().getCanvasRect()?.height - 50;
|
||||||
|
},
|
||||||
|
fill: 'grey',
|
||||||
|
fillOpacity: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
player: {
|
||||||
|
type: 'continuous',
|
||||||
|
orient: 'bottom',
|
||||||
|
auto: true,
|
||||||
|
loop: true,
|
||||||
|
dx: 80,
|
||||||
|
position: 'middle',
|
||||||
|
interval: duration,
|
||||||
|
specs: dataSpecs,
|
||||||
|
slider: {
|
||||||
|
railStyle: {
|
||||||
|
height: 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: {
|
||||||
|
backward: {
|
||||||
|
style: {
|
||||||
|
size: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
forward: {
|
||||||
|
style: {
|
||||||
|
size: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
order: 1,
|
||||||
|
position: 'end'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stackedDashAreaSpec = {
|
||||||
|
type: 'area',
|
||||||
|
data: {
|
||||||
|
values: [
|
||||||
|
{ month: 'Jan', country: 'Africa', value: 4229 },
|
||||||
|
{ month: 'Jan', country: 'EU', value: 4376 },
|
||||||
|
{ month: 'Jan', country: 'China', value: 3054 },
|
||||||
|
{ month: 'Jan', country: 'USA', value: 12814 },
|
||||||
|
{ month: 'Feb', country: 'Africa', value: 3932 },
|
||||||
|
{ month: 'Feb', country: 'EU', value: 3987 },
|
||||||
|
{ month: 'Feb', country: 'China', value: 5067 },
|
||||||
|
{ month: 'Feb', country: 'USA', value: 13012 },
|
||||||
|
{ month: 'Mar', country: 'Africa', value: 5221 },
|
||||||
|
{ month: 'Mar', country: 'EU', value: 3574 },
|
||||||
|
{ month: 'Mar', country: 'China', value: 7004 },
|
||||||
|
{ month: 'Mar', country: 'USA', value: 11624 },
|
||||||
|
{ month: 'Apr', country: 'Africa', value: 9256 },
|
||||||
|
{ month: 'Apr', country: 'EU', value: 4376 },
|
||||||
|
{ month: 'Apr', country: 'China', value: 9054 },
|
||||||
|
{ month: 'Apr', country: 'USA', value: 8814 },
|
||||||
|
{ month: 'May', country: 'Africa', value: 3308 },
|
||||||
|
{ month: 'May', country: 'EU', value: 4572 },
|
||||||
|
{ month: 'May', country: 'China', value: 12043 },
|
||||||
|
{ month: 'May', country: 'USA', value: 12998 },
|
||||||
|
{ month: 'Jun', country: 'Africa', value: 5432 },
|
||||||
|
{ month: 'Jun', country: 'EU', value: 3417 },
|
||||||
|
{ month: 'Jun', country: 'China', value: 15067 },
|
||||||
|
{ month: 'Jun', country: 'USA', value: 12321 },
|
||||||
|
{ month: 'Jul', country: 'Africa', value: 13701 },
|
||||||
|
{ month: 'Jul', country: 'EU', value: 5231 },
|
||||||
|
{ month: 'Jul', country: 'China', value: 10119 },
|
||||||
|
{ month: 'Jul', country: 'USA', value: 10342 },
|
||||||
|
{ month: 'Aug', country: 'Africa', value: 4008, forecast: true },
|
||||||
|
{ month: 'Aug', country: 'EU', value: 4572, forecast: true },
|
||||||
|
{ month: 'Aug', country: 'China', value: 12043, forecast: true },
|
||||||
|
{ month: 'Aug', country: 'USA', value: 22998, forecast: true },
|
||||||
|
{ month: 'Sept', country: 'Africa', value: 18712, forecast: true },
|
||||||
|
{ month: 'Sept', country: 'EU', value: 6134, forecast: true },
|
||||||
|
{ month: 'Sept', country: 'China', value: 10419, forecast: true },
|
||||||
|
{ month: 'Sept', country: 'USA', value: 11261, forecast: true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
stack: true,
|
||||||
|
xField: 'month',
|
||||||
|
yField: 'value',
|
||||||
|
seriesField: 'country',
|
||||||
|
point: {
|
||||||
|
style: {
|
||||||
|
size: 0
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
dimension_hover: {
|
||||||
|
size: 10,
|
||||||
|
outerBorder: {
|
||||||
|
distance: 0,
|
||||||
|
lineWidth: 6,
|
||||||
|
strokeOpacity: 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
style: {
|
||||||
|
// Configure the lineDash attribute based on the forecast field value of the data
|
||||||
|
lineDash: (data: any) => {
|
||||||
|
if (data.forecast) {
|
||||||
|
return [5, 5];
|
||||||
|
}
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
style: {
|
||||||
|
fillOpacity: 0.5,
|
||||||
|
textureColor: '#fff',
|
||||||
|
textureSize: 14,
|
||||||
|
// Configure the texture attribute based on the forecast field value of the data
|
||||||
|
texture: (data: any) => {
|
||||||
|
if (data.forecast) {
|
||||||
|
return 'bias-rl';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legends: [{ visible: true, position: 'middle', orient: 'bottom' }],
|
||||||
|
crosshair: {
|
||||||
|
xField: {
|
||||||
|
visible: true,
|
||||||
|
line: {
|
||||||
|
type: 'line'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const barMarkPointSpec = {
|
||||||
|
type: 'bar',
|
||||||
|
height: 300,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'barData',
|
||||||
|
values: [
|
||||||
|
{ time: '10:20', cost: 2 },
|
||||||
|
{ time: '10:30', cost: 1 },
|
||||||
|
{ time: '10:40', cost: 1 },
|
||||||
|
{ time: '10:50', cost: 2 },
|
||||||
|
{ time: '11:00', cost: 2 },
|
||||||
|
{ time: '11:10', cost: 2 },
|
||||||
|
{ time: '11:20', cost: 1 },
|
||||||
|
{ time: '11:30', cost: 1 },
|
||||||
|
{ time: '11:40', cost: 2 },
|
||||||
|
{ time: '11:50', cost: 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xField: 'time',
|
||||||
|
yField: 'cost',
|
||||||
|
crosshair: {
|
||||||
|
xField: {
|
||||||
|
visible: true,
|
||||||
|
line: {
|
||||||
|
type: 'rect',
|
||||||
|
style: {
|
||||||
|
fill: 'rgb(85,208,93)',
|
||||||
|
fillOpacity: 0.1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bindingAxesIndex: [1],
|
||||||
|
defaultSelect: {
|
||||||
|
axisIndex: 1,
|
||||||
|
datum: '10:20'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
visible: true,
|
||||||
|
animation: false,
|
||||||
|
formatMethod: (datum: any) => `${datum}分钟`,
|
||||||
|
style: {
|
||||||
|
fill: 'rgb(155,155,155)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
style: {
|
||||||
|
fill: 'rgb(85,208,93)',
|
||||||
|
cornerRadius: [4, 4, 0, 0],
|
||||||
|
width: 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
markPoint: [
|
||||||
|
{
|
||||||
|
coordinate: {
|
||||||
|
time: '10:20',
|
||||||
|
cost: 2
|
||||||
|
},
|
||||||
|
itemContent: {
|
||||||
|
type: 'text',
|
||||||
|
autoRotate: false,
|
||||||
|
offsetY: -10,
|
||||||
|
text: {
|
||||||
|
dy: 14,
|
||||||
|
text: '2分钟',
|
||||||
|
style: {
|
||||||
|
fill: 'white',
|
||||||
|
fontSize: 14
|
||||||
|
},
|
||||||
|
labelBackground: {
|
||||||
|
padding: [5, 10, 5, 10],
|
||||||
|
style: {
|
||||||
|
fill: '#000',
|
||||||
|
cornerRadius: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemLine: {
|
||||||
|
endSymbol: {
|
||||||
|
visible: true,
|
||||||
|
style: {
|
||||||
|
angle: Math.PI,
|
||||||
|
scaleY: 0.4,
|
||||||
|
fill: '#000',
|
||||||
|
dy: 4,
|
||||||
|
stroke: '#000'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startSymbol: { visible: false },
|
||||||
|
line: {
|
||||||
|
style: {
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
animationUpdate: false,
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
orient: 'left',
|
||||||
|
max: 10,
|
||||||
|
label: { visible: false },
|
||||||
|
grid: {
|
||||||
|
style: { lineDash: [4, 4] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orient: 'bottom',
|
||||||
|
label: {
|
||||||
|
formatMethod: (datum: any) => {
|
||||||
|
return datum === '10:20' ? '当前' : datum;
|
||||||
|
},
|
||||||
|
style: (datum: any) => {
|
||||||
|
return {
|
||||||
|
fontSize: datum === '10:20' ? 14 : 12,
|
||||||
|
fill: datum === '10:20' ? 'black' : 'grey'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
paddingOuter: 0.5,
|
||||||
|
paddingInner: 0,
|
||||||
|
grid: {
|
||||||
|
visible: true,
|
||||||
|
alignWithLabel: false,
|
||||||
|
style: { lineDash: [4, 4] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const histogramDifferentBinSpec = {
|
||||||
|
type: 'histogram',
|
||||||
|
xField: 'from',
|
||||||
|
x2Field: 'to',
|
||||||
|
yField: 'profit',
|
||||||
|
seriesField: 'type',
|
||||||
|
bar: {
|
||||||
|
style: {
|
||||||
|
stroke: 'white',
|
||||||
|
lineWidth: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: 'Profit',
|
||||||
|
textStyle: {
|
||||||
|
align: 'center',
|
||||||
|
height: 50,
|
||||||
|
lineWidth: 3,
|
||||||
|
fill: '#333',
|
||||||
|
fontSize: 25,
|
||||||
|
fontFamily: 'Times New Roman'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
visible: true,
|
||||||
|
mark: {
|
||||||
|
title: {
|
||||||
|
key: 'title',
|
||||||
|
value: 'profit'
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
key: (datum: any) => `${datum.from}~${datum.to}`,
|
||||||
|
value: (datum: any) => datum.profit
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
orient: 'bottom',
|
||||||
|
nice: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: 'data1',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
to: 10,
|
||||||
|
profit: 2,
|
||||||
|
type: 'A'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 10,
|
||||||
|
to: 16,
|
||||||
|
profit: 3,
|
||||||
|
type: 'B'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 16,
|
||||||
|
to: 18,
|
||||||
|
profit: 15,
|
||||||
|
type: 'C'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 18,
|
||||||
|
to: 26,
|
||||||
|
profit: 12,
|
||||||
|
type: 'D'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 26,
|
||||||
|
to: 32,
|
||||||
|
profit: 22,
|
||||||
|
type: 'E'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 32,
|
||||||
|
to: 56,
|
||||||
|
profit: 7,
|
||||||
|
type: 'F'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 56,
|
||||||
|
to: 62,
|
||||||
|
profit: 17,
|
||||||
|
type: 'G'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -1,61 +1,36 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onUnmounted } from 'vue';
|
import { onUnmounted } from 'vue';
|
||||||
// import { useEcharts } from '@/hooks/common/echarts';
|
import type {
|
||||||
// import {
|
IAreaChartSpec,
|
||||||
// barOptions,
|
IBarChartSpec,
|
||||||
// gaugeOptions,
|
ICircularProgressChartSpec,
|
||||||
// getPictorialBarOption,
|
IHistogramChartSpec,
|
||||||
// getScatterOption,
|
ILiquidChartSpec
|
||||||
// lineOptions,
|
} from '@visactor/vchart';
|
||||||
// pieOptions,
|
import { useVChart } from '@/hooks/common/vchart';
|
||||||
// radarOptions
|
import {
|
||||||
// } from './data';
|
barMarkPointSpec,
|
||||||
|
circularProgressTickSpec,
|
||||||
|
histogramDifferentBinSpec,
|
||||||
|
liquidChartSmartInvertSpec,
|
||||||
|
rankingBarSpec,
|
||||||
|
shapeWordCloudSpec,
|
||||||
|
stackedDashAreaSpec
|
||||||
|
} from './data';
|
||||||
|
|
||||||
// const { domRef: pieRef } = useEcharts(() => pieOptions, { onRender() {} });
|
const { domRef: stackedDashAreaRef } = useVChart(() => stackedDashAreaSpec as IAreaChartSpec, { onRender() {} });
|
||||||
// const { domRef: lineRef } = useEcharts(() => lineOptions, { onRender() {} });
|
const { domRef: barMarkPointRef } = useVChart(() => barMarkPointSpec as IBarChartSpec, { onRender() {} });
|
||||||
// const { domRef: barRef } = useEcharts(() => barOptions, { onRender() {} });
|
const { domRef: histogramDifferentBinRef } = useVChart(() => histogramDifferentBinSpec as IHistogramChartSpec, {
|
||||||
// const { domRef: pictorialBarRef } = useEcharts(() => getPictorialBarOption(), { onRender() {} });
|
onRender() {}
|
||||||
// const { domRef: radarRef } = useEcharts(() => radarOptions, { onRender() {} });
|
});
|
||||||
// const { domRef: scatterRef } = useEcharts(() => getScatterOption(), { onRender() {} });
|
const { domRef: rankingBarRef } = useVChart(() => rankingBarSpec as IBarChartSpec, { onRender() {} });
|
||||||
// const { domRef: gaugeRef, setOptions: setGaugeOptions } = useEcharts(() => gaugeOptions, { onRender() {} });
|
const { domRef: shapeWordCloudRef } = useVChart(() => shapeWordCloudSpec, { onRender() {} });
|
||||||
|
const { domRef: circularProgressTickRef } = useVChart(() => circularProgressTickSpec as ICircularProgressChartSpec, {
|
||||||
// let intervalId: NodeJS.Timeout;
|
onRender() {}
|
||||||
|
});
|
||||||
// function initGaugeChart() {
|
const { domRef: liquidChartSmartInvertRef } = useVChart(() => liquidChartSmartInvertSpec as ILiquidChartSpec, {
|
||||||
// intervalId = setInterval(() => {
|
onRender() {}
|
||||||
// const date = new Date();
|
});
|
||||||
// const second = date.getSeconds();
|
|
||||||
// const minute = date.getMinutes() + second / 60;
|
|
||||||
// const hour = (date.getHours() % 12) + minute / 60;
|
|
||||||
|
|
||||||
// setGaugeOptions({
|
|
||||||
// animationDurationUpdate: 300,
|
|
||||||
// series: [
|
|
||||||
// {
|
|
||||||
// name: 'hour',
|
|
||||||
// animation: hour !== 0,
|
|
||||||
// data: [{ value: hour }]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'minute',
|
|
||||||
// animation: minute !== 0,
|
|
||||||
// data: [{ value: minute }]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// animation: second !== 0,
|
|
||||||
// name: 'second',
|
|
||||||
// data: [{ value: second }]
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// });
|
|
||||||
// }, 1000);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function clearGaugeChart() {
|
|
||||||
// clearInterval(intervalId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// initGaugeChart();
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// clearGaugeChart();
|
// clearGaugeChart();
|
||||||
@ -64,7 +39,27 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NSpace vertical :size="16">
|
<NSpace vertical :size="16">
|
||||||
<div>vchart</div>
|
<NCard title="List Chart" :bordered="false" class="h-full card-wrapper">
|
||||||
|
<div ref="stackedDashAreaRef" class="h-400px" />
|
||||||
|
</NCard>
|
||||||
|
<NCard title="List Chart" :bordered="false" class="h-full card-wrapper">
|
||||||
|
<div ref="barMarkPointRef" class="h-400px" />
|
||||||
|
</NCard>
|
||||||
|
<NCard title="List Chart" :bordered="false" class="h-full card-wrapper">
|
||||||
|
<div ref="histogramDifferentBinRef" class="h-400px" />
|
||||||
|
</NCard>
|
||||||
|
<NCard title="List Chart" :bordered="false" class="h-full card-wrapper">
|
||||||
|
<div ref="rankingBarRef" class="h-400px" />
|
||||||
|
</NCard>
|
||||||
|
<NCard title="List Chart" :bordered="false" class="h-full card-wrapper">
|
||||||
|
<div ref="circularProgressTickRef" class="h-400px" />
|
||||||
|
</NCard>
|
||||||
|
<NCard title="List Chart" :bordered="false" class="h-full card-wrapper">
|
||||||
|
<div ref="liquidChartSmartInvertRef" class="h-400px" />
|
||||||
|
</NCard>
|
||||||
|
<NCard title="List Chart" :bordered="false" class="h-full card-wrapper">
|
||||||
|
<div ref="shapeWordCloudRef" class="h-400px" />
|
||||||
|
</NCard>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user