This commit is contained in:
孟帅
2024-03-07 20:08:56 +08:00
parent 6dd8cbadad
commit 0fbc1ad47c
246 changed files with 9441 additions and 2293 deletions

186
web/src/utils/highHtml.ts Normal file
View File

@@ -0,0 +1,186 @@
// 关键词配置
interface IKeywordOption {
keyword: string | RegExp;
color?: string;
bgColor?: string;
style?: Record<string, any>;
// 高亮标签名
tagName?: string;
// 忽略大小写
caseSensitive?: boolean;
// 自定义渲染高亮html
// eslint-disable-next-line no-unused-vars
renderHighlightKeyword?: (content: string) => any;
}
type IKeyword = string | IKeywordOption;
export interface IMatchIndex {
index: number;
subString: string;
}
// 关键词索引
export interface IKeywordParseIndex {
keyword: string | RegExp;
indexList: IMatchIndex[];
option?: IKeywordOption;
}
// 关键词
export interface IKeywordParseResult {
start: number;
end: number;
subString?: string;
option?: IKeywordOption;
}
// 计算
const getKeywordIndexList = (content: string, keyword: string | RegExp, flags = 'ig') => {
const reg = new RegExp(keyword, flags);
const res = (content as any).matchAll(reg);
const arr = [...res];
const allIndexArr: IMatchIndex[] = arr.map((e) => ({
index: e.index,
subString: e['0'],
}));
return allIndexArr;
};
// 驼峰转换横线
function humpToLine(name: string) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase();
}
const renderNodeTag = (subStr: string, option: IKeywordOption) => {
const s = subStr;
if (!option) {
return s;
}
const { tagName = 'mark', bgColor, color, style = {}, renderHighlightKeyword } = option;
if (typeof renderHighlightKeyword === 'function') {
return renderHighlightKeyword(subStr);
}
style.backgroundColor = bgColor;
style.color = color;
const styleContent = Object.keys(style)
.map((k) => `${humpToLine(k)}:${style[k]}`)
.join(';');
const styleStr = `style="${styleContent}"`;
return `<${tagName} ${styleStr}>${s}</${tagName}>`;
};
const renderHighlightHtml = (content: string, list: any[]) => {
let str = '';
list.forEach((item) => {
const { start, end, option } = item;
const s = content.slice(start, end);
const subStr = renderNodeTag(s, option);
str += subStr;
item.subString = subStr;
});
return str;
};
// 解析关键词为索引
const parseHighlightIndex = (content: string, keywords: IKeyword[]) => {
const result: IKeywordParseIndex[] = [];
keywords.forEach((keywordOption: IKeyword) => {
let option: IKeywordOption = { keyword: '' };
if (typeof keywordOption === 'string') {
option = { keyword: keywordOption };
} else {
option = keywordOption;
}
const { keyword, caseSensitive = true } = option;
const indexList = getKeywordIndexList(content, keyword, caseSensitive ? 'g' : 'gi');
const res = {
keyword,
indexList,
option,
};
result.push(res);
});
return result;
};
const parseHighlightString = (content: string, keywords: IKeyword[]) => {
const result = parseHighlightIndex(content, keywords);
const splitList: IKeywordParseResult[] = [];
const findSplitIndex = (index: number, len: number) => {
for (let i = 0; i < splitList.length; i++) {
const cur = splitList[i];
// 有交集
if (
(index > cur.start && index < cur.end) ||
(index + len > cur.start && index + len < cur.end) ||
(cur.start > index && cur.start < index + len) ||
(cur.end > index && cur.end < index + len) ||
(index === cur.start && index + len === cur.end)
) {
return -1;
}
// 没有交集,且在当前的前面
if (index + len <= cur.start) {
return i;
}
// 没有交集,且在当前的后面的,放在下个迭代处理
}
return splitList.length;
};
result.forEach(({ indexList, option }: IKeywordParseIndex) => {
indexList.forEach((e) => {
const { index, subString } = e;
const item = {
start: index,
end: index + subString.length,
option,
};
const splitIndex = findSplitIndex(index, subString.length);
if (splitIndex !== -1) {
splitList.splice(splitIndex, 0, item);
}
});
});
// 补上没有匹配关键词的部分
const list: IKeywordParseResult[] = [];
splitList.forEach((cur, i) => {
const { start, end } = cur;
const next = splitList[i + 1];
// 第一个前面补一个
if (i === 0 && start > 0) {
list.push({ start: 0, end: start, subString: content.slice(0, start) });
}
list.push({ ...cur, subString: content.slice(start, end) });
// 当前和下一个中间补一个
if (next?.start > end) {
list.push({
start: end,
end: next.start,
subString: content.slice(end, next.start),
});
}
// 最后一个后面补一个
if (i === splitList.length - 1 && end < content.length - 1) {
list.push({
start: end,
end: content.length - 1,
subString: content.slice(end, content.length),
});
}
});
return list;
};
// 生成关键词高亮的html字符串
const highHtml = (content: string, keywords: IKeyword[]) => {
const splitList = parseHighlightString(content, keywords);
return {
highText: renderHighlightHtml(content, splitList),
highList: splitList,
};
};
export default highHtml;

