@gadmin2n/schematics 0.0.111 → 0.0.113
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/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/taihu.ts +8 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/authProvider.ts +26 -27
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/login.ts +0 -24
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useUserPageAccess.ts +17 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/index.tsx +21 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +10 -9
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +12 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasCell.tsx +103 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasComponentLibrary.tsx +449 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +794 -713
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +3 -239
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasContextMenuRegistry.tsx +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasContextMenu.tsx +79 -8
- package/package.json +1 -1
|
@@ -31,6 +31,12 @@ export default class TaiHuId {
|
|
|
31
31
|
static config = {
|
|
32
32
|
clientId: process.env.TAIHU_ODC_CLIENT_ID,
|
|
33
33
|
clientSecret: process.env.TAIHU_ODC_APP_TOKEN || 'gadmin-default-secret',
|
|
34
|
+
// JWT secret 与 portal 共享,独立于太湖 client_secret;
|
|
35
|
+
// 分支只用它 verify portal 签发的 token,不 sign。
|
|
36
|
+
jwtSecret:
|
|
37
|
+
process.env.GADMIN_JWT_SECRET ||
|
|
38
|
+
process.env.TAIHU_ODC_APP_TOKEN ||
|
|
39
|
+
'gadmin-default-secret',
|
|
34
40
|
apiBase: process.env.TAIHU_BASE,
|
|
35
41
|
callback: process.env.TAIHU_CALLBACK,
|
|
36
42
|
logout: process.env.TAIHU_LOGOUT,
|
|
@@ -138,7 +144,7 @@ export default class TaiHuId {
|
|
|
138
144
|
return new Promise<string>((resolve, reject) => {
|
|
139
145
|
jwt.sign(
|
|
140
146
|
payload,
|
|
141
|
-
TaiHuId.config.
|
|
147
|
+
TaiHuId.config.jwtSecret,
|
|
142
148
|
{ expiresIn },
|
|
143
149
|
(err, token) => {
|
|
144
150
|
if (err) {
|
|
@@ -163,7 +169,7 @@ export default class TaiHuId {
|
|
|
163
169
|
return;
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
jwt.verify(token, TaiHuId.config.
|
|
172
|
+
jwt.verify(token, TaiHuId.config.jwtSecret, {}, (err, decoded) => {
|
|
167
173
|
if (err) {
|
|
168
174
|
reject(err);
|
|
169
175
|
return;
|
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
import { AuthBindings } from '@refinedev/core';
|
|
2
2
|
import { getApiUrl } from 'config/http';
|
|
3
|
-
import { requestHeaders, GAdminTokenName
|
|
3
|
+
import { requestHeaders, GAdminTokenName } from 'helpers';
|
|
4
|
+
import { login as taihuLogin, logout as taihuLogout } from './helpers/login';
|
|
4
5
|
import Cookies from 'js-cookie';
|
|
5
6
|
import memoize from 'lodash.memoize';
|
|
6
7
|
|
|
7
|
-
const getUserInfoOnce = memoize(async () => {
|
|
8
|
+
export const getUserInfoOnce = memoize(async () => {
|
|
8
9
|
const headers = requestHeaders();
|
|
9
|
-
|
|
10
|
-
if (import.meta.env.DEV) {
|
|
11
|
-
console.log('[getUserInfoOnce] Fetching /userinfo');
|
|
12
|
-
console.log('[getUserInfoOnce] Request headers:', headers);
|
|
13
|
-
console.log('[getUserInfoOnce] Hostname:', window.location.hostname);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
10
|
const res = await fetch(`${getApiUrl()}/userinfo`, { headers });
|
|
17
11
|
|
|
18
12
|
if (!res.ok) {
|
|
19
|
-
|
|
20
|
-
if (res.status === 401) {
|
|
21
|
-
console.error('[getUserInfoOnce] 401 - Missing or invalid credentials');
|
|
22
|
-
}
|
|
13
|
+
throw new Error(`userinfo ${res.status}`);
|
|
23
14
|
}
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
return user;
|
|
16
|
+
return res.json();
|
|
27
17
|
});
|
|
28
18
|
|
|
29
19
|
export const authProvider: AuthBindings = {
|
|
@@ -107,9 +97,12 @@ export const authProvider: AuthBindings = {
|
|
|
107
97
|
},
|
|
108
98
|
logout: async () => {
|
|
109
99
|
localStorage.removeItem('email');
|
|
100
|
+
// taihuLogout() 通过 window.location.href 跳转后端 /logout,
|
|
101
|
+
// 后端清 cookie 后再 302 到太湖 IDP 退出 SSO,最后回到首页重新触发 OAuth。
|
|
102
|
+
// 因此这里不需要再返回 redirectTo——页面已经在跳走了。
|
|
103
|
+
taihuLogout();
|
|
110
104
|
return {
|
|
111
105
|
success: true,
|
|
112
|
-
redirectTo: '/',
|
|
113
106
|
};
|
|
114
107
|
},
|
|
115
108
|
onError: async (error) => {
|
|
@@ -117,19 +110,25 @@ export const authProvider: AuthBindings = {
|
|
|
117
110
|
return { error };
|
|
118
111
|
},
|
|
119
112
|
check: async () => {
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
try {
|
|
114
|
+
await getUserInfoOnce();
|
|
115
|
+
sessionStorage.removeItem('gadmin-relogin-inflight');
|
|
122
116
|
return { authenticated: true };
|
|
117
|
+
} catch {
|
|
118
|
+
// 清掉本地缓存,让 helpers/login.ts 的 login() 重新走一遍 OAuth。
|
|
119
|
+
// 不要 redirectTo: '/login' —— 前端没有这个路由,会进死路。
|
|
120
|
+
// 防重入:避免 401 拦截器和 check() 同时跳登录。
|
|
121
|
+
if (!sessionStorage.getItem('gadmin-relogin-inflight')) {
|
|
122
|
+
sessionStorage.setItem('gadmin-relogin-inflight', '1');
|
|
123
|
+
getUserInfoOnce.cache.clear?.();
|
|
124
|
+
taihuLogin();
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
authenticated: false,
|
|
128
|
+
logout: true,
|
|
129
|
+
error: { message: 'Check failed', name: 'Not authenticated' },
|
|
130
|
+
};
|
|
123
131
|
}
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
authenticated: false,
|
|
127
|
-
redirectTo: '/login',
|
|
128
|
-
error: {
|
|
129
|
-
message: 'Check failed',
|
|
130
|
-
name: 'Not authenticated',
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
132
|
},
|
|
134
133
|
getPermissions: async () => ['admin'],
|
|
135
134
|
getIdentity: async () => {
|
|
@@ -42,30 +42,6 @@ export function isWoaDomain(): boolean {
|
|
|
42
42
|
return hostname === 'woa.com' || hostname.endsWith('.woa.com');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* 通过cookie检测是否登录及参数转cookie(for dev)
|
|
47
|
-
*/
|
|
48
|
-
export function checkLogin(): boolean {
|
|
49
|
-
// 优先处理 URL 参数中的 token,确保 token 被写入 cookie(任何域名下都需要)
|
|
50
|
-
const params = getUrlParams();
|
|
51
|
-
const gadminToken = params[GAdminTokenName];
|
|
52
|
-
if (gadminToken) {
|
|
53
|
-
setLoginCookie({ gadminToken });
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (Cookies.get(GAdminTokenName)) {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// woa.com 域名下 SmartGate 保证进入页面时已有登录态,无需跳转 Taihu
|
|
62
|
-
if (isWoaDomain()) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
45
|
export function setLoginCookie({ gadminToken }: Required<LoginCookie>) {
|
|
70
46
|
const expires = 1; // 本地登录态缓存1天
|
|
71
47
|
Cookies.set(GAdminTokenName, gadminToken, { expires });
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useUserPageAccess.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import { customRequest } from 'helpers/http';
|
|
3
|
+
import { getUserInfoOnce } from 'authProvider';
|
|
3
4
|
import type { Role } from 'types/role';
|
|
4
5
|
|
|
5
6
|
const RoleCacheKey = 'oiterproles';
|
|
@@ -82,12 +83,12 @@ function writeCache(key: string, value: unknown): void {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
export const useUserPageAccess = (): UseUserPageAccessResult => {
|
|
86
|
+
const [isIdentityLoading, setIsIdentityLoading] = useState(true);
|
|
87
|
+
|
|
85
88
|
// Role names — initialise from cache so first render is non-empty
|
|
86
89
|
const [roleNames, setRoleNames] = useState<string[]>(
|
|
87
90
|
() => readCache<string[]>(RoleCacheKey) ?? [],
|
|
88
91
|
);
|
|
89
|
-
// isRoleLoading only stays true while we wait for the live /userinfo response
|
|
90
|
-
const [isRoleLoading, setIsRoleLoading] = useState(true);
|
|
91
92
|
|
|
92
93
|
// Data states — initialise from cache for instant first render
|
|
93
94
|
const [rolesData, setRolesData] = useState<Role[] | null>(() =>
|
|
@@ -114,35 +115,23 @@ export const useUserPageAccess = (): UseUserPageAccessResult => {
|
|
|
114
115
|
}
|
|
115
116
|
}, []);
|
|
116
117
|
|
|
117
|
-
// Step 1:
|
|
118
|
+
// Step 1: 从 getUserInfoOnce 提取角色(复用缓存,不额外发请求)
|
|
118
119
|
useEffect(() => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const roles = res?.configPerms?.roles;
|
|
120
|
+
getUserInfoOnce()
|
|
121
|
+
.then((identity) => {
|
|
122
|
+
const roles = identity?.configPerms?.roles;
|
|
123
123
|
if (roles && Array.isArray(roles)) {
|
|
124
124
|
updateRoles(roles as string[]);
|
|
125
|
-
}
|
|
126
|
-
} catch (error) {
|
|
127
|
-
console.error(
|
|
128
|
-
'[useUserPageAccess] Failed to fetch roles from backend:',
|
|
129
|
-
error,
|
|
130
|
-
);
|
|
131
|
-
// Fallback to sessionStorage if backend request fails
|
|
132
|
-
try {
|
|
125
|
+
} else {
|
|
133
126
|
const cached = readCache<string[]>(RoleCacheKey);
|
|
134
|
-
if (cached)
|
|
135
|
-
updateRoles(cached);
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {
|
|
138
|
-
console.error('[useUserPageAccess] Failed to parse cached roles:', e);
|
|
127
|
+
if (cached) updateRoles(cached);
|
|
139
128
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
129
|
+
})
|
|
130
|
+
.catch(() => {
|
|
131
|
+
const cached = readCache<string[]>(RoleCacheKey);
|
|
132
|
+
if (cached) updateRoles(cached);
|
|
133
|
+
})
|
|
134
|
+
.finally(() => setIsIdentityLoading(false));
|
|
146
135
|
}, [updateRoles]);
|
|
147
136
|
|
|
148
137
|
// Step 2: Fetch all page definitions
|
|
@@ -331,7 +320,7 @@ export const useUserPageAccess = (): UseUserPageAccessResult => {
|
|
|
331
320
|
const isLoading = useMemo(() => {
|
|
332
321
|
// If cache was available on mount, never block rendering
|
|
333
322
|
if (hasCache && pagesData && rolePagesData) return false;
|
|
334
|
-
if (
|
|
323
|
+
if (isIdentityLoading) return true;
|
|
335
324
|
if (!pagesData) return true;
|
|
336
325
|
// 等待 /role/findMany 返回(roleNames 已拿到但 rolesData 还在路上)
|
|
337
326
|
if (roleNames.length > 0 && !rolesData) return true;
|
|
@@ -339,7 +328,7 @@ export const useUserPageAccess = (): UseUserPageAccessResult => {
|
|
|
339
328
|
if (roleIds.length > 0 && !rolePagesData) return true;
|
|
340
329
|
return false;
|
|
341
330
|
}, [
|
|
342
|
-
|
|
331
|
+
isIdentityLoading,
|
|
343
332
|
pagesData,
|
|
344
333
|
roleNames,
|
|
345
334
|
rolesData,
|
|
@@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client';
|
|
|
4
4
|
import App from './App';
|
|
5
5
|
import './i18n';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { GAdminTokenName, setLoginCookie, getUrlParams } from './helpers/login';
|
|
8
8
|
|
|
9
9
|
// 屏蔽 ECharts 在 RadarChart 多 Y 轴场景下的 alignTicks 噪音警告
|
|
10
10
|
const _warn = console.warn.bind(console);
|
|
@@ -13,15 +13,24 @@ console.warn = (...args: any[]) => {
|
|
|
13
13
|
_warn(...args);
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<React.Suspense fallback="loading">
|
|
24
|
-
<App />
|
|
25
|
-
</React.Suspense>,
|
|
26
|
-
);
|
|
16
|
+
// DEV 模式下支持 URL 参数传 token(真跨 host 部署用,例如 portal 子域跳回到分支独立域名)。
|
|
17
|
+
// 同 host 部署时 portal 已通过 httpOnly cookie 完成认证,此处 setLoginCookie 写入会被
|
|
18
|
+
// 浏览器静默拒绝(RFC 6265 §5.3:JS 不可覆盖同名 httpOnly cookie),属于 no-op,无副作用。
|
|
19
|
+
if (import.meta.env.DEV) {
|
|
20
|
+
const params = getUrlParams();
|
|
21
|
+
const gadminToken = params[GAdminTokenName];
|
|
22
|
+
if (gadminToken) setLoginCookie({ gadminToken });
|
|
27
23
|
}
|
|
24
|
+
|
|
25
|
+
// 不再用 `Cookies.get(GAdminTokenName)` 探测:portal 现在用 httpOnly cookie,js 端永远读不到,
|
|
26
|
+
// 否则会陷入"探测失败 → login → callback → 探测仍失败"的死循环。
|
|
27
|
+
// 认证统一交给 authProvider.check()——发 /api/userinfo(同源会自动带 httpOnly cookie),
|
|
28
|
+
// 401 时由 check() 内部的防重入逻辑触发 taihuLogin。
|
|
29
|
+
const container = document.getElementById('root') as HTMLElement;
|
|
30
|
+
const root = createRoot(container);
|
|
31
|
+
|
|
32
|
+
root.render(
|
|
33
|
+
<React.Suspense fallback="loading">
|
|
34
|
+
<App />
|
|
35
|
+
</React.Suspense>,
|
|
36
|
+
);
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"emptyHint": "Drag components from the library",
|
|
160
160
|
"exitPreview": "Exit Preview",
|
|
161
161
|
"config": "Configure",
|
|
162
|
-
"configCharts": "Configure
|
|
162
|
+
"configCharts": "Configure switchable chart types",
|
|
163
163
|
"editCode": "Edit Code",
|
|
164
164
|
"duplicate": "Duplicate",
|
|
165
165
|
"delete": "Delete",
|
|
@@ -245,14 +245,14 @@
|
|
|
245
245
|
"changePageSize": "Change Page Size",
|
|
246
246
|
"removePagination": "Remove Pagination",
|
|
247
247
|
"addPagination": "Add Pagination",
|
|
248
|
-
"direction": "Direction",
|
|
248
|
+
"direction": "Chart Direction",
|
|
249
249
|
"vertical": "Vertical",
|
|
250
250
|
"horizontal": "Horizontal",
|
|
251
251
|
"hideGrid": "Hide Grid",
|
|
252
252
|
"showGrid": "Show Grid",
|
|
253
253
|
"hideLabel": "Hide Labels",
|
|
254
254
|
"showLabel": "Show Labels",
|
|
255
|
-
"switchVariant": "Switch Style",
|
|
255
|
+
"switchVariant": "Switch Display Style",
|
|
256
256
|
"pie": "Pie",
|
|
257
257
|
"ring": "Ring",
|
|
258
258
|
"topN": "Top N Items",
|
|
@@ -262,22 +262,23 @@
|
|
|
262
262
|
"showDots": "Show Dots",
|
|
263
263
|
"hideDotsLabel": "Hide Dot Values",
|
|
264
264
|
"showDotsLabel": "Show Dot Values",
|
|
265
|
-
"disableStacked": "Disable Stacking",
|
|
266
|
-
"enableStacked": "Enable Stacking",
|
|
267
|
-
"disablePercent": "Disable Percent",
|
|
268
|
-
"enablePercent": "Enable Percent",
|
|
265
|
+
"disableStacked": "Disable Data Stacking",
|
|
266
|
+
"enableStacked": "Enable Data Stacking",
|
|
267
|
+
"disablePercent": "Disable Percent Stacking",
|
|
268
|
+
"enablePercent": "Enable Percent Stacking",
|
|
269
269
|
"disableArea": "Disable Area Fill",
|
|
270
270
|
"enableArea": "Enable Area Fill",
|
|
271
271
|
"disableSmooth": "Disable Smooth",
|
|
272
272
|
"enableSmooth": "Enable Smooth",
|
|
273
|
-
"switchTo": "Switch
|
|
273
|
+
"switchTo": "Switch Chart Type",
|
|
274
274
|
"showDownload": "Show Download",
|
|
275
275
|
"hideDownload": "Hide Download",
|
|
276
276
|
"enableSorting": "Enable Sorting",
|
|
277
277
|
"disableSorting": "Disable Sorting",
|
|
278
278
|
"enableFiltering": "Enable Filtering",
|
|
279
279
|
"disableFiltering": "Disable Filtering",
|
|
280
|
-
"configColumns": "Configure Columns"
|
|
280
|
+
"configColumns": "Configure Columns",
|
|
281
|
+
"visibilityGroup": "Display Elements"
|
|
281
282
|
},
|
|
282
283
|
"modal": {
|
|
283
284
|
"columnConfigTitle": "Column Settings",
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json
CHANGED
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"emptyHint": "从组件库拖拽组件到此处",
|
|
162
162
|
"exitPreview": "退出预览",
|
|
163
163
|
"config": "配置",
|
|
164
|
-
"configCharts": "
|
|
164
|
+
"configCharts": "配置可切换的图表类型",
|
|
165
165
|
"editCode": "编辑代码",
|
|
166
166
|
"duplicate": "复制",
|
|
167
167
|
"delete": "删除",
|
|
@@ -247,14 +247,14 @@
|
|
|
247
247
|
"changePageSize": "修改分页数量",
|
|
248
248
|
"removePagination": "移除分页",
|
|
249
249
|
"addPagination": "添加分页",
|
|
250
|
-
"direction": "
|
|
250
|
+
"direction": "图表方向",
|
|
251
251
|
"vertical": "纵向",
|
|
252
252
|
"horizontal": "横向",
|
|
253
253
|
"hideGrid": "隐藏网格",
|
|
254
254
|
"showGrid": "显示网格",
|
|
255
255
|
"hideLabel": "隐藏数据标签",
|
|
256
256
|
"showLabel": "显示数据标签",
|
|
257
|
-
"switchVariant": "
|
|
257
|
+
"switchVariant": "切换显示样式",
|
|
258
258
|
"pie": "饼图",
|
|
259
259
|
"ring": "环形图",
|
|
260
260
|
"topN": "显示项数",
|
|
@@ -264,22 +264,23 @@
|
|
|
264
264
|
"showDots": "显示坐标点",
|
|
265
265
|
"hideDotsLabel": "隐藏坐标点数据",
|
|
266
266
|
"showDotsLabel": "显示坐标点数据",
|
|
267
|
-
"disableStacked": "
|
|
268
|
-
"enableStacked": "
|
|
269
|
-
"disablePercent": "
|
|
270
|
-
"enablePercent": "
|
|
271
|
-
"disableArea": "
|
|
272
|
-
"enableArea": "
|
|
267
|
+
"disableStacked": "关闭数据堆叠",
|
|
268
|
+
"enableStacked": "开启数据堆叠",
|
|
269
|
+
"disablePercent": "关闭百分比堆叠",
|
|
270
|
+
"enablePercent": "开启百分比堆叠",
|
|
271
|
+
"disableArea": "关闭面积填充",
|
|
272
|
+
"enableArea": "开启面积填充",
|
|
273
273
|
"disableSmooth": "关闭拟合曲线",
|
|
274
274
|
"enableSmooth": "开启拟合曲线",
|
|
275
|
-
"switchTo": "
|
|
275
|
+
"switchTo": "切换图表类型",
|
|
276
276
|
"showDownload": "显示下载按钮",
|
|
277
277
|
"hideDownload": "隐藏下载按钮",
|
|
278
278
|
"enableSorting": "启用排序",
|
|
279
279
|
"disableSorting": "禁用排序",
|
|
280
280
|
"enableFiltering": "启用过滤",
|
|
281
281
|
"disableFiltering": "禁用过滤",
|
|
282
|
-
"configColumns": "配置列"
|
|
282
|
+
"configColumns": "配置列",
|
|
283
|
+
"visibilityGroup": "显示元素"
|
|
283
284
|
},
|
|
284
285
|
"modal": {
|
|
285
286
|
"columnConfigTitle": "列配置",
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasCell.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { Tooltip } from 'antd';
|
|
3
|
+
import { CopyOutlined, DeleteOutlined, MoreOutlined } from '@ant-design/icons';
|
|
2
4
|
import IsolatedLivePreview from './IsolatedLivePreview';
|
|
3
5
|
import { useIsAgentAllowed } from 'config/agentAllowed';
|
|
4
6
|
|
|
@@ -10,6 +12,9 @@ interface CanvasCellProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
10
12
|
onClick: (e: React.MouseEvent) => void;
|
|
11
13
|
onDoubleClick: (e: React.MouseEvent) => void;
|
|
12
14
|
onContextMenu: (e: React.MouseEvent) => void;
|
|
15
|
+
onDuplicate?: () => void;
|
|
16
|
+
onDelete?: () => void;
|
|
17
|
+
onMore?: (e: React.MouseEvent) => void;
|
|
13
18
|
className?: string;
|
|
14
19
|
style?: React.CSSProperties;
|
|
15
20
|
children?: React.ReactNode; // react-grid-layout injects resize handles here
|
|
@@ -26,6 +31,9 @@ const CanvasCell = React.forwardRef<HTMLDivElement, CanvasCellProps>(
|
|
|
26
31
|
onClick,
|
|
27
32
|
onDoubleClick,
|
|
28
33
|
onContextMenu,
|
|
34
|
+
onDuplicate,
|
|
35
|
+
onDelete,
|
|
36
|
+
onMore,
|
|
29
37
|
className,
|
|
30
38
|
style,
|
|
31
39
|
children,
|
|
@@ -35,6 +43,7 @@ const CanvasCell = React.forwardRef<HTMLDivElement, CanvasCellProps>(
|
|
|
35
43
|
) => {
|
|
36
44
|
const isSection = componentType === 'Section';
|
|
37
45
|
const isAgentAllowed = useIsAgentAllowed();
|
|
46
|
+
const showToolbar = !isPreview && isAgentAllowed && selected;
|
|
38
47
|
|
|
39
48
|
return (
|
|
40
49
|
<div
|
|
@@ -77,6 +86,84 @@ const CanvasCell = React.forwardRef<HTMLDivElement, CanvasCellProps>(
|
|
|
77
86
|
>
|
|
78
87
|
<IsolatedLivePreview code={code} />
|
|
79
88
|
</div>
|
|
89
|
+
|
|
90
|
+
{/* 选中态悬浮工具栏 */}
|
|
91
|
+
{showToolbar && (
|
|
92
|
+
<div
|
|
93
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
94
|
+
onClick={(e) => e.stopPropagation()}
|
|
95
|
+
style={{
|
|
96
|
+
position: 'absolute',
|
|
97
|
+
top: 6,
|
|
98
|
+
right: 6,
|
|
99
|
+
zIndex: 10,
|
|
100
|
+
display: 'flex',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
gap: 2,
|
|
103
|
+
padding: 2,
|
|
104
|
+
background: '#fff',
|
|
105
|
+
border: '1px solid #d6e4ff',
|
|
106
|
+
borderRadius: 6,
|
|
107
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<Tooltip title="复制">
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={(e) => {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
onDuplicate?.();
|
|
116
|
+
}}
|
|
117
|
+
style={toolbarBtnStyle}
|
|
118
|
+
onMouseEnter={(e) =>
|
|
119
|
+
(e.currentTarget.style.background = '#f0f5ff')
|
|
120
|
+
}
|
|
121
|
+
onMouseLeave={(e) =>
|
|
122
|
+
(e.currentTarget.style.background = 'transparent')
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
<CopyOutlined />
|
|
126
|
+
</button>
|
|
127
|
+
</Tooltip>
|
|
128
|
+
<Tooltip title="更多">
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={(e) => {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
onMore?.(e);
|
|
134
|
+
}}
|
|
135
|
+
style={toolbarBtnStyle}
|
|
136
|
+
onMouseEnter={(e) =>
|
|
137
|
+
(e.currentTarget.style.background = '#f0f5ff')
|
|
138
|
+
}
|
|
139
|
+
onMouseLeave={(e) =>
|
|
140
|
+
(e.currentTarget.style.background = 'transparent')
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
<MoreOutlined />
|
|
144
|
+
</button>
|
|
145
|
+
</Tooltip>
|
|
146
|
+
<Tooltip title="删除">
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={(e) => {
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
onDelete?.();
|
|
152
|
+
}}
|
|
153
|
+
style={{ ...toolbarBtnStyle, color: '#ff4d4f' }}
|
|
154
|
+
onMouseEnter={(e) =>
|
|
155
|
+
(e.currentTarget.style.background = '#fff1f0')
|
|
156
|
+
}
|
|
157
|
+
onMouseLeave={(e) =>
|
|
158
|
+
(e.currentTarget.style.background = 'transparent')
|
|
159
|
+
}
|
|
160
|
+
>
|
|
161
|
+
<DeleteOutlined />
|
|
162
|
+
</button>
|
|
163
|
+
</Tooltip>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
|
|
80
167
|
{/* Resize handles injected by react-grid-layout */}
|
|
81
168
|
{children}
|
|
82
169
|
</div>
|
|
@@ -84,6 +171,22 @@ const CanvasCell = React.forwardRef<HTMLDivElement, CanvasCellProps>(
|
|
|
84
171
|
},
|
|
85
172
|
);
|
|
86
173
|
|
|
174
|
+
const toolbarBtnStyle: React.CSSProperties = {
|
|
175
|
+
border: 'none',
|
|
176
|
+
background: 'transparent',
|
|
177
|
+
width: 24,
|
|
178
|
+
height: 24,
|
|
179
|
+
borderRadius: 4,
|
|
180
|
+
cursor: 'pointer',
|
|
181
|
+
display: 'flex',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
justifyContent: 'center',
|
|
184
|
+
fontSize: 14,
|
|
185
|
+
color: '#555',
|
|
186
|
+
padding: 0,
|
|
187
|
+
transition: 'background 120ms ease',
|
|
188
|
+
};
|
|
189
|
+
|
|
87
190
|
CanvasCell.displayName = 'CanvasCell';
|
|
88
191
|
|
|
89
192
|
export default CanvasCell;
|