feat: Introduce TinyMqttClient interface and implementation, update auth store for MQTT connection management

This commit is contained in:
2026-02-08 23:59:48 +07:00
parent 66028d934a
commit 85af2da6ad
6 changed files with 38 additions and 45 deletions

32
components.d.ts vendored
View File

@@ -17,9 +17,7 @@ declare module 'vue' {
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default'] ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default'] ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
Bell: typeof import('./src/components/icons/Bell.vue')['default'] Bell: typeof import('./src/components/icons/Bell.vue')['default']
Button: typeof import('primevue/button')['default']
Chart: typeof import('./src/components/icons/Chart.vue')['default'] Chart: typeof import('./src/components/icons/Chart.vue')['default']
Checkbox: typeof import('primevue/checkbox')['default']
CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default'] CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default'] CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
@@ -29,41 +27,27 @@ declare module 'vue' {
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
DashboardNav: typeof import('./src/components/DashboardNav.vue')['default'] DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default'] EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
FloatLabel: typeof import('primevue/floatlabel')['default']
GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default'] GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
Home: typeof import('./src/components/icons/Home.vue')['default'] Home: typeof import('./src/components/icons/Home.vue')['default']
IconField: typeof import('primevue/iconfield')['default']
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default'] InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
InputIcon: typeof import('primevue/inputicon')['default']
InputText: typeof import('primevue/inputtext')['default'] InputText: typeof import('primevue/inputtext')['default']
Layout: typeof import('./src/components/icons/Layout.vue')['default'] Layout: typeof import('./src/components/icons/Layout.vue')['default']
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default'] LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
Message: typeof import('primevue/message')['default']
NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default'] NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default'] PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
Paginator: typeof import('primevue/paginator')['default']
PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default'] PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
Password: typeof import('primevue/password')['default']
PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default'] PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
RootLayout: typeof import('./src/components/RootLayout.vue')['default'] RootLayout: typeof import('./src/components/RootLayout.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('primevue/select')['default']
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default'] SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
Skeleton: typeof import('primevue/skeleton')['default']
StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default'] StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
Tag: typeof import('primevue/tag')['default']
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default'] TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default'] TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
Upload: typeof import('./src/components/icons/Upload.vue')['default'] Upload: typeof import('./src/components/icons/Upload.vue')['default']
Video: typeof import('./src/components/icons/Video.vue')['default'] Video: typeof import('./src/components/icons/Video.vue')['default']
VideoEditForm: typeof import('./src/components/video/VideoEditForm.vue')['default']
VideoHeader: typeof import('./src/components/video/VideoHeader.vue')['default']
VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default'] VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default']
VideoInfoPanel: typeof import('./src/components/video/VideoInfoPanel.vue')['default']
VideoPlayer: typeof import('./src/components/video/VideoPlayer.vue')['default']
VideoSkeleton: typeof import('./src/components/video/VideoSkeleton.vue')['default']
VueHead: typeof import('./src/components/VueHead.tsx')['default'] VueHead: typeof import('./src/components/VueHead.tsx')['default']
XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default'] XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default']
} }
@@ -76,9 +60,7 @@ declare global {
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default'] const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default'] const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
const Bell: typeof import('./src/components/icons/Bell.vue')['default'] const Bell: typeof import('./src/components/icons/Bell.vue')['default']
const Button: typeof import('primevue/button')['default']
const Chart: typeof import('./src/components/icons/Chart.vue')['default'] const Chart: typeof import('./src/components/icons/Chart.vue')['default']
const Checkbox: typeof import('primevue/checkbox')['default']
const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default'] const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default'] const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
@@ -88,41 +70,27 @@ declare global {
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
const DashboardNav: typeof import('./src/components/DashboardNav.vue')['default'] const DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default'] const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
const FloatLabel: typeof import('primevue/floatlabel')['default']
const GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default'] const GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
const Home: typeof import('./src/components/icons/Home.vue')['default'] const Home: typeof import('./src/components/icons/Home.vue')['default']
const IconField: typeof import('primevue/iconfield')['default']
const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default'] const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
const InputIcon: typeof import('primevue/inputicon')['default']
const InputText: typeof import('primevue/inputtext')['default'] const InputText: typeof import('primevue/inputtext')['default']
const Layout: typeof import('./src/components/icons/Layout.vue')['default'] const Layout: typeof import('./src/components/icons/Layout.vue')['default']
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default'] const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
const Message: typeof import('primevue/message')['default']
const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default'] const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default'] const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
const Paginator: typeof import('primevue/paginator')['default']
const PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default'] const PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
const Password: typeof import('primevue/password')['default']
const PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default'] const PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
const RootLayout: typeof import('./src/components/RootLayout.vue')['default'] const RootLayout: typeof import('./src/components/RootLayout.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink'] const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView'] const RouterView: typeof import('vue-router')['RouterView']
const Select: typeof import('primevue/select')['default']
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default'] const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
const Skeleton: typeof import('primevue/skeleton')['default']
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default'] const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
const Tag: typeof import('primevue/tag')['default']
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default'] const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default'] const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
const Upload: typeof import('./src/components/icons/Upload.vue')['default'] const Upload: typeof import('./src/components/icons/Upload.vue')['default']
const Video: typeof import('./src/components/icons/Video.vue')['default'] const Video: typeof import('./src/components/icons/Video.vue')['default']
const VideoEditForm: typeof import('./src/components/video/VideoEditForm.vue')['default']
const VideoHeader: typeof import('./src/components/video/VideoHeader.vue')['default']
const VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default'] const VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default']
const VideoInfoPanel: typeof import('./src/components/video/VideoInfoPanel.vue')['default']
const VideoPlayer: typeof import('./src/components/video/VideoPlayer.vue')['default']
const VideoSkeleton: typeof import('./src/components/video/VideoSkeleton.vue')['default']
const VueHead: typeof import('./src/components/VueHead.tsx')['default'] const VueHead: typeof import('./src/components/VueHead.tsx')['default']
const XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default'] const XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default']
} }

