feat:添加接口签名

This commit is contained in:
2026-04-08 11:26:40 +08:00
parent 8229c2cc67
commit 2d7ea7e5c8
10 changed files with 753 additions and 208 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
.DS_Store
dist
xSynergy-manage
dist-ssr
coverage
*.local

View File

@@ -27,3 +27,24 @@ npm run dev
```sh
npm run build
```
## 注意事项
1.在本地启动项目时要修改签名的host,在utils/tools.js
```
// 获取host从baseURL或当前域名 本地启动要求host为xsynergy.gxtech.ltd
// let host = 'xsynergy.gxtech.ltd';
let host = '';
if (config.baseURL) {
try {
const urlObj = new URL(config.baseURL);
host = urlObj.host;
} catch (e) {
host = window.location.host;
}
} else {
host = window.location.host;
}
```

BIN
dist.zip

Binary file not shown.

12
package-lock.json generated
View File

@@ -15,9 +15,11 @@
"@vueuse/core": "^9.5.0",
"autoprefixer": "^10.4.21",
"axios": "^0.27.2",
"blakejs": "^1.2.1",
"code-inspector-plugin": "^0.20.12",
"crypto-js": "^4.2.0",
"element-plus": "^2.2.27",
"hashids": "^1.2.2",
"js-cookie": "^3.0.1",
"livekit-client": "^2.7.5",
"mitt": "^3.0.0",
@@ -2144,6 +2146,11 @@
"readable-stream": "^4.2.0"
}
},
"node_modules/blakejs": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz",
"integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ=="
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz",
@@ -4563,6 +4570,11 @@
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hashids": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/hashids/-/hashids-1.2.2.tgz",
"integrity": "sha512-dEHCG2LraR6PNvSGxosZHIRgxF5sNLOIBFEHbj8lfP9WWmu/PWPMzsip1drdVSOFi51N2pU7gZavrgn7sbGFuw=="
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",

View File

@@ -15,9 +15,11 @@
"@vueuse/core": "^9.5.0",
"autoprefixer": "^10.4.21",
"axios": "^0.27.2",
"blakejs": "^1.2.1",
"code-inspector-plugin": "^0.20.12",
"crypto-js": "^4.2.0",
"element-plus": "^2.2.27",
"hashids": "^1.2.2",
"js-cookie": "^3.0.1",
"livekit-client": "^2.7.5",
"mitt": "^3.0.0",

View File

