diff --git a/src/apis/client.ts b/src/apis/client.ts index b390966..ad5dedb 100644 --- a/src/apis/client.ts +++ b/src/apis/client.ts @@ -1,27 +1,74 @@ import type { AxiosError } from 'axios'; import { Request } from '@/utils/request'; +import { useUserStore } from '@/stores/user'; +import { getAppEnvConfig } from '@/utils/env'; -export const ndmClient = new Request({ +import router from '@/router'; + +export const userClient = new Request({ requestInterceptor: (config) => { + const userStore = useUserStore(); + const { lampAuthorization, lampClientId, lampClientSecret } = getAppEnvConfig(); + const newAuthorization = window.btoa(`${lampClientId}:${lampClientSecret}`); + const authorization = lampAuthorization.trim() !== '' ? lampAuthorization : newAuthorization; + config.headers.set('accept-language', 'zh-CN,zh;q=0.9'); + config.headers.set('accept', 'application/json, text/plain, */*'); + config.headers.set('Applicationid', ''); + config.headers.set('Tenantid', '1'); + config.headers.set('Authorization', authorization); + config.headers.set('token', userStore.userLoginResult?.token ?? ''); return config; }, responseInterceptor: (response) => { return response; }, responseErrorInterceptor: (error) => { + const err = error as AxiosError; + if (err.response?.status === 401) { + window.$message.error('登录超时,请重新登录'); + const userStore = useUserStore(); + userStore.resetStore(); + } + if (err.response?.status === 404) { + router.push('/404'); + } return Promise.reject(error); }, }); -export const userClient = new Request({ - responseErrorInterceptor: (error) => { +export const ndmClient = new Request({ + requestInterceptor: async (config) => { + // 当OCC的token失效时,虽然不会影响车站请求,但是需要重新登录,所以在车站请求之前需要校验OCC的登录状态 + const [err] = await userClient.post(`/api/ndm/ndmKeepAlive/verify`, {}, { timeout: 5000 }); + if (err) { + window.$message.error('登录超时,请重新登录'); + const userStore = useUserStore(); + userStore.resetStore(); + return Promise.reject(err); + } + const userStore = useUserStore(); + const { lampAuthorization, lampClientId, lampClientSecret } = getAppEnvConfig(); + const newAuthorization = window.btoa(`${lampClientId}:${lampClientSecret}`); + const authorization = lampAuthorization.trim() !== '' ? lampAuthorization : newAuthorization; + config.headers.set('accept-language', 'zh-CN,zh;q=0.9'); + config.headers.set('accept', 'application/json, text/plain, */*'); + config.headers.set('Applicationid', ''); + config.headers.set('Tenantid', '1'); + config.headers.set('Authorization', authorization); + const staticCode = config.url?.split('/api').at(0)?.split('/').at(-1) ?? ''; + config.headers.set('token', userStore.lampLoginResultRecord?.[staticCode]?.token ?? ''); + return config; + }, + responseErrorInterceptor: async (error) => { const err = error as AxiosError; if (err.response?.status === 401) { - // TODO: 处理 401 错误,例如跳转到登录页 - } - if (err.response?.status === 404) { - // TODO: 处理 404 错误 + // 当车站请求由于token时效而失败时,需要重新登录车站获取token,然后重新请求 + const stationCode = err.config?.url?.split('/api').at(0)?.split('/').at(-1) ?? ''; + const userStore = useUserStore(); + await userStore.lampLogin(stationCode); + error.config.headers.token = userStore.lampLoginResultRecord?.[stationCode]?.token ?? ''; + return ndmClient.requestInstance(error.config); } return Promise.reject(error); }, diff --git a/src/apis/domains/station.ts b/src/apis/domains/station.ts index 8ecd64c..76de2a8 100644 --- a/src/apis/domains/station.ts +++ b/src/apis/domains/station.ts @@ -1,5 +1,5 @@ export interface Station { - id: string; + // id: string; code: string; name: string; online: boolean; diff --git a/src/enums/device-type.ts b/src/enums/device-type.ts new file mode 100644 index 0000000..ff055c2 --- /dev/null +++ b/src/enums/device-type.ts @@ -0,0 +1,22 @@ +export enum DeviceType { + Switch = '220', + Server = '401+403', + MediaServer = '403', + VideoServer = '401', + Decoder = '114', + Camera = '132', + Nvr = '118', + SecurityBox = '222', + Keyboard = '141', +} + +export const deviceTypesMap = new Map(); +deviceTypesMap.set(DeviceType.Switch, '交换机'); +deviceTypesMap.set(DeviceType.Server, '服务器'); +deviceTypesMap.set(DeviceType.MediaServer, '媒体服务器'); +deviceTypesMap.set(DeviceType.VideoServer, '视频服务器'); +deviceTypesMap.set(DeviceType.Decoder, '解码器'); +deviceTypesMap.set(DeviceType.Camera, '摄像机'); +deviceTypesMap.set(DeviceType.Nvr, '网络录像机'); +deviceTypesMap.set(DeviceType.SecurityBox, '智能安防箱'); +deviceTypesMap.set(DeviceType.Keyboard, '网络键盘'); diff --git a/src/enums/perm-code.ts b/src/enums/perm-code.ts new file mode 100644 index 0000000..5bb1cdc --- /dev/null +++ b/src/enums/perm-code.ts @@ -0,0 +1,55 @@ +export const PermCode = { + // 车站配置面板 + ...{ + STATION_ACCESS: 'ndmapp:station', + STATION_ADD: 'ndmapp:station:add', + STATION_DELETE: 'ndmapp:station:delete', + STATION_EDIT: 'ndmapp:station:edit', + STATION_EXPORT: 'ndmapp:station:export', + STATION_IMPORT: 'ndmapp:station:import', + STATION_VIEW: 'ndmapp:station:view', + }, + // 数据看板页 + ...{ + DASHBOARD_ACCESS: 'ndmapp:dashboard', + DASHBOARD_ADD: 'ndmapp:dashboard:add', + DASHBOARD_DELETE: 'ndmapp:dashboard:delete', + DASHBOARD_EDIT: 'ndmapp:dashboard:edit', + DASHBOARD_EXPORT: 'ndmapp:dashboard:export', + DASHBOARD_IMPORT: 'ndmapp:dashboard:import', + DASHBOARD_VIEW: 'ndmapp:dashboard:view', + }, + // 设备状态页 + ...{ + DEVICES_ACCESS: 'ndmapp:devices', + DEVICES_ADD: 'ndmapp:devices:add', + DEVICES_DELETE: 'ndmapp:devices:delete', + DEVICES_EDIT: 'ndmapp:devices:edit', + DEVICES_EXPORT: 'ndmapp:devices:export', + DEVICES_IMPORT: 'ndmapp:devices:import', + DEVICES_VIEW: 'ndmapp:devices:view', + DEVICES_CONFIG: 'ndmapp:devices:config', + }, + // 数据统计页 + ...{ + STATISTICS_ACCESS: 'ndmapp:statistics', + STATISTICS_ADD: 'ndmapp:statistics:add', + STATISTICS_DELETE: 'ndmapp:statistics:delete', + STATISTICS_EDIT: 'ndmapp:statistics:edit', + STATISTICS_EXPORT: 'ndmapp:statistics:export', + STATISTICS_IMPORT: 'ndmapp:statistics:import', + STATISTICS_VIEW: 'ndmapp:statistics:view', + }, + // 系统日志页 + ...{ + LOGS_ACCESS: 'ndmapp:logs', + LOGS_ADD: 'ndmapp:logs:add', + LOGS_DELETE: 'ndmapp:logs:delete', + LOGS_EDIT: 'ndmapp:logs:edit', + LOGS_EXPORT: 'ndmapp:logs:export', + LOGS_IMPORT: 'ndmapp:logs:import', + LOGS_VIEW: 'ndmapp:logs:view', + }, +}; + +export type PermoCodeType = keyof typeof PermCode; diff --git a/src/router/index.ts b/src/router/index.ts index e1eab52..48f5762 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,8 +1,65 @@ -import { createRouter, createWebHistory } from 'vue-router' +import { createRouter, createWebHistory } from 'vue-router'; +import { useUserStore } from '@/stores/user'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), - routes: [], -}) + routes: [ + { + path: '/login', + component: () => import('@/pages/login-page.vue'), + }, + { + path: '/', + component: () => import('@/layouts/app-layout.vue'), + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + component: () => import('@/pages/dashboard-page.vue'), + }, + { + path: 'device', + component: () => import('@/pages/device-page.vue'), + }, + { + path: 'alarm', + component: () => import('@/pages/alarm-page.vue'), + }, + { + path: 'statistics', + component: () => import('@/pages/statistics-page.vue'), + }, + { + path: 'log', + component: () => import('@/pages/log-page.vue'), + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/pages/not-found-page.vue'), + }, + ], + }, + ], +}); -export default router +const whiteList = ['/login']; + +router.beforeEach((to, from, next) => { + const userStore = useUserStore(); + if (userStore.userLoginResult?.token) { + if (to.path === '/login') { + next({ path: '/' }); + } else { + next(); + } + } else { + if (whiteList.includes(to.path)) { + next(); + } else { + next('/login'); + } + } +}); + +export default router; diff --git a/src/stores/user.ts b/src/stores/user.ts new file mode 100644 index 0000000..ddcd4bb --- /dev/null +++ b/src/stores/user.ts @@ -0,0 +1,161 @@ +import { userClient } from '@/apis/client'; +import type { LoginParams, LoginResult } from '@/apis/models/user'; +import type { Result } from '@/axios'; +import { AesEncryption } from '@/utils/cipher'; +import { getAppEnvConfig } from '@/utils/env'; +import axios, { AxiosError } from 'axios'; +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import dayjs from 'dayjs'; +import { useStationStore } from './station'; +import { onlineManager } from '@tanstack/vue-query'; + +const getHeaders = () => { + const { lampAuthorization, lampClientId, lampClientSecret } = getAppEnvConfig(); + const newAuthorization = window.btoa(`${lampClientId}:${lampClientSecret}`); + const authorization = lampAuthorization.trim() !== '' ? lampAuthorization : newAuthorization; + return { + 'content-type': 'application/json', + 'accept-language': 'zh-CN,zh;q=0.9', + accept: 'application/json, text/plain, */*', + ApplicationId: '1', + TenantId: '1', + Authorization: authorization, + }; +}; + +const aesEncryption = new AesEncryption(); + +export const useUserStore = defineStore( + 'ndm-user-store', + () => { + const userLoginResult = ref(null); + const userInfo = ref(null); + const userResourceList = ref([]); + const lampLoginResultRecord = ref | null>(null); + + const resetStore = () => { + userLoginResult.value = null; + userInfo.value = null; + userResourceList.value = []; + lampLoginResultRecord.value = null; + }; + + const userLogin = async (loginParams: LoginParams) => { + const { username, password, code, key, grantType } = loginParams; + const data = { + username: aesEncryption.encryptByAES(username), + password: aesEncryption.encryptByAES(password), + code, + key, + grantType, + }; + const headers = getHeaders(); + const { data: respData } = await axios.post>(`/api/oauth/anyTenant/login`, data, { headers }); + if (!respData.isSuccess) { + console.error(respData); + window.$message.destroyAll(); + window.$dialog.error({ + closable: false, + maskClosable: false, + title: '错误提示', + content: respData.msg, + positiveText: '确认', + onPositiveClick: () => { + window.$message.destroyAll(); + }, + }); + throw new AxiosError(respData.msg, `${respData.code}`); + } else { + userLoginResult.value = respData.data; + } + }; + + const userLogout = async () => { + const [err] = await userClient.post(`/api/oauth/anyUser/logout`, { token: userLoginResult.value?.token }); + if (err) throw err; + resetStore(); + }; + + const userGetInfo = async () => { + const [err, userInfo] = await userClient.get(`/api/oauth/anyone/getUserInfoById`); + if (err || !userInfo) { + throw err; + } + userInfo.value = userInfo; + }; + + const userGetResourceList = async () => { + interface NdmApplication { + appKey: string; + id: string; + name: string; + } + const [e, ndmApplicationList] = await userClient.get(`/api/system/anyone/findMyApplication?_t=${dayjs().valueOf()}`); + if (e || !ndmApplicationList) { + throw e; + } + const { ndmAppKey } = getAppEnvConfig(); + const applicationId = ndmApplicationList.find((app) => app.appKey === ndmAppKey)?.id ?? ''; + const [err, ndmAppResource] = await userClient.get<{ resourceList: string[] }>(`/api/oauth/anyone/visible/resource?applicationId=${applicationId}&_t=${dayjs().valueOf()}`); + if (err || !ndmAppResource) { + throw err; + } + userResourceList.value = ndmAppResource.resourceList; + }; + + const lampLogin = async (stationCode: string) => { + const { data: accountRecord } = await axios.get>(`/minio/ndm/ndm-accounts.json?_t=${dayjs().unix()}`); + const data = { + username: aesEncryption.encryptByAES(accountRecord[stationCode].username), + password: aesEncryption.encryptByAES(accountRecord[stationCode].password), + grantType: 'PASSWORD', + }; + const headers = getHeaders(); + const { data: respData } = await axios.post>(`/${stationCode}/api/oauth/anyTenant/login`, data, { headers }); + // 如果登录返回失败,需要提示用户检查用户名和密码配置,并全局停止轮询 + if (!respData.isSuccess) { + console.error(respData); + const stationStore = useStationStore(); + console.log('stationList:', stationStore.stationList); + const stationName = stationStore.stationList.find((station) => station.code === stationCode)?.name ?? ''; + window.$dialog.destroyAll(); + window.$dialog.error({ + closable: false, + maskClosable: false, + draggable: true, + title: `${stationName}登录失败`, + content: `请检查该车站的用户名和密码配置,并在确认无误后刷新页面!`, + positiveText: '刷新', + onPositiveClick: () => { + window.location.reload(); + }, + }); + onlineManager.setOnline(false); + throw new AxiosError(respData.msg, `${respData.code}`); + } else { + if (lampLoginResultRecord.value === null) { + lampLoginResultRecord.value = {}; + } + lampLoginResultRecord.value[stationCode] = respData.data; + } + }; + + return { + userLoginResult, + userInfo, + userResourceList, + lampLoginResultRecord, + + resetStore, + userLogin, + userLogout, + userGetInfo, + userGetResourceList, + lampLogin, + }; + }, + { + persist: true, + }, +); diff --git a/src/utils/request.ts b/src/utils/request.ts index 6742949..734ad2b 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,18 +1,9 @@ -import type { - AxiosError, - AxiosInstance, - AxiosRequestConfig, - AxiosResponse, - CreateAxiosDefaults, - InternalAxiosRequestConfig, -} from 'axios'; +import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'; import axios from 'axios'; import type { Result } from '@/axios'; -import { getAppEnvConfig } from './env'; - export type Response = [err: AxiosError | null, data: T | null, resp: Result | null]; export interface RequestOptions extends CreateAxiosDefaults { @@ -42,18 +33,6 @@ export class Request { this.lastAbortController = this.abortController; this.abortController = new AbortController(); return config; - - // 业务登录所需headers - // const { lampAuthorization, lampClientId, lampClientSecret } = getAppEnvConfig(); - // const newAuthorization = window.btoa(`${lampClientId}:${lampClientSecret}`); - // const authorization = lampAuthorization.trim() !== '' ? lampAuthorization : newAuthorization; - // config.headers.set('accept-language', 'zh-CN,zh;q=0.9'); - // config.headers.set('accept', 'application/json, text/plain, */*'); - // config.headers.set('Applicationid', '') - // config.headers.set('Tenantid', '1'); - // config.headers.set('Authorization', authorization); - // config.headers.set('token', this.extraInfo?.token ?? '') - // return config; }); const requestInterceptor = config?.requestInterceptor ?? Request.defaultRequestInterceptor; @@ -76,14 +55,16 @@ export class Request { private static defaultResponseErrorInterceptor(error: any) { const err = error as AxiosError; if (err.status === 401) { - // } if (err.status === 404) { - // } return Promise.reject(error); } + public get requestInstance(): AxiosInstance { + return this.instance; + } + get(url: string, option?: AxiosRequestConfig & { uniq?: boolean }): Promise> { const { uniq, ...reqConfig } = option ?? {}; this.uniq = !!uniq; diff --git a/vite.config.ts b/vite.config.ts index 21961da..9ce2940 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,34 +1,37 @@ -import { fileURLToPath, URL } from 'node:url' +import { fileURLToPath, URL } from 'node:url'; -import { defineConfig, ProxyOptions } from 'vite' -import vue from '@vitejs/plugin-vue' -import vueJsx from '@vitejs/plugin-vue-jsx' -import vueDevTools from 'vite-plugin-vue-devtools' +import { defineConfig, ProxyOptions } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import vueJsx from '@vitejs/plugin-vue-jsx'; +import vueDevTools from 'vite-plugin-vue-devtools'; const apiProxyList: [string, string][] = [ ['/minio', 'http://172.16.6.248:9002'], // ['/api', 'http://172.16.6.248:18760/api'], - ['/api', 'http://localhost:3000/api'], - ['/10/api', 'http://localhost:3000/api'], - ['/11/api', 'http://localhost:3000/api'], -] + // ['/api', 'http://localhost:3000/api'], + // ['/10/api', 'http://localhost:3000/api'], + // ['/11/api', 'http://localhost:3000/api'], + ['/api', 'http://172.16.6.113:18760/api'], + ['/113/api', 'http://172.16.6.113:18760/api'], + ['/114/api', 'http://172.16.6.114:18760/api'], +]; // https://vite.dev/config/ export default defineConfig((/* { command, mode } */) => { - const viteProxy: Record = {} + const viteProxy: Record = {}; apiProxyList.forEach((apiProxy) => { - const [prefix, target] = apiProxy + const [prefix, target] = apiProxy; viteProxy[prefix] = { target, changeOrigin: true, rewrite: (path) => { - console.log(`请求路径: ${path}`) - const rewrittenPath = path.replace(new RegExp(`^${prefix}`), '') - console.log(`将代理到: ${target}${rewrittenPath}`) - return rewrittenPath + console.log(`请求路径: ${path}`); + const rewrittenPath = path.replace(new RegExp(`^${prefix}`), ''); + console.log(`将代理到: ${target}${rewrittenPath}`); + return rewrittenPath; }, - } - }) + }; + }); return { plugins: [vue(), vueJsx(), vueDevTools()], @@ -41,5 +44,5 @@ export default defineConfig((/* { command, mode } */) => { port: 9654, proxy: viteProxy, }, - } -}) + }; +});