Introduce Storybook and refactor Vue components (#524)

This commit is contained in:
andig 2020-12-23 17:56:26 +01:00 • committed by GitHub
parent 95cfe455b8
commit 2d3bf1bec9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 17513 additions and 8161 deletions

View file

@ -1,3 +1,5 @@
.cache
.storybook
.vscode
*.conf
*.Dockerfile
@ -5,7 +7,6 @@
*.sh
*.yaml
Dockerfile
.cache
dist
node_modules
!entrypoint.sh

4
.storybook/main.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
stories: ["../assets/js/**/*.stories.mdx", "../assets/js/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
};

View file

@ -0,0 +1,2 @@
<link href="/css/bootstrap.min.css" rel="stylesheet" />
<link href="/css/app.css" rel="stylesheet" />

4
.storybook/preview.js Normal file
View file

@ -0,0 +1,4 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
}

View file

@ -2,40 +2,11 @@ import "popper.js";
import "bootstrap";
import Vue from "vue";
import axios from "axios";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
import { faArrowDown } from "@fortawesome/free-solid-svg-icons/faArrowDown";
import { faBatteryThreeQuarters } from "@fortawesome/free-solid-svg-icons/faBatteryThreeQuarters";
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
import { faThermometerHalf } from "@fortawesome/free-solid-svg-icons/faThermometerHalf";
import { faLeaf } from "@fortawesome/free-solid-svg-icons/faLeaf";
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import App from "./views/App";
import Toasts from "./components/Toasts";
import router from "./router";
import store from "./store";
library.add(
faSun,
faArrowUp,
faArrowDown,
faBatteryThreeQuarters,
faTemperatureLow,
faTemperatureHigh,
faThermometerHalf,
faLeaf,
faChevronUp,
faChevronDown,
faExclamationTriangle
);
Vue.component("fa-icon", FontAwesomeIcon);
const loc = window.location;
axios.defaults.baseURL =
loc.protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname + "api";

View file

@ -0,0 +1,29 @@
import Loadpoint from "./Loadpoint.vue";
export default {
title: "Main/Loadpoint",
component: Loadpoint,
argTypes: {
mode: { control: { type: "inline-radio", options: ["off", "now", "minpv", "pv"] } },
climater: { control: { type: "inline-radio", options: ["on", "heating", "cooling"] } },
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Loadpoint },
template: '<Loadpoint v-bind="$props"></Loadpoint>',
});
export const Base = Template.bind({});
Base.args = {
id: 0,
pvConfigured: true,
};
export const WithLevels = Template.bind({});
WithLevels.args = {
id: 0,
pvConfigured: true,
socLevels: [20, 50, 80, 100],
};

View file

