layout changes, status bar addition, drawer, and leaflet store

This commit is contained in:
2026-03-13 14:08:19 -04:00
parent d1cb31b2c8
commit 8214f0543a
10 changed files with 273 additions and 50 deletions

View File

@@ -112,7 +112,7 @@ export default defineConfig((/* ctx */) => {
config: { config: {
dark: true, dark: true,
}, },
// iconSet: 'material-icons', // Quasar icon set iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack // lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact // For special cases outside of where the auto-import strategy can have an impact

View File

@@ -37,7 +37,7 @@ interface ClientToServerEvents {
request_update: (callback: (response: { statusUpdate: StatusUpdate }) => void) => void; request_update: (callback: (response: { statusUpdate: StatusUpdate }) => void) => void;
command: ( command: (
command: string, command: string,
callback: (response: { success: boolean; message?: string }) => void, callback: (response: { status: boolean; message?: string }) => void,
) => void; ) => void;
shutdown: ( shutdown: (
delay: number, delay: number,

View File

@@ -0,0 +1,32 @@
<template>
<q-dialog ref="dlgRef" persistent>
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="add_location" color="primary" text-color="white" />
<span class="q-ml-sm">
Are you sure you want to {{ name }} ?
</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="OK" color="primary" @click="onOkClick" />
<q-btn flat label="Cancel" color="primary" @click="onDialogCancel" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { useDialogPluginComponent } from 'quasar';
const props = defineProps({
name: { type: String, required: true },
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
function onOkClick() {
onDialogOK();
}
</script>

View File

@@ -2,6 +2,13 @@
import { useLeafletStore } from 'stores/leaflet'; import { useLeafletStore } from 'stores/leaflet';
import type { coords } from 'src/types'; import type { coords } from 'src/types';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { socket } from 'boot/socket';
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
const route = useRoute();
const $q = useQuasar();
const leafletStore = useLeafletStore(); const leafletStore = useLeafletStore();
const { center, markerLatLng } = storeToRefs(leafletStore); const { center, markerLatLng } = storeToRefs(leafletStore);
@@ -15,6 +22,38 @@ const home = {
icon: 'home', icon: 'home',
}; };
interface Control {
id: number;
name: string;
cmd: string;
icon: string;
confirm: boolean;
}
const controls: Control[] = [
{
id: 1,
name: 'Start Location Sim',
cmd: 'start_location_simulation',
icon: 'play_arrow',
confirm: false,
},
{
id: 3,
name: 'End Location Sim',
cmd: 'end_location_simulation',
icon: 'stop',
confirm: true,
},
{
id: 3,
name: 'Shutdown',
cmd: 'shutdown',
icon: 'power_settings_new',
confirm: true,
},
];
const favorites = [ const favorites = [
{ {
id: 1, id: 1,
@@ -49,17 +88,47 @@ function handleFavClick(coords: coords) {
center.value = [coords.lat, coords.lng]; center.value = [coords.lat, coords.lng];
markerLatLng.value = [coords.lat, coords.lng]; markerLatLng.value = [coords.lat, coords.lng];
} }
function handleControlClick(ctrl: Control) {
if (ctrl.confirm) {
$q.dialog({
component: ConfirmCommandDialog,
componentProps: {
name: ctrl.name,
},
})
.onOk(() => {
socket.emit('command', ctrl.cmd, (response) => {
console.log(response.status);
});
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
} else {
socket.emit('command', ctrl.cmd, (response) => {
console.log(response.status);
});
}
}
</script> </script>
<template> <template>
<q-toolbar class="bg-primary text-white"> <q-toolbar class="bg-primary text-white">
<q-btn flat round dense icon="menu" class="q-mr-sm" /> <q-btn @click="$emit('drawer')" flat round dense icon="menu" class="q-mr-sm" />
<q-separator dark vertical inset /> <q-separator dark vertical inset />
<q-btn stretch flat :icon="home.icon" @click="handleFavClick(home.coords)" />
<q-space /> <q-space />
<q-btn
<q-btn-dropdown stretch flat label="Favorites"> stretch
flat
:icon="home.icon"
@click="handleFavClick(home.coords)"
v-if="route.name === 'Leaflet'"
/>
<q-btn-dropdown stretch flat label="Favorites" v-if="route.name === 'Leaflet'">
<q-list> <q-list>
<q-item <q-item
v-for="fav in favorites" v-for="fav in favorites"
@@ -78,6 +147,25 @@ function handleFavClick(coords: coords) {
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown>
<q-btn-dropdown stretch flat label="Controls">
<q-list>
<q-item
v-for="ctrl in controls"
:key="ctrl.id"
clickable
v-ripple
v-close-popup
@click="handleControlClick(ctrl)"
>
<q-item-section avatar>
<q-avatar :icon="ctrl.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label>{{ ctrl.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-toolbar> </q-toolbar>
</template> </template>

View File

@@ -5,7 +5,7 @@ import { computed } from 'vue';
const statusStore = useStatusStore(); const statusStore = useStatusStore();
const socketStatus = computed(() => statusStore.socketConnected ? 'green' : 'red'); const socketStatus = computed(() => statusStore.statusList.socketConnected ? 'green' : 'red');
socket.off(); socket.off();
statusStore.socketConnect(); statusStore.socketConnect();

View File

@@ -1,38 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { useQuasar } from 'quasar'; import { useStatusStore } from 'stores/status';
import { ref, onMounted } from 'vue'; const statusStore = useStatusStore();
import { api } from 'boot/axios'; const statusList = statusStore.statusList;
const $q = useQuasar(); function statusDevColor(state: boolean): string {
return state ? 'green' : 'red';
const statusDev = ref({ color: 'red', deviceName: 'No connected Device' });
async function getStatus() {
try {
const { data } = await api({
method: 'get',
url: '/status',
});
console.log('API Call: usbmux/status returned:', data);
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error setting location:', error.message);
$q.notify({ type: 'negative', message: error.message });
} else {
console.error('Error setting location:', error);
}
}
} }
onMounted(async () => {
await getStatus();
});
</script> </script>
<template> <template>
<q-toolbar class="bg-primary text-white"> <q-toolbar class="bg-primary text-white">
<q-badge :color="statusDev.color" rounded class="q-mr-sm" />{{ statusDev.deviceName }} <div class="flex col q-gutter-md align-center justify-start content-center">
<span>Status:</span>
<span>
<q-badge :color="statusDevColor(statusList.socketConnected)" rounded class="q-mr-sm" />
WebSocket
</span>
<span>
<q-badge :color="statusDevColor(statusList.deviceConnected)" rounded class="q-mr-sm" />
Device Connection
</span>
<span>
<q-badge :color="statusDevColor(statusList.tunnelConnected)" rounded class="q-mr-sm" />
tunneld
</span>
<span>
<q-badge :color="statusDevColor(statusList.simulationRunning)" rounded class="q-mr-sm" />
Location Simulation
</span>
</div>
</q-toolbar> </q-toolbar>
</template> </template>

View File

@@ -12,12 +12,12 @@
// to match your app's branding. // to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website. // Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary: #1976d2; $primary: #02006c;
$secondary: #26a69a; $secondary: #010057;
$accent: #9c27b0; $accent: #9c27b0;
$dark: #1d1d1d; $dark: #1d1d1d;
$dark-page: #121212; $dark-page: #03002e;
$positive: #21ba45; $positive: #21ba45;
$negative: #c10015; $negative: #c10015;

View File

@@ -1,12 +1,34 @@
<template> <template>
<q-layout view="lHh lpr lFf"> <q-layout view="hHh Lpr fff">
<q-header elevated> <q-header>
<MenuBar /> <MenuBar @drawer="drawer = !drawer" />
</q-header> </q-header>
<q-footer elevated> <q-footer>
<StatusBar /> <StatusBar />
</q-footer> </q-footer>
<q-drawer
v-model="drawer"
:width="200"
:breakpoint="500"
overlay
:class="$q.dark.isActive ? 'bg-grey-8' : 'bg-grey-3'"
>
<q-scroll-area class="fit">
<q-list>
<template v-for="(menuItem, index) in menuList" :key="index">
<q-item clickable :active="menuItem.route === route.name" v-ripple @click="$router.push({ name: menuItem.route })">
<q-item-section avatar>
<q-icon :name="menuItem.icon" />
</q-item-section>
<q-item-section>
{{ menuItem.label }}
</q-item-section>
</q-item>
<q-separator :key="'sep' + index" v-if="menuItem.separator" />
</template>
</q-list>
</q-scroll-area>
</q-drawer>
<q-page-container> <q-page-container>
<router-view /> <router-view />
</q-page-container> </q-page-container>
@@ -14,6 +36,69 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import MenuBar from 'components/MenuBar.vue' import MenuBar from 'components/MenuBar.vue';
import StatusBar from 'components/StatusBar.vue' import StatusBar from 'components/StatusBar.vue';
import { ref } from 'vue';
const drawer = ref(false);
import { useRoute } from 'vue-router';
const route = useRoute();
const menuList = [
{
icon: 'map',
label: 'Map',
separator: false,
route: 'Leaflet',
},
{
icon: 'report_problem',
label: 'Test',
separator: false,
route: 'Test',
},
{
icon: 'inbox',
label: 'Inbox',
separator: true,
route: 'ErrorNotFound',
},
{
icon: 'send',
label: 'Outbox',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'delete',
label: 'Trash',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'error',
label: 'Spam',
separator: true,
route: 'ErrorNotFound',
},
{
icon: 'settings',
label: 'Settings',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'feedback',
label: 'Send Feedback',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'help',
iconColor: 'primary',
label: 'Help',
separator: false,
route: 'ErrorNotFound',
},
];
</script> </script>

View File

@@ -5,8 +5,23 @@ const routes: RouteRecordRaw[] = [
path: '/', path: '/',
component: () => import('layouts/MainLayout.vue'), component: () => import('layouts/MainLayout.vue'),
children: [ children: [
{ path: '', component: () => import('pages/IndexPage.vue') }, {
{ path: 'test', component: () => import('pages/TestPage.vue') }, path: '',
name: 'home',
redirect: {
name: 'Leaflet',
},
},
{
path: 'leaflet',
name: 'Leaflet',
component: () => import('pages/IndexPage.vue'),
},
{
path: 'test',
name: 'Test',
component: () => import('pages/TestPage.vue')
},
], ],
}, },
@@ -14,6 +29,7 @@ const routes: RouteRecordRaw[] = [
// but you can also remove it // but you can also remove it
{ {
path: '/:catchAll(.*)*', path: '/:catchAll(.*)*',
name: 'ErrorNotFound',
component: () => import('pages/ErrorNotFound.vue'), component: () => import('pages/ErrorNotFound.vue'),
}, },
]; ];

View File

@@ -4,15 +4,20 @@ import { socket } from 'boot/socket';
export const useStatusStore = defineStore('status', { export const useStatusStore = defineStore('status', {
state: () => ({ state: () => ({
device: {}, device: {},
socketConnected: false, statusList: {
socketConnected: false,
deviceConnected: false,
tunnelConnected: false,
simulationRunning: false,
},
}), }),
actions: { actions: {
bindEvents() { bindEvents() {
socket.on('connect', () => { socket.on('connect', () => {
this.socketConnected = true; this.statusList.socketConnected = true;
}); });
socket.on('disconnect', () => { socket.on('disconnect', () => {
this.socketConnected = false; this.statusList.socketConnected = false;
}); });
socket.on('status_update', (data) => { socket.on('status_update', (data) => {
this.$patch((state) => { this.$patch((state) => {