@gadmin2n/schematics 0.0.111 → 0.0.112

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.
@@ -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.clientSecret,
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.clientSecret, {}, (err, decoded) => {
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, checkLogin } from 'helpers';
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
- console.error(`[getUserInfoOnce] Got ${res.status} response`);
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
- const user = await res.json();
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
- // checkLogin() 会处理:URL 参数中的 token 写入 cookie、cookie 检查
121
- if (checkLogin()) {
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 });
@@ -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: Fetch roles directly from backend to ensure fresh data
118
+ // Step 1: getUserInfoOnce 提取角色(复用缓存,不额外发请求)
118
119
  useEffect(() => {
119
- const fetchRolesFromBackend = async () => {
120
- try {
121
- const res = await customRequest<any>('userinfo', 'GET', {});
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
- } finally {
141
- setIsRoleLoading(false);
142
- }
143
- };
144
-
145
- fetchRolesFromBackend();
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 (isRoleLoading) return true;
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
- isRoleLoading,
331
+ isIdentityLoading,
343
332
  pagesData,
344
333
  roleNames,
345
334
  rolesData,
@@ -4,7 +4,13 @@ import { createRoot } from 'react-dom/client';
4
4
  import App from './App';
5
5
  import './i18n';
6
6
 
7
- import { checkLogin, login } from './helpers/login';
7
+ import {
8
+ login,
9
+ GAdminTokenName,
10
+ setLoginCookie,
11
+ getUrlParams,
12
+ } from './helpers/login';
13
+ import Cookies from 'js-cookie';
8
14
 
9
15
  // 屏蔽 ECharts 在 RadarChart 多 Y 轴场景下的 alignTicks 噪音警告
10
16
  const _warn = console.warn.bind(console);
@@ -13,7 +19,14 @@ console.warn = (...args: any[]) => {
13
19
  _warn(...args);
14
20
  };
15
21
 
16
- if (!checkLogin()) {
22
+ // DEV 模式下支持 URL 参数传 token(跨域开发用)
23
+ if (import.meta.env.DEV) {
24
+ const params = getUrlParams();
25
+ const gadminToken = params[GAdminTokenName];
26
+ if (gadminToken) setLoginCookie({ gadminToken });
27
+ }
28
+
29
+ if (!Cookies.get(GAdminTokenName)) {
17
30
  login();
18
31
  } else {
19
32
  const container = document.getElementById('root') as HTMLElement;
@@ -159,7 +159,7 @@
159
159
  "emptyHint": "Drag components from the library",
160
160
  "exitPreview": "Exit Preview",
161
161
  "config": "Configure",
162
- "configCharts": "Configure Charts & Order",
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 to",
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",
@@ -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": "列配置",
@@ -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;