Convert admin page to Vue

This commit is contained in:
2023-12-16 09:19:40 +01:00
parent f674b0d934
commit 7853c6e285
12 changed files with 444 additions and 85 deletions

67
src/Spot.vue Normal file
View File

@@ -0,0 +1,67 @@
<script>
import Project from './components/project.vue';
import Admin from './components/admin.vue';
const aoRoutes = {
'project': Project,
'admin': Admin
};
export default {
data() {
return {
//spot: window.oSpot,
hash: {}
};
},
inject: ['spot'],
computed: {
currentView() {
this.spot.vars('page', this.hash.page);
return aoRoutes[this.hash.page];
}
},
mounted() {
window.addEventListener('hashchange', () => {this.onHashChange();});
var oEvent = new Event('hashchange');
window.dispatchEvent(oEvent);
}
,
methods: {
_hash(hash, bReboot) {
bReboot = bReboot || false;
if(!hash) return window.location.hash.slice(1);
else window.location.hash = '#'+hash;
if(bReboot) location.reload();
},
onHashChange() {
let asHash = this.getHash();
if(asHash.hash !='' && asHash.page != '') this.hash = asHash;
else if(!this.hash.page) this.setHash(this.spot.consts.default_page);
},
getHash() {
let sHash = this._hash();
let asHash = sHash.split(this.spot.consts.hash_sep);
let sPage = asHash.shift() || '';
return {hash:sHash, page:sPage, items:asHash};
},
setHash(sPage, asItems, bReboot) {
bReboot = bReboot || false;
sPage = sPage || '';
asItems = asItems || [];
if(typeof asItems == 'string') asItems = [asItems];
if(sPage != '') {
let sItems = (asItems.length > 0)?this.spot.consts.hash_sep+asItems.join(this.spot.consts.hash_sep):'';
this._hash(sPage+sItems, bReboot);
}
}
}
}
</script>
<template>
<div id="#main">
<component :is="currentView" />
</div>
</template>

231
src/components/admin.vue Normal file
View File

