Finished Vue single file component implementation

This commit is contained in:
Mark van Renswoude 2020-09-23 20:09:40 +02:00
parent 7ae2f49ee8
commit e2ba4e0d4c
12 changed files with 369 additions and 343 deletions

View File

@ -46,7 +46,7 @@
<div class="navigation tabs">
<router-link to="/" class="button" active-class="active" exact="true">{{ $t('status.tabTitle') }}</router-link><router-link to="/connection" class="button" active-class="active">{{ $t('connection.tabTitle') }}</router-link><router-link to="/system" class="button" active-class="active">{{ $t('system.tabTitle') }}</router-link>
<router-link to="/" class="button" active-class="active" :exact="true">{{ $t('status.tabTitle') }}</router-link><router-link to="/connection" class="button" active-class="active">{{ $t('connection.tabTitle') }}</router-link><router-link to="/system" class="button" active-class="active">{{ $t('system.tabTitle') }}</router-link>
</div>
@ -66,8 +66,11 @@
<script>
import axios from 'axios'
import LoadingIndicator from '@/components/loadingIndicator.vue';
import BaseVM from '@/BaseVM';
export default {
mixins: [BaseVM],
components: {
LoadingIndicator
},
@ -117,7 +120,6 @@ export default {
computed: {
notification() { return this.$store.state.notification; },
saving() { return this.$store.state.saving; },
hasResetError()
{
@ -199,7 +201,7 @@ export default {
updateStatus()
{
var self = this;
const self = this;
return axios.get('/api/status', { retry: 10, retryDelay: 1000 })
.then(response =>
@ -259,12 +261,6 @@ export default {
},
handleAPIError(messageId, error)
{
this.$store.dispatch('notifyAPIError', { message: this.$i18n.t(messageId), error });
},
hideNotification()
{
this.$store.dispatch('hideNotification');
@ -276,6 +272,7 @@ export default {
<style lang="scss">
@import "variables.scss";
// TODO check which parts are app-wide and which should be moved to components/view
html
{

28
web/src/BaseVM.js Normal file
View File

@ -0,0 +1,28 @@
export default {
computed: {
saving() { return this.$store.state.saving; }
},
methods: {
setSaving(value)
{
self.$store.commit('saving', value);
},
showNotification(message, isError)
{
this.$store.dispatch('showNotification', {
message: message,
isError: isError || false
});
},
handleAPIError(messageId, error)
{
this.$store.dispatch('notifyAPIError', { message: this.$i18n.t(messageId), error });
}
}
}

View File

@ -1,147 +0,0 @@
function startApp()
{
var app = new Vue({
el: '#app',
data: {
},
created: function()
{
var self = this;
self.notificationTimer = null;
// Sequential loading of all the settings makes sure
// we don't overload the ESP8266 with requests, as that
// can cause it to run out of memory easily.
// This is a horrible way to implement it, but I don't feel like
// including a big library or working out a clean short solution
// at the moment, and it works :)
self.loadStatus().then(function()
{
self.loadConnection().then(function()
{
self.loadSystem().then(function()
{
self.stopLoadingIndicator();
self.loading = false;
});
});
});
},
methods: {
loadConnection: function()
{
var self = this;
return axios.get('/api/connection', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.connection = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadConnection'));
},
loadSystem: function()
{
var self = this;
return axios.get('/api/system', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.system = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadSystem'));
},
applyConnection: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
axios.post('/api/connection', {
hostname: self.connection.hostname,
accesspoint: self.connection.accesspoint,
station: self.connection.station,
ssid: self.connection.ssid,
password: self.connection.password,
dhcp: self.connection.dhcp,
ip: self.connection.ip,
subnetmask: self.connection.subnetmask,
gateway: self.connection.gateway,
}, { retry: 10, retryDelay: 1000, headers: { 'Content-Type': 'application/json' } })
.then(function(response)
{
})
.catch(self.handleAPIError.bind(self, 'error.applyConnection'))
.then(function()
{
self.saving = false;
});
},
applySystem: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
axios.post('/api/system', self.system, { retry: 10, retryDelay: 1000, headers: { 'Content-Type': 'application/json' } })
.then(function(response)
{
self.showNotification(i18n.t('rebootPending'));
})
.catch(self.handleAPIError.bind(self, 'error.applySystem'))
.then(function()
{
self.saving = false;
});
},
uploadFirmware: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
self.uploadProgress = 0;
var data = new FormData();
data.append('file', document.getElementById('firmwareFile').files[0]);
var config = {
timeout: 360000,
onUploadProgress: function(progressEvent)
{
self.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
}
};
axios.post('/api/firmware', data, config)
.then(function(response)
{
self.showNotification(i18n.t('rebootPending'));
})
.catch(self.handleAPIError.bind(self, 'error.uploadFirmware'))
.then(function()
{
self.uploadProgress = false;
self.saving = false;
document.getElementById('firmware').reset();
});
},
}
});
}

View File

@ -69,7 +69,9 @@ export default {
pinLEDSTA: 'Station Mode status LED pin (+3.3v)',
pinAPButton: 'Enable Access Point button pin (active low)',
ledCount: 'Number of LEDs on strip'
ledCount: 'Number of LEDs on strip',
noFileSelected: 'No firmware file selected'
},
error: {

View File

@ -69,7 +69,9 @@ export default {
pinLEDSTA: 'WiFi status LED pin (+3.3v)',
pinAPButton: 'Access Point inschakelen knop pin (actief laag)',
ledCount: 'Aantal LEDs op strip'
ledCount: 'Aantal LEDs op strip',
noFileSelected: 'Geen firmware bestand geselecteerd'
},
error: {

View File

@ -1,127 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>RGBWifi</title>
<meta name="theme-color" content="#000000">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="bundle.css">
<script src="bundle.js"></script>
</head>
<body>
<div id="app">
<div v-cloak>
<div v-if="activeTab == 'status'">
<!--
Status tab
-->
</div>
<div v-if="activeTab == 'connection'">
<!--
Connection tab
-->
<form @submit.prevent="applyConnection">
<h3>{{ $t('connection.title') }}</h3>
<check v-model.boolean="connection.accesspoint" :title="$t('connection.accesspoint')"></check>
<span class="hint">{{ $t('connection.accesspointHint') }}</span>
<check v-model.boolean="connection.station" :title="$t('connection.stationmode')"></check>
<span class="hint">{{ $t('connection.stationmodeHint') }}</span>
<label for="ssid">{{ $t('connection.ssid') }}</label>
<input type="text" id="ssid" v-model="connection.ssid" :disabled="!connection.station">
<label for="password">{{ $t('connection.password') }}</label>
<input type="password" id="password" v-model="connection.password" :disabled="!connection.station">
<check v-model.boolean="connection.dhcp" :disabled="!connection.station" :title="$t('connection.dhcp')" class="form-control"></check>
<span class="hint">{{ $t('connection.dhcpHint') }}</span>
<div class="suboptions">
<label for="ip">{{ $t('connection.ipaddress') }}</label>
<input type="text" id="ip" v-model="connection.ip" :disabled="!connection.station || connection.dhcp">
<label for="subnetmask">{{ $t('connection.subnetmask') }}</label>
<input type="text" id="subnetmask" v-model="connection.subnetmask" :disabled="!connection.station || connection.dhcp">
<label for="gateway">{{ $t('connection.gateway') }}</label>
<input type="text" id="gateway" v-model="connection.gateway" :disabled="!connection.station || connection.dhcp">
</div>
<label for="hostname">{{ $t('connection.hostname') }}</label>
<input type="text" :placeholder="$t('connection.hostnamePlaceholder')" id="hostname" v-model="connection.hostname" :disabled="!connection.station">
<span class="hint">{{ $t('connection.hostnameHint') }}</span>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
</div>
<div v-if="activeTab == 'system'">
<!--
System tab
-->
<form @submit.prevent="uploadFirmware">
<h3>{{ $t('system.firmwareTitle') }}</h3>
<input type="file" id="firmwareFile">
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
<div v-if="uploadProgress !== false">
{{ uploadProgress }}%
</div>
</form>
<form @submit.prevent="applySystem">
<h3>{{ $t('system.pinsTitle') }}</h3>
<div class="horizontal">
<label for="pinLEDAP">{{ $t('system.pinLEDAP') }}</label>
<input type="number" id="pinLEDAP" v-model.number="system.pins.ledAP">
</div>
<div class="horizontal">
<label for="pinLEDSTA">{{ $t('system.pinLEDSTA') }}</label>
<input type="number" id="pinLEDSTA" v-model.number="system.pins.ledSTA">
</div>
<div class="horizontal">
<label for="pinAPButton">{{ $t('system.pinAPButton') }}</label>
<input type="number" id="pinAPButton" v-model.number="system.pins.apButton">
</div>
<h3>{{ $t('system.ledStripTitle') }}</h3>
<div class="horizontal">
<label for="ledCount">{{ $t('system.ledCount') }}</label>
<input type="number" id="ledCount" v-model.number="system.ledCount">
</div>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
</div>
<script language="javascript">
startApp();
</script>
</body>
</html>

View File

@ -1,8 +1,8 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Status from '../views/Status.vue'
//import Connection from '../views/Connection.vue'
//import System from '../views/System.vue'
import Connection from '../views/Connection.vue'
import System from '../views/System.vue'
Vue.use(VueRouter)
@ -12,6 +12,16 @@ const routes = [
path: '/',
name: 'Status',
component: Status
},
{
path: '/connection',
name: 'Connection',
component: Connection
},
{
path: '/system',
name: 'System',
component: System
}
]

View File

@ -7,46 +7,17 @@ export default new Vuex.Store({
state: {
notification: null,
notificationTimeout: null,
saving: false,
saving: false
},
connection: {
hostname: null,
accesspoint: true,
station: false,
ssid: null,
password: null,
dhcp: true,
ip: null,
subnetmask: null,
gateway: null
mutations: {
saving(state, value)
{
state.saving = value;
},
system: {
pins: {
ledAP: null,
ledSTA: null,
apButton: null,
},
ledCount: null
}
/*
settingStatic: false,
loadingIndicator: '|',
uploadProgress: false,
static: {
r: 0,
g: 0,
b: 0,
w: 0
}
*/
},
mutations: {
_setNotification(state, payload)
{
state.notification = payload.notification;

View File

@ -0,0 +1,120 @@
<template>
<form @submit.prevent="save">
<h3>{{ $t('connection.title') }}</h3>
<div v-if="connection === null" class="loading">
<LoadingIndicator></LoadingIndicator>
</div>
<div v-else>
<check v-model.boolean="connection.accesspoint" :title="$t('connection.accesspoint')"></check>
<span class="hint">{{ $t('connection.accesspointHint') }}</span>
<check v-model.boolean="connection.station" :title="$t('connection.stationmode')"></check>
<span class="hint">{{ $t('connection.stationmodeHint') }}</span>
<label for="ssid">{{ $t('connection.ssid') }}</label>
<input type="text" id="ssid" v-model="connection.ssid" :disabled="!connection.station">
<label for="password">{{ $t('connection.password') }}</label>
<input type="password" id="password" v-model="connection.password" :disabled="!connection.station">
<check v-model.boolean="connection.dhcp" :disabled="!connection.station" :title="$t('connection.dhcp')" class="form-control"></check>
<span class="hint">{{ $t('connection.dhcpHint') }}</span>
<div class="suboptions">
<label for="ip">{{ $t('connection.ipaddress') }}</label>
<input type="text" id="ip" v-model="connection.ip" :disabled="!connection.station || connection.dhcp">
<label for="subnetmask">{{ $t('connection.subnetmask') }}</label>
<input type="text" id="subnetmask" v-model="connection.subnetmask" :disabled="!connection.station || connection.dhcp">
<label for="gateway">{{ $t('connection.gateway') }}</label>
<input type="text" id="gateway" v-model="connection.gateway" :disabled="!connection.station || connection.dhcp">
</div>
<label for="hostname">{{ $t('connection.hostname') }}</label>
<input type="text" :placeholder="$t('connection.hostnamePlaceholder')" id="hostname" v-model="connection.hostname" :disabled="!connection.station">
<span class="hint">{{ $t('connection.hostnameHint') }}</span>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</div>
</form>
</template>
<script>
import axios from 'axios';
import LoadingIndicator from '@/components/loadingIndicator.vue';
import check from '@/components/check.vue';
import BaseVM from '@/BaseVM';
export default {
mixins: [BaseVM],
components: {
LoadingIndicator,
check
},
data()
{
return {
connection: null
}
},
mounted()
{
this.load();
},
methods: {
load()
{
const self = this;
return axios.get('/api/connection', { retry: 10, retryDelay: 1000 })
.then(response =>
{
if (typeof response.data == 'object')
self.connection = response.data;
})
.catch(e => self.handleAPIError('error.loadConnection', e));
},
save()
{
const self = this;
if (self.saving)
return;
self.setSaving(true);
axios.post('/api/connection', {
hostname: self.connection.hostname,
accesspoint: self.connection.accesspoint,
station: self.connection.station,
ssid: self.connection.ssid,
password: self.connection.password,
dhcp: self.connection.dhcp,
ip: self.connection.ip,
subnetmask: self.connection.subnetmask,
gateway: self.connection.gateway,
}, { retry: 10, retryDelay: 1000, headers: { 'Content-Type': 'application/json' } })
.then(response => {})
.catch(e => self.handleAPIError('error.applyConnection', e))
.then(() =>
{
self.setSaving(false);
});
}
}
}
</script>

View File

@ -30,8 +30,11 @@
<script>
import axios from 'axios';
import LoadingIndicator from '@/components/loadingIndicator.vue';
import BaseVM from '@/BaseVM';
export default {
mixins: [BaseVM],
components: {
LoadingIndicator
},
@ -51,22 +54,34 @@ export default {
self.disableSetStatic = false;
self.setStaticTimer = false;
// TODO load current settings (no API for it yet)
self.static = {
r: 0,
g: 0,
b: 0,
w: 0
};
self.$watch('static', () =>
{
self.staticChanged();
}, { deep: true });
self.load()
.then(() =>
{
self.$watch('static', () =>
{
self.staticChanged();
}, { deep: true });
});
},
methods: {
load()
{
const self = this;
// TODO load current settings (no API for it yet)
self.static = {
r: 0,
g: 0,
b: 0,
w: 0
};
return Promise.resolve(true);
},
staticOff()
{
this.static = {
@ -80,7 +95,7 @@ export default {
staticChanged()
{
var self = this;
const self = this;
console.log(self.setStaticTimer);
if (self.setStaticTimer === false)
@ -93,7 +108,7 @@ export default {
setStatic()
{
var self = this;
const self = this;
if (self.settingStatic)
{
@ -118,12 +133,6 @@ export default {
{
self.settingStatic = false;
});
},
handleAPIError(messageId, error)
{
this.$store.dispatch('notifyAPIError', { message: this.$i18n.t(messageId), error });
}
}
}

161
web/src/views/System.vue Normal file
View File

@ -0,0 +1,161 @@
<template>
<div>
<h3>{{ $t('system.firmwareTitle') }}</h3>
<div v-if="system === null" class="loading">
<LoadingIndicator></LoadingIndicator>
</div>
<div v-else>
<form @submit.prevent="uploadFirmware">
<input type="file" id="firmwareFile">
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
<div v-if="uploadProgress !== false">
{{ uploadProgress }}%
</div>
</form>
<form @submit.prevent="applySystem">
<h3>{{ $t('system.pinsTitle') }}</h3>
<div class="horizontal">
<label for="pinLEDAP">{{ $t('system.pinLEDAP') }}</label>
<input type="number" id="pinLEDAP" v-model.number="system.pins.ledAP">
</div>
<div class="horizontal">
<label for="pinLEDSTA">{{ $t('system.pinLEDSTA') }}</label>
<input type="number" id="pinLEDSTA" v-model.number="system.pins.ledSTA">
</div>
<div class="horizontal">
<label for="pinAPButton">{{ $t('system.pinAPButton') }}</label>
<input type="number" id="pinAPButton" v-model.number="system.pins.apButton">
</div>
<h3>{{ $t('system.ledStripTitle') }}</h3>
<div class="horizontal">
<label for="ledCount">{{ $t('system.ledCount') }}</label>
<input type="number" id="ledCount" v-model.number="system.ledCount">
</div>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
</div>
</div>
</template>
<script>
import axios from 'axios';
import LoadingIndicator from '@/components/loadingIndicator.vue';
import BaseVM from '@/BaseVM';
export default {
mixins: [BaseVM],
components: {
LoadingIndicator
},
data()
{
return {
system: null,
uploadProgress: false
}
},
mounted()
{
this.load();
},
methods: {
load()
{
const self = this;
return axios.get('/api/system', { retry: 10, retryDelay: 1000 })
.then(response =>
{
if (typeof response.data == 'object')
self.system = response.data;
})
.catch(e => self.handleAPIError('error.loadSystem', e));
},
save()
{
const self = this;
if (self.saving)
return;
self.setSaving(true);
axios.post('/api/system', self.system, { retry: 10, retryDelay: 1000, headers: { 'Content-Type': 'application/json' } })
.then(response =>
{
self.showNotification(i18n.t('rebootPending'));
})
.catch(e => self.handleAPIError('error.applySystem', e))
.then(() =>
{
self.setSaving(false);
});
},
uploadFirmware()
{
const self = this;
if (self.saving) return;
const fileElement = document.getElementById('firmwareFile');
if (fileElement.files.length == 0)
{
self.showNotification(self.$i18n.t('system.noFileSelected'), true);
return;
}
self.saving = true;
self.uploadProgress = 0;
const data = new FormData();
data.append('file', fileElement.files[0]);
var config = {
timeout: 360000,
onUploadProgress: progressEvent =>
{
self.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
}
};
axios.post('/api/firmware', data, config)
.then(response =>
{
self.showNotification(self.$i18n.t('rebootPending'));
})
.catch(e => self.handleAPIError('error.uploadFirmware', e))
.then(() =>
{
self.uploadProgress = false;
self.saving = false;
document.getElementById('firmware').reset();
});
}
}
}
</script>

View File

@ -8,7 +8,7 @@ module.exports = merge(config, {
historyApiFallback: true,
proxy:{
'/api': {
target: 'http://localhost:3000'
target: 'http://127.0.0.1:3000'
}
},
},