yingzios-platform-frontend-devtools 1.0.1
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/README.md +77 -0
- package/dist/base-path.d.ts +2 -0
- package/dist/base-path.d.ts.map +1 -0
- package/dist/base-path.js +2 -0
- package/dist/components/DevUserMenu/JwtGeneratorModal.d.ts +13 -0
- package/dist/components/DevUserMenu/JwtGeneratorModal.d.ts.map +1 -0
- package/dist/components/DevUserMenu/JwtGeneratorModal.js +137 -0
- package/dist/components/DevUserMenu/index.d.ts +31 -0
- package/dist/components/DevUserMenu/index.d.ts.map +1 -0
- package/dist/components/DevUserMenu/index.js +219 -0
- package/dist/components/DevUserMenu/jwt-utils.d.ts +34 -0
- package/dist/components/DevUserMenu/jwt-utils.d.ts.map +1 -0
- package/dist/components/DevUserMenu/jwt-utils.js +85 -0
- package/dist/components/DevUserMenu/styles.d.ts +5 -0
- package/dist/components/DevUserMenu/styles.d.ts.map +1 -0
- package/dist/components/DevUserMenu/styles.js +225 -0
- package/dist/components/StandaloneAppLayout/index.d.ts +63 -0
- package/dist/components/StandaloneAppLayout/index.d.ts.map +1 -0
- package/dist/components/StandaloneAppLayout/index.js +161 -0
- package/dist/components/StandaloneAppLayout/styles.d.ts +5 -0
- package/dist/components/StandaloneAppLayout/styles.d.ts.map +1 -0
- package/dist/components/StandaloneAppLayout/styles.js +230 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/manifest-loader.d.ts +50 -0
- package/dist/manifest-loader.d.ts.map +1 -0
- package/dist/manifest-loader.js +90 -0
- package/package.json +50 -0
- package/src/base-path.ts +2 -0
- package/src/components/DevUserMenu/JwtGeneratorModal.tsx +219 -0
- package/src/components/DevUserMenu/index.tsx +372 -0
- package/src/components/DevUserMenu/jwt-utils.ts +121 -0
- package/src/components/DevUserMenu/styles.ts +229 -0
- package/src/components/StandaloneAppLayout/index.tsx +332 -0
- package/src/components/StandaloneAppLayout/styles.ts +234 -0
- package/src/index.ts +4 -0
- package/src/manifest-loader.ts +146 -0
- package/src/types/scss.d.ts +19 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { devUserMenuStyles, modalStyles, formGroupStyles } from './styles';
|
|
3
|
+
import { JwtGeneratorModal } from './JwtGeneratorModal';
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEYS = {
|
|
6
|
+
accessToken: 'accessToken',
|
|
7
|
+
refreshToken: 'refreshToken',
|
|
8
|
+
lastPhone: 'devtools:lastPhone',
|
|
9
|
+
userInfo: 'devtools:userInfo',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
interface UserInfo {
|
|
13
|
+
userId: string;
|
|
14
|
+
nickname: string;
|
|
15
|
+
avatar?: string;
|
|
16
|
+
phone: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DevUserMenuProps {
|
|
20
|
+
/**
|
|
21
|
+
* API 基础路径,默认 '/portal/api'
|
|
22
|
+
*/
|
|
23
|
+
apiBasePath?: string;
|
|
24
|
+
/**
|
|
25
|
+
* 是否显示切换组织按钮
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
showOrgSwitch?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* 组织切换页面路径
|
|
31
|
+
* @default '/org-select'
|
|
32
|
+
*/
|
|
33
|
+
orgSwitchPath?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 开发环境用户菜单组件
|
|
38
|
+
*
|
|
39
|
+
* 提供简化的登录功能,方便本地独立开发调试。
|
|
40
|
+
* - 手机号 + 验证码登录
|
|
41
|
+
* - 自动记录上次登录的手机号
|
|
42
|
+
* - 验证码默认填写 1024
|
|
43
|
+
*/
|
|
44
|
+
export function DevUserMenu({
|
|
45
|
+
apiBasePath = '/portal/api',
|
|
46
|
+
showOrgSwitch = true,
|
|
47
|
+
orgSwitchPath = '/org-select',
|
|
48
|
+
}: DevUserMenuProps) {
|
|
49
|
+
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
50
|
+
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
|
51
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
52
|
+
const [showJwtGenerator, setShowJwtGenerator] = useState(false);
|
|
53
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
|
|
55
|
+
// 初始化时检查登录状态
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const token = localStorage.getItem(STORAGE_KEYS.accessToken);
|
|
58
|
+
const savedUserInfo = localStorage.getItem(STORAGE_KEYS.userInfo);
|
|
59
|
+
|
|
60
|
+
if (token && savedUserInfo) {
|
|
61
|
+
try {
|
|
62
|
+
setUserInfo(JSON.parse(savedUserInfo));
|
|
63
|
+
setIsLoggedIn(true);
|
|
64
|
+
} catch {
|
|
65
|
+
// 解析失败,清除无效数据
|
|
66
|
+
clearAuth();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// 点击外部关闭下拉菜单
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
function handleClickOutside(event: MouseEvent) {
|
|
74
|
+
if (
|
|
75
|
+
dropdownRef.current &&
|
|
76
|
+
!dropdownRef.current.contains(event.target as Node)
|
|
77
|
+
) {
|
|
78
|
+
setShowDropdown(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
83
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const clearAuth = useCallback(() => {
|
|
87
|
+
localStorage.removeItem(STORAGE_KEYS.accessToken);
|
|
88
|
+
localStorage.removeItem(STORAGE_KEYS.refreshToken);
|
|
89
|
+
localStorage.removeItem(STORAGE_KEYS.userInfo);
|
|
90
|
+
setIsLoggedIn(false);
|
|
91
|
+
setUserInfo(null);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const handleLogout = useCallback(() => {
|
|
95
|
+
clearAuth();
|
|
96
|
+
setShowDropdown(false);
|
|
97
|
+
}, [clearAuth]);
|
|
98
|
+
|
|
99
|
+
const handleJwtGeneratorSuccess = useCallback(() => {
|
|
100
|
+
// 重新加载用户信息
|
|
101
|
+
const token = localStorage.getItem(STORAGE_KEYS.accessToken);
|
|
102
|
+
const savedUserInfo = localStorage.getItem(STORAGE_KEYS.userInfo);
|
|
103
|
+
|
|
104
|
+
if (token && savedUserInfo) {
|
|
105
|
+
try {
|
|
106
|
+
setUserInfo(JSON.parse(savedUserInfo));
|
|
107
|
+
setIsLoggedIn(true);
|
|
108
|
+
setShowJwtGenerator(false);
|
|
109
|
+
} catch {
|
|
110
|
+
clearAuth();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}, [clearAuth]);
|
|
114
|
+
|
|
115
|
+
const getAvatarContent = () => {
|
|
116
|
+
if (userInfo?.avatar) {
|
|
117
|
+
return (
|
|
118
|
+
<img
|
|
119
|
+
src={userInfo.avatar}
|
|
120
|
+
alt="avatar"
|
|
121
|
+
style={devUserMenuStyles.avatarImage as React.CSSProperties}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (userInfo?.nickname) {
|
|
127
|
+
return userInfo.nickname.charAt(0).toUpperCase();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return '?';
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleSwitchOrganization = useCallback(() => {
|
|
134
|
+
window.location.href = orgSwitchPath;
|
|
135
|
+
}, [orgSwitchPath]);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div style={devUserMenuStyles.container as React.CSSProperties} ref={dropdownRef}>
|
|
139
|
+
{isLoggedIn ? (
|
|
140
|
+
<>
|
|
141
|
+
<button
|
|
142
|
+
style={devUserMenuStyles.avatar}
|
|
143
|
+
onClick={() => setShowDropdown(!showDropdown)}
|
|
144
|
+
title={userInfo?.nickname || '用户'}
|
|
145
|
+
>
|
|
146
|
+
{getAvatarContent()}
|
|
147
|
+
</button>
|
|
148
|
+
{showDropdown && (
|
|
149
|
+
<div style={devUserMenuStyles.dropdown as React.CSSProperties}>
|
|
150
|
+
<div style={devUserMenuStyles.dropdownHeader as React.CSSProperties}>
|
|
151
|
+
<span style={devUserMenuStyles.dropdownNickname}>
|
|
152
|
+
{userInfo?.nickname || '未知用户'}
|
|
153
|
+
</span>
|
|
154
|
+
<span style={devUserMenuStyles.dropdownPhone}>
|
|
155
|
+
{userInfo?.phone}
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div style={devUserMenuStyles.dropdownDivider} />
|
|
159
|
+
<button
|
|
160
|
+
style={devUserMenuStyles.dropdownItem as React.CSSProperties}
|
|
161
|
+
onClick={handleLogout}
|
|
162
|
+
>
|
|
163
|
+
退出登录
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</>
|
|
168
|
+
) : (
|
|
169
|
+
<button
|
|
170
|
+
style={devUserMenuStyles.loginButton}
|
|
171
|
+
onClick={() => setShowJwtGenerator(true)}
|
|
172
|
+
>
|
|
173
|
+
本地签发 JWT
|
|
174
|
+
</button>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{showJwtGenerator && (
|
|
178
|
+
<JwtGeneratorModal
|
|
179
|
+
onClose={() => setShowJwtGenerator(false)}
|
|
180
|
+
onSuccess={handleJwtGeneratorSuccess}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface LoginModalProps {
|
|
188
|
+
apiBasePath: string;
|
|
189
|
+
onClose: () => void;
|
|
190
|
+
onSuccess: (userInfo: UserInfo) => void;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function LoginModal({ apiBasePath, onClose, onSuccess }: LoginModalProps) {
|
|
194
|
+
const [phone, setPhone] = useState(() => {
|
|
195
|
+
return localStorage.getItem(STORAGE_KEYS.lastPhone) || '';
|
|
196
|
+
});
|
|
197
|
+
const [code, setCode] = useState('1024');
|
|
198
|
+
const [loading, setLoading] = useState(false);
|
|
199
|
+
const [error, setError] = useState<string | null>(null);
|
|
200
|
+
const [codeSent, setCodeSent] = useState(false);
|
|
201
|
+
const [countdown, setCountdown] = useState(0);
|
|
202
|
+
|
|
203
|
+
// 验证码倒计时
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (countdown > 0) {
|
|
206
|
+
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
|
207
|
+
return () => clearTimeout(timer);
|
|
208
|
+
}
|
|
209
|
+
}, [countdown]);
|
|
210
|
+
|
|
211
|
+
const sendCode = async () => {
|
|
212
|
+
if (!phone || phone.length !== 11) {
|
|
213
|
+
setError('请输入有效的手机号');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setLoading(true);
|
|
218
|
+
setError(null);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const response = await fetch(`${apiBasePath}/base/sms/send-code`, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { 'Content-Type': 'application/json' },
|
|
224
|
+
body: JSON.stringify({ phone, action: 'login' }),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const data = await response.json();
|
|
228
|
+
|
|
229
|
+
if (data.code === '000000') {
|
|
230
|
+
setCodeSent(true);
|
|
231
|
+
setCountdown(60);
|
|
232
|
+
// 保存手机号
|
|
233
|
+
localStorage.setItem(STORAGE_KEYS.lastPhone, phone);
|
|
234
|
+
} else {
|
|
235
|
+
setError(data.message || '发送验证码失败');
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
setError('网络错误,请稍后重试');
|
|
239
|
+
} finally {
|
|
240
|
+
setLoading(false);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleLogin = async (e: React.FormEvent) => {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
|
|
247
|
+
if (!phone || !code) {
|
|
248
|
+
setError('请填写手机号和验证码');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
setLoading(true);
|
|
253
|
+
setError(null);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(`${apiBasePath}/base/auth/login`, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
method: 'phone_sms',
|
|
261
|
+
phone,
|
|
262
|
+
code,
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const data = await response.json();
|
|
267
|
+
|
|
268
|
+
if (data.code === '000000' && data.data) {
|
|
269
|
+
const loginData = data.data;
|
|
270
|
+
|
|
271
|
+
// 保存 token
|
|
272
|
+
localStorage.setItem(STORAGE_KEYS.accessToken, loginData.accessToken);
|
|
273
|
+
localStorage.setItem(STORAGE_KEYS.refreshToken, loginData.refreshToken);
|
|
274
|
+
localStorage.setItem(STORAGE_KEYS.lastPhone, phone);
|
|
275
|
+
|
|
276
|
+
// 构建用户信息
|
|
277
|
+
const userInfo: UserInfo = {
|
|
278
|
+
userId: loginData.userId,
|
|
279
|
+
nickname: loginData.nickname || phone,
|
|
280
|
+
avatar: loginData.avatar,
|
|
281
|
+
phone,
|
|
282
|
+
};
|
|
283
|
+
localStorage.setItem(STORAGE_KEYS.userInfo, JSON.stringify(userInfo));
|
|
284
|
+
|
|
285
|
+
onSuccess(userInfo);
|
|
286
|
+
} else {
|
|
287
|
+
setError(data.message || '登录失败');
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
setError('网络错误,请稍后重试');
|
|
291
|
+
} finally {
|
|
292
|
+
setLoading(false);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div style={modalStyles.overlay as React.CSSProperties} onClick={onClose}>
|
|
298
|
+
<div
|
|
299
|
+
style={modalStyles.modal}
|
|
300
|
+
onClick={(e) => e.stopPropagation()}
|
|
301
|
+
>
|
|
302
|
+
<div style={modalStyles.header}>
|
|
303
|
+
<h3 style={modalStyles.title}>开发登录</h3>
|
|
304
|
+
<button style={modalStyles.closeButton} onClick={onClose}>
|
|
305
|
+
×
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<form style={modalStyles.form} onSubmit={handleLogin}>
|
|
310
|
+
<div style={formGroupStyles.group}>
|
|
311
|
+
<label style={formGroupStyles.label}>手机号</label>
|
|
312
|
+
<input
|
|
313
|
+
type="tel"
|
|
314
|
+
style={formGroupStyles.input}
|
|
315
|
+
value={phone}
|
|
316
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
317
|
+
placeholder="请输入手机号"
|
|
318
|
+
maxLength={11}
|
|
319
|
+
autoComplete="tel"
|
|
320
|
+
/>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div style={formGroupStyles.group}>
|
|
324
|
+
<label style={formGroupStyles.label}>验证码</label>
|
|
325
|
+
<div style={formGroupStyles.inputRow}>
|
|
326
|
+
<input
|
|
327
|
+
type="text"
|
|
328
|
+
style={{ ...formGroupStyles.input, flex: 1 }}
|
|
329
|
+
value={code}
|
|
330
|
+
onChange={(e) => setCode(e.target.value)}
|
|
331
|
+
placeholder="验证码"
|
|
332
|
+
maxLength={6}
|
|
333
|
+
/>
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
style={{
|
|
337
|
+
...formGroupStyles.sendButton,
|
|
338
|
+
...(loading || countdown > 0 ? formGroupStyles.sendButtonDisabled : {}),
|
|
339
|
+
}}
|
|
340
|
+
onClick={sendCode}
|
|
341
|
+
disabled={loading || countdown > 0}
|
|
342
|
+
>
|
|
343
|
+
{countdown > 0 ? `${countdown}s` : codeSent ? '重新发送' : '发送验证码'}
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{error && <div style={modalStyles.error}>{error}</div>}
|
|
349
|
+
|
|
350
|
+
<div style={modalStyles.tip}>
|
|
351
|
+
💡 开发环境验证码默认为 <code style={modalStyles.tipCode}>1024</code>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<button
|
|
355
|
+
type="submit"
|
|
356
|
+
style={{
|
|
357
|
+
...modalStyles.submitButton,
|
|
358
|
+
...(loading ? modalStyles.submitButtonDisabled : {}),
|
|
359
|
+
}}
|
|
360
|
+
disabled={loading}
|
|
361
|
+
>
|
|
362
|
+
{loading ? '登录中...' : '登录'}
|
|
363
|
+
</button>
|
|
364
|
+
</form>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
DevUserMenu.displayName = 'DevUserMenu';
|
|
371
|
+
|
|
372
|
+
export default DevUserMenu;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT 生成工具(浏览器端实现)
|
|
3
|
+
* 使用 Web Crypto API 实现 HS256 签名
|
|
4
|
+
*
|
|
5
|
+
* JWT Payload 字段说明(与 portal-backend auth.service.ts 保持一致):
|
|
6
|
+
* - id: 用户ID(JwtStrategy 优先从此字段提取 userId)
|
|
7
|
+
* - sub: 用户ID(标准 JWT 字段,作为 id 的备选)
|
|
8
|
+
* - aud: 受众,固定为 'internal'
|
|
9
|
+
* - nickname: 用户昵称
|
|
10
|
+
* - avatar: 用户头像 URL
|
|
11
|
+
* - phone: 手机号
|
|
12
|
+
* - orgId: 当前组织 ID(可选)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface JwtPayload {
|
|
16
|
+
id: string;
|
|
17
|
+
sub: string;
|
|
18
|
+
aud: string;
|
|
19
|
+
nickname: string;
|
|
20
|
+
avatar?: string;
|
|
21
|
+
phone: string;
|
|
22
|
+
orgId?: string;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Base64Url 编码(支持 Unicode 字符)
|
|
28
|
+
*/
|
|
29
|
+
function base64UrlEncode(str: string): string {
|
|
30
|
+
// 使用 TextEncoder 将字符串转换为 UTF-8 字节数组
|
|
31
|
+
const encoder = new TextEncoder();
|
|
32
|
+
const bytes = encoder.encode(str);
|
|
33
|
+
|
|
34
|
+
// 将字节数组转换为二进制字符串
|
|
35
|
+
const binary = String.fromCharCode(...bytes);
|
|
36
|
+
|
|
37
|
+
// Base64 编码
|
|
38
|
+
const base64 = btoa(binary);
|
|
39
|
+
|
|
40
|
+
// 转换为 Base64Url 格式
|
|
41
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* ArrayBuffer 转 Base64Url
|
|
46
|
+
* 注意:不能调用 base64UrlEncode,因为那个函数会用 TextEncoder 重新编码
|
|
47
|
+
*/
|
|
48
|
+
function arrayBufferToBase64Url(buffer: ArrayBuffer): string {
|
|
49
|
+
const bytes = new Uint8Array(buffer);
|
|
50
|
+
const binary = String.fromCharCode(...bytes);
|
|
51
|
+
|
|
52
|
+
// 直接进行 Base64 编码,不使用 TextEncoder
|
|
53
|
+
const base64 = btoa(binary);
|
|
54
|
+
|
|
55
|
+
// 转换为 Base64Url 格式
|
|
56
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 使用 HS256 算法签名 JWT
|
|
61
|
+
*/
|
|
62
|
+
export async function signJwtHS256(
|
|
63
|
+
payload: JwtPayload,
|
|
64
|
+
secret: string,
|
|
65
|
+
expiresIn: number = 86400, // 默认 1 天(秒)
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
// 1. JWT Header
|
|
68
|
+
const header = {
|
|
69
|
+
alg: 'HS256',
|
|
70
|
+
typ: 'JWT',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// 2. 添加过期时间到 payload
|
|
74
|
+
const now = Math.floor(Date.now() / 1000);
|
|
75
|
+
const fullPayload = {
|
|
76
|
+
...payload,
|
|
77
|
+
iat: now,
|
|
78
|
+
exp: now + expiresIn,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 3. 编码 header 和 payload
|
|
82
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
83
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));
|
|
84
|
+
const message = `${encodedHeader}.${encodedPayload}`;
|
|
85
|
+
|
|
86
|
+
// 4. 使用 Web Crypto API 进行 HMAC-SHA256 签名
|
|
87
|
+
const encoder = new TextEncoder();
|
|
88
|
+
const keyData = encoder.encode(secret);
|
|
89
|
+
const messageData = encoder.encode(message);
|
|
90
|
+
|
|
91
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
92
|
+
'raw',
|
|
93
|
+
keyData,
|
|
94
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
95
|
+
false,
|
|
96
|
+
['sign'],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
|
|
100
|
+
|
|
101
|
+
// 5. 组装完整的 JWT
|
|
102
|
+
const encodedSignature = arrayBufferToBase64Url(signature);
|
|
103
|
+
return `${message}.${encodedSignature}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 获取默认的 JWT Payload
|
|
108
|
+
* 格式与 portal-backend auth.service.ts 中签发的内部 JWT 保持一致
|
|
109
|
+
*/
|
|
110
|
+
export function getDefaultJwtPayload(): JwtPayload {
|
|
111
|
+
const userId = '1000000001';
|
|
112
|
+
return {
|
|
113
|
+
id: userId,
|
|
114
|
+
sub: userId,
|
|
115
|
+
aud: 'internal',
|
|
116
|
+
nickname: '开发用户',
|
|
117
|
+
avatar: '',
|
|
118
|
+
phone: '13800138000',
|
|
119
|
+
orgId: '',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
export const devUserMenuStyles: Record<string, CSSProperties> = {
|
|
4
|
+
container: {
|
|
5
|
+
position: 'relative',
|
|
6
|
+
display: 'flex',
|
|
7
|
+
alignItems: 'center',
|
|
8
|
+
gap: 10,
|
|
9
|
+
},
|
|
10
|
+
switchOrgButton: {
|
|
11
|
+
height: 32,
|
|
12
|
+
padding: '0 14px',
|
|
13
|
+
border: '1px solid #d8e0eb',
|
|
14
|
+
borderRadius: 16,
|
|
15
|
+
background: '#ffffff',
|
|
16
|
+
color: '#10233d',
|
|
17
|
+
fontSize: 13,
|
|
18
|
+
fontWeight: 500,
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
transition: 'border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease',
|
|
21
|
+
},
|
|
22
|
+
avatar: {
|
|
23
|
+
width: 32,
|
|
24
|
+
height: 32,
|
|
25
|
+
borderRadius: '50%',
|
|
26
|
+
background: '#1890ff',
|
|
27
|
+
color: '#fff',
|
|
28
|
+
border: 'none',
|
|
29
|
+
cursor: 'pointer',
|
|
30
|
+
display: 'flex',
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
justifyContent: 'center',
|
|
33
|
+
fontSize: 14,
|
|
34
|
+
fontWeight: 500,
|
|
35
|
+
transition: 'opacity 0.2s',
|
|
36
|
+
},
|
|
37
|
+
avatarImage: {
|
|
38
|
+
width: '100%',
|
|
39
|
+
height: '100%',
|
|
40
|
+
borderRadius: '50%',
|
|
41
|
+
objectFit: 'cover',
|
|
42
|
+
},
|
|
43
|
+
loginButton: {
|
|
44
|
+
padding: '6px 16px',
|
|
45
|
+
background: '#1890ff',
|
|
46
|
+
color: '#fff',
|
|
47
|
+
border: 'none',
|
|
48
|
+
borderRadius: 4,
|
|
49
|
+
cursor: 'pointer',
|
|
50
|
+
fontSize: 14,
|
|
51
|
+
transition: 'background 0.2s',
|
|
52
|
+
},
|
|
53
|
+
dropdown: {
|
|
54
|
+
position: 'absolute',
|
|
55
|
+
top: '100%',
|
|
56
|
+
right: 0,
|
|
57
|
+
marginTop: 8,
|
|
58
|
+
minWidth: 180,
|
|
59
|
+
background: '#fff',
|
|
60
|
+
borderRadius: 6,
|
|
61
|
+
boxShadow: '0 3px 12px rgba(0, 0, 0, 0.15)',
|
|
62
|
+
zIndex: 1000,
|
|
63
|
+
overflow: 'hidden',
|
|
64
|
+
},
|
|
65
|
+
dropdownHeader: {
|
|
66
|
+
padding: '12px 16px',
|
|
67
|
+
display: 'flex',
|
|
68
|
+
flexDirection: 'column',
|
|
69
|
+
gap: 4,
|
|
70
|
+
},
|
|
71
|
+
dropdownNickname: {
|
|
72
|
+
fontSize: 14,
|
|
73
|
+
fontWeight: 500,
|
|
74
|
+
color: '#333',
|
|
75
|
+
},
|
|
76
|
+
dropdownPhone: {
|
|
77
|
+
fontSize: 12,
|
|
78
|
+
color: '#999',
|
|
79
|
+
},
|
|
80
|
+
dropdownDivider: {
|
|
81
|
+
height: 1,
|
|
82
|
+
background: '#f0f0f0',
|
|
83
|
+
margin: '0 8px',
|
|
84
|
+
},
|
|
85
|
+
dropdownItem: {
|
|
86
|
+
display: 'block',
|
|
87
|
+
width: '100%',
|
|
88
|
+
padding: '10px 16px',
|
|
89
|
+
textAlign: 'left',
|
|
90
|
+
background: 'none',
|
|
91
|
+
border: 'none',
|
|
92
|
+
fontSize: 14,
|
|
93
|
+
color: '#333',
|
|
94
|
+
cursor: 'pointer',
|
|
95
|
+
transition: 'background 0.2s',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const modalStyles: Record<string, CSSProperties> = {
|
|
100
|
+
overlay: {
|
|
101
|
+
position: 'fixed',
|
|
102
|
+
top: 0,
|
|
103
|
+
left: 0,
|
|
104
|
+
right: 0,
|
|
105
|
+
bottom: 0,
|
|
106
|
+
background: 'rgba(0, 0, 0, 0.45)',
|
|
107
|
+
display: 'flex',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
justifyContent: 'center',
|
|
110
|
+
zIndex: 10000,
|
|
111
|
+
},
|
|
112
|
+
modal: {
|
|
113
|
+
width: 360,
|
|
114
|
+
background: '#fff',
|
|
115
|
+
borderRadius: 8,
|
|
116
|
+
boxShadow: '0 6px 24px rgba(0, 0, 0, 0.2)',
|
|
117
|
+
},
|
|
118
|
+
header: {
|
|
119
|
+
display: 'flex',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
justifyContent: 'space-between',
|
|
122
|
+
padding: '16px 20px',
|
|
123
|
+
borderBottom: '1px solid #f0f0f0',
|
|
124
|
+
},
|
|
125
|
+
title: {
|
|
126
|
+
margin: 0,
|
|
127
|
+
fontSize: 16,
|
|
128
|
+
fontWeight: 500,
|
|
129
|
+
color: '#333',
|
|
130
|
+
},
|
|
131
|
+
closeButton: {
|
|
132
|
+
width: 24,
|
|
133
|
+
height: 24,
|
|
134
|
+
background: 'none',
|
|
135
|
+
border: 'none',
|
|
136
|
+
fontSize: 20,
|
|
137
|
+
color: '#999',
|
|
138
|
+
cursor: 'pointer',
|
|
139
|
+
display: 'flex',
|
|
140
|
+
alignItems: 'center',
|
|
141
|
+
justifyContent: 'center',
|
|
142
|
+
borderRadius: 4,
|
|
143
|
+
transition: 'background 0.2s',
|
|
144
|
+
},
|
|
145
|
+
form: {
|
|
146
|
+
padding: 20,
|
|
147
|
+
},
|
|
148
|
+
tip: {
|
|
149
|
+
margin: '12px 0',
|
|
150
|
+
padding: '8px 12px',
|
|
151
|
+
background: '#fffbe6',
|
|
152
|
+
border: '1px solid #ffe58f',
|
|
153
|
+
borderRadius: 4,
|
|
154
|
+
fontSize: 12,
|
|
155
|
+
color: '#8c6d1f',
|
|
156
|
+
},
|
|
157
|
+
tipCode: {
|
|
158
|
+
padding: '2px 6px',
|
|
159
|
+
background: 'rgba(0, 0, 0, 0.06)',
|
|
160
|
+
borderRadius: 3,
|
|
161
|
+
fontFamily: 'monospace',
|
|
162
|
+
},
|
|
163
|
+
submitButton: {
|
|
164
|
+
width: '100%',
|
|
165
|
+
padding: 10,
|
|
166
|
+
background: '#1890ff',
|
|
167
|
+
color: '#fff',
|
|
168
|
+
border: 'none',
|
|
169
|
+
borderRadius: 4,
|
|
170
|
+
fontSize: 14,
|
|
171
|
+
cursor: 'pointer',
|
|
172
|
+
transition: 'background 0.2s',
|
|
173
|
+
},
|
|
174
|
+
submitButtonDisabled: {
|
|
175
|
+
opacity: 0.65,
|
|
176
|
+
cursor: 'not-allowed',
|
|
177
|
+
},
|
|
178
|
+
error: {
|
|
179
|
+
marginBottom: 12,
|
|
180
|
+
padding: '8px 12px',
|
|
181
|
+
background: '#fff2f0',
|
|
182
|
+
border: '1px solid #ffccc7',
|
|
183
|
+
borderRadius: 4,
|
|
184
|
+
fontSize: 13,
|
|
185
|
+
color: '#cf1322',
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const formGroupStyles: Record<string, CSSProperties> = {
|
|
190
|
+
group: {
|
|
191
|
+
marginBottom: 16,
|
|
192
|
+
},
|
|
193
|
+
label: {
|
|
194
|
+
display: 'block',
|
|
195
|
+
marginBottom: 6,
|
|
196
|
+
fontSize: 14,
|
|
197
|
+
color: '#333',
|
|
198
|
+
},
|
|
199
|
+
input: {
|
|
200
|
+
width: '100%',
|
|
201
|
+
padding: '8px 12px',
|
|
202
|
+
border: '1px solid #d9d9d9',
|
|
203
|
+
borderRadius: 4,
|
|
204
|
+
fontSize: 14,
|
|
205
|
+
outline: 'none',
|
|
206
|
+
transition: 'border-color 0.2s',
|
|
207
|
+
boxSizing: 'border-box',
|
|
208
|
+
},
|
|
209
|
+
inputRow: {
|
|
210
|
+
display: 'flex',
|
|
211
|
+
gap: 8,
|
|
212
|
+
},
|
|
213
|
+
sendButton: {
|
|
214
|
+
flexShrink: 0,
|
|
215
|
+
padding: '8px 12px',
|
|
216
|
+
background: '#fff',
|
|
217
|
+
border: '1px solid #d9d9d9',
|
|
218
|
+
borderRadius: 4,
|
|
219
|
+
fontSize: 14,
|
|
220
|
+
color: '#333',
|
|
221
|
+
cursor: 'pointer',
|
|
222
|
+
whiteSpace: 'nowrap',
|
|
223
|
+
transition: 'border-color 0.2s, color 0.2s',
|
|
224
|
+
},
|
|
225
|
+
sendButtonDisabled: {
|
|
226
|
+
color: '#bfbfbf',
|
|
227
|
+
cursor: 'not-allowed',
|
|
228
|
+
},
|
|
229
|
+
};
|