@@ -0,0 +1,231 @@
<script>
import SpotButton from './spotButton.vue';
import AdminInput from './adminInput.vue';
export default {
components: {
SpotButton,
AdminInput
},
data() {
return {
elems: {},
feedbacks: []
};
},
inject: ['spot'],
methods: {
l(id) {
return this.spot.lang(id);
},
async setProjects() {
let aoElemTypes = await this.spot.get2('admin_get');
for(const [sType, aoElems] of Object.entries(aoElemTypes)) {
this.elems[sType] = {};
for(const [iKey, oElem] of Object.entries(aoElems)) {
oElem.type = sType;
this.elems[sType][oElem.id] = oElem;
}
}
},
createElem(sType) {
this.spot.get2('admin_create', {type: sType})
.then((aoNewElemTypes) => {
console.log(aoNewElemTypes);
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
oNewElem.type = sType;
this.elems[sType][oNewElem.id] = oNewElem;
this.spot.onFeedback('success', this.spot.lang('admin_create_success'), {'create':sType});
}
}
})
.catch((sMsg) => {console.log(sMsg);this.spot.onFeedback('error', sMsg, {'create':sType});});
},
deleteElem(oElem) {
const asInputs = {
type: oElem.type,
id: oElem.id
};
this.spot.get(
'admin_delete',
(asData) => {
delete this.elems[asInputs.type][asInputs.id];
this.spot.onFeedback('success', this.spot.lang('admin_delete_success'), asInputs);
},
asInputs,
(sError) => {
this.spot.onFeedback('error', sError, asInputs);
}
);
},
updateElem(oElem, oEvent) {
if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait'));
let sOldVal = this.elems[oElem.type][oElem.id][oEvent.target.name];
let sNewVal = oEvent.target.value;
if(sOldVal != sNewVal) {
let asInputs = {
type: oElem.type,
id: oElem.id,
field: oEvent.target.name,
value: sNewVal
};
this.spot.get2('admin_set', asInputs)
.then((asData) => {
this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal;
this.spot.onFeedback('success', this.spot.lang('admin_save_success'), asInputs);
})
.catch((sError) => {
oEvent.target.value = sOldVal;
this.spot.onFeedback('error', sError, asInputs);
});
}
},
queue(oElem, oEvent) {
if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait'));
this.spot.tmp('wait', setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000));
},
updateProject() {
this.spot.get2('update_project')
.then((asData, sMsg) => {this.spot.onFeedback('success', sMsg, {'update':'project'});})
.catch((sMsg) => {this.spot.onFeedback('error', sMsg, {'update':'project'});});
}
},
mounted() {
this.spot.addPage('admin', {
onFeedback: (sType, sMsg, asContext) => {
delete asContext.a;
delete asContext.t;
sMsg += ' (';
for(const [sKey, sElem] of Object.entries(asContext)) {
sMsg += sKey+'='+sElem+' / ' ;
}
sMsg = sMsg.slice(0, -3)+')';
this.feedbacks.push({type:sType, msg:sMsg});
}
});
this.setProjects();
}
}
</script>
<template>
<div id="admin">
<a name="back" class="button" href="#project"><i class="fa fa-back push"></i>{{ l('nav_back') }}</a>
<h1>{{ l('projects') }}</h1>
<div id="project_section">
<table>
<thead>
<tr>
<th>{{ l('id_project') }}</th>
<th>{{ l('project') }}</th>
<th>{{ l('mode') }}</th>
<th>{{ l('code_name') }}</th>
<th>{{ l('start') }}</th>
<th>{{ l('end') }}</th>
<th>{{ l('delete') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="project in elems.project">
<td>{{ project.id }}</td>
<td><AdminInput :type="'text'" :name="'name'" :elem="project" /></td>
<td>{{ project.mode }}</td>
<td><AdminInput :type="'text'" :name="'codename'" :elem="project" /></td>
<td><AdminInput :type="'date'" :name="'active_from'" :elem="project" /></td>
<td><AdminInput :type="'date'" :name="'active_to'" :elem="project" /></td>
<td><SpotButton :iconClass="'close fa-lg'" @click="deleteElem(project)" /></td>
</tr>
</tbody>
</table>
<SpotButton :buttonClass="'new'" :buttonText="l('new_project')" :iconClass="'new'" @click="createElem('project')" />
</div>
<h1>{{ l('feeds') }}</h1>
<div id="feed_section">
<table>
<thead>
<tr>
<th>{{ l('id_feed') }}</th>
<th>{{ l('ref_feed_id') }}</th>
<th>{{ l('id_spot') }}</th>
<th>{{ l('id_project') }}</th>
<th>{{ l('name') }}</th>
<th>{{ l('status') }}</th>
<th>{{ l('last_update') }}</th>
<th>{{ l('delete') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="feed in elems.feed">
<td>{{ feed.id }}</td>
<td><AdminInput :type="'text'" :name="'ref_feed_id'" :elem="feed" /></td>
<td><AdminInput :type="'number'" :name="'id_spot'" :elem="feed" /></td>
<td><AdminInput :type="'number'" :name="'id_project'" :elem="feed" /></td>
<td>{{ feed.name }}</td>
<td>{{ feed.status }}</td>
<td>{{ feed.last_update }}</td>
<td><SpotButton :iconClass="'close fa-lg'" @click="deleteElem(feed)" /></td>
</tr>
</tbody>
</table>
<SpotButton :buttonClass="'new'" :buttonText="l('new_feed')" :iconClass="'new'" @click="createElem('feed')" />
</div>
<h1>Spots</h1>
<div id="spot_section">
<table>
<thead>
<tr>
<th>{{ l('id_spot') }}</th>
<th>{{ l('ref_spot_id') }}</th>
<th>{{ l('name') }}</th>
<th>{{ l('model') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="spot in elems.spot">
<td>{{ spot.id }}</td>
<td>{{ spot.ref_spot_id }}</td>
<td>{{ spot.name }}</td>
<td>{{ spot.model }}</td>
</tr>
</tbody>
</table>
</div>
<h1>{{ l('active_users') }}</h1>
<div id="user_section">
<table>
<thead>
<tr>
<th>{{ l('id_user') }}</th>
<th>{{ l('user_name') }}</th>
<th>{{ l('language') }}</th>
<th>{{ l('time_zone') }}</th>
<th>{{ l('clearance') }}</th>
<th>{{ l('delete') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="user in elems.user">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.language }}</td>
<td>{{ user.timezone }}</td>
<td><AdminInput :type="'number'" :name="'clearance'" :elem="user" /></td>
<td><SpotButton :iconClass="'close fa-lg'" @click="deleteElem(user)" /></td>
</tr>
</tbody>
</table>
</div>
<h1>{{ l('toolbox') }}</h1>
<div id="toolbox">
<SpotButton :buttonClass="'refresh'" :buttonText="l('update_project')" :iconClass="'refresh'" @click="updateProject" />
</div>
<div id="feedback" class="feedback">
<p v-for="feedback in feedbacks" :class="feedback.type">{{ feedback.msg }}</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script>
export default {
props: {
type: String,
name: String,
elem: Object
},
computed: {
value() {
return this.elem[this.name];
}
}
}
</script>
<template>
<input :type="type" :name="name" :value="value" @change="$parent.updateElem(elem, $event)" @keyup="$parent.queue(elem, $event)" />
</template>

View File

@@ -0,0 +1,44 @@
<script>
//Leaflet
import 'leaflet';
import 'leaflet-geometryutil';
import 'leaflet.heightgraph';
import '../scripts/leaflet.helpers';
import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet";
export default {
components: {
LMap,
LTileLayer
},
data() {
return {
server: this.spot.consts.server,
zoom: 13
};
},
inject: ['spot'],
mounted() {
if(this.$parent.hash.items.length==0) this.$parent.setHash(this.$parent.hash.page, [this.spot.vars('default_project_codename')]);
}
}
</script>
<template>
<div id="projects">
<div id="background"></div>
<div id="submap">
<div class="loader fa fa-fw fa-map flicker" id="map_loading"></div>
</div>
<div id="map">
<l-map ref="map" v-model:zoom="zoom" :center="[47.41322, -1.219482]">
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
></l-tile-layer>
</l-map>
</div>
</div>
<div id="mobile" class="mobile"></div>
</template>

View File

@@ -0,0 +1,22 @@
<script>
import SpotIcon from './spotIcon.vue';
export default {
components: {
SpotIcon
},
props: {
buttonClass: String,
buttonText: String,
iconClass: String
},
data() {
return {
margin: !!this.buttonText
}
}
}
</script>
<template>
<button :class="buttonClass"><SpotIcon :iconClass="iconClass" :margin="margin" />{{ buttonText }}</button>
</template>

View File

@@ -0,0 +1,17 @@
<script>
export default {
props: {
iconClass: String,
margin: Boolean,
otherClasses: String
},
data() {
return {
classNames: 'fa fa-'+this.iconClass+(this.margin?' push':'')+(this.otherClasses?' '+this.otherClasses:'')
}
}
}
</script>
<template>
<i :class="classNames"></i>
</template>

View File

@@ -1,71 +0,0 @@
<div id="admin">
<a name="back" class="button" href="[#]server[#]"><i class="fa fa-back push"></i>[#]lang:nav_back[#]</a>
<h1>[#]lang:projects[#]</h1>
<div id="project_section">
<table>
<thead>
<tr>
<th>[#]lang:id_project[#]</th>
<th>[#]lang:project[#]</th>
<th>[#]lang:mode[#]</th>
<th>[#]lang:code_name[#]</th>
<th>[#]lang:start[#]</th>
<th>[#]lang:end[#]</th>
<th>[#]lang:delete[#]</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div id="new"></div>
</div>
<h1>[#]lang:feeds[#]</h1>
<div id="feed_section">
<table>
<thead>
<tr>
<th>[#]lang:id_feed[#]</th>
<th>[#]lang:ref_feed_id[#]</th>
<th>[#]lang:id_spot[#]</th>
<th>[#]lang:id_project[#]</th>
<th>[#]lang:name[#]</th>
<th>[#]lang:status[#]</th>
<th>[#]lang:last_update[#]</th>
<th>[#]lang:delete[#]</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<h1>Spots</h1>
<div id="spot_section">
<table>
<thead>
<tr>
<th>[#]lang:id_spot[#]</th>
<th>[#]lang:ref_spot_id[#]</th>
<th>[#]lang:name[#]</th>
<th>[#]lang:model[#]</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<h1>[#]lang:active_users[#]</h1>
<div id="user_section">
<table>
<thead>
<tr>
<th>[#]lang:id_user[#]</th>
<th>[#]lang:user_name[#]</th>
<th>[#]lang:language[#]</th>
<th>[#]lang:time_zone[#]</th>
<th>[#]lang:clearance[#]</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<h1>[#]lang:toolbox[#]</h1>
<div id="toolbox"></div>
<div id="feedback" class="feedback"></div>
</div>

View File

@@ -20,12 +20,12 @@
<meta name="msapplication-config" content="images/icons/browserconfig.xml?v=GvmqYyKwbb"> <meta name="msapplication-config" content="images/icons/browserconfig.xml?v=GvmqYyKwbb">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<script type="text/javascript">window.params = [#]GLOBAL_VARS[#];</script> <script type="text/javascript">window.params = [#]GLOBAL_VARS[#];</script>
<script type="text/javascript" src="[#]filepath_js[#]"></script>
<title>Spotty</title> <title>Spotty</title>
</head> </head>
<body> <body>
<div id="container"> <div id="container">
<div id="main"></div> <div id="main"></div>
</div> </div>
<script type="module" src="[#]filepath_js[#]"></script>
</body> </body>
</html> </html>

View File

@@ -16,21 +16,26 @@ console.log(Logo);
//Masks //Masks
import Spot from './spot.js'; import Spot from './spot.js';
import Project from './page.project.js'; //import Project from './page.project.js';
import Upload from './page.upload.js'; //import Upload from './page.upload.js';
import Admin from './page.admin.js'; //import Admin from './page.admin.js';
//const Upload = () => import('@scripts/page.upload.js'); window.oSpot = new Spot(params);
let oSpot = new Spot(params); //let oProject = new Project(oSpot);
//oSpot.addPage('project', oProject);
let oProject = new Project(oSpot); //let oUpload = new Upload(oSpot);
oSpot.addPage('project', oProject); //oSpot.addPage('upload', oUpload);
let oUpload = new Upload(oSpot); //let oAdmin = new Admin(oSpot);
oSpot.addPage('upload', oUpload); //oSpot.addPage('admin', oAdmin);
let oAdmin = new Admin(oSpot); //$(() => {oSpot.init();});
oSpot.addPage('admin', oAdmin);
$(() => {oSpot.init();}); import { createApp } from 'vue';
import SpotVue from '../Spot.vue';
const oSpotVue = createApp(SpotVue);
oSpotVue.provide('spot', window.oSpot);
oSpotVue.mount('#main');

View File

@@ -75,7 +75,7 @@ export default class Spot {
if(oData.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oData.desc = this.lang(oData.desc.substr(5)); if(oData.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oData.desc = this.lang(oData.desc.substr(5));
if(oData.result==this.consts.error) fOnError(oData.desc); if(oData.result==this.consts.error) fOnError(oData.desc);
else fOnSuccess(oData.data, oData.desc); else if(fOnSuccess) fOnSuccess(oData.data, oData.desc);
}) })
.fail((jqXHR, textStatus, errorThrown) => { .fail((jqXHR, textStatus, errorThrown) => {
fonProgress('fail'); fonProgress('fail');
@@ -83,6 +83,32 @@ export default class Spot {
}); });
} }
async get2(sAction, oVars) {
oVars = oVars || {};
oVars['a'] = sAction;
oVars['t'] = this.consts.timezone;
let oUrl = new URL(this.consts.server+this.consts.process_page);
oUrl.search = new URLSearchParams(oVars).toString();
try {
let oUrl = new URL(this.consts.server+this.consts.process_page);
oUrl.search = new URLSearchParams(oVars).toString();
const oRequest = await fetch(oUrl, {method: 'GET', /*body: JSON.stringify(oVars),*/ headers: {"Content-Type": "application/json"}});
if(!oRequest.ok) throw new Error('Error HTTP '+oRequest.status+': '+oRequest.statusText);
else {
let oResponse = await oRequest.json();
if(oResponse.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oResponse.desc = this.lang(oData.desc.substr(this.consts.lang_prefix.length));
if(oResponse.result == this.consts.error) return Promise.reject(oResponse.desc);
else return Promise.resolve(oResponse.data, oResponse.desc);
}
}
catch(oError) {
throw oError;
}
}
lang(sKey, asParams) { lang(sKey, asParams) {
asParams = asParams || []; asParams = asParams || [];
if(typeof asParams == 'string') asParams = [asParams]; if(typeof asParams == 'string') asParams = [asParams];
@@ -176,6 +202,7 @@ export default class Spot {
asContext = asContext || {}; asContext = asContext || {};
let sPage = this.vars('page'); let sPage = this.vars('page');
if(this.pages[sPage].onFeedback) this.pages[sPage].onFeedback(sType, sMsg, asContext); if(this.pages[sPage].onFeedback) this.pages[sPage].onFeedback(sType, sMsg, asContext);
else console.log({type:sType, msg:sMsg, context:asContext});
} }
onKeydown(oEvent) { onKeydown(oEvent) {