4
src/lib/interface.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface ITinyMqttClient {
connect(): void;
disconnect(): void;
}

View File

@@ -1,5 +1,7 @@
import { ITinyMqttClient } from "./interface";
export type MessageCallback = (topic: string, payload: string) => void; export type MessageCallback = (topic: string, payload: string) => void;
export class TinyMqttClient { export class TinyMqttClient implements ITinyMqttClient {
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
private encoder = new TextEncoder(); private encoder = new TextEncoder();
private decoder = new TextDecoder(); private decoder = new TextDecoder();

View File

@@ -195,20 +195,11 @@ const createAppRouter = () => {
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const auth = useAuthStore(); const auth = useAuthStore();
const head = inject(headSymbol); const head = inject(headSymbol);
let client: any;
(head as any).push(to.meta.head || {}); (head as any).push(to.meta.head || {});
if (to.matched.some((record) => record.meta.requiresAuth)) { if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!auth.user) { if (!auth.user) {
if(client?.disconnect) (client as any)?.disconnect();
next({ name: "login" }); next({ name: "login" });
} else { } else {
client = new TinyMqttClient(
'wss://broker.emqx.io:8084/mqtt',
[['ecos1231231'+auth.user.id+'#'].join("/")],
(topic, msg) => console.log(`Tín hiệu nhận được [${topic}]:`, msg)
);
client.connect();
next(); next();
} }
} else { } else {

View File

@@ -2,6 +2,7 @@ import { defineStore } from 'pinia';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ref } from 'vue'; import { ref } from 'vue';
import { client, ResponseResponse, type ModelUser } from '@/api/client'; import { client, ResponseResponse, type ModelUser } from '@/api/client';
import { TinyMqttClient } from '@/lib/liteMqtt';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const user = ref<ModelUser | null>(null); const user = ref<ModelUser | null>(null);
@@ -9,7 +10,25 @@ export const useAuthStore = defineStore('auth', () => {
const loading = ref(false); const loading = ref(false);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const initialized = ref(false); const initialized = ref(false);
watch(user, (newUser) => {
if (import.meta.env.SSR) return;
let client: TinyMqttClient | undefined;
if (newUser?.id) {
client = new TinyMqttClient(
// 'wss://broker.emqx.io:8084/mqtt',
'wss://mqtt-dashboard.com:8884/mqtt',
[['ecos1231231',newUser.id,'#'].join("/")],
(topic, msg) => console.log(`Tín hiệu nhận được [${topic}]:`, msg)
);
client.connect();
// client.auth.clearToken();
}
else {
if(client?.disconnect) client.disconnect();
client = undefined;
}
}, { deep: true });
// Initial check for session could go here if there was a /me endpoint or token check // Initial check for session could go here if there was a /me endpoint or token check
async function init() { async function init() {
if (initialized.value) return; if (initialized.value) return;

View File

@@ -109,14 +109,23 @@ export default function ssrPlugin(): Plugin[] {
config.define = config.define || {}; config.define = config.define || {};
}, },
resolveId(id, importer, options) { resolveId(id, importer, options) {
if (!id.startsWith('@httpClientAdapter')) return if (!['@httpClientAdapter', '@liteMqtt'].includes(id)) return
switch (id) {
case '@httpClientAdapter':
return path.resolve( return path.resolve(
__dirname, __dirname,
options?.ssr options?.ssr
? "./src/api/httpClientAdapter.server.ts" ? "./src/api/httpClientAdapter.server.ts"
: "./src/api/httpClientAdapter.client.ts" : "./src/api/httpClientAdapter.client.ts"
); );
case '@liteMqtt':
return path.resolve(
__dirname,
options?.ssr
? "./src/lib/liteMqtt.server.ts"
: "./src/lib/liteMqtt.ts"
);
}
}, },
async configResolved(config) { async configResolved(config) {
const viteConfig = config as any; const viteConfig = config as any;