migrate desktop pie chart to composition API and typescript

This commit is contained in:
MaysWind
2025-01-23 00:06:05 +08:00
parent 85557c2879
commit 70428b6c96
+170 -150
View File
@@ -3,126 +3,152 @@
@click="clickItem" @legendselectchanged="onLegendSelectChanged" /> @click="clickItem" @legendselectchanged="onLegendSelectChanged" />
</template> </template>
<script> <script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useTheme } from 'vuetify'; import { useTheme } from 'vuetify';
import { mapStores } from 'pinia'; import type { ECElementEvent } from 'echarts/core';
import { useSettingsStore } from '@/stores/setting.ts'; import type { CallbackDataParams } from 'echarts/types/dist/shared';
import { useUserStore } from '@/stores/user.ts';
import { DEFAULT_ICON_COLOR, DEFAULT_CHART_COLORS } from '@/consts/color.ts'; import { useI18n } from '@/locales/helpers.ts';
import type { ColorValue } from '@/core/color.ts';
import { ThemeType } from '@/core/theme.ts'; import { ThemeType } from '@/core/theme.ts';
import { DEFAULT_ICON_COLOR, DEFAULT_CHART_COLORS } from '@/consts/color.ts';
import { isNumber } from '@/lib/common.ts';
import { formatPercent } from '@/lib/numeral.ts'; import { formatPercent } from '@/lib/numeral.ts';
export default { interface DesktopPieChartDataItem {
props: [ id: string;
'skeleton', name: string;
'items', displayName: string;
'idField', value: number;
'nameField', percent: number;
'valueField', actualPercent: number;
'percentField', itemStyle: {
'colorField', color: ColorValue;
'hiddenField',
'minValidPercent',
'defaultCurrency',
'showValue',
'enableClickItem'
],
emits: [
'click'
],
data() {
return {
selectedLegends: null,
selectedIndex: 0
}; };
}, selected: boolean;
computed: { sourceItem: Record<string, unknown>;
...mapStores(useSettingsStore, useUserStore), displayPercent?: string;
isDarkMode() { displayValue?: string;
return this.globalTheme.global.name.value === ThemeType.Dark; }
},
itemsMap: function () {
const map = {};
for (let i = 0; i < this.items.length; i++) { const props = defineProps<{
const item = this.items[i]; skeleton?: boolean;
items: Record<string, unknown>[];
idField?: string;
nameField: string;
valueField: string;
percentField?: string;
colorField?: string;
hiddenField?: string;
minValidPercent?: number;
defaultCurrency?: string;
showValue?: boolean;
enableClickItem?: boolean;
}>();
const emit = defineEmits<{
(e: 'click', value: Record<string, unknown>): void;
}>();
const theme = useTheme();
const { formatAmountWithCurrency } = useI18n();
const selectedLegends = ref<Record<string, boolean> | null>(null);
const selectedIndex = ref<number>(0);
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
const itemsMap = computed<Record<string, Record<string, unknown>>>(() => {
const map: Record<string, Record<string, unknown>> = {};
for (let i = 0; i < props.items.length; i++) {
const item = props.items[i];
let id = ''; let id = '';
if (this.idField && item[this.idField]) { if (props.idField && item[props.idField]) {
id = item[this.idField]; id = item[props.idField] as string;
} else { } else {
id = item[this.nameField]; id = item[props.nameField] as string;;
} }
map[id] = item; map[id] = item;
} }
return map; return map;
}, });
validItems: function () {
const validItems = computed<DesktopPieChartDataItem[]>(() => {
let totalValidValue = 0; let totalValidValue = 0;
for (let i = 0; i < this.items.length; i++) { for (let i = 0; i < props.items.length; i++) {
const item = this.items[i]; const item = props.items[i];
const value = item[props.valueField];
if (item[this.valueField] && item[this.valueField] > 0 && (!this.hiddenField || !item[this.hiddenField])) { if (isNumber(value) && value > 0 && (!props.hiddenField || !item[props.hiddenField])) {
totalValidValue += item[this.valueField]; totalValidValue += value;
} }
} }
const validItems = []; const validItems: DesktopPieChartDataItem[] = [];
for (let i = 0; i < this.items.length; i++) { for (let i = 0; i < props.items.length; i++) {
const item = this.items[i]; const item = props.items[i];
const value = item[props.valueField];
const percent = props.percentField ? item[props.percentField] : -1;
if (item[this.valueField] && item[this.valueField] > 0 && if (isNumber(value) && value > 0 &&
(!this.hiddenField || !item[this.hiddenField]) && (!props.hiddenField || !item[props.hiddenField]) &&
(!this.minValidPercent || item[this.valueField] / totalValidValue > this.minValidPercent)) { (!props.minValidPercent || value / totalValidValue > props.minValidPercent)) {
const finalItem = { const finalItem: DesktopPieChartDataItem = {
id: (this.idField && item[this.idField]) ? item[this.idField] : item[this.nameField], id: (props.idField && item[props.idField]) ? item[props.idField] as string : item[props.nameField] as string,
name: (this.idField && item[this.idField]) ? item[this.idField] : item[this.nameField], name: (props.idField && item[props.idField]) ? item[props.idField] as string : item[props.nameField] as string,
displayName: item[this.nameField], displayName: item[props.nameField] as string,
value: item[this.valueField], value: value,
percent: (item[this.percentField] > 0 || item[this.percentField] === 0 || item[this.percentField] === '0') ? item[this.percentField] : (item[this.valueField] / totalValidValue * 100), percent: (isNumber(percent) && percent >= 0) ? percent : (value / totalValidValue * 100),
actualPercent: item[this.valueField] / totalValidValue, actualPercent: value / totalValidValue,
itemStyle: { itemStyle: {
color: this.getColor(item[this.colorField] ? item[this.colorField] : DEFAULT_CHART_COLORS[validItems.length % DEFAULT_CHART_COLORS.length]), color: getColor((props.colorField && item[props.colorField]) ? item[props.colorField] as ColorValue : DEFAULT_CHART_COLORS[validItems.length % DEFAULT_CHART_COLORS.length]),
}, },
selected: true, selected: true,
sourceItem: item sourceItem: item
}; };
finalItem.displayPercent = formatPercent(finalItem.percent, 2, '&lt;0.01'); finalItem.displayPercent = formatPercent(finalItem.percent, 2, '&lt;0.01');
finalItem.displayValue = this.getDisplayCurrency(finalItem.value, this.defaultCurrency); finalItem.displayValue = formatAmountWithCurrency(finalItem.value, props.defaultCurrency) as string;
validItems.push(finalItem); validItems.push(finalItem);
} }
} }
return validItems; return validItems;
}, });
hasUnselectedItem: function () {
for (let i = 0; i < this.validItems.length; i++) {
const item = this.validItems[i];
if (this.selectedLegends && !this.selectedLegends[item.id]) { const hasUnselectedItem = computed<boolean>(() => {
for (let i = 0; i < validItems.value.length; i++) {
const item = validItems.value[i];
if (selectedLegends.value && !selectedLegends.value[item.id]) {
return true; return true;
} }
} }
return false; return false;
}, });
firstItemAndHalfCurrentItemTotalPercent: function () {
const firstItemAndHalfCurrentItemTotalPercent = computed<number>(() => {
let totalValue = 0; let totalValue = 0;
let firstValue = null; let firstValue = null;
let firstToCurrentTotalValue = 0; let firstToCurrentTotalValue = 0;
for (let i = 0; i < this.validItems.length; i++) { for (let i = 0; i < validItems.value.length; i++) {
const item = this.validItems[i]; const item = validItems.value[i];
if (this.selectedLegends && !this.selectedLegends[item.id]) { if (selectedLegends.value && !selectedLegends.value[item.id]) {
continue; continue;
} }
@@ -131,9 +157,9 @@ export default {
} }
if (firstValue !== null) { if (firstValue !== null) {
if (i < this.selectedIndex) { if (i < selectedIndex.value) {
firstToCurrentTotalValue += item.value; firstToCurrentTotalValue += item.value;
} else if (i === this.selectedIndex) { } else if (i === selectedIndex.value) {
firstToCurrentTotalValue += item.value / 2; firstToCurrentTotalValue += item.value / 2;
} }
} }
@@ -146,24 +172,24 @@ export default {
} else { } else {
return 0; return 0;
} }
}, });
chartOptions: function () {
const self = this;
const chartOptions = computed(() => {
return { return {
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
backgroundColor: self.isDarkMode ? '#333' : '#fff', backgroundColor: isDarkMode.value ? '#333' : '#fff',
borderColor: self.isDarkMode ? '#333' : '#fff', borderColor: isDarkMode.value ? '#333' : '#fff',
textStyle: { textStyle: {
color: self.isDarkMode ? '#eee' : '#333' color: isDarkMode.value ? '#eee' : '#333'
}, },
formatter: params => { formatter: (params: CallbackDataParams) => {
const name = params.data ? params.data.displayName : ''; const dataItem = params.data as DesktopPieChartDataItem;
const value = params.data ? params.data.displayValue : self.getDisplayCurrency(params.value); const name = dataItem ? dataItem.displayName : '';
let percent = params.data ? params.data.displayPercent : (params.percent + '%'); const value = dataItem ? dataItem.displayValue : formatAmountWithCurrency(params.value as number);
let percent = dataItem ? dataItem.displayPercent : (params.percent + '%');
if (self.hasUnselectedItem) { if (hasUnselectedItem.value) {
percent = params.percent + '%'; percent = params.percent + '%';
} }
@@ -182,21 +208,22 @@ export default {
}, },
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
data: self.validItems.map(item => item.name), data: validItems.value.map(item => item.name),
selected: self.selectedLegends, selected: selectedLegends.value,
textStyle: { textStyle: {
color: self.isDarkMode ? '#eee' : '#333' color: isDarkMode.value ? '#eee' : '#333'
}, },
formatter: id => { formatter: (id: string) => {
return self.itemsMap[id] && self.nameField && self.itemsMap[id][self.nameField] ? self.itemsMap[id][self.nameField] : id; const item = itemsMap.value[id];
return item && props.nameField && item[props.nameField] ? item[props.nameField] as string : id;
} }
}, },
series: [ series: [
{ {
type: 'pie', type: 'pie',
data: self.validItems, data: validItems.value,
top: 50, top: 50,
startAngle: -90 + self.firstItemAndHalfCurrentItemTotalPercent * 360, startAngle: -90 + firstItemAndHalfCurrentItemTotalPercent.value * 360,
emphasis: { emphasis: {
itemStyle: { itemStyle: {
shadowBlur: 10, shadowBlur: 10,
@@ -205,18 +232,19 @@ export default {
} }
}, },
label: { label: {
color: self.isDarkMode ? '#eee' : '#333', color: isDarkMode.value ? '#eee' : '#333',
formatter: params => { formatter: (params: CallbackDataParams) => {
return params.data ? params.data.displayName : ''; const dataItem = params.data as DesktopPieChartDataItem;
return dataItem ? dataItem.displayName : '';
} }
}, },
animation: !self.skeleton animation: !props.skeleton
} }
], ],
media: [ media: [
{ {
query: { query: {
minWidth: 600 minWidth: 600,
}, },
option: { option: {
legend: { legend: {
@@ -229,72 +257,64 @@ export default {
top: 0 top: 0
} }
] ]
} },
} }
] ]
}
}
},
watch: {
'items': function () {
this.selectedIndex = 0;
}
},
setup() {
const theme = useTheme();
return {
globalTheme: theme
}; };
}, });
methods: {
clickItem: function (e) {
if (!this.enableClickItem || e.componentType !== 'series' || e.seriesType !=='pie') {
return;
}
if (e.event && e.event.target && e.event.target.currentStates && e.event.target.currentStates[0] && e.event.target.currentStates[0] === 'emphasis') { function getColor(color: string): ColorValue {
this.selectedIndex = e.dataIndex;
return;
}
if (!e.data || !e.data.sourceItem) {
return;
}
this.$emit('click', e.data.sourceItem);
},
onLegendSelectChanged: function (e) {
this.selectedLegends = e.selected;
const selectedItem = this.validItems[this.selectedIndex];
if (!selectedItem || !this.selectedLegends[selectedItem.id]) {
let newSelectedIndex = 0;
for (let i = 0; i < this.validItems.length; i++) {
const item = this.validItems[i];
if (this.selectedLegends[item.id]) {
newSelectedIndex = i;
break;
}
}
this.selectedIndex = newSelectedIndex;
}
},
getColor: function (color) {
if (color && color !== DEFAULT_ICON_COLOR) { if (color && color !== DEFAULT_ICON_COLOR) {
color = '#' + color; color = '#' + color;
} }
return color; return color;
}, }
getDisplayCurrency(value, currencyCode) {
return this.$locale.formatAmountWithCurrency(this.settingsStore, this.userStore, value, currencyCode); function clickItem(e: ECElementEvent): void {
if (!props.enableClickItem || e.componentType !== 'series' || e.seriesType !=='pie') {
return;
} }
if (e.event && e.event.target && e.event.target.currentStates && e.event.target.currentStates[0] && e.event.target.currentStates[0] === 'emphasis') {
selectedIndex.value = e.dataIndex;
return;
}
if (!e.data) {
return;
}
const data = e.data as object;
if ('sourceItem' in data) {
emit('click', data.sourceItem as Record<string, unknown>);
} }
} }
function onLegendSelectChanged(e: { selected: Record<string, boolean> }): void {
selectedLegends.value = e.selected;
const selectedItem = validItems.value[selectedIndex.value];
if (!selectedItem || !selectedLegends.value[selectedItem.id]) {
let newSelectedIndex = 0;
for (let i = 0; i < validItems.value.length; i++) {
const item = validItems.value[i];
if (selectedLegends.value[item.id]) {
newSelectedIndex = i;
break;
}
}
selectedIndex.value = newSelectedIndex;
}
}
watch(() => props.items, () => {
selectedIndex.value = 0;
});
</script> </script>
<style scoped> <style scoped>