View File

@@ -84,3 +84,17 @@ export function timeFix() {
? '下午好'
: '晚上好';
}
// 随机浅色
export function rdmLightRgbColor(): string {
const letters = '456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
if (i === 0) {
color += 'F'; // 确保第一个字符较亮
} else {
color += letters[Math.floor(Math.random() * letters.length)];
}
}
return color;
}

View File

@@ -1,10 +1,11 @@
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';
import axios from 'axios';
import { AxiosCanceler } from './axiosCancel';
import { isFunction } from '@/utils/is';
import { isFunction, isString, isUrl } from '@/utils/is';
import { cloneDeep } from 'lodash-es';
import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
import { ContentTypeEnum } from '@/enums/httpEnum';
import { useGlobSetting } from '@/hooks/setting';
export * from './axiosTransform';
@@ -107,19 +108,29 @@ export class VAxios {
/**
* @description: 文件上传
*/
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData();
const customFilename = params.name || 'file';
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams, options?: RequestOptions) {
const transform = this.getTransform();
const { requestCatch, transformRequestData } = transform || {};
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions, options);
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
} else {
formData.append(customFilename, params.file);
const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix || '';
const apiUrl = globSetting.apiUrl || '';
const isUrlStr = isUrl(config.url as string);
if (!isUrlStr) {
config.url = `${urlPrefix}${config.url}`;
}
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key];
if (!isUrlStr && apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
const formData = new window.FormData();
if (params) {
Object.keys(params).forEach((key) => {
const value = params![key];
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item);
@@ -127,18 +138,42 @@ export class VAxios {
return;
}
formData.append(key, params.data![key]);
formData.append(key, params![key]);
});
}
return this.axiosInstance.request<T>({
method: 'POST',
data: formData,
headers: {
'Content-type': ContentTypeEnum.FORM_DATA,
ignoreCancelToken: true,
},
...config,
return new Promise((resolve, reject) => {
this.axiosInstance
.request<T>({
method: 'POST',
data: formData,
headers: {
'Content-type': ContentTypeEnum.FORM_DATA,
ignoreCancelToken: true,
},
...config,
})
.then((res: AxiosResponse<Result>) => {
// 请求是否被取消
const isCancel = axios.isCancel(res);
if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
try {
const ret = transformRequestData(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('request error!'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error) => {
if (requestCatch && isFunction(requestCatch)) {
reject(requestCatch(e));
return;
}
reject(e);
});
});
}

View File

@@ -1,69 +1,21 @@
import { SocketEnum } from '@/enums/socketEnum';
import { notificationStoreWidthOut } from '@/store/modules/notification';
import { useUserStoreWidthOut } from '@/store/modules/user';
import { TABS_ROUTES } from '@/store/mutation-types';
import { isJsonString } from '@/utils/is';
import { registerGlobalMessage } from '@/utils/websocket/registerMessage';
// WebSocket消息格式
export interface WebSocketMessage {
event: string;
data: any;
code: number;
timestamp: number;
}
let socket: WebSocket;
let isActive: boolean;
const messageHandler: Map<string, Function> = new Map();
export function getSocket(): WebSocket {
if (socket === undefined) {
location.reload();
}
return socket;
}
export function getActive(): boolean {
return isActive;
}
export function sendMsg(event: string, data = null, isRetry = true) {
if (socket === undefined || !isActive) {
if (!isRetry) {
console.log('socket连接异常发送失败');
return;
}
console.log('socket连接异常等待重试..');
setTimeout(function () {
sendMsg(event, data);
}, 200);
return;
}
try {
socket.send(
JSON.stringify({
event: event,
data: data,
})
);
} catch (err) {
// @ts-ignore
console.log('ws发送消息失败等待重试err' + err.message);
if (!isRetry) {
return;
}
setTimeout(function () {
sendMsg(event, data);
}, 100);
}
}
export function addOnMessage(onMessageList: any, func: Function) {
let exist = false;
for (let i = 0; i < onMessageList.length; i++) {
if (onMessageList[i].name == func.name) {
onMessageList[i] = func;
exist = true;
}
}
if (!exist) {
onMessageList.push(func);
}
}
export default (onMessage: Function) => {
export default () => {
const heartCheck = {
timeout: 5000,
timeoutObj: setTimeout(() => {}),
@@ -85,35 +37,37 @@ export default (onMessage: Function) => {
})
);
self.serverTimeoutObj = setTimeout(function () {
console.log('关闭服务');
console.log('[WebSocket] 关闭服务');
socket.close();
}, self.timeout);
}, this.timeout);
},
};
const notificationStore = notificationStoreWidthOut();
const useUserStore = useUserStoreWidthOut();
let lockReconnect = false;
let timer: ReturnType<typeof setTimeout>;
const createSocket = () => {
console.log('createSocket...');
console.log('[WebSocket] createSocket...');
if (useUserStore.token === '') {
console.error('[WebSocket] 用户未登录,稍后重试...');
reconnect();
return;
}
try {
if (useUserStore.token === '') {
throw new Error('用户未登录,稍后重试...');
}
socket = new WebSocket(useUserStore.config?.wsAddr + '?authorization=' + useUserStore.token);
socket = new WebSocket(`${useUserStore.config?.wsAddr}?authorization=${useUserStore.token}`);
init();
} catch (e) {
console.log('createSocket err:' + e);
console.error(`[WebSocket] createSocket err: ${e}`);
reconnect();
}
if (lockReconnect) {
lockReconnect = false;
}
};
const reconnect = () => {
console.log('lockReconnect:' + lockReconnect);
console.log('[WebSocket] lockReconnect:' + lockReconnect);
if (lockReconnect) return;
lockReconnect = true;
clearTimeout(timer);
@@ -124,7 +78,7 @@ export default (onMessage: Function) => {
const init = () => {
socket.onopen = function (_) {
console.log('WebSocket:已连接');
console.log('[WebSocket] 已连接');
heartCheck.reset().start();
isActive = true;
};
@@ -133,47 +87,25 @@ export default (onMessage: Function) => {
isActive = true;
// console.log('WebSocket:收到一条消息', event.data);
let isHeart = false;
if (!isJsonString(event.data)) {
console.log('socket message incorrect format:' + JSON.stringify(event));
console.log('[WebSocket] message incorrect format:' + JSON.stringify(event));
return;
}
const message = JSON.parse(event.data);
if (message.event === 'ping') {
isHeart = true;
}
// 强制退出
if (message.event === 'kick') {
useUserStore.logout().then(() => {
// 移除标签页
localStorage.removeItem(TABS_ROUTES);
location.reload();
});
return;
}
// 通知
if (message.event === 'notice') {
notificationStore.triggerNewMessages(message.data);
return;
}
if (onMessage && !isHeart) {
onMessage.call(null, event);
}
heartCheck.reset().start();
const message = JSON.parse(event.data) as WebSocketMessage;
onMessage(message);
};
socket.onerror = function (_) {
console.log('WebSocket:发生错误');
console.log('[WebSocket] 发生错误');
reconnect();
isActive = false;
};
socket.onclose = function (_) {
console.log('WebSocket:已关闭');
console.log('[WebSocket] 已关闭');
heartCheck.reset();
reconnect();
isActive = false;
@@ -186,4 +118,63 @@ export default (onMessage: Function) => {
};
createSocket();
registerGlobalMessage();
};
function onMessage(message: WebSocketMessage) {
let handled = false;
messageHandler.forEach((value: Function, key: string) => {
if (message.event === key || key === '*') {
handled = true;
value.call(null, message);
}
});
if (!handled) {
console.log('[WebSocket] messageHandler not registered. message:' + JSON.stringify(message));
}
}
// 发送消息
export function sendMsg(event: string, data: any = null, isRetry = true) {
if (socket === undefined || !isActive) {
if (!isRetry) {
console.log('[WebSocket] 连接异常,发送失败!');
return;
}
console.log('[WebSocket] 连接异常,等待重试..');
setTimeout(() => {
sendMsg(event, data);
}, 200);
return;
}
try {
socket.send(JSON.stringify({ event, data }));
} catch (err: any) {
console.log('[WebSocket] 发送消息失败err', err.message);
if (!isRetry) {
return;
}
console.log('[WebSocket] 等待重试..');
setTimeout(() => {
sendMsg(event, data);
}, 100);
}
}
// 添加消息处理
export function addOnMessage(key: string, value: Function): void {
messageHandler.set(key, value);
}
// 移除消息处理
export function removeOnMessage(key: string): boolean {
return messageHandler.delete(key);
}
// 查看所有消息处理
export function getAllOnMessage(): Map<string, Function> {
return messageHandler;
}

View File

@@ -0,0 +1,32 @@
import { TABS_ROUTES } from '@/store/mutation-types';
import { SocketEnum } from '@/enums/socketEnum';
import { useUserStoreWidthOut } from '@/store/modules/user';
import { notificationStoreWidthOut } from '@/store/modules/notification';
import { addOnMessage, WebSocketMessage } from '@/utils/websocket/index';
// 注册全局消息监听
export function registerGlobalMessage() {
// 心跳
addOnMessage(SocketEnum.EventPing, function (_message: WebSocketMessage) {
// console.log('ping..');
});
// 强制退出
addOnMessage(SocketEnum.EventKick, function (_message: WebSocketMessage) {
const useUserStore = useUserStoreWidthOut();
useUserStore.logout().then(() => {
// 移除标签页
localStorage.removeItem(TABS_ROUTES);
location.reload();
});
});
// 消息通知
addOnMessage(SocketEnum.EventNotice, function (message: WebSocketMessage) {
const notificationStore = notificationStoreWidthOut();
notificationStore.triggerNewMessages(message.data);
});
// 更多全局消息处理都可以在这里注册
// ...
}