227 lines
8.4 KiB
Vue
227 lines
8.4 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue';
|
|
import Highcharts from 'highcharts';
|
|
import { DataAnomaly } from '~/types/ArticleMetaData';
|
|
import 'overlayscrollbars/overlayscrollbars.css';
|
|
import useColorModeStore from '~/stores/colorModeStore';
|
|
import useIconStore from '~/stores/iconStore';
|
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue';
|
|
import type { OverflowBehavior, ScrollbarsAutoHideBehavior, ScrollbarsVisibilityBehavior } from 'overlayscrollbars';
|
|
|
|
const props = withDefaults(defineProps<{
|
|
techStack?: string[] | DataAnomaly;
|
|
techStackPercent?: number[] | DataAnomaly;
|
|
techStackIconNames?: string[] | DataAnomaly;
|
|
techStackThemeColors?: string[] | DataAnomaly;
|
|
asyncKey: string;
|
|
}>(), {
|
|
techStack: () => ['Vue', 'Nuxt', 'TypeScript', 'JavaScript', 'CSS', 'HTML', 'Node.js', 'Python', 'Java', 'C#'],
|
|
techStackPercent: () => [5, 3, 4, 2, 1, 1, 2, 3, 2, 1],
|
|
techStackIconNames: () => ['mdi:vuejs', 'lineicons:nuxt', 'mdi:language-typescript', 'mdi:language-javascript', 'material-symbols:css', 'mdi:language-html5', 'mdi:nodejs', 'mdi:language-python', 'mdi:language-java', 'mdi:language-csharp'],
|
|
techStackThemeColors: () => ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#FFB347', '#98D8C8', '#F7DC6F', '#BB8FCE'],
|
|
});
|
|
|
|
const chartRef = ref<HTMLDivElement | null>(null);
|
|
const techStackLightIconSVG = ref<string[]>([]);
|
|
const techStackDarkIconSVG = ref<string[]>([]);
|
|
const { colorMode } = storeToRefs(useColorModeStore());
|
|
|
|
const { pending } = useAsyncData(props.asyncKey, async () => {
|
|
if (props.techStackIconNames === DataAnomaly.DataNotFound || props.techStackIconNames === DataAnomaly.Invalid)
|
|
return [];
|
|
const iconStore = useIconStore();
|
|
for (const iconName of props.techStackIconNames) {
|
|
await iconStore.setIconInfo(iconName);
|
|
}
|
|
for (const iconName of props.techStackIconNames) {
|
|
techStackLightIconSVG.value.push(iconStore.getColoredIcon(iconName, '#666'));
|
|
techStackDarkIconSVG.value.push(iconStore.getColoredIcon(iconName, '#aaa'));
|
|
}
|
|
return [];
|
|
});
|
|
|
|
function toPercent(num: number | undefined): string {
|
|
if (num === undefined || isNaN(num)) return '0%';
|
|
num *= 100;
|
|
const rounded = Math.round(num * 100) / 100;
|
|
return (rounded === 0 ? '0' : rounded.toFixed(2).replace(/\.?0+$/, '')) + '%';
|
|
}
|
|
|
|
const noDataAvailable = ref(false);
|
|
const renderChart = () => {
|
|
const techStack = props.techStack as string[];
|
|
const techStackPercent = props.techStackPercent as number[];
|
|
if (!chartRef.value) return;
|
|
const sum = techStackPercent.reduce((acc, val) => acc + val, 0);
|
|
const fullArr: [string, number, string, string, string][] = techStack.map((name, index) => [name, techStackPercent[index] / sum, techStackLightIconSVG.value[index] || '', techStackDarkIconSVG.value[index] || '', props.techStackThemeColors[index]] as [string, number, string, string, string]).sort((a, b) => b[1] - a[1]);
|
|
const dataArr: [string, number][] = fullArr.map((x) => [x[0], x[1]]);
|
|
const barHeight = 20;
|
|
const gap = 10;
|
|
const axisColor = '#aaa';
|
|
chartRef.value.style.height = `${dataArr.length * (barHeight + gap) + 25}px`;
|
|
Highcharts.chart(chartRef.value, {
|
|
chart: {
|
|
type: 'bar',
|
|
backgroundColor: 'transparent',
|
|
},
|
|
credits: {
|
|
enabled: false,
|
|
},
|
|
title: {
|
|
text: undefined,
|
|
},
|
|
legend: {
|
|
enabled: false,
|
|
},
|
|
|
|
xAxis: {
|
|
categories: dataArr.map(([name]) => name),
|
|
title: {
|
|
text: undefined,
|
|
},
|
|
labels: {
|
|
useHTML: true,
|
|
formatter: function () {
|
|
return `<div style="width: 25px; height: 25px;" title="${fullArr[this.pos][0]}">
|
|
${colorMode.value === 'light' ? fullArr[this.pos][2] : fullArr[this.pos][3]}
|
|
</div>`;
|
|
},
|
|
},
|
|
lineColor: axisColor, // 设置 x 轴线颜色
|
|
tickColor: axisColor, // 设置 x 轴刻度颜色
|
|
},
|
|
yAxis: {
|
|
min: 0,
|
|
title: {
|
|
text: undefined, // 修复 null 类型错误
|
|
},
|
|
labels: {
|
|
enabled: false, // 关闭 y 轴标签显示
|
|
},
|
|
gridLineWidth: 0, // 关闭网格线
|
|
lineWidth: 0, // 关闭 y 轴线
|
|
tickWidth: 0, // 关闭刻度线
|
|
},
|
|
series: [
|
|
{
|
|
name: 'Tech Stack',
|
|
data: dataArr.map(([, value]) => value),
|
|
type: 'bar',
|
|
colorByPoint: true,
|
|
colors: fullArr.map((x) => x[4]),
|
|
},
|
|
],
|
|
tooltip: {
|
|
formatter: function () {
|
|
return `${fullArr[this.x][0]} ${toPercent(this.y)}`;
|
|
},
|
|
},
|
|
plotOptions: {
|
|
bar: {
|
|
borderRadius: 5,
|
|
pointWidth: 20,
|
|
groupPadding: 0.1,
|
|
pointPadding: 0.05,
|
|
borderWidth: 0,
|
|
dataLabels: {
|
|
enabled: true,
|
|
style: {
|
|
color: '#fff',
|
|
},
|
|
formatter: function () {
|
|
return toPercent(this.y); // 自定义条形图值的显示格式
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, () => {
|
|
});
|
|
};
|
|
|
|
let colorModeCallBackKey = '';
|
|
onMounted(() => {
|
|
if (props.techStack === DataAnomaly.DataNotFound || props.techStack === DataAnomaly.Invalid
|
|
|| props.techStackPercent === DataAnomaly.DataNotFound || props.techStackPercent === DataAnomaly.Invalid
|
|
|| props.techStackThemeColors === DataAnomaly.DataNotFound || props.techStackThemeColors === DataAnomaly.Invalid
|
|
|| props.techStack.length === 0 || props.techStackPercent.length === 0 || props.techStackThemeColors.length === 0) {
|
|
noDataAvailable.value = true;
|
|
return;
|
|
}
|
|
window.addEventListener('resize', renderChart);
|
|
colorModeCallBackKey = useColorModeStore().registerCallBack(renderChart);
|
|
watch(pending, (isPending) => {
|
|
if (!isPending) {
|
|
// 数据加载完成后的回调逻辑
|
|
renderChart();
|
|
}
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', renderChart);
|
|
useColorModeStore().unregisterCallBack(colorModeCallBackKey);
|
|
});
|
|
|
|
const scrollbarOptions = {
|
|
scrollbars: {
|
|
theme: 'os-theme-thin-dark',
|
|
visibility: 'auto' as ScrollbarsVisibilityBehavior,
|
|
autoHide: 'move' as ScrollbarsAutoHideBehavior,
|
|
autoHideDelay: 1300,
|
|
dragScroll: true,
|
|
clickScroll: true,
|
|
},
|
|
overflow: {
|
|
x: 'hidden' as OverflowBehavior,
|
|
y: 'scroll' as OverflowBehavior,
|
|
},
|
|
};
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div class="h-full">
|
|
<div v-if="noDataAvailable" class="flex items-center justify-center h-full p-8">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 112.01">
|
|
<g id="_图层_1" data-name="图层 1">
|
|
<polyline class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" points="1.5 28.63 1.5 1.5 63.5 1.59"/>
|
|
<polyline class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" points="254.5 83.38 254.5 110.5 192.5 110.41"/>
|
|
<polyline class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10] stroke-3" points="254.5 28.63 254.5 1.5 192.5 1.59"/>
|
|
<polyline class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10] stroke-3" points="1.5 83.38 1.5 110.5 63.5 110.41"/>
|
|
</g>
|
|
<g id="_图层_2" data-name="图层 2">
|
|
<text class="[font-family:ArialMT,Arial] text-xl stroke-old-neutral-600 dark:stroke-old-neutral-400 fill-old-neutral-600 dark:fill-old-neutral-400" transform="translate(89.48 61.61)"><tspan x="0" y="0">No Data</tspan></text>
|
|
</g>
|
|
<g id="_图层_3" data-name="图层 3">
|
|
<line class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" x1="16" y1="11" x2="90" y2="39.8"/>
|
|
<line class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" x1="166" y1="72.21" x2="240" y2="101"/>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
<overlay-scrollbars-component v-else class="max-h-full" :options="scrollbarOptions">
|
|
<div ref="chartRef" class="w-full"/>
|
|
</overlay-scrollbars-component>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
|
|
:deep(.os-scrollbar) {
|
|
--os-size: 4px;
|
|
--os-padding-perpendicular: 0px;
|
|
--os-padding-axis: 0px;
|
|
--os-track-border-radius: 2px;
|
|
--os-handle-border-radius: 2px;
|
|
--os-handle-bg: rgba(156, 163, 175, 0.5);
|
|
--os-handle-bg-hover: rgba(156, 163, 175, 0.8);
|
|
--os-handle-bg-active: rgba(107, 114, 128, 1);
|
|
}
|
|
|
|
/* 暗色模式 */
|
|
.dark :deep(.os-scrollbar) {
|
|
--os-handle-bg: rgba(75, 85, 99, 0.5);
|
|
--os-handle-bg-hover: rgba(75, 85, 99, 0.8);
|
|
--os-handle-bg-active: rgba(107, 114, 128, 1);
|
|
}
|
|
</style>
|