mirror of
https://github.com/bufanyun/hotgo.git
synced 2026-01-23 23:56:00 +08:00
发布v2.13.1版本,更新内容请查看:https://github.com/bufanyun/hotgo/blob/v2.0/docs/guide-zh-CN/start-update-log.md
This commit is contained in:
186
web/src/utils/highHtml.ts
Normal file
186
web/src/utils/highHtml.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
32
web/src/utils/websocket/registerMessage.ts
Normal file
32
web/src/utils/websocket/registerMessage.ts
Normal 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);
|
||||
});
|
||||
|
||||
// 更多全局消息处理都可以在这里注册
|
||||
// ...
|
||||
}
|
||||
Reference in New Issue
Block a user