@ -2,11 +2,11 @@
<div>
<div class="row" v-if="multi">
<div class="col-12 col-md-4 d-md-flex mt-3 mt-md-5 align-items-end">
<span class="h1 align-bottom">{{ state.title || "Ladepunkt" }}</span>
<span class="h1 align-bottom">{{ title || "Ladepunkt" }}</span>
</div>
<div class="col-12 col-md-8 d-none d-md-block mt-3 mt-md-5">
<LoadpointDetails v-bind:state="state"></LoadpointDetails>
<LoadpointDetails v-bind="details"> </LoadpointDetails>
</div>
<div class="col-12 d-md-none">
@ -14,17 +14,17 @@
<div class="col-12 mt-3">
<Mode
class="w-100"
v-bind:mode="state.mode"
:pv="pv"
v-on:updated="targetMode"
:mode="mode"
:pvConfigured="pvConfigured"
v-on:updated="setTargetMode"
></Mode>
</div>
<div class="col-12 mt-3" v-if="hasTargetSoC">
<Soc
class="w-100"
v-bind:soc="state.targetSoC"
:levels="state.socLevels"
v-on:updated="targetSoC"
:soc="targetSoC"
:levels="socLevels"
v-on:updated="setTargetSoC"
></Soc>
</div>
</div>
@ -32,23 +32,20 @@
</div>
<div class="row d-none d-md-flex mt-5 py-3 pb-4 text-center bg-light" v-if="!multi">
<div
class="mt-3"
v-bind:class="{ 'col-md-6': hasTargetSoC, 'col-md-12': !hasTargetSoC }"
>
<div class="mt-3" :class="{ 'col-md-6': hasTargetSoC, 'col-md-12': !hasTargetSoC }">
<Mode
v-bind:mode="state.mode"
:pv="pv"
:mode="mode"
:pvConfigured="pvConfigured"
:caption="true"
v-on:updated="targetMode"
v-on:updated="setTargetMode"
></Mode>
</div>
<div class="col-md-6 mt-3" v-if="hasTargetSoC">
<Soc
v-bind:soc="state.targetSoC"
:levels="state.socLevels"
:soc="targetSoC"
:levels="socLevels"
:caption="true"
v-on:updated="targetSoC"
v-on:updated="setTargetSoC"
></Soc>
</div>
</div>
@ -57,35 +54,32 @@
<div class="col-12 mt-3">
<Mode
class="w-100"
v-bind:mode="state.mode"
:pv="pv"
v-on:updated="targetMode"
:mode="mode"
:pvConfigured="pvConfigured"
v-on:updated="setTargetMode"
></Mode>
</div>
<div class="col-12 mt-3" v-if="hasTargetSoC">
<Soc
class="w-100"
v-bind:soc="state.targetSoC"
:levels="state.socLevels"
v-on:updated="targetSoC"
:soc="targetSoC"
:levels="socLevels"
v-on:updated="setTargetSoC"
></Soc>
</div>
</div>
<div class="row" v-if="!multi">
<div class="col-12 col-md-4 d-none d-md-flex mt-3 mt-md-5">
<span class="h1">{{ state.title || "Ladepunkt" }}</span>
<span class="h1">{{ title || "Ladepunkt" }}</span>
</div>
<div
class="col-12 col-md-8 d-flex d-md-flex mt-3 mt-md-5 pt-3"
v-if="state.remoteDisabled"
>
<div class="col-12 col-md-8 d-flex d-md-flex mt-3 mt-md-5 pt-3" v-if="remoteDisabled">
<h5 class="w-100">
<span class="badge badge-warning w-100" v-if="state.remoteDisabled == 'soft'">
{{ state.remoteDisabledSource }}: Adaptives PV-Laden deaktiviert
<span class="badge badge-warning w-100" v-if="remoteDisabled == 'soft'">
{{ remoteDisabledSource }}: Adaptives PV-Laden deaktiviert
</span>
<span class="badge badge-danger w-100" v-if="state.remoteDisabled == 'hard'">
{{ state.remoteDisabledSource }}: Deaktiviert
<span class="badge badge-danger w-100" v-if="remoteDisabled == 'hard'">
{{ remoteDisabledSource }}: Deaktiviert
</span>
</h5>
</div>
@ -95,34 +89,41 @@
<div class="row">
<div class="col-12 col-md-4 mt-3 mb-3 mb-md-0">
<Vehicle v-bind:state="state"></Vehicle>
<Vehicle
:charging="charging"
:connected="connected"
:minSoC="minSoC"
:soc="soc"
:socCharge="socCharge"
:socTitle="socTitle"
></Vehicle>
</div>
<div class="col-12 col-md-4 d-none d-md-block mt-3" v-if="multi">
<div class="mb-2">Modus</div>
<Mode
class="btn-group-sm"
v-bind:mode="state.mode"
:pv="pv"
v-on:updated="targetMode"
:mode="mode"
:pvConfigured="pvConfigured"
v-on:updated="setTargetMode"
></Mode>
</div>
<div class="col-12 col-md-4 d-none d-md-block mt-3" v-if="multi && hasTargetSoC">
<div class="mb-2">Ladeziel</div>
<Soc
class="btn-group-sm"
v-bind:soc="state.targetSoC"
:levels="state.socLevels"
v-on:updated="targetSoC"
:soc="targetSoC"
:levels="socLevels"
v-on:updated="setTargetSoC"
></Soc>
</div>
<div class="col-md-8 d-none d-md-block" v-if="!multi">
<LoadpointDetails v-bind:state="state"></LoadpointDetails>
<LoadpointDetails v-bind="details"></LoadpointDetails>
</div>
<div class="col-12 d-md-none">
<LoadpointDetails v-bind:state="state"></LoadpointDetails>
<LoadpointDetails v-bind="details"></LoadpointDetails>
</div>
</div>
</div>
@ -138,28 +139,72 @@ import formatter from "../mixins/formatter";
export default {
name: "Loadpoint",
props: ["state", "id", "pv", "multi"],
props: {
id: Number,
multi: Boolean,
pvConfigured: Boolean,
// main
title: String,
mode: String,
targetSoC: Number,
socLevels: Array,
remoteDisabled: Boolean,
remoteDisabledSource: String,
chargeDuration: Number,
charging: Boolean,
// vehicle
connected: Boolean,
// charging: Boolean,
socTitle: String,
socCharge: Number,
minSoC: Number,
// details
chargePower: Number,
chargedEnergy: Number,
// chargeDuration: Number,
soc: Boolean,
climater: String,
range: Number,
chargeEstimate: Number,
},
components: { LoadpointDetails, Soc, Mode, Vehicle },
mixins: [formatter],
data: function () {
return {
tickerHandle: null,
chargeDurationDisplayed: null,
};
},
computed: {
details: function () {
return {
connected: this.connected,
minSoC: this.minSoC,
socCharge: this.socCharge,
climater: this.climater,
chargePower: this.chargePower,
chargedEnergy: this.chargedEnergy,
range: this.range,
chargeDuration: this.chargeDurationDisplayed,
chargeEstimate: this.chargeEstimate,
};
},
hasTargetSoC: function () {
return this.state.socLevels != null && this.state.socLevels.length > 0;
return this.socLevels != null && this.socLevels.length > 0;
},
},
watch: {
"state.chargeDuration": function () {
chargeDuration: function () {
window.clearInterval(this.tickerHandle);
// only ticker if actually charging
if (this.state.charging && this.state.chargeDuration >= 0) {
if (this.charging && this.chargeDuration >= 0) {
this.chargeDurationDisplayed = this.chargeDuration;
this.tickerHandle = window.setInterval(
function () {
// eslint-disable-next-line vue/no-mutating-props
this.state.chargeDuration += 1;
this.chargeDurationDisplayed += 1;
}.bind(this),
1000
);
@ -170,24 +215,24 @@ export default {
api: function (func) {
return "loadpoints/" + this.id + "/" + func;
},
targetMode: function (mode) {
setTargetMode: function (mode) {
axios
.post(this.api("mode") + "/" + mode)
.then(
function (response) {
// eslint-disable-next-line vue/no-mutating-props
this.state.mode = response.data.mode;
this.mode = response.data.mode;
}.bind(this)
)
.catch(window.toasts.error);
},
targetSoC: function (soc) {
setTargetSoC: function (soc) {
axios
.post(this.api("targetsoc") + "/" + soc)
.then(
function (response) {
// eslint-disable-next-line vue/no-mutating-props
this.state.targetSoC = response.data.targetSoC;
this.targetSoC = response.data.targetSoC;
}.bind(this)
)
.catch(window.toasts.error);

View file

@ -0,0 +1,55 @@
import LoadpointDetails from "./LoadpointDetails.vue";
export default {
title: "Main/LoadpointDetails",
component: LoadpointDetails,
argTypes: {
climater: { control: { type: "inline-radio", options: ["on", "heating", "cooling"] } },
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { LoadpointDetails },
template: '<LoadpointDetails v-bind="$props"></LoadpointDetails>',
});
export const Base = Template.bind({});
Base.args = {};
export const Charging = Template.bind({});
Charging.args = {
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
};
export const Vehicle = Template.bind({});
Vehicle.args = {
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
soc: true,
chargeEstimate: 5 * 3600,
};
export const VehicleRange = Template.bind({});
VehicleRange.args = {
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
soc: true,
range: 240.123,
chargeEstimate: 5 * 3600,
};
export const VehicleClimater = Template.bind({});
VehicleClimater.args = {
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
soc: true,
range: 240.123,
chargeEstimate: 5 * 3600,
climater: "on",
};

View file

@ -6,36 +6,36 @@
<fa-icon
class="text-primary ml-1"
icon="temperature-low"
v-if="state.climater == 'heating'"
v-if="climater == 'heating'"
></fa-icon>
<fa-icon
class="text-primary ml-1"
icon="temperature-high"
v-if="state.climater == 'cooling'"
v-if="climater == 'cooling'"
></fa-icon>
<fa-icon
class="text-primary ml-1"
icon="thermometer-half"
v-if="state.climater == 'on'"
v-if="climater == 'on'"
></fa-icon>
</div>
<h2 class="value">
{{ fmt(state.chargePower) }}
<small class="text-muted">{{ fmtUnit(state.chargePower) }}W</small>
{{ fmt(chargePower) }}
<small class="text-muted">{{ fmtUnit(chargePower) }}W</small>
</h2>
</div>
<div class="col-6 col-md-3 mt-3">
<div class="mb-2 value">Geladen</div>
<h2 class="value">
{{ fmt(state.chargedEnergy) }}
<small class="text-muted">{{ fmtUnit(state.chargedEnergy) }}Wh</small>
{{ fmt(chargedEnergy) }}
<small class="text-muted">{{ fmtUnit(chargedEnergy) }}Wh</small>
</h2>
</div>
<div class="col-6 col-md-3 mt-3" v-if="state.range >= 0">
<div class="col-6 col-md-3 mt-3" v-if="range >= 0">
<div class="mb-2 value">Reichweite</div>
<h2 class="value">
{{ state.range }}
{{ Math.round(range) }}
<small class="text-muted">km</small>
</h2>
</div>
@ -43,36 +43,36 @@
<div class="col-6 col-md-3 mt-3" v-else>
<div class="mb-2 value">Dauer</div>
<h2 class="value">
{{ fmtShortDuration(state.chargeDuration) }}
<small class="text-muted">{{ fmtShortDurationUnit(state.chargeDuration) }}</small>
{{ fmtShortDuration(chargeDuration) }}
<small class="text-muted">{{ fmtShortDurationUnit(chargeDuration) }}</small>
</h2>
</div>
<div class="col-6 col-md-3 mt-3" v-if="state.soc">
<div class="col-6 col-md-3 mt-3" v-if="soc">
<div class="mb-2 value">Restzeit</div>
<h2 class="value">
{{ fmtShortDuration(state.chargeEstimate) }}
<small class="text-muted">{{ fmtShortDurationUnit(state.chargeEstimate) }}</small>
{{ fmtShortDuration(chargeEstimate) }}
<small class="text-muted">{{ fmtShortDurationUnit(chargeEstimate) }}</small>
</h2>
</div>
</div>
</template>
<script>
import "../icons";
import formatter from "../mixins/formatter";
export default {
name: "LoadpointDetails",
props: ["state"],
mixins: [formatter],
computed: {
minSoCActive: function () {
return (
this.state.connected &&
this.state.minSoC > 0 &&
this.state.socCharge < this.state.minSoC
);
},
props: {
chargePower: Number,
chargedEnergy: Number,
chargeDuration: Number,
soc: Boolean,
climater: String,
range: Number,
chargeEstimate: Number,
},
mixins: [formatter],
};
</script>

View file

@ -0,0 +1,25 @@
import Mode from "./Mode.vue";
export default {
title: "Main/Mode",
component: Mode,
argTypes: {
mode: { control: { type: "inline-radio", options: ["off", "now", "minpv", "pv"] } },
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Mode },
template: '<Mode v-bind="$props"></Mode>',
});
export const Base = Template.bind({});
Base.args = {};
export const CaptionAndPV = Template.bind({});
CaptionAndPV.args = {
caption: true,
pvConfigured: true,
mode: "pv",
};

View file

@ -6,22 +6,27 @@
>
Modus
</label>
<label class="btn btn-outline-primary" :class="{ active: mode == 'off', first: !caption }">
<input type="radio" value="off" v-on:click="setTargetMode('off')" />Stop
</label>
<label class="btn btn-outline-primary" :class="{ active: mode == 'now' }">
<input type="radio" value="now" v-on:click="setTargetMode('now')" />Sofort
</label>
<label
class="btn btn-outline-primary"
v-bind:class="{ active: mode == 'off', first: !caption }"
:class="{ active: mode == 'minpv' }"
v-if="pvConfigured"
>
<input type="radio" value="off" v-on:click="targetMode('off')" />Stop
</label>
<label class="btn btn-outline-primary" v-bind:class="{ active: mode == 'now' }">
<input type="radio" value="now" v-on:click="targetMode('now')" />Sofort
</label>
<label class="btn btn-outline-primary" v-bind:class="{ active: mode == 'minpv' }" v-if="pv">
<input type="radio" value="minpv" v-on:click="targetMode('minpv')" />
<input type="radio" value="minpv" v-on:click="setTargetMode('minpv')" />
<span class="d-inline d-lg-none">Min</span>
<span class="d-none d-lg-inline">Min + PV</span>
</label>
<label class="btn btn-outline-primary" v-bind:class="{ active: mode == 'pv' }" v-if="pv">
<input type="radio" value="pv" v-on:click="targetMode('pv')" />
<label
class="btn btn-outline-primary"
:class="{ active: mode == 'pv' }"
v-if="pvConfigured"
>
<input type="radio" value="pv" v-on:click="setTargetMode('pv')" />
<span class="d-inline d-md-none">PV</span>
<span class="d-none d-md-inline">Nur PV</span>
</label>
@ -31,9 +36,13 @@
<script>
export default {
name: "Mode",
props: ["mode", "pv", "caption"],
props: {
mode: String,
pvConfigured: Boolean,
caption: Boolean,
},
methods: {
targetMode: function (mode) {
setTargetMode: function (mode) {
this.$emit("updated", mode);
},
},

View file

@ -0,0 +1,62 @@
import Site from "./Site.vue";
export default {
title: "Main/Site",
component: Site,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Site },
template: '<Site v-bind="$props"></Site>',
});
export const Base = Template.bind({});
Base.args = {
gridConfigured: true,
pvConfigured: true,
gridPower: 100,
pvPower: 100,
batteryConfigured: true,
batteryPower: 100,
batterySoC: 0,
loadpoints: [],
};
export const Single = Template.bind({});
Single.args = {
gridConfigured: true,
pvConfigured: true,
gridPower: 100,
pvPower: 100,
batteryConfigured: true,
batteryPower: 100,
batterySoC: 0,
loadpoints: [
{
title: "Ladepunkt 1",
socLevels: [20, 50, 80, 100],
},
],
};
export const Multi = Template.bind({});
Multi.args = {
gridConfigured: true,
pvConfigured: true,
gridPower: 100,
pvPower: 100,
batteryConfigured: true,
batteryPower: 100,
batterySoC: 0,
loadpoints: [
{
title: "Ladepunkt 1",
socLevels: [20, 50, 80, 100],
},
{
title: "Ladepunkt 2",
},
],
};

View file

@ -2,10 +2,18 @@
<div>
<div class="row">
<div class="d-none d-md-flex col-12 col-md-4 mt-md-4 align-items-end">
<p class="h1">{{ state.title || "Home" }}</p>
<p class="h1">{{ title || "Home" }}</p>
</div>
<div class="col-12 col-md-8 mt-md-4" v-if="multi">
<SiteDetails v-bind:state="state"></SiteDetails>
<SiteDetails
:gridConfigured="gridConfigured"
:gridPower="gridPower"
:pvConfigured="pvConfigured"
:pvPower="pvPower"
:batteryConfigured="batteryConfigured"
:batteryPower="batteryPower"
:batterySoC="batterySoC"
></SiteDetails>
</div>
</div>
@ -14,17 +22,25 @@
<div class="row" v-if="!multi">
<div class="d-none d-md-block col-md-4"></div>
<div class="col-12 col-md-8">
<SiteDetails v-bind:state="state"></SiteDetails>
<SiteDetails
:gridConfigured="gridConfigured"
:gridPower="gridPower"
:pvConfigured="pvConfigured"
:pvPower="pvPower"
:batteryConfigured="batteryConfigured"
:batteryPower="batteryPower"
:batterySoC="batterySoC"
></SiteDetails>
</div>
</div>
<Loadpoint
v-for="(loadpoint, id) in state.loadpoints"
v-bind:id="id"
v-for="(loadpoint, id) in loadpoints"
v-bind="loadpoint"
:id="id"
:key="id"
:state="loadpoint"
:pv="state.gridConfigured"
:multi="multi"
:pvConfigured="pvConfigured"
>
</Loadpoint>
</div>
@ -37,13 +53,25 @@ import formatter from "../mixins/formatter";
export default {
name: "Site",
props: ["state"],
props: {
title: String,
loadpoints: Array,
// details
gridConfigured: Boolean,
gridPower: Number,
pvConfigured: Boolean,
pvPower: Number,
batteryConfigured: Boolean,
batteryPower: Number,
batterySoC: Number,
},
components: { SiteDetails, Loadpoint },
mixins: [formatter],
computed: {
multi: function () {
// TODO fix compact
return this.state.loadpoints.length > 1 /* || app.compact*/;
return this.loadpoints.length > 1 /* || app.compact*/;
},
},
};

View file

@ -0,0 +1,32 @@
import SiteDetails from "./SiteDetails.vue";
export default {
title: "Main/SiteDetails",
component: SiteDetails,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { SiteDetails },
template: '<SiteDetails v-bind="$props"></SiteDetails>',
});
export const GridAndPV = Template.bind({});
GridAndPV.args = {
gridConfigured: true,
pvConfigured: true,
gridPower: 100,
pvPower: 100,
};
export const WithBattery = Template.bind({});
WithBattery.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
gridPower: 100,
pvPower: 100,
batteryPower: 100,
batterySoC: 0,
};

View file

@ -1,74 +1,75 @@
<template>
<div class="row">
<div class="col-6 col-md-3 mt-3" v-if="state.gridConfigured">
<div class="mb-2 value" v-if="state.gridPower > 0">
Bezug <fa-icon icon="arrow-down" class="text-primary"></fa-icon>
<div class="col-6 col-md-3 mt-3" v-if="gridConfigured">
<div class="mb-2 value" v-if="gridPower > 0">
Bezug <fa-icon icon="arrow-down" class="text-primary" />
</div>
<div class="mb-2 value" v-else>
Einspeisung <fa-icon icon="arrow-up" class="text-primary"></fa-icon>
</div>
<h2 class="value">
{{ fmt(state.gridPower) }}
<small class="text-muted">{{ fmtUnit(state.gridPower) }}W</small>
{{ fmt(gridPower) }}
<small class="text-muted">{{ fmtUnit(gridPower) }}W</small>
</h2>
</div>
<div class="col-6 col-md-3 mt-3" v-if="state.pvConfigured">
<div class="col-6 col-md-3 mt-3" v-if="pvConfigured">
<div class="mb-2 value">
Erzeugung
<fa-icon
icon="sun"
v-bind:class="{
'text-primary': state.pvPower < 0,
'text-muted': state.pvPower >= 0,
:class="{
'text-primary': pvPower < 0,
'text-muted': pvPower >= 0,
}"
></fa-icon>
</div>
<h2 class="value">
{{ fmt(state.pvPower) }}
<small class="text-muted">{{ fmtUnit(state.pvPower) }}W</small>
{{ fmt(pvPower) }}
<small class="text-muted">{{ fmtUnit(pvPower) }}W</small>
</h2>
</div>
<div class="d-md-block col-6 col-md-3 mt-3" v-if="state.batteryConfigured">
<div class="d-md-block col-6 col-md-3 mt-3" v-if="batteryConfigured">
<div class="mb-2 value">
Batterie
<fa-icon
class="text-primary"
icon="arrow-down"
v-if="state.batteryPower < 0"
></fa-icon>
<fa-icon
class="text-primary"
icon="arrow-up"
v-if="state.batteryPower > 0"
></fa-icon>
<fa-icon class="text-primary" icon="arrow-down" v-if="batteryPower < 0"></fa-icon>
<fa-icon class="text-primary" icon="arrow-up" v-if="batteryPower > 0"></fa-icon>
</div>
<h2 class="value">
{{ fmt(state.batteryPower) }}
<small class="text-muted">{{ fmtUnit(state.batteryPower) }}W</small>
{{ fmt(batteryPower) }}
<small class="text-muted">{{ fmtUnit(batteryPower) }}W</small>
</h2>
</div>
<div class="col-6 col-md-3 mt-3" v-if="state.batteryConfigured">
<div class="col-6 col-md-3 mt-3" v-if="batteryConfigured">
<div class="mb-2 value">
SoC
<fa-icon
icon="battery-three-quarters"
v-bind:class="{
'text-primary': state.batteryPower > 0,
'text-muted': state.batteryPower < 0,
:class="{
'text-primary': batteryPower > 0,
'text-muted': batteryPower < 0,
}"
></fa-icon>
</div>
<h2 class="value">{{ state.batterySoC || 0 }} <small class="text-muted">%</small></h2>
<h2 class="value">{{ batterySoC }} <small class="text-muted">%</small></h2>
</div>
</div>
</template>
<script>
import "../icons";
import formatter from "../mixins/formatter";
export default {
name: "SiteDetails",
props: ["state"],
props: {
gridConfigured: Boolean,
gridPower: Number,
pvConfigured: Boolean,
pvPower: Number,
batteryConfigured: Boolean,
batteryPower: Number,
batterySoC: Number,
},
mixins: [formatter],
};
</script>

View file

@ -0,0 +1,26 @@
import Soc from "./Soc.vue";
export default {
title: "Main/Soc",
component: Soc,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Soc },
template: '<Soc v-bind="$props"></Soc>',
});
export const Base = Template.bind({});
Base.args = {
soc: 80,
levels: [20, 50, 80, 100],
};
export const Caption = Template.bind({});
Caption.args = {
caption: true,
soc: 80,
levels: [20, 50, 80, 100],
};

View file

@ -7,11 +7,11 @@
class="btn btn-outline-primary"
v-for="(level, id) in levelsOrDefault"
:key="id"
v-bind:level="level"
:level="level"
:id="id"
:class="{ active: soc == level, first: !caption && id == 0 }"
>
<input type="radio" v-bind:value="level" v-on:click="targetSoC(level)" />{{ level }}%
<input type="radio" :value="level" v-on:click="setTargetSoC(level)" />{{ level }}%
</label>
</div>
</template>
@ -19,7 +19,11 @@
<script>
export default {
name: "Soc",
props: ["soc", "caption", "levels"],
props: {
soc: Number,
caption: Boolean,
levels: Array,
},
computed: {
levelsOrDefault: function () {
if (this.levels == null || this.levels.length == 0) {
@ -29,7 +33,7 @@ export default {
},
},
methods: {
targetSoC: function (mode) {
setTargetSoC: function (mode) {
this.$emit("updated", mode);
},
},

View file

@ -1,5 +1,5 @@
<template>
<div class="toast" data-delay="10000" v-bind:data-autohide="true">
<div class="toast" data-delay="10000" :data-autohide="true">
<div class="toast-header">
<strong class="mr-auto" v-if="item.type != 'warn'"
><fa-icon class="text-danger" icon="exclamation-triangle"></fa-icon> Error</strong
@ -18,10 +18,11 @@
</template>
<script>
import "../icons";
import $ from "jquery";
export default {
name: "MessageToast",
name: "ToastMessage",
props: ["item"],
mounted: function () {
const id = "#message-id-" + this.item.id;

View file

@ -0,0 +1,22 @@
import Toasts from "./Toasts.vue";
export default {
title: "Main/Toasts",
component: Toasts,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Toasts },
template: '<Toasts v-bind="$props"></Toasts>',
});
export const Base = Template.bind({});
Base.args = {
items: {
id: 1,
message: "Evil warning",
type: "warn",
},
};

View file

@ -1,22 +1,25 @@
<template>
<div aria-atomic="true" style="position: absolute; top: 4rem; right: 0.5rem">
<!-- Position it -->
<MessageToast
<ToastMessage
v-for="item in items"
v-bind:item="item"
:item="item"
:id="'message-id-' + item.id"
:key="item.id"
>
</MessageToast>
</ToastMessage>
</div>
</template>
<script>
import MessageToast from "./MessageToast";
import "../icons";
import ToastMessage from "./ToastMessage";
export default {
name: "Toasts",
components: { MessageToast },
props: ["items", "count"],
components: { ToastMessage },
props: {
items: Object,
},
};
</script>

View file

@ -0,0 +1,37 @@
import Vehicle from "./Vehicle.vue";
export default {
title: "Main/Vehicle",
component: Vehicle,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Vehicle },
template: '<Vehicle v-bind="$props"></Vehicle>',
});
export const Base = Template.bind({});
Base.args = {
soc: false,
socTitle: "Mein Auto",
connected: true,
};
export const Car = Template.bind({});
Car.args = {
soc: true,
socTitle: "Mein Auto",
connected: true,
socCharge: 15,
};
export const CarWithMin = Template.bind({});
CarWithMin.args = {
soc: true,
socTitle: "Mein Auto",
connected: true,
socCharge: 15,
minSoC: 30,
};

View file

@ -1,31 +1,31 @@
<template>
<div>
<div class="mb-2">{{ state.socTitle || "Fahrzeug" }}</div>
<div class="mb-2">{{ socTitle || "Fahrzeug" }}</div>
<div class="progress" style="height: 24px; font-size: 100%; margin-top: 16px">
<div
class="progress-bar"
role="progressbar"
v-bind:class="{
'progress-bar-striped': state.charging,
'progress-bar-animated': state.charging,
'bg-light': !state.connected,
'text-secondary': !state.connected,
'bg-warning': state.connected && minSoCActive,
:class="{
'progress-bar-striped': charging,
'progress-bar-animated': charging,
'bg-light': !connected,
'text-secondary': !connected,
'bg-warning': connected && minSoCActive,
}"
v-bind:style="{ width: socChargeDisplayWidth + '%' }"
:style="{ width: socChargeDisplayWidth + '%' }"
>
{{ socChargeDisplayValue }}
</div>
<div
class="progress-bar"
role="progressbar"
v-bind:class="{
'progress-bar-striped': state.charging,
'progress-bar-animated': state.charging,
:class="{
'progress-bar-striped': charging,
'progress-bar-animated': charging,
'bg-warning': true,
'bg-muted': true,
}"
v-bind:style="{ width: minSoCRemainingDisplayWidth + '%' }"
:style="{ width: minSoCRemainingDisplayWidth + '%' }"
v-if="minSoCActive && socChargeDisplayWidth < 100"
></div>
</div>
@ -35,38 +35,45 @@
<script>
export default {
name: "Vehicle",
props: ["state"],
props: {
socTitle: String,
connected: Boolean,
charging: Boolean,
soc: Boolean,
socCharge: Number,
minSoC: Number,
},
computed: {
socChargeDisplayWidth: function () {
if (this.state.soc && this.state.socCharge >= 0) {
return this.state.socCharge;
if (this.soc && this.socCharge >= 0) {
return this.socCharge;
}
return 100;
},
socChargeDisplayValue: function () {
// no soc or no soc value
if (!this.state.soc || this.state.socCharge < 0) {
if (!this.soc || this.socCharge < 0) {
let chargeStatus = "getrennt";
if (this.state.charging) {
if (this.charging) {
chargeStatus = "laden";
} else if (this.state.connected) {
} else if (this.connected) {
chargeStatus = "verbunden";
}
return chargeStatus;
}
// percent value if enough space
let socCharge = this.state.socCharge;
let socCharge = this.socCharge;
if (socCharge >= 10) {
socCharge += "%";
}
return socCharge;
},
minSoCActive: function () {
return this.state.minSoC > 0 && this.state.socCharge < this.state.minSoC;
return this.minSoC > 0 && this.socCharge < this.minSoC;
},
minSoCRemainingDisplayWidth: function () {
return this.state.minSoC - this.state.socCharge;
return this.minSoC - this.socCharge;
},
},
};

View file

@ -0,0 +1,22 @@
import Version from "./Version.vue";
export default {
title: "Main/Version",
component: Version,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Version },
template: '<Version v-bind="$props"></Version>',
});
export const Base = Template.bind({});
Base.args = {
installed: "0.36",
available: "0.40",
// soc: false,
// socTitle: "Mein Auto",
// connected: true,
};

View file

@ -2,13 +2,12 @@
<div class="collapse" ref="bar">
<div class="row p-3 bg-warning">
<div class="col-12">
Neue Version verfügbar! Installiert: {{ installed }}. Verfügbar:
{{ state.availableVersion }}.
Neue Version verfügbar! Installiert: {{ installed }}. Verfügbar: {{ available }}.
<b
class="px-3"
data-toggle="collapse"
data-target="#release-notes"
v-if="state.releaseNotes"
v-if="releaseNotes"
>
<a href="#" class="text-body">
Release notes
@ -18,9 +17,7 @@
</b>
<b class="px-3">
<a
v-bind:href="
'https://github.com/andig/evcc/releases/tag/' + state.availableVersion
"
:href="'https://github.com/andig/evcc/releases/tag/' + available"
class="text-body"
>
Download <fa-icon icon="chevron-down"></fa-icon>
@ -39,20 +36,24 @@
</div>
</div>
<div class="row p-3 bg-light collapse" id="release-notes" ref="notes">
<div class="col-12" v-html="state.releaseNotes"></div>
<div class="col-12" v-html="releaseNotes"></div>
</div>
</div>
</template>
<script>
import "../icons";
import $ from "jquery";
export default {
name: "Version",
props: ["installed"],
props: {
installed: String,
available: String,
releaseNotes: String,
},
data: function () {
return {
state: this.$root.$data.store.state,
notesShown: false,
};
},
@ -72,11 +73,11 @@ export default {
);
},
watch: {
"state.availableVersion": function () {
available: function () {
if (
this.installed != window.evcc.version && // go template parsed?
this.installed != "[[.Version]]" && // go template parsed?
this.installed != "0.0.1-alpha" && // make used?
this.state.availableVersion != this.installed
this.available != this.installed
) {
$(this.$refs.bar).collapse("show");
}

30
assets/js/icons.js Normal file
View file

@ -0,0 +1,30 @@
import Vue from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
import { faArrowDown } from "@fortawesome/free-solid-svg-icons/faArrowDown";
import { faBatteryThreeQuarters } from "@fortawesome/free-solid-svg-icons/faBatteryThreeQuarters";
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
import { faThermometerHalf } from "@fortawesome/free-solid-svg-icons/faThermometerHalf";
import { faLeaf } from "@fortawesome/free-solid-svg-icons/faLeaf";
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
library.add(
faSun,
faArrowUp,
faArrowDown,
faBatteryThreeQuarters,
faTemperatureLow,
faTemperatureHigh,
faThermometerHalf,
faLeaf,
faChevronUp,
faChevronDown,
faExclamationTriangle
);
Vue.component("fa-icon", FontAwesomeIcon);

View file

@ -31,13 +31,19 @@
</div>
</nav>
<Version :installed="installed" id="version-bar"></Version>
<Version
id="version-bar"
:installed="installedVersion"
:available="store.state.availableVersion"
:releaseNotes="store.state.releaseNotes"
></Version>
<router-view></router-view>
</div>
</template>
<script>
import "../icons";
import Version from "../components/Version";
import store from "../store";
@ -48,7 +54,7 @@ export default {
return {
compact: false,
store: this.$root.$data.store,
installed: window.evcc.version,
installedVersion: window.evcc.version,
};
},
methods: {

View file

@ -1,11 +1,11 @@
<template>
<div class="container">
<!-- <h1 class="display-4 pt-3 mx-auto text-center">Konfiguration</h1>
<p class="lead mx-auto text-center">Details der Fahrzeug-, Wallbox- und Zählerkonfiguration.</p> -->
<p class="lead mx-auto text-center">Details der Fahrzeug-, Wallbox- und Zählerkonfiguration.</p> -->
<div class="row mt-4 border-bottom">
<div class="col-12">
<p class="h1">{{ state.title || "Home" }}</p>
<p class="h1">{{ title || "Home" }}</p>
</div>
</div>
@ -13,24 +13,24 @@
<div class="col-md-4"></div>
<div class="col-6 col-md-2 py-3">
Netzzähler:
<span class="text-primary" v-if="state.gridConfigured">✓</span>
<span class="text-primary" v-if="gridConfigured">✓</span>
<span class="text-primary" v-else>&mdash;</span>
</div>
<div class="col-6 col-md-2 py-3">
PV Zähler:
<span class="text-primary" v-if="state.pvConfigured">✓</span>
<span class="text-primary" v-if="pvConfigured">✓</span>
<span class="text-primary" v-else>&mdash;</span>
</div>
<div class="col-6 col-md-2 py-3">
Batteriezähler:
<span class="text-primary" v-if="state.batteryConfigured">✓</span>
<span class="text-primary" v-if="batteryConfigured">✓</span>
<span class="text-primary" v-else>&mdash;</span>
</div>
</div>
<div
v-for="(loadpoint, id) in state.loadpoints"
v-bind:loadpoint="loadpoint"
v-for="(loadpoint, id) in loadpoints"
:loadpoint="loadpoint"
:key="id"
:id="'loadpoint-' + id"
>
@ -87,9 +87,7 @@
export default {
name: "Config",
data: function () {
return {
state: this.$root.$data.store.state, // global state
};
return this.$root.$data.store.state; // global state
},
};
</script>

View file

@ -1,6 +1,6 @@
<template>
<div class="container">
<Site v-bind:state="state" v-if="configured"></Site>
<Site v-bind="state" v-if="configured"></Site>
<div v-else>
<div class="row py-5">
<div class="col12">
@ -86,9 +86,7 @@ export default {
name: "Main",
components: { Site },
data: function () {
return {
state: this.$root.$data.store.state, // global state
};
return this.$root.$data.store; // global state
},
computed: {
configured: function () {

10188
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,13 +7,21 @@
"build": "rimraf dist; parcel build assets/* --public-url ./dist/",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "prettier assets --write && eslint assets/**/*.js assets/**/*.vue --fix",
"storybook": "start-storybook -p 6006 -s assets,node_modules/bootstrap/dist",
"build-storybook": "build-storybook",
"fix-audit": "npm-force-resolutions"
},
"author": "",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.12.9",
"@storybook/addon-actions": "^6.1.10",
"@storybook/addon-essentials": "^6.1.10",
"@storybook/addon-links": "^6.1.10",
"@storybook/vue": "^6.1.10",
"@vue/component-compiler-utils": "^3.2.0",
"@vue/eslint-config-standard": "^5.1.2",
"babel-loader": "^8.2.2",
"eslint": "^7.14.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-import": "^2.22.1",
@ -22,12 +30,11 @@
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^7.1.0",
"husky": "^4.3.0",
"lint-staged": "^10.5.1",
"npm-force-resolutions": "0.0.3",
"parcel-bundler": "^1.12.4",
"prettier": "^2.2.0",
"rimraf": "^3.0.2",
"vue-loader": "^15.9.5",
"vue-template-compiler": "^2.6.12"
},
"engines": {
@ -52,18 +59,5 @@
},
"resolutions": {
"node-forge": "0.10.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"assets/**/*.{js,vue,json}": [
"eslint --fix"
],
"assets/**/*.{html,js,vue,json,css}": [
"prettier --write"
]
}
}

File diff suppressed because it is too large Load diff