mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 15:07:33 +08:00
320 lines
10 KiB
Vue
320 lines
10 KiB
Vue
<template>
|
|
<div class="pie-chart-container">
|
|
<svg class="pie-chart" :viewBox="`${-diameter} ${-diameter} ${diameter * 2} ${diameter * 2}`">
|
|
<circle class="pie-chart-background" cx="0" cy="0" :r="diameter"></circle>
|
|
|
|
<circle class="pie-chart-item"
|
|
v-for="(item, idx) in validItems" :key="idx"
|
|
fill="transparent"
|
|
cx="0" cy="0"
|
|
:r="diameter / 2"
|
|
:stroke="item.color | defaultIconColor('var(--default-icon-color)')"
|
|
:stroke-width="diameter"
|
|
:stroke-dasharray="item | itemStrokeDash(circumference)"
|
|
:stroke-dashoffset="item | itemDashOffset(validItems, circumference, itemCommonDashOffset)">
|
|
</circle>
|
|
|
|
<circle class="pie-chart-text-background"
|
|
cx="0" cy="0"
|
|
stroke="#ddd"
|
|
:style="{ '--pie-chart-text-background': centerTextBackground ? centerTextBackground : '#7f2020' }"
|
|
:r="diameter / 2.5"
|
|
v-if="showCenterText"/>
|
|
|
|
<circle cx="0" cy="0"
|
|
stroke="#ddd"
|
|
fill="transparent"
|
|
:r="diameter / 2.5"
|
|
v-if="showCenterText"/>
|
|
|
|
<g class="pie-chart-text-group" v-if="showCenterText">
|
|
<slot></slot>
|
|
</g>
|
|
</svg>
|
|
<div class="pie-chart-toolbox-container padding-horizontal" v-if="showSelectedItemInfo">
|
|
<div class="pie-chart-toolbox">
|
|
<f7-link class="pie-chart-toolbox-button" :class="{ 'disabled': !!skeleton || !validItems || validItems.length <= 1 }" @click="switchSelectedItem(1)">
|
|
<f7-icon f7="arrow_left"></f7-icon>
|
|
</f7-link>
|
|
|
|
<div class="pie-chart-toolbox-info">
|
|
<p v-if="selectedItem">
|
|
<f7-chip class="chip-placeholder" outline v-if="skeleton">
|
|
<span class="skeleton-text">Percent</span>
|
|
</f7-chip>
|
|
<f7-chip outline
|
|
:text="(selectedItem.percent) | percent(2, '<0.01')"
|
|
:style="(selectedItem ? selectedItem.color : '') | iconStyle('default', 'var(--default-icon-color)', '--f7-chip-outline-border-color')"
|
|
v-else-if="!skeleton"></f7-chip>
|
|
</p>
|
|
<p v-else-if="!validItems || !validItems.length">
|
|
<f7-chip outline text="---"></f7-chip>
|
|
</p>
|
|
<f7-link :no-link-class="!enableClickItem" v-if="selectedItem" @click="clickItem(selectedItem)">
|
|
<span class="skeleton-text" v-if="skeleton">Name</span>
|
|
<span v-else-if="!skeleton && selectedItem.name">{{ selectedItem.name }}</span>
|
|
<span class="skeleton-text" v-if="skeleton">Value</span>
|
|
<span v-else-if="!skeleton" :style="(selectedItem ? selectedItem.color : '') | iconStyle('default', 'var(--default-icon-color)')">{{ selectedItem.value | currency(selectedItem.currency || defaultCurrency) }}</span>
|
|
<f7-icon class="item-navigate-icon" f7="chevron_right" v-if="enableClickItem"></f7-icon>
|
|
</f7-link>
|
|
<f7-link :no-link-class="true" v-else-if="!validItems || !validItems.length">
|
|
{{ $t('No transaction data') }}
|
|
</f7-link>
|
|
</div>
|
|
|
|
<f7-link class="pie-chart-toolbox-button" :class="{ 'disabled': !!skeleton || !validItems || validItems.length <= 1 }" @click="switchSelectedItem(-1)">
|
|
<f7-icon f7="arrow_right"></f7-icon>
|
|
</f7-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
const defaultColors = [
|
|
'cc4a66',
|
|
'e3564a',
|
|
'fc892c',
|
|
'ffc349',
|
|
'4dd291',
|
|
'24ceb3',
|
|
'2ab4d0',
|
|
'065786',
|
|
'713670',
|
|
'8e1d51'
|
|
];
|
|
|
|
export default {
|
|
props: [
|
|
'skeleton',
|
|
'items',
|
|
'nameField',
|
|
'valueField',
|
|
'percentField',
|
|
'currencyField',
|
|
'colorField',
|
|
'hiddenField',
|
|
'minValidPercent',
|
|
'defaultCurrency',
|
|
'showCenterText',
|
|
'showSelectedItemInfo',
|
|
'enableClickItem',
|
|
'centerTextBackground',
|
|
],
|
|
data: function () {
|
|
const diameter = 100;
|
|
|
|
return {
|
|
diameter: diameter,
|
|
circumference: diameter * Math.PI,
|
|
selectedIndex: 0
|
|
}
|
|
},
|
|
computed: {
|
|
validItems: function () {
|
|
let totalValidValue = 0;
|
|
|
|
for (let i = 0; i < this.items.length; i++) {
|
|
const item = this.items[i];
|
|
|
|
if (item[this.valueField] && item[this.valueField] > 0 && (!this.hiddenField || !item[this.hiddenField])) {
|
|
totalValidValue += item[this.valueField];
|
|
}
|
|
}
|
|
|
|
const validItems = [];
|
|
|
|
for (let i = 0; i < this.items.length; i++) {
|
|
const item = this.items[i];
|
|
|
|
if (item[this.valueField] && item[this.valueField] > 0 &&
|
|
(!this.hiddenField || !item[this.hiddenField]) &&
|
|
(!this.minValidPercent || item[this.valueField] / totalValidValue > this.minValidPercent)) {
|
|
validItems.push({
|
|
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,
|
|
currency: item[this.currencyField],
|
|
color: item[this.colorField] ? item[this.colorField] : defaultColors[validItems.length % defaultColors.length],
|
|
sourceItem: item
|
|
});
|
|
}
|
|
}
|
|
|
|
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': function () {
|
|
this.selectedIndex = 0;
|
|
}
|
|
},
|
|
methods: {
|
|
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);
|
|
}
|
|
}
|
|
},
|
|
filters: {
|
|
itemStrokeDash(item, circumference) {
|
|
const length = item.actualPercent * circumference;
|
|
return `${length} ${circumference - length}`;
|
|
},
|
|
itemDashOffset(item, items, circumference, 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 += circumference / 4;
|
|
} else {
|
|
offset = circumference / 4;
|
|
}
|
|
|
|
if (allPreviousPercent <= 0) {
|
|
return offset;
|
|
}
|
|
|
|
const allPreviousLength = allPreviousPercent * circumference;
|
|
return circumference - allPreviousLength + offset;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.pie-chart-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.pie-chart {
|
|
margin: 24px 24px 0 24px;
|
|
}
|
|
|
|
.pie-chart-toolbox-container {
|
|
margin-top: 16px;
|
|
padding-bottom: 16px;
|
|
}
|
|
|
|
.pie-chart-toolbox {
|
|
display: inline-flex;
|
|
width: 100%;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.pie-chart-toolbox-info {
|
|
--f7-chip-height: 30px;
|
|
--f7-chip-font-size: 18px;
|
|
font-size: 16px;
|
|
align-self: center;
|
|
}
|
|
|
|
.pie-chart-toolbox-info p {
|
|
text-align: center;
|
|
margin: 0 0 4px 0;
|
|
}
|
|
|
|
.pie-chart-toolbox-info a {
|
|
color: var(--f7-text-color);
|
|
}
|
|
|
|
.pie-chart-toolbox-info a > span + span {
|
|
padding-left: 8px;
|
|
}
|
|
|
|
.pie-chart-toolbox-info .item-navigate-icon {
|
|
color: rgba(0, 0, 0, 0.2);
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
padding-left: 4px;
|
|
}
|
|
|
|
.pie-chart-toolbox-button {
|
|
color: var(--f7-text-color);
|
|
}
|
|
|
|
.pie-chart-background {
|
|
fill: #f0f0f0;
|
|
}
|
|
|
|
.theme-dark .pie-chart-background {
|
|
fill: #181818;
|
|
}
|
|
|
|
.pie-chart-text-background {
|
|
--pie-chart-text-background: #7f2020;
|
|
fill: var(--pie-chart-text-background);
|
|
}
|
|
|
|
.pie-chart-text-group {
|
|
font-size: 0.7em;
|
|
-moz-transform: translateY(-1em);
|
|
-ms-transform: translateY(-1em);
|
|
-webkit-transform: translateY(-1em);
|
|
transform: translateY(-1em);
|
|
}
|
|
</style>
|