aldehyde 0.2.449 → 0.2.450
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/controls/enum-badge/index.d.ts.map +1 -1
- package/lib/controls/enum-badge/index.js +21 -3
- package/lib/controls/enum-badge/index.js.map +1 -1
- package/lib/controls/enum-tag/index.d.ts.map +1 -1
- package/lib/controls/enum-tag/index.js +34 -19
- package/lib/controls/enum-tag/index.js.map +1 -1
- package/lib/lowcode-components/base-image/index.d.ts.map +1 -1
- package/lib/lowcode-components/base-image/index.js +5 -2
- package/lib/lowcode-components/base-image/index.js.map +1 -1
- package/lib/table/query-table.js +1 -2
- package/lib/table/query-table.js.map +1 -1
- package/lib/tmpl/hc-data-source.d.ts +2 -2
- package/lib/tmpl/hc-data-source.d.ts.map +1 -1
- package/lib/tmpl/hc-data-source.js +24 -18
- package/lib/tmpl/hc-data-source.js.map +1 -1
- package/lib/tmpl/hcservice-v3.d.ts +1 -0
- package/lib/tmpl/hcservice-v3.d.ts.map +1 -1
- package/lib/tmpl/hcservice-v3.js +11 -0
- package/lib/tmpl/hcservice-v3.js.map +1 -1
- package/lib/tmpl/web-socket.d.ts +49 -0
- package/lib/tmpl/web-socket.d.ts.map +1 -0
- package/lib/tmpl/web-socket.js +327 -0
- package/lib/tmpl/web-socket.js.map +1 -0
- package/package.json +3 -1
- package/src/aldehyde/controls/enum-badge/index.tsx +14 -4
- package/src/aldehyde/controls/enum-tag/index.tsx +29 -21
- package/src/aldehyde/lowcode-components/base-image/index.tsx +5 -2
- package/src/aldehyde/table/query-table.tsx +1 -1
- package/src/aldehyde/tmpl/hc-data-source.tsx +13 -7
- package/src/aldehyde/tmpl/hcservice-v3.tsx +10 -0
- package/src/aldehyde/tmpl/web-socket.ts +337 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Space
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { Space } from "antd";
|
|
3
3
|
import { VControlProps } from "../../tmpl/interface";
|
|
4
4
|
import HCDataSource from "../../tmpl/hc-data-source";
|
|
5
5
|
import { useLocale } from "../../locale/useLocale";
|
|
@@ -8,30 +8,38 @@ interface EnumTagsProps extends VControlProps { }
|
|
|
8
8
|
|
|
9
9
|
const EnumTags: React.FC<EnumTagsProps> = (props) => {
|
|
10
10
|
const { translate } = useLocale();
|
|
11
|
+
const [tagList, setTagList] = useState<any[]>([]);
|
|
12
|
+
const { value, fieldConfig, serverKey } = props;
|
|
11
13
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (value instanceof Array) {
|
|
18
|
-
} else {
|
|
19
|
-
if (value.indexOf("@,@") > 0) {
|
|
20
|
-
v = value.split("@,@");
|
|
14
|
+
const getTagList = async () => {
|
|
15
|
+
if (value) {
|
|
16
|
+
const temArr = [];
|
|
17
|
+
let v = value;
|
|
18
|
+
if (value instanceof Array) {
|
|
21
19
|
} else {
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
if (value.indexOf("@,@") > 0) {
|
|
21
|
+
v = value.split("@,@");
|
|
22
|
+
} else {
|
|
23
|
+
v = value.split(",");
|
|
24
|
+
}
|
|
24
25
|
|
|
26
|
+
}
|
|
27
|
+
for (let index = 0; index < v.length; index++) {
|
|
28
|
+
const item = v[index];
|
|
29
|
+
let color = await HCDataSource.getEnumValueColor(fieldConfig.mstrucId, value, serverKey) || "#1677FF";
|
|
30
|
+
temArr.push(<span key={item} className="tag" style={{ fontSize: "12px", padding: "4px 10px", borderRadius: "9999px", color, background: `${color}1A` }}>
|
|
31
|
+
{translate("${" + item + "}")}
|
|
32
|
+
</span>)
|
|
33
|
+
}
|
|
34
|
+
setTagList(temArr);
|
|
25
35
|
}
|
|
26
|
-
v.forEach((v) => {
|
|
27
|
-
let color = HCDataSource.getEnumValueColor(fieldConfig.mstrucId, value) || "#1677FF";
|
|
28
|
-
tagList.push(<span key={v} className="tag" style={{ fontSize: "12px", padding: "4px 10px", borderRadius: "9999px", color, background: `${color}1A` }}>
|
|
29
|
-
{translate("${" + v + "}")}
|
|
30
|
-
</span>)
|
|
31
|
-
});
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
getTagList();
|
|
40
|
+
}, [value]);
|
|
41
|
+
|
|
42
|
+
return <Space size={[8, 8]} wrap>{tagList}</Space>;
|
|
35
43
|
};
|
|
36
44
|
|
|
37
45
|
export default EnumTags;
|
|
@@ -56,9 +56,11 @@ const BaseImageComponent = forwardRef((props: BaseImageComponentProps, ref: Forw
|
|
|
56
56
|
const [initSearchParams, setInitSearchParams] = useState<{ [key: string]: any }>();
|
|
57
57
|
const [localImgCode, setLocalImgCode] = useState<string>();
|
|
58
58
|
const [localImgDel, setLocalImgDel] = useState<boolean>(false);
|
|
59
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
59
60
|
|
|
60
61
|
// 处理数据源图片地址
|
|
61
62
|
const handleDataImgSrc = (val) => {
|
|
63
|
+
setLoading(false);
|
|
62
64
|
const src = handleData(val);
|
|
63
65
|
if (!src) {
|
|
64
66
|
setImgSrc("");
|
|
@@ -157,8 +159,9 @@ const BaseImageComponent = forwardRef((props: BaseImageComponentProps, ref: Forw
|
|
|
157
159
|
} else {
|
|
158
160
|
getInitData();
|
|
159
161
|
}
|
|
160
|
-
} else
|
|
162
|
+
} else {
|
|
161
163
|
setImgSrc("");
|
|
164
|
+
setLoading(false);
|
|
162
165
|
}
|
|
163
166
|
}, [dataConfig?.sourceType, dataConfig?.sourceId, initSearchParams]);
|
|
164
167
|
|
|
@@ -199,7 +202,7 @@ const BaseImageComponent = forwardRef((props: BaseImageComponentProps, ref: Forw
|
|
|
199
202
|
|
|
200
203
|
return (
|
|
201
204
|
<div style={{ width: '100%', height: '100%' }}>
|
|
202
|
-
{!(imgSrc || defImgSrc) ? emptyImg :
|
|
205
|
+
{(loading || !(imgSrc || defImgSrc)) ? emptyImg :
|
|
203
206
|
isDesignMode && localImgDel ? renderLocalImgDel :
|
|
204
207
|
<img alt={'图片组件'} style={{ opacity: config?.opacity }} width={'100%'} height={'100%'} src={imgSrc || defImgSrc} />}
|
|
205
208
|
</div>
|
|
@@ -177,7 +177,7 @@ class QueryTable extends React.PureComponent<QueryTableProps, QueryTableStat> {
|
|
|
177
177
|
queryKey,
|
|
178
178
|
pageInfo
|
|
179
179
|
);
|
|
180
|
-
if (queryData?.key
|
|
180
|
+
if (queryData?.key !== queryKey) {
|
|
181
181
|
this.setState({ loading: false, errorText: translate("${查询异常}"), dataSource: [] });
|
|
182
182
|
return;
|
|
183
183
|
} else {
|
|
@@ -364,14 +364,19 @@ HCDataSource.getEnumsValueMap = function (
|
|
|
364
364
|
return map;
|
|
365
365
|
};
|
|
366
366
|
|
|
367
|
-
HCDataSource.getEnumOption = function (
|
|
367
|
+
HCDataSource.getEnumOption = async function (
|
|
368
368
|
mstrucId: string,
|
|
369
|
-
enumValue: string
|
|
370
|
-
|
|
369
|
+
enumValue: string,
|
|
370
|
+
serverKey?: string
|
|
371
|
+
) {
|
|
371
372
|
if (!enumValue) {
|
|
372
373
|
return null;
|
|
373
374
|
}
|
|
374
375
|
let options = this.getEnums(mstrucId);
|
|
376
|
+
if (!options) { // 枚举不存在查询
|
|
377
|
+
await loadEnum(serverKey, [mstrucId]);
|
|
378
|
+
options = this.getEnums(mstrucId);
|
|
379
|
+
}
|
|
375
380
|
let enumItem: EnumItem = undefined;
|
|
376
381
|
if (options) {
|
|
377
382
|
options.forEach((option) => {
|
|
@@ -383,11 +388,12 @@ HCDataSource.getEnumOption = function (
|
|
|
383
388
|
}
|
|
384
389
|
return enumItem;
|
|
385
390
|
};
|
|
386
|
-
HCDataSource.getEnumValueColor = (
|
|
391
|
+
HCDataSource.getEnumValueColor = async (
|
|
387
392
|
mstrucId: string,
|
|
388
|
-
enumValue: string
|
|
389
|
-
|
|
390
|
-
|
|
393
|
+
enumValue: string,
|
|
394
|
+
serverKey?: string,
|
|
395
|
+
) => {
|
|
396
|
+
let enumItem: EnumItem = await HCDataSource.getEnumOption(mstrucId, enumValue, serverKey);
|
|
391
397
|
if (enumItem) {
|
|
392
398
|
return enumItem.color;
|
|
393
399
|
} else {
|
|
@@ -1496,4 +1496,14 @@ export default class HcserviceV3 {
|
|
|
1496
1496
|
return false;
|
|
1497
1497
|
}
|
|
1498
1498
|
}
|
|
1499
|
+
|
|
1500
|
+
// 测试ws发送消息
|
|
1501
|
+
static async wsPush(hydrocarbonToken) {
|
|
1502
|
+
await Super.super({
|
|
1503
|
+
url: `/v3/ws-push/${hydrocarbonToken}`,
|
|
1504
|
+
data: { message: "测试数据" },
|
|
1505
|
+
method: "POST",
|
|
1506
|
+
});
|
|
1507
|
+
message.success(translate("${已发送}"));
|
|
1508
|
+
}
|
|
1499
1509
|
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import SockJS from 'sockjs-client';
|
|
2
|
+
import { Client, IFrame, IMessage, StompSubscription } from '@stomp/stompjs';
|
|
3
|
+
import Units from "../units";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 使用方法:
|
|
7
|
+
* const webSocket= WebSocket.getInstance(WS_URL); // 创建连接
|
|
8
|
+
* const subscribe = webSocket.subscribe('/topic/test',{}, (val)=>{console.log("推送的数据:",val)}); // 订阅
|
|
9
|
+
* subscribe.unsubscribe(); // 销毁订阅
|
|
10
|
+
* webSocket.disconnect(); // 关闭连接
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// 获取订阅地址
|
|
14
|
+
const getWebSocketConfig = (path, options) => {
|
|
15
|
+
const serverKey = options.serverKey;
|
|
16
|
+
const header = options.connectionHeaders || {};
|
|
17
|
+
const programCode = header.programCode || Units.programCode(serverKey);
|
|
18
|
+
let server = new URL(options.hydrocarbonServer ? options.hydrocarbonServer : Units.api(serverKey));
|
|
19
|
+
let hydrocarbonToken = null;
|
|
20
|
+
if (header?.hydrocarbonToken) {
|
|
21
|
+
hydrocarbonToken = header.hydrocarbonToken;
|
|
22
|
+
} else if (serverKey) {
|
|
23
|
+
hydrocarbonToken = Units.serverHydrocarbonToken(serverKey)
|
|
24
|
+
} else if (Units.hydrocarbonToken(programCode)) {
|
|
25
|
+
hydrocarbonToken = Units.hydrocarbonToken(programCode);
|
|
26
|
+
} else {
|
|
27
|
+
hydrocarbonToken = Units.getAnoHydrocarbonToken()
|
|
28
|
+
}
|
|
29
|
+
const url = `http://${server.host}${server.pathname}/${path}`;
|
|
30
|
+
return { url, hydrocarbonToken, programCode };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WebSocketOptions {
|
|
34
|
+
reconnectDelay?: number; // 重连间隔(ms)
|
|
35
|
+
heartbeatIncoming?: number; // 接收心跳最大间隔
|
|
36
|
+
heartbeatOutgoing?: number; // 发送心跳最小间隔
|
|
37
|
+
debug?: boolean; // 是否开启调试日志
|
|
38
|
+
connectionHeaders?: { [key: string]: string }; // 创建连接请求头信息
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 全局订阅Map类型
|
|
42
|
+
interface SubscriptionRecord {
|
|
43
|
+
topic: string;
|
|
44
|
+
callback: (message: any, rawMessage: IMessage) => void;
|
|
45
|
+
subscription: StompSubscription | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 订阅队列
|
|
49
|
+
interface QueuedOperation {
|
|
50
|
+
type: 'SUBSCRIBE' | 'SEND';
|
|
51
|
+
payload: any;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 连接状态枚举
|
|
55
|
+
enum ConnectionState {
|
|
56
|
+
DISCONNECTED = 'DISCONNECTED', // 断开
|
|
57
|
+
CONNECTING = 'CONNECTING', // 连接中
|
|
58
|
+
CONNECTED = 'CONNECTED', // 已连接
|
|
59
|
+
RECONNECTING = 'RECONNECTING', // 重连中
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class WebSocket {
|
|
63
|
+
private static instances = new Map<string, WebSocket>();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 获取单例实例的唯一入口
|
|
67
|
+
* @param url WebSocket 服务器地址
|
|
68
|
+
* @param options 配置项
|
|
69
|
+
* @returns 返回对应 URL 的唯一 WebSocket 实例
|
|
70
|
+
*/
|
|
71
|
+
public static getInstance(url: string, options: WebSocketOptions = {}): WebSocket {
|
|
72
|
+
if (!WebSocket.instances.has(url)) {
|
|
73
|
+
// 注意:这里调用的是私有的构造函数
|
|
74
|
+
const instance = new WebSocket(url, options);
|
|
75
|
+
WebSocket.instances.set(url, instance);
|
|
76
|
+
}
|
|
77
|
+
return WebSocket.instances.get(url)!;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 私有构造函数
|
|
82
|
+
* 防止外部直接使用 new WebSocket() 导致单例失效
|
|
83
|
+
*/
|
|
84
|
+
private constructor(url: string, options: WebSocketOptions = {}) {
|
|
85
|
+
this.url = url;
|
|
86
|
+
this.config = { ...this.config, ...options };
|
|
87
|
+
this.connect();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private url: string;
|
|
91
|
+
private client: Client | null = null;
|
|
92
|
+
private subscriptions = new Map<string, SubscriptionRecord>();
|
|
93
|
+
private operationQueue: QueuedOperation[] = [];
|
|
94
|
+
private state: ConnectionState = ConnectionState.DISCONNECTED;
|
|
95
|
+
private reconnectTimer: any = null;
|
|
96
|
+
private connectPromises = new Map<string, { resolve: () => void; reject: (error: Error) => void }>();
|
|
97
|
+
private config: Required<WebSocketOptions> = {
|
|
98
|
+
reconnectDelay: 10000,
|
|
99
|
+
heartbeatIncoming: 10000,
|
|
100
|
+
heartbeatOutgoing: 10000,
|
|
101
|
+
debug: false,
|
|
102
|
+
connectionHeaders: {},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// 建立连接
|
|
106
|
+
public connect(): Promise<void> {
|
|
107
|
+
if (this.state === ConnectionState.CONNECTED) { // 当前已连接不重复创建连接
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
}
|
|
110
|
+
this.state = ConnectionState.CONNECTING;
|
|
111
|
+
const { url: wsUrl, hydrocarbonToken, programCode } = getWebSocketConfig(this.url, this.config);
|
|
112
|
+
return new Promise<void>((resolve, reject) => {
|
|
113
|
+
const currentPromiseId = Date.now().toString();
|
|
114
|
+
this.connectPromises.set(currentPromiseId, { resolve, reject });
|
|
115
|
+
const { heartbeatIncoming, heartbeatOutgoing, reconnectDelay } = this.config;
|
|
116
|
+
const socket = new SockJS(wsUrl);
|
|
117
|
+
this.client = new Client({
|
|
118
|
+
webSocketFactory: () => socket,
|
|
119
|
+
// brokerURL: wsUrl,
|
|
120
|
+
debug: this.config.debug ? (str) => console.log('[STOMP]', str) : () => { },
|
|
121
|
+
heartbeatIncoming,
|
|
122
|
+
heartbeatOutgoing,
|
|
123
|
+
reconnectDelay,
|
|
124
|
+
connectHeaders: {
|
|
125
|
+
"hydrocarbon-token": hydrocarbonToken,
|
|
126
|
+
"hydrocarbon-program-token": programCode
|
|
127
|
+
},
|
|
128
|
+
onConnect: (frame) => { // 连接成功回调
|
|
129
|
+
console.log("连接成功:frame", frame);
|
|
130
|
+
this.state = ConnectionState.CONNECTED;
|
|
131
|
+
this.connectPromises.forEach(({ resolve }) => resolve());
|
|
132
|
+
this.connectPromises.clear();
|
|
133
|
+
this._restoreSubscriptions();
|
|
134
|
+
this._flushQueue().catch(console.error);
|
|
135
|
+
},
|
|
136
|
+
onStompError: (frame: IFrame) => {
|
|
137
|
+
console.error('[WS] ❌ STOMP 协议错误', frame);
|
|
138
|
+
const error = new Error(frame.headers['message'] || 'STOMP Error');
|
|
139
|
+
if (this.state !== ConnectionState.DISCONNECTED) {
|
|
140
|
+
this.state = ConnectionState.DISCONNECTED;
|
|
141
|
+
}
|
|
142
|
+
this.connectPromises.forEach(({ reject }) => reject(error));
|
|
143
|
+
this.connectPromises.clear();
|
|
144
|
+
},
|
|
145
|
+
onWebSocketClose: () => {
|
|
146
|
+
if ([ConnectionState.CONNECTED, ConnectionState.CONNECTING].includes(this.state)) {
|
|
147
|
+
console.warn('[WS] ⚠️ 连接意外断开,准备重连...');
|
|
148
|
+
this.state = ConnectionState.DISCONNECTED;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
onWebSocketError: (error) => {
|
|
152
|
+
console.error('[WS] ❌ WebSocket 错误', error);
|
|
153
|
+
const wsError = new Error('WebSocket Error');
|
|
154
|
+
if (this.state !== ConnectionState.DISCONNECTED) {
|
|
155
|
+
this.state = ConnectionState.DISCONNECTED;
|
|
156
|
+
}
|
|
157
|
+
this.connectPromises.forEach(({ reject }) => reject(wsError));
|
|
158
|
+
this.connectPromises.clear();
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
this.client.activate();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 订阅请求
|
|
166
|
+
public async subscribe<T = any>(topic: string, params: { [key: string]: any }, callback: (message: T, rawMessage: IMessage) => void): Promise<StompSubscription> {
|
|
167
|
+
if (this.state === ConnectionState.CONNECTED && this.client) {
|
|
168
|
+
return this._doSubscribe(topic, callback, params);
|
|
169
|
+
} else {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
this.operationQueue.push({
|
|
172
|
+
type: 'SUBSCRIBE',
|
|
173
|
+
payload: { topic, callback, resolve, reject, params },
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 执行实际订阅
|
|
180
|
+
private _doSubscribe<T>(topic: string, callback: (message: T, rawMessage: IMessage) => void, params: { [key: string]: any }): Promise<StompSubscription> {
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
if (!this.client) {
|
|
183
|
+
reject(new Error('Client not initialized'));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const stompSub = this.client.subscribe(topic, (message: IMessage) => {
|
|
188
|
+
try {
|
|
189
|
+
let body: any = message.body;
|
|
190
|
+
if (typeof message.body === 'string') {
|
|
191
|
+
body = JSON.parse(message.body);
|
|
192
|
+
}
|
|
193
|
+
callback(body, message);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
console.error('[WS] JSON 解析错误', e);
|
|
196
|
+
callback(message.body as any, message);
|
|
197
|
+
}
|
|
198
|
+
}, params || {});
|
|
199
|
+
// 3. 更新内存记录
|
|
200
|
+
this.subscriptions.set(topic, {
|
|
201
|
+
topic,
|
|
202
|
+
callback,
|
|
203
|
+
subscription: stompSub // 关键:确保新的 subscription 对象被保存
|
|
204
|
+
});
|
|
205
|
+
resolve(stompSub);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
reject(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 发送消息
|
|
213
|
+
public send(destination: string, body: any): void {
|
|
214
|
+
if (this.state === ConnectionState.CONNECTED && this.client) {
|
|
215
|
+
try {
|
|
216
|
+
this.client.publish({
|
|
217
|
+
destination,
|
|
218
|
+
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
219
|
+
headers: { 'content-type': 'application/json' },
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('[WS] 发送消息失败:', error);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
this.operationQueue.push({ type: 'SEND', payload: { destination, body } });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 取消订阅
|
|
230
|
+
public unsubscribe(topic: string): boolean {
|
|
231
|
+
const record = this.subscriptions.get(topic);
|
|
232
|
+
if (record) {
|
|
233
|
+
record.subscription?.unsubscribe();
|
|
234
|
+
this.subscriptions.delete(topic);
|
|
235
|
+
console.log(`[WS] 🚫 已取消订阅: ${topic}`);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 取消所有订阅
|
|
242
|
+
public unsubscribeAll(): void {
|
|
243
|
+
this.subscriptions.forEach((record) => {
|
|
244
|
+
record.subscription?.unsubscribe();
|
|
245
|
+
});
|
|
246
|
+
this.subscriptions.clear();
|
|
247
|
+
console.log('[WS] 🚫 已取消所有订阅');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 断开连接
|
|
251
|
+
public disconnect(): void {
|
|
252
|
+
this.state = ConnectionState.DISCONNECTED;
|
|
253
|
+
if (this.reconnectTimer) {
|
|
254
|
+
clearTimeout(this.reconnectTimer);
|
|
255
|
+
this.reconnectTimer = null;
|
|
256
|
+
}
|
|
257
|
+
if (this.client) {
|
|
258
|
+
WebSocket.instances.delete(this.client.webSocket.url);
|
|
259
|
+
this.client.deactivate();
|
|
260
|
+
this.client = null;
|
|
261
|
+
}
|
|
262
|
+
this.unsubscribeAll();
|
|
263
|
+
this.operationQueue = [];
|
|
264
|
+
this.connectPromises.forEach(({ reject }) => {
|
|
265
|
+
reject(new Error('Connection manually disconnected'));
|
|
266
|
+
});
|
|
267
|
+
this.connectPromises.clear();
|
|
268
|
+
console.log('[WS] 🔌 客户端已主动断开');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 当前订阅数量
|
|
272
|
+
public getSubscriptionCount(): number {
|
|
273
|
+
return this.subscriptions.size;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 连接后恢复订阅
|
|
277
|
+
private _restoreSubscriptions() {
|
|
278
|
+
if (this.subscriptions.size === 0 || !this.client) return;
|
|
279
|
+
console.log(`[WS] 🔄 正在恢复 ${this.subscriptions.size} 个订阅...`);
|
|
280
|
+
this.subscriptions.forEach((record, topic) => {
|
|
281
|
+
try {
|
|
282
|
+
const stompSub = this.client.subscribe(topic, (message: IMessage) => {
|
|
283
|
+
try {
|
|
284
|
+
let body: any = message.body;
|
|
285
|
+
if (typeof message.body === 'string') {
|
|
286
|
+
body = JSON.parse(message.body);
|
|
287
|
+
}
|
|
288
|
+
record.callback(body, message);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
record.callback(message.body, message);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
record.subscription = stompSub;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(`[WS] 恢复订阅失败: ${topic}`, error);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 订阅队列
|
|
301
|
+
private async _flushQueue() {
|
|
302
|
+
if (this.operationQueue.length === 0) return;
|
|
303
|
+
console.log(`[WS] 🚀 正在执行 ${this.operationQueue.length} 个排队操作...`);
|
|
304
|
+
const queue = [...this.operationQueue];
|
|
305
|
+
this.operationQueue = [];
|
|
306
|
+
for (const op of queue) {
|
|
307
|
+
try {
|
|
308
|
+
if (op.type === 'SUBSCRIBE') {
|
|
309
|
+
const { topic, callback, resolve, reject, params } = op.payload;
|
|
310
|
+
try {
|
|
311
|
+
const subscription = await this._doSubscribe(topic, callback, params);
|
|
312
|
+
resolve(subscription);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
reject(error);
|
|
315
|
+
}
|
|
316
|
+
} else if (op.type === 'SEND') {
|
|
317
|
+
const { destination, body } = op.payload;
|
|
318
|
+
this.send(destination, body);
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error('[WS] 执行队列操作失败:', error);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 判断是否已连接
|
|
327
|
+
public isConnected(): boolean {
|
|
328
|
+
return this.state === ConnectionState.CONNECTED;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 获取所有订阅的topic
|
|
332
|
+
public getSubscribedTopics(): string[] {
|
|
333
|
+
return Array.from(this.subscriptions.keys());
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export default WebSocket;
|