generator-mico-cli 0.2.2 → 0.2.4
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/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +90 -8
- package/generators/micro-react/templates/apps/layout/src/app.tsx +8 -3
- package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +34 -0
- package/generators/micro-react/templates/apps/layout/src/common/route-guard.ts +316 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +59 -185
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +493 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useRoutePermissionRefresh.ts +26 -23
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +55 -42
- package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +6 -1
- package/package.json +1 -1
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/container-manager.ts +0 -334
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { layoutLogger } from '@/common/logger';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
extractRoutes,
|
|
4
|
+
filterMenuItems,
|
|
5
|
+
findRouteByPath,
|
|
6
|
+
getWindowMenus,
|
|
7
|
+
} from '@/common/menu';
|
|
8
|
+
import { getAppNameFromEntry } from '@/common/micro';
|
|
3
9
|
import AppTabs from '@/components/AppTabs';
|
|
4
10
|
import MicroAppLoader from '@/components/MicroAppLoader';
|
|
5
11
|
import { NO_AUTH_ROUTE_LIST } from '@/constants';
|
|
@@ -14,26 +20,6 @@ import './index.less';
|
|
|
14
20
|
|
|
15
21
|
const { Content } = Layout;
|
|
16
22
|
|
|
17
|
-
/**
|
|
18
|
-
* 从 entry URL 中提取微应用标识
|
|
19
|
-
* 同一个 entry 的所有路由使用相同的标识,避免频繁卸载/重载微应用
|
|
20
|
-
*
|
|
21
|
-
* 注意:使用完整的 origin + pathname 作为标识,避免同一 host 上多个子应用冲突
|
|
22
|
-
* 例如:http://localhost:8010/app1/ 和 http://localhost:8010/app2/ 会有不同的标识
|
|
23
|
-
*/
|
|
24
|
-
const getAppNameFromEntry = (entry: string): string => {
|
|
25
|
-
try {
|
|
26
|
-
const url = new URL(entry, window.location.href);
|
|
27
|
-
// 使用 origin + pathname 作为标识,确保不同路径的子应用有不同标识
|
|
28
|
-
// 如 "localhost-8010" 或 "localhost-8010-app1"
|
|
29
|
-
const identifier = url.host + url.pathname;
|
|
30
|
-
return identifier.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
31
|
-
} catch {
|
|
32
|
-
// fallback:使用 entry 的 hash
|
|
33
|
-
return entry.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
23
|
/**
|
|
38
24
|
* 主布局组件
|
|
39
25
|
* 注意:Umi Max 使用 React Router 6,应该使用 <Outlet /> 渲染子路由,而不是 children
|
|
@@ -42,7 +28,10 @@ const BasicLayout: React.FC = () => {
|
|
|
42
28
|
const location = useLocation();
|
|
43
29
|
const { initialState } = useModel('@@initialState');
|
|
44
30
|
const currentUser = initialState?.currentUser;
|
|
31
|
+
|
|
45
32
|
// 路由切换时自动刷新用户权限
|
|
33
|
+
// isRefreshing 状态仅用于显示 loading 覆盖层
|
|
34
|
+
// 实际的时序协调由 loadingCoordinator 在 MicroAppManager 层面处理
|
|
46
35
|
const { isRefreshing } = useRoutePermissionRefresh();
|
|
47
36
|
|
|
48
37
|
const filterOptions = useMemo(
|
|
@@ -103,13 +92,10 @@ const BasicLayout: React.FC = () => {
|
|
|
103
92
|
currentRoute,
|
|
104
93
|
isForbidden,
|
|
105
94
|
isRefreshing,
|
|
95
|
+
currentUserChanged: currentUser?.is_superuser,
|
|
96
|
+
sideMenusCount: currentUser?.side_menus?.length,
|
|
106
97
|
});
|
|
107
98
|
|
|
108
|
-
// 权限刷新中,显示 loading 阻止用户操作
|
|
109
|
-
if (isRefreshing) {
|
|
110
|
-
return <Spin dot style={{ width: '100%', marginTop: 100, textAlign: 'center' }} />;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
99
|
// 无权限,显示 403
|
|
114
100
|
if (isForbidden) {
|
|
115
101
|
return <ForbiddenPage />;
|
|
@@ -121,18 +107,45 @@ const BasicLayout: React.FC = () => {
|
|
|
121
107
|
// 使用 entry 的 origin 作为微应用标识,同一个 entry 的所有路由共用一个实例
|
|
122
108
|
const appName = getAppNameFromEntry(currentRoute.entry);
|
|
123
109
|
return (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
<>
|
|
111
|
+
{/* 权限刷新中显示覆盖层,但不卸载 MicroAppLoader,避免子应用被中断 */}
|
|
112
|
+
{isRefreshing && (
|
|
113
|
+
<div style={{
|
|
114
|
+
position: 'absolute',
|
|
115
|
+
top: 0,
|
|
116
|
+
left: 0,
|
|
117
|
+
right: 0,
|
|
118
|
+
bottom: 0,
|
|
119
|
+
display: 'flex',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
justifyContent: 'center',
|
|
122
|
+
background: 'var(--color-bg-1)',
|
|
123
|
+
zIndex: 100,
|
|
124
|
+
}}>
|
|
125
|
+
<Spin dot />
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
<MicroAppLoader
|
|
129
|
+
// 使用 appName 作为 key,确保不同微应用使用不同的组件实例
|
|
130
|
+
// 同一个微应用的不同路由共用同一个实例
|
|
131
|
+
key={appName}
|
|
132
|
+
entry={currentRoute.entry}
|
|
133
|
+
// 使用 entry 生成的标识,而不是 path
|
|
134
|
+
name={appName}
|
|
135
|
+
// 显示名称用于 loading 提示
|
|
136
|
+
displayName={currentRoute.name}
|
|
137
|
+
// 传递当前路由路径,让子应用进行内部路由切换
|
|
138
|
+
routePath={currentRoute.path}
|
|
139
|
+
/>
|
|
140
|
+
</>
|
|
133
141
|
);
|
|
134
142
|
}
|
|
135
143
|
|
|
144
|
+
// 权限刷新中,显示 loading(仅对非微应用路由)
|
|
145
|
+
if (isRefreshing) {
|
|
146
|
+
return <Spin dot style={{ width: '100%', marginTop: 100, textAlign: 'center' }} />;
|
|
147
|
+
}
|
|
148
|
+
|
|
136
149
|
// 默认:使用 Outlet 渲染内部路由
|
|
137
150
|
return <Outlet />;
|
|
138
151
|
};
|
|
@@ -156,13 +169,13 @@ const BasicLayout: React.FC = () => {
|
|
|
156
169
|
<Layout className="layout-content-right">
|
|
157
170
|
<AppTabs />
|
|
158
171
|
<Content className="layout-content-outlet">
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
<Suspense
|
|
173
|
+
fallback={<Spin dot style={{ width: '100%', marginTop: 100 }} />}
|
|
174
|
+
>
|
|
175
|
+
{renderContent()}
|
|
176
|
+
</Suspense>
|
|
177
|
+
</Content>
|
|
178
|
+
</Layout>
|
|
166
179
|
</Layout>
|
|
167
180
|
</Layout>
|
|
168
181
|
);
|
|
@@ -16,7 +16,12 @@ const ForbiddenPage: React.FC = () => {
|
|
|
16
16
|
title="无权限访问"
|
|
17
17
|
subTitle="抱歉,您没有权限访问此页面"
|
|
18
18
|
extra={
|
|
19
|
-
<Button
|
|
19
|
+
<Button
|
|
20
|
+
type="primary"
|
|
21
|
+
onClick={() => {
|
|
22
|
+
history.push('/');
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
20
25
|
返回首页
|
|
21
26
|
</Button>
|
|
22
27
|
}
|
package/package.json
CHANGED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 微应用容器管理器
|
|
3
|
-
*
|
|
4
|
-
* 核心策略:
|
|
5
|
-
* 1. 容器在 document.body 中创建,与 React 生命周期解耦
|
|
6
|
-
* 2. 激活时移动到占位元素内,参与正常文档流
|
|
7
|
-
* 3. 停用时移回 body 并隐藏
|
|
8
|
-
* 4. 加载状态锁防止并发加载冲突
|
|
9
|
-
* 5. unmount 超时机制防止子应用卡死
|
|
10
|
-
*
|
|
11
|
-
* 这样既避免了 React unmount 时删除容器导致的竞态问题,
|
|
12
|
-
* 又能让容器在激活时参与正常的 CSS 布局。
|
|
13
|
-
*
|
|
14
|
-
* @see https://github.com/umijs/qiankun/issues/2845
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { MicroApp } from 'qiankun';
|
|
18
|
-
|
|
19
|
-
// ============================================================================
|
|
20
|
-
// 类型定义
|
|
21
|
-
// ============================================================================
|
|
22
|
-
|
|
23
|
-
type ContainerStatus = 'active' | 'hidden' | 'pending-delete';
|
|
24
|
-
type LoadingStatus = 'idle' | 'loading' | 'mounted' | 'unmounting';
|
|
25
|
-
|
|
26
|
-
interface ContainerEntry {
|
|
27
|
-
container: HTMLElement;
|
|
28
|
-
microApp: MicroApp | null;
|
|
29
|
-
status: ContainerStatus;
|
|
30
|
-
loadingStatus: LoadingStatus;
|
|
31
|
-
deleteTimer: ReturnType<typeof setTimeout> | null;
|
|
32
|
-
/** 当前加载会话 ID,用于取消过期的加载操作 */
|
|
33
|
-
loadSessionId: number;
|
|
34
|
-
/** 等待卸载完成的 Promise */
|
|
35
|
-
unmountPromise: Promise<void> | null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// 常量
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
/** 容器删除延迟(毫秒)- 给 qiankun 足够时间完成清理 */
|
|
43
|
-
const DELETE_DELAY = 5000;
|
|
44
|
-
|
|
45
|
-
/** unmount 超时时间(毫秒)- 防止子应用卡死 */
|
|
46
|
-
const UNMOUNT_TIMEOUT = 10000;
|
|
47
|
-
|
|
48
|
-
/** CSS 类名 */
|
|
49
|
-
const CSS_CLASS = {
|
|
50
|
-
base: 'micro-app-container-managed',
|
|
51
|
-
hidden: 'micro-app-container-managed--hidden',
|
|
52
|
-
active: 'micro-app-container-managed--active',
|
|
53
|
-
} as const;
|
|
54
|
-
|
|
55
|
-
// ============================================================================
|
|
56
|
-
// 状态
|
|
57
|
-
// ============================================================================
|
|
58
|
-
|
|
59
|
-
const containers = new Map<string, ContainerEntry>();
|
|
60
|
-
|
|
61
|
-
/** 全局会话 ID 计数器 */
|
|
62
|
-
let globalSessionId = 0;
|
|
63
|
-
|
|
64
|
-
// ============================================================================
|
|
65
|
-
// 工具函数
|
|
66
|
-
// ============================================================================
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 带超时的 Promise
|
|
70
|
-
*/
|
|
71
|
-
function withTimeout<T>(
|
|
72
|
-
promise: Promise<T>,
|
|
73
|
-
ms: number,
|
|
74
|
-
timeoutMessage: string,
|
|
75
|
-
): Promise<T> {
|
|
76
|
-
return Promise.race([
|
|
77
|
-
promise,
|
|
78
|
-
new Promise<T>((_, reject) => {
|
|
79
|
-
setTimeout(() => reject(new Error(timeoutMessage)), ms);
|
|
80
|
-
}),
|
|
81
|
-
]);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 安全执行 Promise,忽略错误
|
|
86
|
-
*/
|
|
87
|
-
async function safeAwait(promise: Promise<unknown>): Promise<void> {
|
|
88
|
-
try {
|
|
89
|
-
await promise;
|
|
90
|
-
} catch {
|
|
91
|
-
// 忽略
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ============================================================================
|
|
96
|
-
// 私有方法
|
|
97
|
-
// ============================================================================
|
|
98
|
-
|
|
99
|
-
function createContainer(appName: string): HTMLElement {
|
|
100
|
-
const container = document.createElement('div');
|
|
101
|
-
container.id = `micro-app-${appName}`;
|
|
102
|
-
container.className = `${CSS_CLASS.base} ${CSS_CLASS.hidden}`;
|
|
103
|
-
document.body.appendChild(container);
|
|
104
|
-
return container;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function createEntry(container: HTMLElement): ContainerEntry {
|
|
108
|
-
return {
|
|
109
|
-
container,
|
|
110
|
-
microApp: null,
|
|
111
|
-
status: 'hidden',
|
|
112
|
-
loadingStatus: 'idle',
|
|
113
|
-
deleteTimer: null,
|
|
114
|
-
loadSessionId: 0,
|
|
115
|
-
unmountPromise: null,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ============================================================================
|
|
120
|
-
// 公开 API
|
|
121
|
-
// ============================================================================
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 获取或创建容器
|
|
125
|
-
*/
|
|
126
|
-
export function getContainer(appName: string): HTMLElement {
|
|
127
|
-
let entry = containers.get(appName);
|
|
128
|
-
|
|
129
|
-
if (entry) {
|
|
130
|
-
// 取消待删除的计时器(容器被复用)
|
|
131
|
-
if (entry.deleteTimer) {
|
|
132
|
-
clearTimeout(entry.deleteTimer);
|
|
133
|
-
entry.deleteTimer = null;
|
|
134
|
-
}
|
|
135
|
-
entry.status = 'hidden';
|
|
136
|
-
return entry.container;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 创建新容器
|
|
140
|
-
const container = createContainer(appName);
|
|
141
|
-
entry = createEntry(container);
|
|
142
|
-
containers.set(appName, entry);
|
|
143
|
-
|
|
144
|
-
return container;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* 激活容器:移动到占位元素内,参与正常文档流
|
|
149
|
-
*/
|
|
150
|
-
export function activateContainer(appName: string, target: HTMLElement): void {
|
|
151
|
-
const entry = containers.get(appName);
|
|
152
|
-
if (!entry) return;
|
|
153
|
-
|
|
154
|
-
const { container } = entry;
|
|
155
|
-
|
|
156
|
-
// 移动容器到占位元素内(参与文档流)
|
|
157
|
-
target.appendChild(container);
|
|
158
|
-
|
|
159
|
-
// 切换样式
|
|
160
|
-
container.classList.remove(CSS_CLASS.hidden);
|
|
161
|
-
container.classList.add(CSS_CLASS.active);
|
|
162
|
-
|
|
163
|
-
entry.status = 'active';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* 停用容器:移回 body 并隐藏
|
|
168
|
-
*/
|
|
169
|
-
export function deactivateContainer(appName: string): void {
|
|
170
|
-
const entry = containers.get(appName);
|
|
171
|
-
if (!entry) return;
|
|
172
|
-
|
|
173
|
-
const { container } = entry;
|
|
174
|
-
|
|
175
|
-
// 移回 body(脱离 React 树)
|
|
176
|
-
document.body.appendChild(container);
|
|
177
|
-
|
|
178
|
-
// 切换样式
|
|
179
|
-
container.classList.remove(CSS_CLASS.active);
|
|
180
|
-
container.classList.add(CSS_CLASS.hidden);
|
|
181
|
-
|
|
182
|
-
entry.status = 'hidden';
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 开始加载会话
|
|
187
|
-
* 返回会话 ID,用于后续检查该会话是否仍然有效
|
|
188
|
-
*/
|
|
189
|
-
export function startLoadSession(appName: string): number {
|
|
190
|
-
const entry = containers.get(appName);
|
|
191
|
-
if (!entry) return 0;
|
|
192
|
-
|
|
193
|
-
const sessionId = ++globalSessionId;
|
|
194
|
-
entry.loadSessionId = sessionId;
|
|
195
|
-
entry.loadingStatus = 'loading';
|
|
196
|
-
|
|
197
|
-
return sessionId;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* 检查加载会话是否仍然有效
|
|
202
|
-
* 如果返回 false,说明已经有新的加载请求,当前加载应该取消
|
|
203
|
-
*/
|
|
204
|
-
export function isLoadSessionValid(appName: string, sessionId: number): boolean {
|
|
205
|
-
const entry = containers.get(appName);
|
|
206
|
-
return entry?.loadSessionId === sessionId;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* 标记加载完成
|
|
211
|
-
*/
|
|
212
|
-
export function markLoadComplete(appName: string, sessionId: number): boolean {
|
|
213
|
-
const entry = containers.get(appName);
|
|
214
|
-
if (!entry || entry.loadSessionId !== sessionId) {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
entry.loadingStatus = 'mounted';
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 获取当前加载状态
|
|
223
|
-
*/
|
|
224
|
-
export function getLoadingStatus(appName: string): LoadingStatus {
|
|
225
|
-
return containers.get(appName)?.loadingStatus ?? 'idle';
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* 等待当前卸载操作完成(如果有)
|
|
230
|
-
*/
|
|
231
|
-
export async function waitForUnmount(appName: string): Promise<void> {
|
|
232
|
-
const entry = containers.get(appName);
|
|
233
|
-
if (entry?.unmountPromise) {
|
|
234
|
-
await entry.unmountPromise;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* 设置微应用实例
|
|
240
|
-
*/
|
|
241
|
-
export function setMicroApp(appName: string, microApp: MicroApp): void {
|
|
242
|
-
const entry = containers.get(appName);
|
|
243
|
-
if (entry) {
|
|
244
|
-
entry.microApp = microApp;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* 获取微应用实例
|
|
250
|
-
*/
|
|
251
|
-
export function getMicroApp(appName: string): MicroApp | null {
|
|
252
|
-
return containers.get(appName)?.microApp ?? null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* 安全卸载微应用并安排容器延迟删除
|
|
257
|
-
* 带超时机制,防止子应用 unmount 卡死
|
|
258
|
-
*/
|
|
259
|
-
export async function unmountApp(appName: string): Promise<void> {
|
|
260
|
-
const entry = containers.get(appName);
|
|
261
|
-
if (!entry) return;
|
|
262
|
-
|
|
263
|
-
// 如果已经在卸载中,等待卸载完成
|
|
264
|
-
if (entry.unmountPromise) {
|
|
265
|
-
await entry.unmountPromise;
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const { microApp } = entry;
|
|
270
|
-
|
|
271
|
-
if (microApp) {
|
|
272
|
-
entry.loadingStatus = 'unmounting';
|
|
273
|
-
entry.microApp = null;
|
|
274
|
-
|
|
275
|
-
// 创建卸载 Promise 供其他调用方等待
|
|
276
|
-
entry.unmountPromise = (async () => {
|
|
277
|
-
// 等待各阶段完成再卸载(带超时)
|
|
278
|
-
try {
|
|
279
|
-
await withTimeout(
|
|
280
|
-
Promise.all([
|
|
281
|
-
safeAwait(microApp.loadPromise),
|
|
282
|
-
safeAwait(microApp.bootstrapPromise),
|
|
283
|
-
safeAwait(microApp.mountPromise),
|
|
284
|
-
]),
|
|
285
|
-
UNMOUNT_TIMEOUT / 2,
|
|
286
|
-
'Waiting for micro-app lifecycle timeout',
|
|
287
|
-
);
|
|
288
|
-
} catch (err) {
|
|
289
|
-
console.warn(`[container-manager] ${appName} lifecycle wait timeout:`, err);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// 执行卸载(带超时)
|
|
293
|
-
try {
|
|
294
|
-
await withTimeout(
|
|
295
|
-
microApp.unmount(),
|
|
296
|
-
UNMOUNT_TIMEOUT / 2,
|
|
297
|
-
'Micro-app unmount timeout',
|
|
298
|
-
);
|
|
299
|
-
} catch (err) {
|
|
300
|
-
console.warn(`[container-manager] ${appName} unmount error/timeout:`, err);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
entry.loadingStatus = 'idle';
|
|
304
|
-
entry.unmountPromise = null;
|
|
305
|
-
})();
|
|
306
|
-
|
|
307
|
-
await entry.unmountPromise;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// 安排延迟删除
|
|
311
|
-
if (!entry.deleteTimer) {
|
|
312
|
-
entry.status = 'pending-delete';
|
|
313
|
-
entry.deleteTimer = setTimeout(() => {
|
|
314
|
-
deleteContainer(appName);
|
|
315
|
-
}, DELETE_DELAY);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* 删除容器
|
|
321
|
-
*/
|
|
322
|
-
function deleteContainer(appName: string): void {
|
|
323
|
-
const entry = containers.get(appName);
|
|
324
|
-
if (!entry) return;
|
|
325
|
-
|
|
326
|
-
// 只删除 pending-delete 状态的容器(未被重新激活)
|
|
327
|
-
if (entry.status === 'pending-delete') {
|
|
328
|
-
entry.container.remove();
|
|
329
|
-
containers.delete(appName);
|
|
330
|
-
} else {
|
|
331
|
-
// 容器被重新激活了,清除计时器引用
|
|
332
|
-
entry.deleteTimer = null;
|
|
333
|
-
}
|
|
334
|
-
}
|