Improve session info discoverability (#9783)
This commit is contained in:
parent
6f7e45db41
commit
5ef8825582
8 changed files with 216 additions and 126 deletions
|
|
@ -1,13 +1,17 @@
|
|||
<template>
|
||||
<div class="root">
|
||||
<div class="mb-2 label" :class="labelClass">{{ label }}</div>
|
||||
<div class="mb-2 label" :class="labelClass">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
</div>
|
||||
<slot>
|
||||
<h3 class="value m-0 d-block d-sm-flex align-items-baseline" :class="valueClass">
|
||||
<slot name="value">
|
||||
<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 }}
|
||||
</div>
|
||||
</slot>
|
||||
</h3>
|
||||
</slot>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -78,66 +78,7 @@
|
|||
:value="fmtEnergy(chargedEnergy)"
|
||||
align="center"
|
||||
/>
|
||||
<div class="sessionInfo" @click="nextSessionInfo">
|
||||
<LabelAndValue
|
||||
v-if="sessionInfo === SESSION.SOLAR"
|
||||
:label="$t('main.loadpoint.solar')"
|
||||
:value="sessionSolarPercentage"
|
||||
:valueFmt="fmtSolar"
|
||||
align="end"
|
||||
/>
|
||||
<div v-else-if="sessionInfo === SESSION.AVG_PRICE">
|
||||
<LabelAndValue
|
||||
class="d-block d-sm-none"
|
||||
:label="$t('main.loadpoint.avgPrice')"
|
||||
:value="sessionPricePerKWh"
|
||||
:valueFmt="fmtAvgPriceShort"
|
||||
align="end"
|
||||
/>
|
||||
<LabelAndValue
|
||||
class="d-none d-sm-block"
|
||||
:label="$t('main.loadpoint.avgPrice')"
|
||||
:value="sessionPricePerKWh"
|
||||
:valueFmt="fmtAvgPrice"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
<LabelAndValue
|
||||
v-else-if="sessionInfo === SESSION.PRICE"
|
||||
:label="$t('main.loadpoint.price')"
|
||||
:value="sessionPrice"
|
||||
:valueFmt="fmtPrice"
|
||||
align="end"
|
||||
/>
|
||||
<div v-else-if="sessionInfo === SESSION.CO2">
|
||||
<LabelAndValue
|
||||
class="d-block d-sm-none"
|
||||
:label="$t('main.loadpoint.co2')"
|
||||
:value="sessionCo2PerKWh"
|
||||
:valueFmt="fmtCo2Short"
|
||||
align="end"
|
||||
/>
|
||||
<LabelAndValue
|
||||
class="d-none d-sm-block"
|
||||
:label="$t('main.loadpoint.co2')"
|
||||
:value="sessionCo2PerKWh"
|
||||
:valueFmt="fmtCo2Medium"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
<LabelAndValue
|
||||
v-else-if="chargeRemainingDurationInterpolated"
|
||||
:label="$t('main.loadpoint.remaining')"
|
||||
:value="fmtDuration(chargeRemainingDurationInterpolated)"
|
||||
align="end"
|
||||
/>
|
||||
<LabelAndValue
|
||||
v-else
|
||||
:label="$t('main.loadpoint.duration')"
|
||||
:value="fmtDuration(chargeDurationInterpolated)"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
<LoadpointSessionInfo v-bind="sessionInfoProps" />
|
||||
</div>
|
||||
<hr class="divider" />
|
||||
<Vehicle
|
||||
|
|
@ -167,7 +108,7 @@ import collector from "../mixins/collector";
|
|||
import LoadpointSettingsButton from "./LoadpointSettingsButton.vue";
|
||||
import LoadpointSettingsModal from "./LoadpointSettingsModal.vue";
|
||||
import VehicleIcon from "./VehicleIcon";
|
||||
import { getSessionInfo, setSessionInfo, SESSION } from "../sessionInfo";
|
||||
import LoadpointSessionInfo from "./LoadpointSessionInfo.vue";
|
||||
|
||||
export default {
|
||||
name: "Loadpoint",
|
||||
|
|
@ -178,6 +119,7 @@ export default {
|
|||
LabelAndValue,
|
||||
LoadpointSettingsButton,
|
||||
LoadpointSettingsModal,
|
||||
LoadpointSessionInfo,
|
||||
VehicleIcon,
|
||||
},
|
||||
mixins: [formatter, collector],
|
||||
|
|
@ -265,8 +207,6 @@ export default {
|
|||
guardRemainingInterpolated: this.guardRemaining,
|
||||
chargeDurationInterpolated: this.chargeDuration,
|
||||
chargeRemainingDurationInterpolated: this.chargeRemainingDuration,
|
||||
sessionInfo: getSessionInfo(this.id),
|
||||
SESSION,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -279,6 +219,9 @@ export default {
|
|||
phasesProps: function () {
|
||||
return this.collectProps(Phases);
|
||||
},
|
||||
sessionInfoProps: function () {
|
||||
return this.collectProps(LoadpointSessionInfo);
|
||||
},
|
||||
settingsModal: function () {
|
||||
return this.collectProps(LoadpointSettingsModal);
|
||||
},
|
||||
|
|
@ -294,17 +237,6 @@ export default {
|
|||
socBasedCharging: function () {
|
||||
return (!this.vehicleFeatureOffline && this.vehiclePresent) || this.vehicleSoc > 0;
|
||||
},
|
||||
sessionOptions: function () {
|
||||
const result = [SESSION.TIME, SESSION.SOLAR];
|
||||
if (this.currency) {
|
||||
result.push(SESSION.PRICE);
|
||||
result.push(SESSION.AVG_PRICE);
|
||||
}
|
||||
if (this.tariffCo2 > 0 || this.sessionCo2PerKWh > 0) {
|
||||
result.push(SESSION.CO2);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
phaseRemaining() {
|
||||
|
|
@ -391,25 +323,6 @@ export default {
|
|||
const inKw = value == 0 || value >= 1000;
|
||||
return this.fmtKWh(value, inKw);
|
||||
},
|
||||
fmtPrice(value) {
|
||||
return `${this.fmtMoney(value, this.currency)} ${this.fmtCurrencySymbol(
|
||||
this.currency
|
||||
)}`;
|
||||
},
|
||||
fmtAvgPrice(value) {
|
||||
return this.fmtPricePerKWh(value, this.currency, false);
|
||||
},
|
||||
fmtAvgPriceShort(value) {
|
||||
return this.fmtPricePerKWh(value, this.currency, true);
|
||||
},
|
||||
fmtSolar(value) {
|
||||
return `${this.fmtNumber(value, 1)}%`;
|
||||
},
|
||||
nextSessionInfo() {
|
||||
const index = this.sessionOptions.indexOf(this.sessionInfo);
|
||||
this.sessionInfo = this.sessionOptions[index + 1] || this.sessionOptions[0];
|
||||
setSessionInfo(this.id, this.sessionInfo);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -449,8 +362,4 @@ export default {
|
|||
margin: 0 -1.5rem;
|
||||
}
|
||||
}
|
||||
.sessionInfo {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
158
assets/js/components/LoadpointSessionInfo.vue
Normal file
158
assets/js/components/LoadpointSessionInfo.vue
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<template>
|
||||
<div class="sessionInfo">
|
||||
<LabelAndValue class="d-block" align="end">
|
||||
<template #label>
|
||||
<CustomSelect
|
||||
:selected="selectedKey"
|
||||
:options="selectOptions"
|
||||
data-testid="sessionInfoSelect"
|
||||
@change="selectOption($event.target.value)"
|
||||
>
|
||||
<span class="text-decoration-underline" data-testid="sessionInfoLabel">
|
||||
{{ label }}
|
||||
</span>
|
||||
</CustomSelect>
|
||||
</template>
|
||||
<template #value>
|
||||
<div data-testid="sessionInfoValue" @click="nextSessionInfo">
|
||||
<div :class="{ 'd-none d-sm-block': showSm }">{{ value }}</div>
|
||||
<div v-if="showSm" class="d-block d-sm-none">{{ valueSm }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</LabelAndValue>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LabelAndValue from "./LabelAndValue.vue";
|
||||
import formatter from "../mixins/formatter";
|
||||
import CustomSelect from "./CustomSelect.vue";
|
||||
import { getSessionInfo, setSessionInfo } from "../sessionInfo";
|
||||
|
||||
export default {
|
||||
name: "LoadpointSessionInfo",
|
||||
components: {
|
||||
LabelAndValue,
|
||||
CustomSelect,
|
||||
},
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
id: Number,
|
||||
sessionEnergy: Number,
|
||||
sessionCo2PerKWh: Number,
|
||||
sessionPricePerKWh: Number,
|
||||
sessionPrice: Number,
|
||||
currency: String,
|
||||
sessionSolarPercentage: Number,
|
||||
chargeRemainingDurationInterpolated: Number,
|
||||
chargeDurationInterpolated: Number,
|
||||
tariffCo2: Number,
|
||||
tariffGrid: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedKey: getSessionInfo(this.id),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
options: function () {
|
||||
const result = [
|
||||
{
|
||||
key: "remaining",
|
||||
value: this.fmtDuration(this.chargeRemainingDurationInterpolated),
|
||||
available: this.chargeRemainingDurationInterpolated > 0,
|
||||
},
|
||||
{
|
||||
key: "duration",
|
||||
value: this.fmtDuration(this.chargeDurationInterpolated),
|
||||
},
|
||||
{
|
||||
key: "solar",
|
||||
value: this.solarFormatted,
|
||||
},
|
||||
{
|
||||
key: "avgPrice",
|
||||
value: this.fmtAvgPrice(this.sessionPricePerKWh),
|
||||
valueSm: this.fmtAvgPriceShort(this.sessionPricePerKWh),
|
||||
available: this.tariffGrid !== undefined,
|
||||
},
|
||||
{
|
||||
key: "price",
|
||||
value: this.priceFormatted,
|
||||
available: this.tariffGrid !== undefined,
|
||||
},
|
||||
{
|
||||
key: "co2",
|
||||
value: this.fmtCo2Medium(this.sessionCo2PerKWh),
|
||||
valueSm: this.fmtCo2Short(this.sessionCo2PerKWh),
|
||||
available: this.tariffCo2 !== undefined,
|
||||
},
|
||||
];
|
||||
// only show options that are available
|
||||
return result.filter(({ available }) => available === undefined || available);
|
||||
},
|
||||
optionKeys() {
|
||||
return this.options.map((option) => option.key);
|
||||
},
|
||||
selectOptions() {
|
||||
return this.optionKeys.map((key) => ({
|
||||
name: this.$t(`main.loadpoint.${key}`),
|
||||
value: key,
|
||||
}));
|
||||
},
|
||||
selectedOption() {
|
||||
return (
|
||||
this.options.find((option) => option.key === this.selectedKey) || this.options[0]
|
||||
);
|
||||
},
|
||||
label() {
|
||||
return this.$t(`main.loadpoint.${this.selectedOption.key}`);
|
||||
},
|
||||
value() {
|
||||
return this.selectedOption.value;
|
||||
},
|
||||
valueSm() {
|
||||
return this.selectedOption.valueSm;
|
||||
},
|
||||
showSm() {
|
||||
return this.valueSm !== undefined;
|
||||
},
|
||||
solarFormatted() {
|
||||
return `${this.fmtNumber(this.sessionSolarPercentage, 1)}%`;
|
||||
},
|
||||
priceFormatted() {
|
||||
return `${this.fmtMoney(this.sessionPrice, this.currency)} ${this.fmtCurrencySymbol(
|
||||
this.currency
|
||||
)}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fmtAvgPrice(value) {
|
||||
return this.fmtPricePerKWh(value, this.currency, false);
|
||||
},
|
||||
fmtAvgPriceShort(value) {
|
||||
return this.fmtPricePerKWh(value, this.currency, true);
|
||||
},
|
||||
nextSessionInfo() {
|
||||
const index = this.optionKeys.indexOf(this.selectedKey);
|
||||
this.selectedKey = this.optionKeys[index + 1] || this.optionKeys[0];
|
||||
this.presist();
|
||||
},
|
||||
selectOption(value) {
|
||||
this.selectedKey = value;
|
||||
this.presist();
|
||||
},
|
||||
presist() {
|
||||
setSessionInfo(this.id, this.selectedKey);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sessionInfo * {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,10 +4,10 @@
|
|||
class="btn btn-link pe-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end"
|
||||
@click="openModal"
|
||||
>
|
||||
<span class="d-inline d-sm-none">{{
|
||||
<span class="d-inline d-sm-none text-decoration-underline">{{
|
||||
$t("footer.savings.footerShort", { percent })
|
||||
}}</span
|
||||
><span class="d-none d-sm-inline">{{
|
||||
><span class="d-none d-sm-inline text-decoration-underline">{{
|
||||
$t("footer.savings.footerLong", { percent })
|
||||
}}</span>
|
||||
<shopicon-regular-sun class="ms-2 text-evcc"></shopicon-regular-sun>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
class="btn btn-link ps-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end"
|
||||
>
|
||||
<Logo class="logo me-2" />
|
||||
v{{ installed }}
|
||||
<span class="text-decoration-underline">v{{ installed }}</span>
|
||||
<shopicon-regular-moonstars class="ms-2 text-gray-light"></shopicon-regular-moonstars>
|
||||
</a>
|
||||
<button
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
@click="openModal"
|
||||
>
|
||||
<shopicon-regular-gift class="me-2"></shopicon-regular-gift>
|
||||
v{{ installed }}
|
||||
<span class="text-decoration-underline">v{{ installed }}</span>
|
||||
<span class="ms-2 d-none d-sm-block text-gray-medium text-decoration-underline">
|
||||
{{ $t("footer.version.availableLong") }}
|
||||
</span>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
class="btn btn-link evcc-default-text ps-0 text-decoration-none text-nowrap d-flex align-items-end"
|
||||
>
|
||||
<Logo class="logo me-2" />
|
||||
v{{ installed }}
|
||||
<span class="text-decoration-underline">v{{ installed }}</span>
|
||||
</a>
|
||||
|
||||
<Teleport to="body">
|
||||
|
|
|
|||
|
|
@ -1,20 +1,7 @@
|
|||
import settings from "./settings";
|
||||
|
||||
const TIME = "time";
|
||||
const CO2 = "co2";
|
||||
const PRICE = "price";
|
||||
const AVG_PRICE = "avgPrice";
|
||||
const SOLAR = "solar";
|
||||
export const SESSION = {
|
||||
TIME,
|
||||
AVG_PRICE,
|
||||
PRICE,
|
||||
SOLAR,
|
||||
CO2,
|
||||
};
|
||||
|
||||
export function getSessionInfo(index) {
|
||||
return settings.sessionInfo[index - 1] || TIME;
|
||||
export function getSessionInfo(index, fallback) {
|
||||
return settings.sessionInfo[index - 1] || fallback;
|
||||
}
|
||||
|
||||
export function setSessionInfo(index, value) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
interval: 0.1s
|
||||
|
||||
site:
|
||||
title: Hello World
|
||||
meters:
|
||||
|
|
@ -10,10 +12,18 @@ meters:
|
|||
source: js
|
||||
script: |
|
||||
1000
|
||||
- name: charger_meter
|
||||
type: custom
|
||||
power:
|
||||
source: js
|
||||
script: |
|
||||
500
|
||||
|
||||
loadpoints:
|
||||
- title: Carport
|
||||
charger: charger
|
||||
meter: charger_meter
|
||||
mode: now
|
||||
|
||||
chargers:
|
||||
- name: charger
|
||||
|
|
@ -24,11 +34,12 @@ chargers:
|
|||
enabled:
|
||||
source: js
|
||||
script: |
|
||||
false
|
||||
true
|
||||
status:
|
||||
source: js
|
||||
script: |
|
||||
"B"
|
||||
"C"
|
||||
maxcurrent:
|
||||
source: js
|
||||
script:
|
||||
script: |
|
||||
16
|
||||
|
|
|
|||
|
|
@ -35,3 +35,24 @@ test.describe("main screen", async () => {
|
|||
await expect(page.getByRole("heading", { name: "Guest vehicle" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("session info", async () => {
|
||||
test("default", async ({ page }) => {
|
||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
|
||||
});
|
||||
test("change value", async ({ page }) => {
|
||||
// by select
|
||||
await page.getByTestId("sessionInfoSelect").first().selectOption({ label: "Solar" });
|
||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
|
||||
// by click on value
|
||||
await page.getByTestId("sessionInfoValue").first().click();
|
||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
|
||||
});
|
||||
test("keep selection on reload", async ({ page }) => {
|
||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
|
||||
await page.getByTestId("sessionInfoSelect").first().selectOption({ label: "Solar" });
|
||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue