mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-20 01:34:24 +08:00
migrate mobile pie chart to composition API and typescript
This commit is contained in:
+200
-187
@@ -64,7 +64,7 @@
|
|||||||
<f7-icon class="item-navigate-icon" f7="chevron_right" v-if="enableClickItem"></f7-icon>
|
<f7-icon class="item-navigate-icon" f7="chevron_right" v-if="enableClickItem"></f7-icon>
|
||||||
</f7-link>
|
</f7-link>
|
||||||
<f7-link :no-link-class="true" v-else-if="!validItems || !validItems.length">
|
<f7-link :no-link-class="true" v-else-if="!validItems || !validItems.length">
|
||||||
{{ $t('No transaction data') }}
|
{{ tt('No transaction data') }}
|
||||||
</f7-link>
|
</f7-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -76,206 +76,219 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapStores } from 'pinia';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useSettingsStore } from '@/stores/setting.ts';
|
|
||||||
import { useUserStore } from '@/stores/user.ts';
|
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import type { ColorValue } from '@/core/color.ts';
|
||||||
import { DEFAULT_ICON_COLOR, DEFAULT_CHART_COLORS } from '@/consts/color.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 MobilePieChartDataItem {
|
||||||
props: [
|
name: string;
|
||||||
'skeleton',
|
value: number;
|
||||||
'items',
|
percent: number;
|
||||||
'nameField',
|
actualPercent: number;
|
||||||
'valueField',
|
color: ColorValue;
|
||||||
'percentField',
|
sourceItem: Record<string, unknown>;
|
||||||
'colorField',
|
displayPercent?: string;
|
||||||
'hiddenField',
|
displayValue?: string;
|
||||||
'minValidPercent',
|
}
|
||||||
'defaultCurrency',
|
|
||||||
'showValue',
|
|
||||||
'showCenterText',
|
|
||||||
'showSelectedItemInfo',
|
|
||||||
'enableClickItem',
|
|
||||||
'centerTextBackground',
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'click'
|
|
||||||
],
|
|
||||||
data: function () {
|
|
||||||
const diameter = 100;
|
|
||||||
|
|
||||||
return {
|
const props = defineProps<{
|
||||||
diameter: diameter,
|
skeleton?: boolean;
|
||||||
circumference: diameter * Math.PI,
|
items: Record<string, unknown>[];
|
||||||
selectedIndex: 0
|
nameField: string;
|
||||||
}
|
valueField: string;
|
||||||
},
|
percentField?: string;
|
||||||
computed: {
|
colorField?: string;
|
||||||
...mapStores(useSettingsStore, useUserStore),
|
hiddenField?: string;
|
||||||
validItems: function () {
|
minValidPercent?: number;
|
||||||
let totalValidValue = 0;
|
defaultCurrency?: string;
|
||||||
|
showValue?: boolean;
|
||||||
|
showCenterText?: boolean;
|
||||||
|
showSelectedItemInfo?: boolean;
|
||||||
|
enableClickItem?: boolean;
|
||||||
|
centerTextBackground?: ColorValue;
|
||||||
|
}>();
|
||||||
|
|
||||||
for (let i = 0; i < this.items.length; i++) {
|
const emit = defineEmits<{
|
||||||
const item = this.items[i];
|
(e: 'click', value: Record<string, unknown>): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
if (item[this.valueField] && item[this.valueField] > 0 && (!this.hiddenField || !item[this.hiddenField])) {
|
const { tt, formatAmountWithCurrency } = useI18n();
|
||||||
totalValidValue += item[this.valueField];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validItems = [];
|
const diameter: number = 100;
|
||||||
|
const circumference: number = diameter * Math.PI;
|
||||||
|
|
||||||
for (let i = 0; i < this.items.length; i++) {
|
const selectedIndex = ref<number>(0);
|
||||||
const item = this.items[i];
|
|
||||||
|
|
||||||
if (item[this.valueField] && item[this.valueField] > 0 &&
|
const validItems = computed<MobilePieChartDataItem[]>(() => {
|
||||||
(!this.hiddenField || !item[this.hiddenField]) &&
|
let totalValidValue = 0;
|
||||||
(!this.minValidPercent || item[this.valueField] / totalValidValue > this.minValidPercent)) {
|
|
||||||
const finalItem = {
|
|
||||||
name: item[this.nameField],
|
|
||||||
value: item[this.valueField],
|
|
||||||
percent: (item[this.percentField] > 0 || item[this.percentField] === 0 || item[this.percentField] === '0') ? item[this.percentField] : (item[this.valueField] / totalValidValue * 100),
|
|
||||||
actualPercent: item[this.valueField] / totalValidValue,
|
|
||||||
color: item[this.colorField] ? item[this.colorField] : DEFAULT_CHART_COLORS[validItems.length % DEFAULT_CHART_COLORS.length],
|
|
||||||
sourceItem: item
|
|
||||||
};
|
|
||||||
|
|
||||||
finalItem.displayPercent = formatPercent(finalItem.percent, 2, '<0.01');
|
for (let i = 0; i < props.items.length; i++) {
|
||||||
finalItem.displayValue = this.getDisplayCurrency(finalItem.value, this.defaultCurrency);
|
const item = props.items[i];
|
||||||
|
const value = item[props.valueField];
|
||||||
|
|
||||||
validItems.push(finalItem);
|
if (isNumber(value) && value > 0 && (!props.hiddenField || !item[props.hiddenField])) {
|
||||||
}
|
totalValidValue += value;
|
||||||
}
|
|
||||||
|
|
||||||
return validItems;
|
|
||||||
},
|
|
||||||
totalValidValue: function () {
|
|
||||||
let totalValidValue = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.validItems.length; i++) {
|
|
||||||
totalValidValue += this.validItems[i].value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalValidValue;
|
|
||||||
},
|
|
||||||
itemCommonDashOffset: function () {
|
|
||||||
if (this.totalValidValue <= 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(this.selectedIndex + 1, this.validItems.length); i++) {
|
|
||||||
const item = this.validItems[i];
|
|
||||||
|
|
||||||
if (item.actualPercent > 0) {
|
|
||||||
if (i === this.selectedIndex) {
|
|
||||||
offset += -this.circumference * (1 - item.actualPercent) / 2;
|
|
||||||
} else {
|
|
||||||
offset += -this.circumference * (1 - item.actualPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return offset;
|
|
||||||
},
|
|
||||||
selectedItem: function () {
|
|
||||||
if (!this.validItems || !this.validItems.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedIndex = this.selectedIndex;
|
|
||||||
|
|
||||||
if (selectedIndex < 0 || selectedIndex >= this.validItems.length) {
|
|
||||||
selectedIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.validItems[selectedIndex];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'items': {
|
|
||||||
handler() {
|
|
||||||
this.selectedIndex = 0;
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
switchSelectedIndex: function (index) {
|
|
||||||
this.selectedIndex = index;
|
|
||||||
},
|
|
||||||
switchSelectedItem: function (offset) {
|
|
||||||
let newSelectedIndex = this.selectedIndex + offset;
|
|
||||||
|
|
||||||
while (newSelectedIndex < 0) {
|
|
||||||
newSelectedIndex += this.validItems.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedIndex = newSelectedIndex % this.validItems.length;
|
|
||||||
},
|
|
||||||
clickItem: function (item) {
|
|
||||||
if (this.enableClickItem) {
|
|
||||||
this.$emit('click', item.sourceItem);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getColor: function (color) {
|
|
||||||
if (color && color !== DEFAULT_ICON_COLOR) {
|
|
||||||
color = '#' + color;
|
|
||||||
} else {
|
|
||||||
color = 'var(--default-icon-color)';
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
},
|
|
||||||
getColorStyle: function (color, additionalFieldName) {
|
|
||||||
const ret = {
|
|
||||||
color: this.getColor(color)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (additionalFieldName) {
|
|
||||||
ret[additionalFieldName] = ret.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
getItemStrokeDash(item) {
|
|
||||||
const length = item.actualPercent * this.circumference;
|
|
||||||
return `${length} ${this.circumference - length}`;
|
|
||||||
},
|
|
||||||
getItemDashOffset(item, items, offset) {
|
|
||||||
let allPreviousPercent = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const curItem = items[i];
|
|
||||||
|
|
||||||
if (curItem === item) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
allPreviousPercent += curItem.actualPercent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset) {
|
|
||||||
offset += this.circumference / 4;
|
|
||||||
} else {
|
|
||||||
offset = this.circumference / 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allPreviousPercent <= 0) {
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allPreviousLength = allPreviousPercent * this.circumference;
|
|
||||||
return this.circumference - allPreviousLength + offset;
|
|
||||||
},
|
|
||||||
getDisplayCurrency(value, currencyCode) {
|
|
||||||
return this.$locale.formatAmountWithCurrency(this.settingsStore, this.userStore, value, currencyCode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validItems: MobilePieChartDataItem[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < props.items.length; i++) {
|
||||||
|
const item = props.items[i];
|
||||||
|
const value = item[props.valueField];
|
||||||
|
const percent = props.percentField ? item[props.percentField] : -1;
|
||||||
|
|
||||||
|
if (isNumber(value) && value > 0 &&
|
||||||
|
(!props.hiddenField || !item[props.hiddenField]) &&
|
||||||
|
(!props.minValidPercent || value / totalValidValue > props.minValidPercent)) {
|
||||||
|
const finalItem: MobilePieChartDataItem = {
|
||||||
|
name: item[props.nameField] as string,
|
||||||
|
value: value,
|
||||||
|
percent: (isNumber(percent) && percent >= 0) ? percent : (value / totalValidValue * 100),
|
||||||
|
actualPercent: value / totalValidValue,
|
||||||
|
color: (props.colorField && item[props.colorField]) ? item[props.colorField] as ColorValue : DEFAULT_CHART_COLORS[validItems.length % DEFAULT_CHART_COLORS.length],
|
||||||
|
sourceItem: item
|
||||||
|
};
|
||||||
|
|
||||||
|
finalItem.displayPercent = formatPercent(finalItem.percent, 2, '<0.01');
|
||||||
|
finalItem.displayValue = formatAmountWithCurrency(finalItem.value, props.defaultCurrency) as string;
|
||||||
|
|
||||||
|
validItems.push(finalItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validItems;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalValidValue = computed<number>(() => {
|
||||||
|
let totalValidValue = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < validItems.value.length; i++) {
|
||||||
|
totalValidValue += validItems.value[i].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalValidValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemCommonDashOffset = computed<number>(() => {
|
||||||
|
if (totalValidValue.value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(selectedIndex.value + 1, validItems.value.length); i++) {
|
||||||
|
const item = validItems.value[i];
|
||||||
|
|
||||||
|
if (item.actualPercent > 0) {
|
||||||
|
if (i === selectedIndex.value) {
|
||||||
|
offset += -circumference * (1 - item.actualPercent) / 2;
|
||||||
|
} else {
|
||||||
|
offset += -circumference * (1 - item.actualPercent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedItem = computed<MobilePieChartDataItem | null>(() => {
|
||||||
|
if (!validItems.value || !validItems.value.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = selectedIndex.value;
|
||||||
|
|
||||||
|
if (index < 0 || index >= validItems.value.length) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validItems.value[index];
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.items, () => {
|
||||||
|
selectedIndex.value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
function switchSelectedIndex(index: number): void {
|
||||||
|
selectedIndex.value = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchSelectedItem(offset: number): void {
|
||||||
|
let newSelectedIndex = selectedIndex.value + offset;
|
||||||
|
|
||||||
|
while (newSelectedIndex < 0) {
|
||||||
|
newSelectedIndex += validItems.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIndex.value = newSelectedIndex % validItems.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickItem(item: MobilePieChartDataItem): void {
|
||||||
|
if (props.enableClickItem) {
|
||||||
|
emit('click', item.sourceItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColor(color: ColorValue): ColorValue {
|
||||||
|
if (color && color !== DEFAULT_ICON_COLOR) {
|
||||||
|
color = '#' + color;
|
||||||
|
} else {
|
||||||
|
color = 'var(--default-icon-color)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorStyle(color: ColorValue, additionalFieldName?: string): Record<string, string> {
|
||||||
|
const ret: Record<string, string> = {
|
||||||
|
color: getColor(color)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (additionalFieldName) {
|
||||||
|
ret[additionalFieldName] = ret.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemStrokeDash(item: MobilePieChartDataItem): string {
|
||||||
|
const length = item.actualPercent * circumference;
|
||||||
|
return `${length} ${circumference - length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemDashOffset(item: MobilePieChartDataItem, items: MobilePieChartDataItem[], offset?: number): number {
|
||||||
|
let allPreviousPercent = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const curItem = items[i];
|
||||||
|
|
||||||
|
if (curItem === item) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
allPreviousPercent += curItem.actualPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
offset += circumference / 4;
|
||||||
|
} else {
|
||||||
|
offset = circumference / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allPreviousPercent <= 0) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPreviousLength = allPreviousPercent * circumference;
|
||||||
|
return circumference - allPreviousLength + offset;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user