This commit is contained in:
yangsy
2025-08-14 12:51:20 +08:00
parent 542625c1d6
commit 941984447b
8 changed files with 381 additions and 55 deletions

View File

@@ -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<void>(`/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);
},

View File

@@ -1,5 +1,5 @@
export interface Station {
id: string;
// id: string;
code: string;
name: string;
online: boolean;

22
src/enums/device-type.ts Normal file
View File

@@ -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<string, string>();
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, '网络键盘');

55
src/enums/perm-code.ts Normal file
View File

@@ -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;

View File

@@ -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;

161
src/stores/user.ts Normal file
View File

@@ -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<LoginResult | null>(null);
const userInfo = ref<any>(null);
const userResourceList = ref<string[]>([]);
const lampLoginResultRecord = ref<Record<string, LoginResult> | 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<Result<LoginResult>>(`/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<any>(`/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<NdmApplication[]>(`/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<Record<string, { username: string; password: string }>>(`/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<Result<LoginResult>>(`/${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,
},
);

View File

@@ -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<T> = [err: AxiosError | null, data: T | null, resp: Result<T> | 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<T>(url: string, option?: AxiosRequestConfig & { uniq?: boolean }): Promise<Response<T>> {
const { uniq, ...reqConfig } = option ?? {};
this.uniq = !!uniq;

View File

@@ -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<string, string | ProxyOptions> = {}
const viteProxy: Record<string, string | ProxyOptions> = {};
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,
},
}
})
};
});