diff --git a/apps/web-antd/.env.development b/apps/web-antd/.env.development index d522a706..aa18ed97 100644 --- a/apps/web-antd/.env.development +++ b/apps/web-antd/.env.development @@ -20,6 +20,8 @@ VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj6 VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE= # 客户端id VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e +# 开启WEBSOCKET +VITE_APP_WEBSOCKET=true # 开启SSE VITE_GLOB_SSE_ENABLE=true diff --git a/apps/web-antd/.env.production b/apps/web-antd/.env.production index d228ac48..a26175cd 100644 --- a/apps/web-antd/.env.production +++ b/apps/web-antd/.env.production @@ -26,6 +26,8 @@ VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj6 VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE= # 客户端id VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e +# 开启WEBSOCKET +VITE_APP_WEBSOCKET=true # 开启SSE VITE_GLOB_SSE_ENABLE=true diff --git a/apps/web-antd/src/api/property/energyManagement/meterInfo/index.ts b/apps/web-antd/src/api/property/energyManagement/meterInfo/index.ts index bf722395..b6af3266 100644 --- a/apps/web-antd/src/api/property/energyManagement/meterInfo/index.ts +++ b/apps/web-antd/src/api/property/energyManagement/meterInfo/index.ts @@ -72,5 +72,5 @@ export function queryTree(params?: any) { * 获取水/电/气表当前读数/状态 */ export function currentReading(params?: any) { - return requestClient.get(`/property/meterInfo/currentReading`, { params }) + return requestClient.get(`/property/meterInfo/currentReading`, { params }) } \ No newline at end of file diff --git a/apps/web-antd/src/api/property/roomBooking/participants/index.ts b/apps/web-antd/src/api/property/roomBooking/participants/index.ts new file mode 100644 index 00000000..e69e5d19 --- /dev/null +++ b/apps/web-antd/src/api/property/roomBooking/participants/index.ts @@ -0,0 +1,61 @@ +import type { ParticipantsVO, ParticipantsForm, ParticipantsQuery } from './model'; + +import type { ID, IDS } from '#/api/common'; +import type { PageResult } from '#/api/common'; + +import { commonExport } from '#/api/helper'; +import { requestClient } from '#/api/request'; + +/** +* 查询会议室参会记录列表 +* @param params +* @returns 会议室参会记录列表 +*/ +export function participantsList(params?: ParticipantsQuery) { + return requestClient.get>('/property/participants/list', { params }); +} + +/** + * 导出会议室参会记录列表 + * @param params + * @returns 会议室参会记录列表 + */ +export function participantsExport(params?: ParticipantsQuery) { + return commonExport('/property/participants/export', params ?? {}); +} + +/** + * 查询会议室参会记录详情 + * @param id id + * @returns 会议室参会记录详情 + */ +export function participantsInfo(id: ID) { + return requestClient.get(`/property/participants/${id}`); +} + +/** + * 新增会议室参会记录 + * @param data + * @returns void + */ +export function participantsAdd(data: ParticipantsForm) { + return requestClient.postWithMsg('/property/participants', data); +} + +/** + * 更新会议室参会记录 + * @param data + * @returns void + */ +export function participantsUpdate(data: ParticipantsForm) { + return requestClient.putWithMsg('/property/participants', data); +} + +/** + * 删除会议室参会记录 + * @param id id + * @returns void + */ +export function participantsRemove(id: ID | IDS) { + return requestClient.deleteWithMsg(`/property/participants/${id}`); +} diff --git a/apps/web-antd/src/api/property/roomBooking/participants/model.d.ts b/apps/web-antd/src/api/property/roomBooking/participants/model.d.ts new file mode 100644 index 00000000..15bb4773 --- /dev/null +++ b/apps/web-antd/src/api/property/roomBooking/participants/model.d.ts @@ -0,0 +1,54 @@ +import type { PageQuery, BaseEntity } from '#/api/common'; + +export interface ParticipantsVO { + /** + * 主键id + */ + id: string | number; + + /** + * 会议室id + */ + meetId: string | number; + + /** + * 入驻人员id + */ + residentPersonId: string | number; + +} + +export interface ParticipantsForm extends BaseEntity { + /** + * 主键id + */ + id?: string | number; + + /** + * 会议室id + */ + meetId?: string | number; + + /** + * 入驻人员id + */ + residentPersonId?: string | number; + +} + +export interface ParticipantsQuery extends PageQuery { + /** + * 会议室id + */ + meetId?: string | number; + + /** + * 入驻人员id + */ + residentPersonId?: string | number; + + /** + * 日期范围参数 + */ + params?: any; +} diff --git a/apps/web-antd/src/api/websocket.ts b/apps/web-antd/src/api/websocket.ts new file mode 100644 index 00000000..8a136fbb --- /dev/null +++ b/apps/web-antd/src/api/websocket.ts @@ -0,0 +1,162 @@ +import { useAccessStore } from '@vben/stores'; + +interface WebSocketCallbacks { + onOpen?: (event: Event) => void; + onMessage?: (event: MessageEvent) => void; + onError?: (error: Event) => void; + onClose?: (event: CloseEvent) => void; +} + +export default class WebSocketService { + private webSocket: WebSocket | null = null; + private webSocketURL: string = ''; + + // 默认回调函数 + private onOpenCallback: (event: Event) => void; + private onMessageCallback: (event: MessageEvent) => void; + private onErrorCallback: (error: Event) => void; + private onCloseCallback: (event: CloseEvent) => void; + + constructor(callbacks?: WebSocketCallbacks) { + // 设置回调函数,使用自定义回调或默认回调 + this.onOpenCallback = callbacks?.onOpen || this.defaultOnOpen; + this.onMessageCallback = callbacks?.onMessage || this.defaultOnMessage; + this.onErrorCallback = callbacks?.onError || this.defaultOnError; + this.onCloseCallback = callbacks?.onClose || this.defaultOnClose; + } + + // 初始化WebSocket连接 + initWebSocket(webSocketURL: string): void { + this.webSocketURL = webSocketURL; + try { + this.webSocket = new WebSocket(webSocketURL); + + this.webSocket.onopen = this.onOpenCallback; + this.webSocket.onmessage = this.onMessageCallback; + this.webSocket.onerror = this.onErrorCallback; + this.webSocket.onclose = this.onCloseCallback; + } catch (error) { + console.error('Failed to initialize WebSocket:', error); + } + } + + // 设置onOpen回调并更新WebSocket事件处理器 + setOnOpenCallback(callback: (event: Event) => void): void { + this.onOpenCallback = callback; + if (this.webSocket) { + this.webSocket.onopen = this.onOpenCallback; + } + } + + // 设置onMessage回调并更新WebSocket事件处理器 + setOnMessageCallback(callback: (event: MessageEvent) => void): void { + this.onMessageCallback = callback; + if (this.webSocket) { + this.webSocket.onmessage = this.onMessageCallback; + } + } + + // 设置onError回调并更新WebSocket事件处理器 + setOnErrorCallback(callback: (error: Event) => void): void { + this.onErrorCallback = callback; + if (this.webSocket) { + this.webSocket.onerror = this.onErrorCallback; + } + } + + // 设置onClose回调并更新WebSocket事件处理器 + setOnCloseCallback(callback: (event: CloseEvent) => void): void { + this.onCloseCallback = callback; + if (this.webSocket) { + this.webSocket.onclose = this.onCloseCallback; + } + } + + // 默认连接建立成功的回调 + private defaultOnOpen(event: Event): void { + console.log('WebSocket连接建立成功', event); + } + + // 默认收到服务器消息的回调 + private defaultOnMessage(event: MessageEvent): void { + console.log('收到服务器消息', event.data); + // 通常这里会解析数据并更新页面 + // const data = JSON.parse(event.data); + // 根据消息类型处理不同业务... + } + + // 默认发生错误的回调 + private defaultOnError(error: Event): void { + console.error('WebSocket连接错误', error); + } + + // 默认连接关闭的回调 + private defaultOnClose(event: CloseEvent): void { + console.log('WebSocket连接关闭', event); + } + + // 关闭连接 + close(): void { + if (this.webSocket) { + this.webSocket.close(); + this.webSocket = null; + } + } + + // 发送消息 + sendMessage(message: string | object): void { + if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) { + const dataToSend = typeof message === 'string' ? message : JSON.stringify(message); + this.webSocket.send(dataToSend); + } else { + console.error('WebSocket连接未就绪'); + } + } + + // 获取当前WebSocket状态 + getReadyState(): number | null { + return this.webSocket ? this.webSocket.readyState : null; + } + + // 获取WebSocket URL + getWebSocketURL(): string { + return this.webSocketURL; + } + + // 重新连接 + reconnect(): void { + if (this.webSocketURL) { + this.close(); + this.initWebSocket(this.webSocketURL); + } + } +} + +// 创建一个可导出的WebSocket实例 +let globalWebSocketService: WebSocketService | null = null; + +/** + * 初始化WebSocket连接的可导出方法 + * @param url WebSocket服务器地址 + * @param callbacks 回调函数对象(可选) + * @returns WebSocketService实例 + */ +export function initWebSocket(callbacks?: WebSocketCallbacks): void { + if (!globalWebSocketService) { + globalWebSocketService = new WebSocketService(callbacks); + } + const accessStore = useAccessStore(); + const clinetId = import.meta.env.VITE_GLOB_APP_CLIENT_ID; + const api = import.meta.env.VITE_GLOB_API_URL; + const host = window.location.host; + const url = `ws://${host}${api}/resource/websocket?clientid=${clinetId}&Authorization=Bearer ${accessStore.accessToken}`; + globalWebSocketService.initWebSocket(url); +} + +/** + * 获取全局WebSocket服务实例 + * @returns WebSocketService实例或null + */ +export function getWebSocketService(): WebSocketService | null { + return globalWebSocketService; +} \ No newline at end of file diff --git a/apps/web-antd/src/assets/tree/player-err.png b/apps/web-antd/src/assets/tree/player-err.png new file mode 100644 index 00000000..cb930672 Binary files /dev/null and b/apps/web-antd/src/assets/tree/player-err.png differ diff --git a/apps/web-antd/src/assets/tree/playering.png b/apps/web-antd/src/assets/tree/playering.png new file mode 100644 index 00000000..46e9952d Binary files /dev/null and b/apps/web-antd/src/assets/tree/playering.png differ diff --git a/apps/web-antd/src/assets/tree/unplayer.png b/apps/web-antd/src/assets/tree/unplayer.png new file mode 100644 index 00000000..a8d8d52b Binary files /dev/null and b/apps/web-antd/src/assets/tree/unplayer.png differ diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index 45d2df24..41501005 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -1,30 +1,30 @@ -import { createApp, watchEffect } from 'vue'; +import { createApp, watchEffect } from 'vue' -import { registerAccessDirective } from '@vben/access'; -import { registerLoadingDirective } from '@vben/common-ui/es/loading'; -import { preferences } from '@vben/preferences'; -import { initStores } from '@vben/stores'; -import '@vben/styles'; -import '@vben/styles/antd'; +import { registerAccessDirective } from '@vben/access' +import { registerLoadingDirective } from '@vben/common-ui/es/loading' +import { preferences } from '@vben/preferences' +import { initStores } from '@vben/stores' +import '@vben/styles' +import '@vben/styles/antd' -import { useTitle } from '@vueuse/core'; +import { useTitle } from '@vueuse/core' -import { setupGlobalComponent } from '#/components/global'; -import { $t, setupI18n } from '#/locales'; +import { setupGlobalComponent } from '#/components/global' +import { $t, setupI18n } from '#/locales' -import { initComponentAdapter } from './adapter/component'; -import { initSetupVbenForm } from './adapter/form'; -import App from './app.vue'; -import { router } from './router'; +import { initComponentAdapter } from './adapter/component' +import { initSetupVbenForm } from './adapter/form' +import App from './app.vue' +import { router } from './router' async function bootstrap(namespace: string) { // 初始化组件适配器 - await initComponentAdapter(); + await initComponentAdapter() // 初始化表单组件 - await initSetupVbenForm(); + await initSetupVbenForm() // // 设置弹窗的默认配置 // setDefaultModalProps({ @@ -35,49 +35,50 @@ async function bootstrap(namespace: string) { // zIndex: 1020, // }); - const app = createApp(App); + + const app = createApp(App) // 全局组件 - setupGlobalComponent(app); + setupGlobalComponent(app) // 注册v-loading指令 registerLoadingDirective(app, { loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令 spinning: 'spinning', - }); + }) // 国际化 i18n 配置 - await setupI18n(app); + await setupI18n(app) // 配置 pinia-tore - await initStores(app, { namespace }); + await initStores(app, { namespace }) // 安装权限指令 - registerAccessDirective(app); + registerAccessDirective(app) // 初始化 tippy - const { initTippy } = await import('@vben/common-ui/es/tippy'); - initTippy(app); + const { initTippy } = await import('@vben/common-ui/es/tippy') + initTippy(app) // 配置路由及路由守卫 - app.use(router); + app.use(router) // 配置Motion插件 - const { MotionPlugin } = await import('@vben/plugins/motion'); - app.use(MotionPlugin); + const { MotionPlugin } = await import('@vben/plugins/motion') + app.use(MotionPlugin) // 动态更新标题 watchEffect(() => { if (preferences.app.dynamicTitle) { - const routeTitle = router.currentRoute.value.meta?.title; + const routeTitle = router.currentRoute.value.meta?.title const pageTitle = - (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name; - useTitle(pageTitle); + (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name + useTitle(pageTitle) } - }); + }) - app.mount('#app'); + app.mount('#app') } -export { bootstrap }; +export { bootstrap } diff --git a/apps/web-antd/src/store/auth.ts b/apps/web-antd/src/store/auth.ts index cd8c4b9f..56a6fdd2 100644 --- a/apps/web-antd/src/store/auth.ts +++ b/apps/web-antd/src/store/auth.ts @@ -1,27 +1,29 @@ -import type { LoginAndRegisterParams } from '@vben/common-ui'; -import type { UserInfo } from '@vben/types'; +import type { LoginAndRegisterParams } from '@vben/common-ui' +import type { UserInfo } from '@vben/types' -import { ref } from 'vue'; -import { useRouter } from 'vue-router'; +import { ref } from 'vue' +import { useRouter } from 'vue-router' -import { LOGIN_PATH } from '@vben/constants'; -import { preferences } from '@vben/preferences'; -import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; +import { LOGIN_PATH } from '@vben/constants' +import { preferences } from '@vben/preferences' +import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores' -import { notification } from 'ant-design-vue'; -import { defineStore } from 'pinia'; +import { notification } from 'ant-design-vue' +import { defineStore } from 'pinia' -import { doLogout, getUserInfoApi, loginApi, seeConnectionClose } from '#/api'; -import { $t } from '#/locales'; +import { doLogout, getUserInfoApi, loginApi, seeConnectionClose } from '#/api' +import { $t } from '#/locales' -import { useDictStore } from './dict'; +import { useDictStore } from './dict' + +import { initWebSocket, getWebSocketService } from '#/api/websocket' export const useAuthStore = defineStore('auth', () => { - const accessStore = useAccessStore(); - const userStore = useUserStore(); - const router = useRouter(); + const accessStore = useAccessStore() + const userStore = useUserStore() + const router = useRouter() - const loginLoading = ref(false); + const loginLoading = ref(false) /** * 异步处理登录操作 @@ -33,30 +35,30 @@ export const useAuthStore = defineStore('auth', () => { onSuccess?: () => Promise | void, ) { // 异步处理用户登录操作并获取 accessToken - let userInfo: null | UserInfo = null; + let userInfo: null | UserInfo = null try { - loginLoading.value = true; - const { access_token } = await loginApi(params); + loginLoading.value = true + const { access_token } = await loginApi(params) // 将 accessToken 存储到 accessStore 中 - accessStore.setAccessToken(access_token); - accessStore.setRefreshToken(access_token); + accessStore.setAccessToken(access_token) + accessStore.setRefreshToken(access_token) // 获取用户信息并存储到 accessStore 中 - userInfo = await fetchUserInfo(); + userInfo = await fetchUserInfo() /** * 设置用户信息 */ - userStore.setUserInfo(userInfo); + userStore.setUserInfo(userInfo) /** * 在这里设置权限 */ - accessStore.setAccessCodes(userInfo.permissions); + accessStore.setAccessCodes(userInfo.permissions) if (accessStore.loginExpired) { - accessStore.setLoginExpired(false); + accessStore.setLoginExpired(false) } else { - onSuccess ? await onSuccess?.() : await router.push('/analytics'); + onSuccess ? await onSuccess?.() : await router.push('/analytics') } if (userInfo?.realName) { @@ -64,48 +66,55 @@ export const useAuthStore = defineStore('auth', () => { description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, duration: 3, message: $t('authentication.loginSuccess'), - }); + }) } } finally { - loginLoading.value = false; + loginLoading.value = false } return { userInfo, - }; + } } async function logout(redirect: boolean = true) { try { - await seeConnectionClose(); - await doLogout(); + await seeConnectionClose() + await doLogout() + /** + * 断开websocket连接 + */ + const ws = getWebSocketService(); + if(ws) { + ws.close(); + } } catch (error) { - console.error(error); + console.error(error) } finally { - resetAllStores(); - accessStore.setLoginExpired(false); + resetAllStores() + accessStore.setLoginExpired(false) // 回登陆页带上当前路由地址 await router.replace({ path: LOGIN_PATH, query: redirect ? { - redirect: encodeURIComponent(router.currentRoute.value.fullPath), - } + redirect: encodeURIComponent(router.currentRoute.value.fullPath), + } : {}, - }); + }) } } async function fetchUserInfo() { - const backUserInfo = await getUserInfoApi(); + const backUserInfo = await getUserInfoApi() /** * 登录超时的情况 */ if (!backUserInfo) { - throw new Error('获取用户信息失败.'); + throw new Error('获取用户信息失败.') } - const { permissions = [], roles = [], user } = backUserInfo; + const { permissions = [], roles = [], user } = backUserInfo /** * 从后台user -> vben user转换 */ @@ -116,19 +125,25 @@ export const useAuthStore = defineStore('auth', () => { roles, userId: user.userId, username: user.userName, - }; - userStore.setUserInfo(userInfo); + } + userStore.setUserInfo(userInfo) + + /** + * 初始化websocket + */ + initWebSocket() + /** * 需要重新加载字典 * 比如退出登录切换到其他租户 */ - const dictStore = useDictStore(); - dictStore.resetCache(); - return userInfo; + const dictStore = useDictStore() + dictStore.resetCache() + return userInfo } function $reset() { - loginLoading.value = false; + loginLoading.value = false } return { @@ -137,5 +152,5 @@ export const useAuthStore = defineStore('auth', () => { fetchUserInfo, loginLoading, logout, - }; -}); + } +}) diff --git a/apps/web-antd/src/views/property/energyManagement/electricEnergy/elctricitySituation/index.vue b/apps/web-antd/src/views/property/energyManagement/electricEnergy/elctricitySituation/index.vue index c4a2fef2..4b6b1c7d 100644 --- a/apps/web-antd/src/views/property/energyManagement/electricEnergy/elctricitySituation/index.vue +++ b/apps/web-antd/src/views/property/energyManagement/electricEnergy/elctricitySituation/index.vue @@ -1,13 +1,44 @@ + + diff --git a/apps/web-antd/src/views/property/roomBooking/participants/participants-modal.vue b/apps/web-antd/src/views/property/roomBooking/participants/participants-modal.vue new file mode 100644 index 00000000..4972eaa2 --- /dev/null +++ b/apps/web-antd/src/views/property/roomBooking/participants/participants-modal.vue @@ -0,0 +1,101 @@ + + + + diff --git a/apps/web-antd/src/views/sis/video/channel-tree.vue b/apps/web-antd/src/views/sis/video/channel-tree.vue index 6ae26b9e..47203245 100644 --- a/apps/web-antd/src/views/sis/video/channel-tree.vue +++ b/apps/web-antd/src/views/sis/video/channel-tree.vue @@ -7,17 +7,20 @@ import type { TreeNode } from '#/api/common'; defineOptions({ inheritAttrs: false }); -withDefaults(defineProps<{ showSearch?: boolean }>(), { showSearch: true }); +withDefaults( + defineProps<{ + showSearch?: boolean; + currentSelectKey?: string; + selectKeys?: string[]; + }>(), + { + showSearch: true, + currentSelectKey: '', + selectKeys: [], + }, +); -const emit = defineEmits<{ - - checked: []; - /** - * 点击节点的事件 - */ - reload: []; - select: []; -}>(); +const emit = defineEmits(['selected', 'reload', 'checked']); const searchValue = defineModel('searchValue', { type: String, @@ -37,6 +40,10 @@ async function loadChannelTree() { showTreeSkeleton.value = false; } +function onSelect(key: any, selectNode: any) { + emit('selected', key, selectNode); +} + async function handleReload() { await loadChannelTree(); emit('reload'); @@ -46,21 +53,11 @@ onMounted(loadChannelTree); - -
+
+ +
- + + diff --git a/apps/web-antd/src/views/sis/video/index.vue b/apps/web-antd/src/views/sis/video/index.vue index 57206b85..82f64f8b 100644 --- a/apps/web-antd/src/views/sis/video/index.vue +++ b/apps/web-antd/src/views/sis/video/index.vue @@ -1,9 +1,15 @@