@@ -1,5 +1,4 @@
import request from '@/utils/request'
// import request from '@/views/custom/Meter/public/request.js'
import request from '@/utils/request'
// 登录方法
export function login(username, password) {

View File

@@ -5,6 +5,7 @@ import {
ElMessage,
} from "element-plus";
import { tansParams } from "@/utils/ruoyi";
import { generateSign } from "@/utils/tools";
import cache from "@/plugins/cache";
import { getToken, removeToken } from "@/utils/auth";
import router from '@/router';
@@ -18,11 +19,11 @@ const getBaseURL = () => {
if (import.meta.env.DEV) {
return import.meta.env.VITE_APP_BASE_API;
}
// 生产环境使用相对路径
// 返回空字符串,让浏览器自动使用当前域名
return '';
// 或者如果后端 API 有固定路径前缀,可以这样设置:
// return '/api'; // 这样请求会变成 https://当前域名/api/xxx
};
@@ -38,21 +39,21 @@ const service = axios.create({
// request拦截器
service.interceptors.request.use(
(config) => {
// 在拦截器内部安全地使用 store
let sudid = ''
try {
const meterStore = useMeterStore()
if (!meterStore.udid) {
meterStore.initUdid();
sudid = meterStore.getSudid();
} else {
sudid = meterStore.getSudid();
}
} catch (error) {
console.warn('MeterStore 初始化失败:', error);
}
async (config) => {
// 在拦截器内部安全地使用 store
let sudid = ''
try {
const meterStore = useMeterStore()
if (!meterStore.udid) {
meterStore.initUdid();
sudid = meterStore.getSudid();
} else {
sudid = meterStore.getSudid();
}
} catch (error) {
console.warn('MeterStore 初始化失败:', error);
}
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
if (getToken() && !isToken) {
@@ -61,12 +62,11 @@ service.interceptors.request.use(
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
if (sudid) {
if (sudid) {
config.headers["X-User-Agent"] = `gxtech/web 1.0.0: c=GxTech, udid=${sudid}, sv=15.4.1, app=stt`;
}
// get请求映射params参数
if (config.method === "get" && config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
@@ -118,79 +118,179 @@ service.interceptors.request.use(
}
}
}
console.log(sudid,'sudid')
// 生成sign并添加到URL
if (sudid) {
try {
const now = Date.now();
const sign = await generateSign(config, sudid, now);
if (sign) {
// 将sign添加到URL
const separator = config.url.includes('?') ? '&' : '?';
config.url += `${separator}sign=${encodeURIComponent(sign)}`;
}
} catch (signError) {
console.error('添加sign失败:', signError);
}
}
return config;
},
(error) => {
(error) => {
Promise.reject(error);
}
);
let isShowingError = false;
const showError = (message) => {
if (isShowingError) return;
isShowingError = true;
ElMessage({
message,
type: 'error',
duration: 3000
});
setTimeout(() => {
isShowingError = false;
}, 2000);
};
service.interceptors.response.use(
(response) => {
// 1. 检查响应是否存在
if (!response) {
ElMessage.error('无响应数据');
return Promise.reject(new Error('无响应数据'));
}
// 2. 安全获取响应数据和状态码
const responseData = response.data || {};
const statusCode = response.status;
const businessCode = responseData.meta?.code || statusCode;
(response) => {
try {
// console.log('原始响应:', response);
// 1. 检查响应是否存在
if (!response) {
ElMessage.error('无响应数据');
return Promise.reject(new Error('无响应数据'));
}
// 2. 安全获取响应数据和状态码
const responseData = response.data || {};
const statusCode = response.status;
const businessCode = responseData.meta?.code || statusCode;
// 3. 二进制数据直接返回
if (
response.request.responseType === 'blob' ||
response.request.responseType === 'arraybuffer'
) {
return responseData;
}
// 3. 二进制数据直接返回
if (
response.request.responseType === 'blob' ||
response.request.responseType === 'arraybuffer'
) {
return responseData;
}
// 4. 根据业务码处理不同情况
switch (businessCode) {
case 200:
case 201:
return Promise.resolve(responseData);
case 401:
// return Promise.resolve(responseData);
// const currentPath = router.currentRoute.value.name;
// if(currentPath == 'ConferencingRoom'){
// return Promise.resolve(responseData);
// }else{
// console.log('响应数据:', responseData);
// 4. 根据业务码处理不同情况
switch (businessCode) {
case 200:
case 201:
return Promise.resolve(responseData);
case 401:
// return Promise.resolve(responseData);
// const currentPath = router.currentRoute.value.name;
// if(currentPath == 'ConferencingRoom'){
// return Promise.resolve(responseData);
// }else{
return handleUnauthorized()
// .then(() => {
// return Promise.reject({ code: 401, message: '未授权' });
// });
// .then(() => {
// return Promise.reject({ code: 401, message: '未授权' });
// });
// }
case 500:
const serverErrorMsg = responseData.meta?.message || '服务器内部错误';
ElMessage({ message: serverErrorMsg, type: 'error' });
return Promise.reject({ code: 500, message: serverErrorMsg });
case 500:
const serverErrorMsg = responseData.meta?.message || responseData.meta?.msg || '服务器内部错误';
ElMessage({ message: serverErrorMsg, type: 'error' });
return Promise.reject({ code: 500, message: serverErrorMsg });
default:
const errorMsg = responseData.meta?.message || `业务错误 (${businessCode})`;
ElNotification.error({ title: errorMsg });
return Promise.reject({ code: businessCode, message: errorMsg });
default:
const errorMsg = responseData.meta?.message || responseData.meta?.msg || `业务错误 (${businessCode})`;
ElNotification.error({ title: errorMsg });
return Promise.reject({ code: businessCode, message: errorMsg });
}
} catch (err) {
console.error('响应拦截器异常:', err);
showError('响应处理异常');
return Promise.reject({
code: -1,
message: '响应处理异常',
raw: err
});
}
},
(error) => {
let { message } = error;
let code = error?.response?.status || -1;
if (message == 'Network Error') {
message = '后端接口连接异常';
ElMessage({ message, type: 'error', duration: 5 * 1000 });
} else if (message.includes('timeout')) {
message = '系统接口请求超时';
ElMessage({ message, type: 'error', duration: 5 * 1000 });
} else if (message.includes('Request failed with status code')) {
// message = '系统接口' + message.substr(message.length - 3) + '异常';
}
// (error) => {
// let { message } = error;
// let code = error?.response?.status || -1;
// if (message == 'Network Error') {
// message = '后端接口连接异常';
// ElMessage({ message, type: 'error', duration: 5 * 1000 });
// } else if (message.includes('timeout')) {
// message = '系统接口请求超时';
// ElMessage({ message, type: 'error', duration: 5 * 1000 });
// } else if (message.includes('Request failed with status code')) {
// // message = '系统接口' + message.substr(message.length - 3) + '异常';
// }
// 返回结构化错误
return Promise.reject({
code,
message,
raw: error // 保留原始 error
});
// // 返回结构化错误
// return Promise.reject({
// code,
// message,
// raw: error // 保留原始 error
// });
// }
(error) => {
try {
let message = error?.message || '请求失败';
let code = error?.response?.status || -1;
if (message === 'Network Error') {
message = '后端接口连接异常';
} else if (message.includes('timeout')) {
message = '系统接口请求超时';
}
else if (code) {
const statusMap = {
400: '请求参数错误',
401: '登录已过期,请重新登录',
403: '没有权限访问',
404: '请求资源不存在',
408: '请求超时,请稍后重试',
409: '数据冲突,请刷新后重试',
422: '参数校验失败',
500: '服务器异常,请联系管理员',
501: '功能暂未实现',
502: '服务器内部错误,请联系管理员',
503: '服务繁忙,请稍后重试',
504: '服务器响应超时,请稍后重试'
};
message = message || statusMap[code];
// 401 特殊处理(不影响你原逻辑)
if (code === 401) {
return handleUnauthorized();
}
}
showError(message);
return Promise.reject({
code,
message,
raw: error
});
} catch (err) {
console.error('错误拦截器异常:', err);
return Promise.reject({
code: -1,
message: '网络异常处理失败',
raw: err
});
}
}
);
@@ -222,17 +322,17 @@ service.interceptors.response.use(
// });
// }
function handleUnauthorized() {
function handleUnauthorized() {
removeToken();
// 使用 nextTick 确保路由状态已更新
import('vue').then(({ nextTick }) => {
nextTick(() => {
const currentPath = router.currentRoute.value.fullPath;
const currentPath = router.currentRoute.value.fullPath;
if (router.currentRoute.value.path !== '/login') {
router.push({
path: '/login',
query: {
query: {
redirect: currentPath !== '/login' ? currentPath : undefined
}
});

View File

@@ -1,3 +1,7 @@
import Hashids from 'hashids';
import blake from 'blakejs';
import CryptoJS from 'crypto-js';
// 获取uuid
export function generateUUID() {
var d = new Date().getTime();
@@ -158,3 +162,429 @@ function isValidBox(x1, y1, x2, y2, imgWidth, imgHeight) {
x2 <= imgWidth && y2 <= imgHeight
);
}
// 全局临时变量,用于存储最近一次请求的签名信息
let lastRequestKey = '';
let lastTimestamp = 0;
let lastUsedTimestamp = 0;
let collisionCount = 0;
/**
* 生成请求唯一标识key
* @param {number} timestamp - 时间戳
* @param {string} method - 请求方法
* @param {string} urlPath - API路径
* @returns {string} 唯一标识
*/
function getRequestKey(timestamp, method, urlPath) {
return `${timestamp}_${method}_${urlPath}`;
}
/**
* 获取去重后的时间戳
* 处理同一毫秒内相同接口的重复请求
* @param {number} originalTimestamp - 原始时间戳
* @param {string} method - 请求方法
* @param {string} urlPath - API路径
* @returns {number} 处理后的时间戳
*/
function getDeduplicatedTimestamp(originalTimestamp, method, urlPath) {
const currentKey = getRequestKey(originalTimestamp, method, urlPath);
// 如果当前请求与上一次请求的时间戳、方法、API路径完全相同
if (currentKey === lastRequestKey && originalTimestamp === lastTimestamp) {
// 碰撞次数增加
collisionCount++;
// 时间戳增加碰撞次数毫秒
const adjustedTimestamp = originalTimestamp + collisionCount;
console.warn(`检测到重复请求: ${method} ${urlPath} @ ${originalTimestamp}ms, 调整时间戳为: ${adjustedTimestamp}ms (碰撞次数: ${collisionCount})`);
return adjustedTimestamp;
} else {
// 不同请求,重置碰撞计数
collisionCount = 0;
// 更新最后一次请求的记录
lastRequestKey = currentKey;
lastTimestamp = originalTimestamp;
return originalTimestamp;
}
}
/**
* 生成sign签名
* @param {Object} config - axios请求配置
* @param {string} udid - 用户设备ID22位短udid
* @param {number} timestamp - 时间戳(毫秒) - 由调用方传入确保与生成salt时使用的时间一致避免因时间差导致的sign不匹配问题·
* @returns {Promise<string>} sign值
*/
export async function generateSign(config, udid, timestamp) {
try {
// 1. 准备模版字典
const reqMap = new Map();
// 获取请求方法
const method = config.method?.toUpperCase() || 'GET';
// 获取host从baseURL或当前域名 本地启动要求host为xsynergy.gxtech.ltd
let host = 'xsynergy.gxtech.ltd';
// let host = '';
// // console.log('请求配置:', config.baseURL)
// if (config.baseURL) {
// try {
// const urlObj = new URL(config.baseURL);
// // console.log('解析baseURL成功:', urlObj);
// host = urlObj.host;
// } catch (e) {
// // console.log('解析baseURL失败使用window.location.host:', window.location);
// host = window.location.host;
// }
// } else {
// host = window.location.host;
// }
// console.log('请求host:', host);
// 获取完整URL路径不含域名和查询参数
let urlPath = config.url || '';
// 移除baseURL部分
if (config.baseURL && urlPath.startsWith(config.baseURL)) {
urlPath = urlPath.substring(config.baseURL.length);
}
// 移除查询参数
const queryIndex = urlPath.indexOf('?');
if (queryIndex !== -1) {
urlPath = urlPath.substring(0, queryIndex);
}
// 获取user-agent和token
const userAgent = config.headers?.['X-User-Agent'];
// const userAgent = config.headers?.['X-User-Agent'] || navigator.userAgent;
const token = config.headers?.['Authorization'] || '';
// 填充基础模板 - agent字段包含userAgent和token
reqMap.set('host', [host]);
reqMap.set('method', [method]);
reqMap.set('agent', [userAgent, token]);
reqMap.set('uri', [urlPath]);
// 2. 处理URL查询参数不包括sign本身
let fullUrl = config.url || '';
if (config.baseURL && !fullUrl.startsWith('http')) {
fullUrl = config.baseURL + fullUrl;
}
try {
const urlObj = new URL(fullUrl, window.location.origin);
const searchParams = urlObj.searchParams;
// 遍历所有查询参数排除sign
for (const [key, value] of searchParams.entries()) {
if (key !== 'sign') {
// 处理数组参数key[]格式)
const normalizedKey = key.replace('[]', '');
if (reqMap.has(normalizedKey)) {
reqMap.get(normalizedKey).push(value);
} else {
reqMap.set(normalizedKey, [value]);
}
}
}
} catch (e) {
console.warn('解析URL参数失败:', e);
}
// 3. 处理GET请求的params参数
if (config.method?.toLowerCase() === 'get' && config.params) {
for (const [key, value] of Object.entries(config.params)) {
if (key !== 'sign') {
const strValue = String(value);
const normalizedKey = key.replace('[]', '');
if (reqMap.has(normalizedKey)) {
reqMap.get(normalizedKey).push(strValue);
} else {
reqMap.set(normalizedKey, [strValue]);
}
}
}
}
// 4. 处理请求体POST、PUT等
if (config.method?.toLowerCase() !== 'get' && config.data) {
const contentType = config.headers?.['Content-Type'] || '';
if (contentType.includes('application/json')) {
// JSON数据 - 后端用"json"作为key
const jsonStr = typeof config.data === 'object'
? JSON.stringify(config.data)
: String(config.data);
reqMap.set('json', [jsonStr]);
} else if (contentType.includes('application/x-www-form-urlencoded')) {
// form-urlencoded数据
let formData = {};
if (typeof config.data === 'object') {
formData = config.data;
} else if (typeof config.data === 'string') {
const params = new URLSearchParams(config.data);
formData = Object.fromEntries(params.entries());
}
// 添加每个表单字段到reqMap
for (const [key, value] of Object.entries(formData)) {
const strValue = String(value);
const normalizedKey = key.replace('[]', '');
if (reqMap.has(normalizedKey)) {
reqMap.get(normalizedKey).push(strValue);
} else {
reqMap.set(normalizedKey, [strValue]);
}
}
} else if (contentType.includes('multipart/form-data')) {
// multipart/form-data
if (config.data instanceof FormData) {
for (const [key, value] of config.data.entries()) {
if (!(value instanceof File || value instanceof Blob)) {
const strValue = String(value);
const normalizedKey = key.replace('[]', '');
if (reqMap.has(normalizedKey)) {
reqMap.get(normalizedKey).push(strValue);
} else {
reqMap.set(normalizedKey, [strValue]);
}
}
}
}
}
}
// 5. 生成salt - 完全匹配后端的genSalt函数
// 后端: GenTokenSalt(uid, udid + signSalt)
// signSalt固定为: publicSignSalt
const signSalt = 'publicSignSalt';
const salt = await generateSalt(udid, signSalt);
// 6. 参数排序并进行HMAC-MD5哈希
const sortedKeys = Array.from(reqMap.keys()).sort();
// console.log('排序后的参数键:', sortedKeys);
// 创建HMAC-MD5
const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.MD5, salt);
// 按排序后的key拼接值
let contentStr = '';
for (const key of sortedKeys) {
const values = reqMap.get(key);
const joinedValue = values.join('');
contentStr += joinedValue;
hmac.update(joinedValue);
}
// 获取最终的hash字符串
const hashStr = hmac.finalize().toString(CryptoJS.enc.Hex).toUpperCase();
// 7. 分段拆散计算
const ints = new Array(6).fill(0);
// 拆封模版
const parts = [
[0, 7],
[7, 7],
[14, 4],
[18, 7],
[25, 7]
];
// 使用模版将hashStr拆为5个hex字串并转为整数再倒序排列
for (let i = 0; i < parts.length; i++) {
// console.log(`拆分hashStr - part ${i}:`, hashStr.substring(parts[i][0], parts[i][0] + parts[i][1]));
const [start, length] = parts[i];
const hexStr = hashStr.substring(start, start + length);
const intValue = parseInt(hexStr, 16);
// console.log(`转换为整数 - part ${i}:`, intValue);
ints[4 - i] = intValue;
}
// 8. 获取调用接口时间戳(毫秒)并进行去重处理
const now = timestamp || Date.now();
// 对时间戳进行去重处理(处理同一毫秒内相同接口的重复请求)
const adjustedTimestamp = getDeduplicatedTimestamp(now, method, urlPath);
const ts = adjustedTimestamp.toString();
// 将时间拆分为两部分
const t0 = parseInt(ts.substring(0, ts.length - 9), 10);
const t1 = parseInt(ts.substring(ts.length - 9), 10);
ints[2] += t0;
ints[5] = t1;
// 记录实际使用的时间戳,用于后续的重复检测
lastUsedTimestamp = adjustedTimestamp;
// console.log('时间戳:', now, '调整后:', adjustedTimestamp, '拆分:', { t0, t1 });
// 9. 使用hashids算法加密
const alphabet = "GxJRk37QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfNhobIcMvKt";
const hashids = new Hashids(salt, 0, alphabet);
// 将ints数组编码为字符串
const sign = hashids.encode(ints);
// 打印调试信息
// console.log('=== Sign生成调试信息 ===');
// console.log('udid:', udid);
// console.log('signSalt:', signSalt);
// console.log('生成的salt:', salt);
// console.log('参数映射:', Object.fromEntries(reqMap));
// console.log('排序后的keys:', sortedKeys);
// console.log('拼接字符串:', contentStr);
// console.log('HMAC-MD5哈希:', hashStr);
// console.log('分割后的ints:', ints);
// console.log('原始时间戳:', now, '调整后时间戳:', adjustedTimestamp, '拆分:', { t0, t1 });
// console.log('最终sign:', sign);
// console.log('========================');
return sign;
} catch (error) {
console.error('生成sign失败:', error);
return '';
}
}
/**
* 重置去重状态(可选,用于测试或特殊场景)
*/
export function resetSignDeduplication() {
lastRequestKey = '';
lastTimestamp = 0;
lastUsedTimestamp = 0;
collisionCount = 0;
console.log('Sign去重状态已重置');
}
/**
* 生成salt - 匹配后端的genSalt函数
* 后端逻辑: GenTokenSalt(uid, udid + signSalt)
*
* @param {string} shortUdid - 22位短udid
* @param {string} signSalt - 固定的signSalt值 'xspotter-s3'
* @returns {Promise<string>} salt值
*/
async function generateSalt(shortUdid, signSalt) {
try {
// 1. 将22位短udid解压为完整UUID
const fullUdid = await unpackUUID(shortUdid);
// 2. 解析UUID并提取每个部分的第一个字符
// UUID格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
const uuidParts = fullUdid.split('-');
if (uuidParts.length !== 5) {
throw new Error('Invalid UUID format');
}
// 提取每个部分的第一个字符组成hexStr
// 后端: for i, item := range strings.Split(udidStr, "-") { hexStr += string(item[i]) }
let hexStr = '';
for (let i = 0; i < uuidParts.length; i++) {
if (uuidParts[i].length > i) {
hexStr += uuidParts[i][i]; // 注意取第i个字符不是第一个
} else {
hexStr += uuidParts[i][0]; // 如果长度不够,取第一个
}
}
// 3. 将hexStr解析为整数16进制
const baseInt = parseInt(hexStr, 16);
// 4. 转换为8进制字符串
const base8Str = baseInt.toString(8);
// 5. secret = udid + signSalt
const secret = shortUdid + signSalt;
// 6. 返回 base8 + secret
const result = base8Str + secret;
return result;
} catch (error) {
console.error('生成salt失败:', error);
throw error;
}
}
/**
* 将22位短udid解压为完整UUID
* 匹配后端的unpackUUID函数
*/
async function unpackUUID(shortUdid) {
try {
// 后端逻辑: shortUdid + "==" 然后base64解码
const base64Str = shortUdid + '==';
// 将url安全的base64转换为标准base64
const standardBase64 = base64Str.replace(/-/g, '+').replace(/_/g, '/');
// base64解码
const binaryStr = atob(standardBase64);
// 转换为16进制格式
const hex = [];
for (let i = 0; i < binaryStr.length; i++) {
const hexByte = binaryStr.charCodeAt(i).toString(16).padStart(2, '0');
hex.push(hexByte);
}
// 格式化为UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
const hexStr = hex.join('');
const uuid = `${hexStr.substring(0, 8)}-${hexStr.substring(8, 12)}-${hexStr.substring(12, 16)}-${hexStr.substring(16, 20)}-${hexStr.substring(20, 32)}`;
return uuid;
} catch (error) {
console.error('解压UUID失败:', error);
// 如果已经是完整UUID格式直接返回
if (shortUdid.includes('-') && shortUdid.length === 36) {
return shortUdid;
}
throw error;
}
}
/**
* 将完整UUID压缩为22位短udid
* 匹配后端的packUDID函数
*/
export function packUUID(fullUuid) {
try {
// 移除UUID中的连字符
const hexStr = fullUuid.replace(/-/g, '');
// 将16进制字符串转换为字节数组
const bytes = [];
for (let i = 0; i < hexStr.length; i += 2) {
bytes.push(parseInt(hexStr.substring(i, i + 2), 16));
}
// 转换为二进制字符串
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
// base64编码并转换为url安全格式
const base64 = btoa(binary);
const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
return base64url;
} catch (error) {
console.error('压缩UUID失败:', error);
return fullUuid;
}
}

View File

@@ -39,39 +39,20 @@
<h2 class="login-title">系统登录</h2>
<el-form
ref="loginRef"
class="login-form"
:model="loginForm"
:rules="loginRules"
>
<el-form ref="loginRef" class="login-form" :model="loginForm" :rules="loginRules">
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入账号"
size="large"
/>
<el-input v-model="loginForm.username" placeholder="请输入账号" size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
@keyup.enter="handleLogin"
/>
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" size="large"
@keyup.enter="handleLogin" />
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
class="login-button"
type="primary"
size="large"
@click.prevent="handleLogin"
>
<el-button :loading="loading" class="login-button" type="primary" size="large"
@click.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else>登录中...</span>
</el-button>
@@ -109,100 +90,101 @@ const loginView = ref(true)
// 监听路由变化,获取重定向参数
watch(() => route, (newRoute) => {
redirect.value = newRoute.query && newRoute.query.redirect;
redirect.value = newRoute.query && newRoute.query.redirect;
}, { immediate: true });
const loginForm = ref({
username: '',
password: '',
username: '',
password: '',
})
const loginRules = {
username: [{ required: true, trigger: 'blur', message: '请输入用户名' }],
password: [{ required: true, trigger: 'blur', message: '请输入密码' }],
username: [{ required: true, trigger: 'blur', message: '请输入用户名' }],
password: [{ required: true, trigger: 'blur', message: '请输入密码' }],
}
const loading = ref(false)
function handleLogin() {
proxy.$refs.loginRef.validate((valid) => {
if (valid) {
loading.value = true
if (!localStorage?.getItem('UDID')) {
ElMessage({
message: '服务错误,请刷新页面',
type: 'warning',
})
return
}
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制全小写)
const randomChars = generateRandomChars(6);
const message = `Gx${randomChars}${loginForm.value.password}`;
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
// 调用action的登录方法
userStore
.login({
// password: loginForm.value.password,
password: ciphertext,
username: loginForm.value.username,
})
.then(async (res) => {
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
await handleLoginSuccess();
})
.catch((e) => {
console.log('登录失败', e)
loading.value = false
})
}
})
requestNotificationPermission()
proxy.$refs.loginRef.validate((valid) => {
if (valid) {
loading.value = true
meterStore.initUdid()
if (!localStorage?.getItem('UDID')) {
ElMessage({
message: '服务错误,请刷新页面',
type: 'warning',
})
return
}
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制全小写)
const randomChars = generateRandomChars(6);
const message = `Gx${randomChars}${loginForm.value.password}`;
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
// 调用action的登录方法
userStore
.login({
// password: loginForm.value.password,
password: ciphertext,
username: loginForm.value.username,
})
.then(async (res) => {
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
await handleLoginSuccess();
})
.catch((e) => {
console.log('登录失败', e)
loading.value = false
})
}
})
requestNotificationPermission()
}
/**
* 处理登录成功后的跳转逻辑
*/
async function handleLoginSuccess() {
try {
// 如果有重定向路径且不是登录页,则跳转到重定向页面
if (redirect.value && redirect.value !== '/login') {
try {
// 如果有重定向路径且不是登录页,则跳转到重定向页面
if (redirect.value && redirect.value !== '/login') {
// 确保路由存在,如果不存在则跳转到默认页面
try {
// 解析路径,检查是否是有效路由
const resolved = router.resolve(redirect.value);
if (resolved.matched.length > 0) {
await router.push(redirect.value);
} else {
console.warn('重定向路径无效,跳转到默认页面');
await router.push('/userManagement');
}
} catch (error) {
console.warn('重定向跳转失败,跳转到默认页面:', error);
await router.push('/userManagement');
}
// 确保路由存在,如果不存在则跳转到默认页面
try {
// 解析路径,检查是否是有效路由
const resolved = router.resolve(redirect.value);
if (resolved.matched.length > 0) {
await router.push(redirect.value);
} else {
// 没有重定向或重定向到登录页,跳转到默认页面
await router.push('/userManagement');
console.warn('重定向路径无效,跳转到默认页面');
await router.push('/userManagement');
}
} catch (error) {
console.error('登录跳转异常:', error);
// 降级处理:跳转到默认页面
} catch (error) {
console.warn('重定向跳转失败,跳转到默认页面:', error);
await router.push('/userManagement');
} finally {
loading.value = false;
}
} else {
// 没有重定向或重定向到登录页,跳转到默认页面
await router.push('/userManagement');
}
} catch (error) {
console.error('登录跳转异常:', error);
// 降级处理:跳转到默认页面
await router.push('/userManagement');
} finally {
loading.value = false;
}
}
// 生成随机字符串
function generateRandomChars(length) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
@@ -210,22 +192,21 @@ function generateRandomChars(length) {
* @returns {*}
*/
function requestNotificationPermission() {
if ('Notification' in window) {
Notification.requestPermission().then(() => { })
} else {
console.log('浏览器不支持通知')
}
if ('Notification' in window) {
Notification.requestPermission().then(() => { })
} else {
console.log('浏览器不支持通知')
}
}
onMounted(async () => {
meterStore.initUdid()
// meterStore.initUdid()
});
</script>
<style lang="scss" scoped>
.loginView {
width: 100vw;
height: 100vh;
@@ -240,9 +221,9 @@ onMounted(async () => {
inset: 0;
background:
radial-gradient(circle at 20% 20%, rgba(64,158,255,.15), transparent 40%),
radial-gradient(circle at 80% 80%, rgba(64,158,255,.15), transparent 40%),
linear-gradient(120deg,#f6f9fc,#eef3ff);
radial-gradient(circle at 20% 20%, rgba(64, 158, 255, .15), transparent 40%),
radial-gradient(circle at 80% 80%, rgba(64, 158, 255, .15), transparent 40%),
linear-gradient(120deg, #f6f9fc, #eef3ff);
}
/* 动态光效 */
@@ -254,11 +235,9 @@ onMounted(async () => {
width: 600px;
height: 600px;
background: radial-gradient(
circle,
rgba(64,158,255,.25),
transparent 70%
);
background: radial-gradient(circle,
rgba(64, 158, 255, .25),
transparent 70%);
filter: blur(80px);
@@ -268,11 +247,11 @@ onMounted(async () => {
@keyframes floatLight {
from {
transform: translate(-200px,-100px);
transform: translate(-200px, -100px);
}
to {
transform: translate(300px,200px);
transform: translate(300px, 200px);
}
}
@@ -302,12 +281,12 @@ onMounted(async () => {
border-radius: 16px;
background: rgba(255,255,255,.9);
background: rgba(255, 255, 255, .9);
backdrop-filter: blur(10px);
box-shadow:
0 20px 50px rgba(0,0,0,.08);
0 20px 50px rgba(0, 0, 0, .08);
overflow: hidden;
}
@@ -345,7 +324,7 @@ onMounted(async () => {
.feature-list {
display: grid;
grid-template-columns: repeat(2,1fr);
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
@@ -403,11 +382,9 @@ onMounted(async () => {
border-radius: 6px;
background: linear-gradient(
135deg,
#409EFF,
#66b1ff
);
background: linear-gradient(135deg,
#409EFF,
#66b1ff);
border: none;
}
@@ -416,7 +393,7 @@ onMounted(async () => {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(64,158,255,.25);
box-shadow: 0 6px 16px rgba(64, 158, 255, .25);
}
/* 移动端 */
@@ -442,5 +419,4 @@ onMounted(async () => {
}
}
</style>

View File

@@ -1,16 +1,20 @@
import { fileURLToPath, URL } from 'node:url'
import path from "path";
import { defineConfig ,loadEnv} from 'vite'
import vue from '@vitejs/plugin-vue'
import createVitePlugins from './vite/plugins/index.js'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import createVitePlugins from './vite/plugins/index.js'
// https://vite.dev/config/
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd());
const { VITE_BASE_PATH } = env;
const { VITE_BASE_PATH } = env;
return {
//生产环境使用相对路径,开发环境用 /
base: command === 'build' ? './' : '/',
build: {
outDir: 'xSynergy-manage',
},
plugins: createVitePlugins(env, command === "build"),
server: {
host: '0.0.0.0', // 关键配置,允许局域网访问
@@ -19,14 +23,14 @@ export default defineConfig(({ mode, command }) => {
hmr: { overlay: false },
proxy: {
'/dev-api': {
target: 'https://xsynergy.gxtech.ltd',
target: 'https://xsynergy.gxtech.ltd',
changeOrigin: true,
ws: true,
rewrite: (path) =>
path.replace(new RegExp(`^/dev-api`), '')
},
'/livekit-api': {
target: 'https://meeting.cnsdt.com/api/v1',
target: 'https://meeting.cnsdt.com/api/v1',
changeOrigin: true,
ws: true,
rewrite: (path) =>