Animate changing values in UI (#3779)
This commit is contained in:
parent
815cc58555
commit
5c719fcb03
16 changed files with 258 additions and 150 deletions
|
|
@ -6,6 +6,7 @@ import { createApp, h } from "vue";
|
|||
import { createMetaManager, plugin as metaPlugin } from "vue-meta";
|
||||
import api from "./api";
|
||||
import App from "./views/App.vue";
|
||||
import VueNumber from "vue-number-animation";
|
||||
import router from "./router";
|
||||
import i18n from "./i18n";
|
||||
import featureflags from "./featureflags";
|
||||
|
|
@ -68,6 +69,7 @@ app.use(router);
|
|||
app.use(createMetaManager());
|
||||
app.use(metaPlugin);
|
||||
app.use(featureflags);
|
||||
app.use(VueNumber);
|
||||
window.app = app.mount("#app");
|
||||
|
||||
window.setInterval(function () {
|
||||
|
|
|
|||
23
assets/js/components/AnimatedNumber.vue
Normal file
23
assets/js/components/AnimatedNumber.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<number :to="to" :format="format" :duration="duration" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const DURATION = 0.5;
|
||||
|
||||
export default {
|
||||
name: "AnimatedNumber",
|
||||
props: {
|
||||
to: { type: Number },
|
||||
format: { type: Function },
|
||||
},
|
||||
data() {
|
||||
return { duration: 0 };
|
||||
},
|
||||
watch: {
|
||||
to: function () {
|
||||
this.duration = DURATION;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -46,7 +46,9 @@
|
|||
>
|
||||
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||
<h3 class="m-0">In</h3>
|
||||
<span class="fw-bold">{{ kw(inPower) }}</span>
|
||||
<span class="fw-bold">
|
||||
<AnimatedNumber :to="inPower" :format="kw" />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<EnergyflowEntry
|
||||
|
|
@ -76,7 +78,9 @@
|
|||
>
|
||||
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||
<h3 class="m-0">Out</h3>
|
||||
<span class="fw-bold">{{ kw(outPower) }}</span>
|
||||
<span class="fw-bold">
|
||||
<AnimatedNumber :to="outPower" :format="kw" />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<EnergyflowEntry
|
||||
|
|
@ -121,10 +125,11 @@ import "@h2d2/shopicons/es/filled/square";
|
|||
import Visualization from "./Visualization.vue";
|
||||
import EnergyflowEntry from "./EnergyflowEntry.vue";
|
||||
import formatter from "../../mixins/formatter";
|
||||
import AnimatedNumber from "../AnimatedNumber.vue";
|
||||
|
||||
export default {
|
||||
name: "Energyflow",
|
||||
components: { Visualization, EnergyflowEntry },
|
||||
components: { Visualization, EnergyflowEntry, AnimatedNumber },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
gridConfigured: Boolean,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@
|
|||
</span>
|
||||
<span class="text-nowrap flex-grow-1 ms-3">{{ name }}</span>
|
||||
<span class="text-end text-nowrap ps-1 fw-bold"
|
||||
><span v-if="hasSoC">{{ soc }}% / </span>{{ kw(power) }}</span
|
||||
>
|
||||
><span v-if="hasSoC">{{ soc }}% / </span>
|
||||
<AnimatedNumber :to="power" :format="kw" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -18,10 +19,11 @@ import "@h2d2/shopicons/es/regular/home";
|
|||
import "@h2d2/shopicons/es/regular/car3";
|
||||
import BatteryIcon from "./BatteryIcon.vue";
|
||||
import formatter from "../../mixins/formatter";
|
||||
import AnimatedNumber from "../AnimatedNumber.vue";
|
||||
|
||||
export default {
|
||||
name: "EnergyflowEntry",
|
||||
components: { BatteryIcon },
|
||||
components: { BatteryIcon, AnimatedNumber },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
name: { type: String },
|
||||
|
|
|
|||
|
|
@ -19,34 +19,34 @@
|
|||
class="site-progress-bar self-consumption"
|
||||
:style="{ width: widthTotal(selfConsumptionAdjusted) }"
|
||||
>
|
||||
<span v-if="powerLabelEnoughSpace(selfConsumption)" class="power">
|
||||
{{ kw(selfConsumption) }}
|
||||
</span>
|
||||
<span v-else-if="powerLabelSomeSpace(selfConsumption)" class="power">
|
||||
{{ kwNoUnit(selfConsumption) }}
|
||||
</span>
|
||||
<AnimatedNumber
|
||||
v-if="selfConsumption"
|
||||
class="power"
|
||||
:to="selfConsumption"
|
||||
:format="fmtBarValue"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="site-progress-bar grid-import"
|
||||
:style="{ width: widthTotal(gridImportAdjusted) }"
|
||||
>
|
||||
<span v-if="powerLabelEnoughSpace(gridImport)" class="power">
|
||||
{{ kw(gridImport) }}
|
||||
</span>
|
||||
<span v-else-if="powerLabelSomeSpace(gridImport)" class="power">
|
||||
{{ kwNoUnit(gridImport) }}
|
||||
</span>
|
||||
<AnimatedNumber
|
||||
v-if="gridImport"
|
||||
class="power"
|
||||
:to="gridImport"
|
||||
:format="fmtBarValue"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="site-progress-bar pv-export"
|
||||
:style="{ width: widthTotal(pvExportAdjusted) }"
|
||||
>
|
||||
<span v-if="powerLabelEnoughSpace(pvExport)" class="power">
|
||||
{{ kw(pvExport) }}
|
||||
</span>
|
||||
<span v-else-if="powerLabelSomeSpace(pvExport)" class="power">
|
||||
{{ kwNoUnit(pvExport) }}
|
||||
</span>
|
||||
<AnimatedNumber
|
||||
v-if="pvExport"
|
||||
class="power"
|
||||
:to="pvExport"
|
||||
:format="fmtBarValue"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="totalAdjusted <= 0" class="site-progress-bar bg-light border no-wrap w-100">
|
||||
<span>{{ $t("main.energyflow.noEnergy") }}</span>
|
||||
|
|
@ -76,13 +76,14 @@
|
|||
import formatter from "../../mixins/formatter";
|
||||
import BatteryIcon from "./BatteryIcon.vue";
|
||||
import LabelBar from "./LabelBar.vue";
|
||||
import AnimatedNumber from "../AnimatedNumber.vue";
|
||||
import "@h2d2/shopicons/es/regular/car3";
|
||||
import "@h2d2/shopicons/es/regular/sun";
|
||||
import "@h2d2/shopicons/es/regular/home";
|
||||
|
||||
export default {
|
||||
name: "Visualization",
|
||||
components: { BatteryIcon, LabelBar },
|
||||
components: { BatteryIcon, LabelBar, AnimatedNumber },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
gridImport: { type: Number, default: 0 },
|
||||
|
|
@ -141,11 +142,9 @@ export default {
|
|||
if (this.totalAdjusted === 0) return "0%";
|
||||
return (100 / this.totalAdjusted) * power + "%";
|
||||
},
|
||||
kw: function (watt) {
|
||||
return this.fmtKw(watt, this.valuesInKw, true);
|
||||
},
|
||||
kwNoUnit: function (watt) {
|
||||
return this.fmtKw(watt, this.valuesInKw, false);
|
||||
fmtBarValue: function (watt) {
|
||||
const valueInKw = this.powerLabelEnoughSpace(watt);
|
||||
return this.fmtKw(watt, this.valuesInKw, valueInKw);
|
||||
},
|
||||
powerLabelAvailableSpace(power) {
|
||||
if (this.totalAdjusted === 0) return 0;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,38 @@
|
|||
<template>
|
||||
<div class="root">
|
||||
<div class="mb-2 label">{{ label }}</div>
|
||||
<div class="mb-2 label" :class="labelClass">{{ label }}</div>
|
||||
<slot>
|
||||
<h3 class="value m-0">
|
||||
{{ value }}
|
||||
<span v-if="extraValue" class="extraValue d-block d-sm-inline text-nowrap">
|
||||
<h3 class="value m-0 d-block d-sm-flex align-items-baseline" :class="valueClass">
|
||||
<AnimatedNumber v-if="valueFmt" :to="value" :format="valueFmt" />
|
||||
<span v-else>{{ value }}</span>
|
||||
<div v-if="extraValue" class="extraValue ms-0 ms-sm-1 text-nowrap">
|
||||
{{ extraValue }}
|
||||
</span>
|
||||
</div>
|
||||
</h3>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AnimatedNumber from "./AnimatedNumber.vue";
|
||||
|
||||
export default {
|
||||
name: "LabelAndValue",
|
||||
components: { AnimatedNumber },
|
||||
props: {
|
||||
label: String,
|
||||
value: String,
|
||||
value: [Number, String],
|
||||
valueFmt: Function,
|
||||
extraValue: String,
|
||||
align: { type: String, default: "center" },
|
||||
},
|
||||
computed: {
|
||||
labelClass() {
|
||||
return `text-${this.align}`;
|
||||
},
|
||||
valueClass() {
|
||||
return `justify-content-${this.align}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@
|
|||
<div class="d-flex align-items-center">
|
||||
<LabelAndValue
|
||||
:label="$t('main.loadpoint.power')"
|
||||
:value="fmtKw(chargePower, formatChargePowerInKw)"
|
||||
:value="chargePower"
|
||||
:valueFmt="fmtkWUnit"
|
||||
class="mb-2"
|
||||
align="start"
|
||||
/>
|
||||
<shopicon-regular-lightning
|
||||
class="text-evcc opacity-transiton"
|
||||
|
|
@ -48,13 +50,18 @@
|
|||
:class="`opacity-${charging ? '100' : '0'}`"
|
||||
/>
|
||||
</div>
|
||||
<LabelAndValue :label="$t('main.loadpoint.charged')" :value="fmtKWh(chargedEnergy)" />
|
||||
<LabelAndValue
|
||||
:label="$t('main.loadpoint.charged')"
|
||||
:value="fmtKWh(chargedEnergy)"
|
||||
align="center"
|
||||
/>
|
||||
<LabelAndValue
|
||||
v-if="chargeRemainingDurationInterpolated"
|
||||
:label="$t('main.loadpoint.remaining')"
|
||||
:value="`
|
||||
${fmtShortDuration(chargeRemainingDurationInterpolated)}
|
||||
${fmtShortDurationUnit(chargeRemainingDurationInterpolated, true)}`"
|
||||
align="end"
|
||||
/>
|
||||
<LabelAndValue
|
||||
v-else
|
||||
|
|
@ -62,6 +69,7 @@
|
|||
:value="`
|
||||
${fmtShortDuration(chargeDurationInterpolated)}
|
||||
${fmtShortDurationUnit(chargeDurationInterpolated)}`"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
<hr class="divider" />
|
||||
|
|
@ -157,9 +165,6 @@ export default {
|
|||
vehicle: function () {
|
||||
return this.collectProps(Vehicle);
|
||||
},
|
||||
formatChargePowerInKw: function () {
|
||||
return this.chargePower == 0 || this.chargePower >= 1000;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
phaseRemaining() {
|
||||
|
|
@ -211,6 +216,10 @@ export default {
|
|||
removeTargetTime: function () {
|
||||
api.delete(this.apiPath("targetcharge"));
|
||||
},
|
||||
fmtkWUnit(value) {
|
||||
const inKw = value == 0 || value >= 1000;
|
||||
return this.fmtKw(value, inKw);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
>
|
||||
<button
|
||||
class="btn btn-link p-0 value text-center"
|
||||
:class="targetChargeEnabled ? 'evcc-default-text' : 'text-gray-light'"
|
||||
:class="targetChargeEnabled ? 'evcc-default-text' : 'text-gray'"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#${modalId}`"
|
||||
:disabled="disabled"
|
||||
|
|
|
|||
|
|
@ -1,28 +1,31 @@
|
|||
<template>
|
||||
<LabelAndValue class="flex-grow-1" :label="$t('main.vehicle.targetSoC')">
|
||||
<h3 class="value m-0">
|
||||
<label class="d-inline-block position-relative">
|
||||
<LabelAndValue class="flex-grow-1" :label="$t('main.vehicle.targetSoC')" align="end">
|
||||
<h3 class="value m-0 d-block d-sm-flex align-items-baseline justify-content-end">
|
||||
<label class="position-relative">
|
||||
<select :value="targetSoc" class="custom-select" @change="change">
|
||||
<option v-for="{ soc, text } in options" :key="soc" :value="soc">
|
||||
{{ text }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-decoration-underline">{{ targetSoc }}%</span>
|
||||
<span class="text-decoration-underline">
|
||||
<AnimatedNumber :to="targetSoc" :format="formatSoC" />
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<span v-if="estimatedTargetRange" class="extraValue d-block d-sm-inline text-nowrap">
|
||||
{{ estimatedTargetRange }} km
|
||||
</span>
|
||||
<div v-if="estimatedTargetRange" class="extraValue ms-0 ms-sm-1 text-nowrap">
|
||||
<AnimatedNumber :to="estimatedTargetRange" :format="formatKm" />
|
||||
</div>
|
||||
</h3>
|
||||
</LabelAndValue>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LabelAndValue from "./LabelAndValue.vue";
|
||||
import AnimatedNumber from "./AnimatedNumber.vue";
|
||||
|
||||
export default {
|
||||
name: "TargetSoCSelect",
|
||||
components: { LabelAndValue },
|
||||
components: { LabelAndValue, AnimatedNumber },
|
||||
props: {
|
||||
targetSoc: Number,
|
||||
rangePerSoc: Number,
|
||||
|
|
@ -33,10 +36,10 @@ export default {
|
|||
options: function () {
|
||||
const result = [];
|
||||
for (let soc = 20; soc <= 100; soc += 5) {
|
||||
let text = `${soc}%`;
|
||||
let text = this.formatSoC(soc);
|
||||
const range = this.estimatedRange(soc);
|
||||
if (range) {
|
||||
text += ` (${range} km)`;
|
||||
text += ` (${this.formatKm(range)})`;
|
||||
}
|
||||
result.push({ soc, text });
|
||||
}
|
||||
|
|
@ -56,6 +59,12 @@ export default {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
formatSoC: function (value) {
|
||||
return `${Math.round(value)}%`;
|
||||
},
|
||||
formatKm: function (value) {
|
||||
return `${Math.round(value)} km`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -65,7 +74,7 @@ export default {
|
|||
font-size: 18px;
|
||||
}
|
||||
.extraValue {
|
||||
color: var(--bs-gray-light);
|
||||
color: var(--evcc-gray);
|
||||
font-size: 14px;
|
||||
}
|
||||
.custom-select {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@
|
|||
<div v-if="vehiclePresent">
|
||||
<div class="details d-flex flex-wrap justify-content-between">
|
||||
<LabelAndValue
|
||||
class="flex-grow-1 text-start"
|
||||
class="flex-grow-1"
|
||||
:label="$t('main.vehicle.vehicleSoC')"
|
||||
:value="vehicleSoC ? `${vehicleSoC}%` : '--'"
|
||||
:extraValue="vehicleRange ? `${vehicleRange} km` : null"
|
||||
align="start"
|
||||
/>
|
||||
<TargetCharge
|
||||
class="flex-grow-1 text-center target-charge"
|
||||
|
|
|
|||
109
dist/assets/index.0f8a9c87.js
vendored
Normal file
109
dist/assets/index.0f8a9c87.js
vendored
Normal file
File diff suppressed because one or more lines are too long
93
dist/assets/index.33cb7319.js
vendored
93
dist/assets/index.33cb7319.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -20,8 +20,8 @@
|
|||
<meta name="theme-color" content="#020318" />
|
||||
|
||||
<title>evcc</title>
|
||||
<script type="module" crossorigin src="./assets/index.33cb7319.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.1555a0f0.css">
|
||||
<script type="module" crossorigin src="./assets/index.0f8a9c87.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.955a5e82.css">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
|
|
|||
27
package-lock.json
generated
27
package-lock.json
generated
|
|
@ -17,6 +17,7 @@
|
|||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vue-meta": "^3.0.0-alpha.2",
|
||||
"vue-number-animation": "^1.1.2",
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -14617,6 +14618,11 @@
|
|||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/gsap": {
|
||||
"version": "3.10.4",
|
||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.10.4.tgz",
|
||||
"integrity": "sha512-6QatdkKxXCMfvCW4rM++0RqyLQAzFX5nwl3yHS0XPgkZBkiSEY3VZVbMltrdtsbER/xZonLtyHt684wRp4erlQ=="
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.7",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
|
||||
|
|
@ -23915,6 +23921,14 @@
|
|||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-number-animation": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-number-animation/-/vue-number-animation-1.1.2.tgz",
|
||||
"integrity": "sha512-3tPgGl8OSWpEVOcoUSl/sikhx8AufYQL+oFAy/xzfQap7o7PeppD3W9WLOn9RvhMi9cXrja5aSeFnfdLS8iFww==",
|
||||
"dependencies": {
|
||||
"gsap": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.16.tgz",
|
||||
|
|
@ -36144,6 +36158,11 @@
|
|||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
|
||||
"dev": true
|
||||
},
|
||||
"gsap": {
|
||||
"version": "3.10.4",
|
||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.10.4.tgz",
|
||||
"integrity": "sha512-6QatdkKxXCMfvCW4rM++0RqyLQAzFX5nwl3yHS0XPgkZBkiSEY3VZVbMltrdtsbER/xZonLtyHt684wRp4erlQ=="
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.7.7",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
|
||||
|
|
@ -43352,6 +43371,14 @@
|
|||
"integrity": "sha512-rYeIGkhm1gKBcReEkPHiV6LV2Y6BZTMGTkGw1mQIZTxfFcVJL0srLZsL1zAmLeTGxMrlDYylMQEVSYRkDnwz3A==",
|
||||
"requires": {}
|
||||
},
|
||||
"vue-number-animation": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-number-animation/-/vue-number-animation-1.1.2.tgz",
|
||||
"integrity": "sha512-3tPgGl8OSWpEVOcoUSl/sikhx8AufYQL+oFAy/xzfQap7o7PeppD3W9WLOn9RvhMi9cXrja5aSeFnfdLS8iFww==",
|
||||
"requires": {
|
||||
"gsap": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.16.tgz",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vue-meta": "^3.0.0-alpha.2",
|
||||
"vue-number-animation": "^1.1.2",
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue