@zhang_libo/resource-hub 1.0.2

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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +80 -0
  3. package/README.ja.md +80 -0
  4. package/README.md +79 -0
  5. package/README.zh-TW.md +80 -0
  6. package/bin/cli.js +10 -0
  7. package/dist/app.d.ts +2 -0
  8. package/dist/app.d.ts.map +1 -0
  9. package/dist/app.js +59 -0
  10. package/dist/app.js.map +1 -0
  11. package/dist/db/index.js +12 -0
  12. package/dist/db/index.js.map +1 -0
  13. package/dist/db/migrate.d.ts +3 -0
  14. package/dist/db/migrate.d.ts.map +1 -0
  15. package/dist/db/migrate.js +169 -0
  16. package/dist/db/migrate.js.map +1 -0
  17. package/dist/db/schema.d.ts +743 -0
  18. package/dist/db/schema.d.ts.map +1 -0
  19. package/dist/db/schema.js +88 -0
  20. package/dist/db/schema.js.map +1 -0
  21. package/dist/i18n.js +309 -0
  22. package/dist/i18n.js.map +1 -0
  23. package/dist/plugins/admin.d.ts +4 -0
  24. package/dist/plugins/admin.d.ts.map +1 -0
  25. package/dist/plugins/admin.js +19 -0
  26. package/dist/plugins/admin.js.map +1 -0
  27. package/dist/plugins/auth.d.ts +4 -0
  28. package/dist/plugins/auth.d.ts.map +1 -0
  29. package/dist/plugins/auth.js +35 -0
  30. package/dist/plugins/auth.js.map +1 -0
  31. package/dist/routes/auth.d.ts +4 -0
  32. package/dist/routes/auth.d.ts.map +1 -0
  33. package/dist/routes/auth.js +352 -0
  34. package/dist/routes/auth.js.map +1 -0
  35. package/dist/routes/categories.d.ts +4 -0
  36. package/dist/routes/categories.d.ts.map +1 -0
  37. package/dist/routes/categories.js +112 -0
  38. package/dist/routes/categories.js.map +1 -0
  39. package/dist/routes/config.d.ts +4 -0
  40. package/dist/routes/config.d.ts.map +1 -0
  41. package/dist/routes/config.js +227 -0
  42. package/dist/routes/config.js.map +1 -0
  43. package/dist/routes/resources.d.ts +4 -0
  44. package/dist/routes/resources.d.ts.map +1 -0
  45. package/dist/routes/resources.js +474 -0
  46. package/dist/routes/resources.js.map +1 -0
  47. package/dist/routes/tags.d.ts +4 -0
  48. package/dist/routes/tags.d.ts.map +1 -0
  49. package/dist/routes/tags.js +37 -0
  50. package/dist/routes/tags.js.map +1 -0
  51. package/dist/routes/users.d.ts +4 -0
  52. package/dist/routes/users.d.ts.map +1 -0
  53. package/dist/routes/users.js +181 -0
  54. package/dist/routes/users.js.map +1 -0
  55. package/dist/services/crypto.js +49 -0
  56. package/dist/services/crypto.js.map +1 -0
  57. package/dist/services/mail.d.ts +16 -0
  58. package/dist/services/mail.d.ts.map +1 -0
  59. package/dist/services/mail.js +33 -0
  60. package/dist/services/mail.js.map +1 -0
  61. package/dist/services/rsa.js +49 -0
  62. package/dist/services/rsa.js.map +1 -0
  63. package/dist/services/token.d.ts +9 -0
  64. package/dist/services/token.d.ts.map +1 -0
  65. package/dist/services/token.js +29 -0
  66. package/dist/services/token.js.map +1 -0
  67. package/dist/types.d.ts +80 -0
  68. package/dist/types.d.ts.map +1 -0
  69. package/dist/types.js +2 -0
  70. package/dist/types.js.map +1 -0
  71. package/package.json +73 -0
  72. package/public/admin/AdminCategories.jsx +310 -0
  73. package/public/admin/AdminConfig.jsx +254 -0
  74. package/public/admin/AdminEmail.jsx +279 -0
  75. package/public/admin/AdminTags.jsx +263 -0
  76. package/public/admin/AdminUsers.jsx +452 -0
  77. package/public/app.jsx +186 -0
  78. package/public/components/ConfirmDialog.jsx +78 -0
  79. package/public/components/DropdownSelect.jsx +281 -0
  80. package/public/components/EmailPreviewModal.jsx +104 -0
  81. package/public/components/EmptyState.jsx +50 -0
  82. package/public/components/Modal.jsx +127 -0
  83. package/public/components/PasswordStrength.jsx +45 -0
  84. package/public/components/Skeleton.jsx +68 -0
  85. package/public/components/Toast.jsx +80 -0
  86. package/public/components/TooltipIconButton.jsx +55 -0
  87. package/public/context/AppContext.jsx +314 -0
  88. package/public/features/BatchResourceModal.jsx +606 -0
  89. package/public/features/ChangePasswordModal.jsx +187 -0
  90. package/public/features/ProfileModal.jsx +170 -0
  91. package/public/features/ResourceCard.jsx +422 -0
  92. package/public/features/ResourceFormModal.jsx +915 -0
  93. package/public/features/ResourceRow.jsx +287 -0
  94. package/public/features/ResourceTimeline.jsx +472 -0
  95. package/public/hooks/useApi.jsx +26 -0
  96. package/public/hooks/useRouter.jsx +35 -0
  97. package/public/index.html +258 -0
  98. package/public/layout/AdminLayout.jsx +167 -0
  99. package/public/layout/AppLayout.jsx +119 -0
  100. package/public/layout/AuthLayout.jsx +503 -0
  101. package/public/layout/Header.jsx +543 -0
  102. package/public/layout/Sidebar.jsx +175 -0
  103. package/public/pages/AdminPage.jsx +30 -0
  104. package/public/pages/ForgotPasswordPage.jsx +93 -0
  105. package/public/pages/HomePage.jsx +2297 -0
  106. package/public/pages/LoginPage.jsx +191 -0
  107. package/public/pages/RegisterPage.jsx +137 -0
  108. package/public/pages/ResetPasswordPage.jsx +169 -0
  109. package/public/pages/SetupPage.jsx +157 -0
  110. package/public/utils/helpers.jsx +152 -0
  111. package/public/utils/i18n.jsx +1374 -0
  112. package/public/utils/preferences.jsx +220 -0
  113. package/public/utils/security.jsx +88 -0
  114. package/public/utils/theme.jsx +24 -0
  115. package/public/vendor/babel.min.js +2 -0
  116. package/public/vendor/lucide-react.min.js +9 -0
  117. package/public/vendor/react-dom.development.js +29869 -0
  118. package/public/vendor/react.development.js +3342 -0
@@ -0,0 +1,78 @@
1
+ // ConfirmDialog.jsx
2
+ function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmText = '确认删除', loading = false }) {
3
+ const { AlertTriangle, Loader } = lucide;
4
+ if (!isOpen) return null;
5
+
6
+ return React.createElement('div', {
7
+ style: {
8
+ position: 'fixed', inset: 0,
9
+ background: 'rgba(19,34,56,0.28)',
10
+ zIndex: 1001,
11
+ display: 'flex',
12
+ alignItems: 'center',
13
+ justifyContent: 'center',
14
+ padding: '16px',
15
+ }
16
+ },
17
+ React.createElement('div', {
18
+ style: {
19
+ width: '400px', maxWidth: '100%',
20
+ background: 'var(--surface-elevated)',
21
+ border: '1px solid var(--border)',
22
+ borderRadius: '16px',
23
+ boxShadow: 'var(--shadow-modal)',
24
+ padding: '24px',
25
+ }
26
+ },
27
+ React.createElement('div', { style: { display: 'flex', gap: '12px', marginBottom: '16px' } },
28
+ React.createElement('div', {
29
+ style: {
30
+ width: '40px', height: '40px', borderRadius: '50%',
31
+ background: 'color-mix(in srgb, var(--danger) 10%, var(--surface-elevated))',
32
+ display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
33
+ }
34
+ }, React.createElement(AlertTriangle, { size: 20, style: { color: 'var(--danger)' } })),
35
+ React.createElement('div', null,
36
+ React.createElement('h3', {
37
+ style: { fontSize: '15px', fontWeight: 600, color: 'var(--text-primary)', margin: '0 0 4px' }
38
+ }, title),
39
+ React.createElement('p', {
40
+ style: { fontSize: '14px', color: 'var(--text-secondary)', margin: 0, lineHeight: '1.5' }
41
+ }, message)
42
+ )
43
+ ),
44
+ React.createElement('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' } },
45
+ React.createElement('button', {
46
+ onClick: onClose,
47
+ disabled: loading,
48
+ style: {
49
+ padding: '8px 16px', border: '1px solid var(--border)',
50
+ background: 'var(--surface-elevated)', color: 'var(--text-primary)',
51
+ borderRadius: '10px', cursor: 'pointer', fontSize: '14px',
52
+ boxShadow: 'var(--shadow-control)',
53
+ }
54
+ }, '取消'),
55
+ React.createElement('button', {
56
+ onClick: onConfirm,
57
+ disabled: loading,
58
+ style: {
59
+ padding: '8px 16px', border: 'none',
60
+ background: 'var(--danger)', color: '#fff',
61
+ borderRadius: '10px', cursor: loading ? 'not-allowed' : 'pointer',
62
+ fontSize: '14px', display: 'flex', alignItems: 'center', gap: '6px',
63
+ opacity: loading ? 0.7 : 1,
64
+ boxShadow: '0 10px 20px color-mix(in srgb, var(--danger) 16%, transparent)',
65
+ }
66
+ },
67
+ loading && React.createElement(Loader, {
68
+ size: 14,
69
+ style: { animation: 'spin 1s linear infinite' },
70
+ }),
71
+ confirmText
72
+ )
73
+ )
74
+ )
75
+ );
76
+ }
77
+
78
+ window.ConfirmDialog = ConfirmDialog;
@@ -0,0 +1,281 @@
1
+ function DropdownSelect({
2
+ value,
3
+ options,
4
+ onChange,
5
+ disabled = false,
6
+ placeholder = '请选择',
7
+ ariaLabel = '下拉选择',
8
+ variant = 'field',
9
+ align = 'left',
10
+ width = null,
11
+ minWidth = null,
12
+ maxWidth = null,
13
+ triggerProps = {},
14
+ menuProps = {},
15
+ renderValue = null,
16
+ }) {
17
+ const { ChevronDown, Check } = lucide;
18
+ const [open, setOpen] = React.useState(false);
19
+ const [highlighted, setHighlighted] = React.useState(value);
20
+ const containerRef = React.useRef(null);
21
+ const triggerRef = React.useRef(null);
22
+ const optionRefs = React.useRef({});
23
+
24
+ const normalizedOptions = Array.isArray(options) ? options : [];
25
+ const selectedOption = normalizedOptions.find((option) => option.value === value) || null;
26
+
27
+ React.useEffect(() => {
28
+ setHighlighted(value);
29
+ }, [value]);
30
+
31
+ React.useEffect(() => {
32
+ if (!open || disabled) return undefined;
33
+
34
+ const currentIndex = () => {
35
+ const index = normalizedOptions.findIndex((option) => option.value === highlighted);
36
+ return index >= 0 ? index : Math.max(normalizedOptions.findIndex((option) => option.value === value), 0);
37
+ };
38
+
39
+ const moveHighlight = (direction) => {
40
+ if (normalizedOptions.length === 0) return;
41
+ const nextIndex = (currentIndex() + direction + normalizedOptions.length) % normalizedOptions.length;
42
+ setHighlighted(normalizedOptions[nextIndex].value);
43
+ };
44
+
45
+ const handleMouseDown = (event) => {
46
+ if (containerRef.current?.contains(event.target)) return;
47
+ setOpen(false);
48
+ };
49
+
50
+ const handleKeyDown = (event) => {
51
+ if (event.key === 'Escape') {
52
+ event.preventDefault();
53
+ setOpen(false);
54
+ triggerRef.current?.focus();
55
+ return;
56
+ }
57
+ if (event.key === 'ArrowDown') {
58
+ event.preventDefault();
59
+ moveHighlight(1);
60
+ return;
61
+ }
62
+ if (event.key === 'ArrowUp') {
63
+ event.preventDefault();
64
+ moveHighlight(-1);
65
+ return;
66
+ }
67
+ if (event.key === 'Enter' || event.key === ' ') {
68
+ event.preventDefault();
69
+ const nextOption = normalizedOptions.find((option) => option.value === highlighted);
70
+ if (nextOption) {
71
+ onChange(nextOption.value);
72
+ setOpen(false);
73
+ triggerRef.current?.focus();
74
+ }
75
+ }
76
+ };
77
+
78
+ document.addEventListener('mousedown', handleMouseDown);
79
+ document.addEventListener('keydown', handleKeyDown);
80
+ const frame = window.requestAnimationFrame(() => {
81
+ const targetValue = highlighted ?? value ?? normalizedOptions[0]?.value;
82
+ optionRefs.current[targetValue]?.focus();
83
+ });
84
+
85
+ return () => {
86
+ document.removeEventListener('mousedown', handleMouseDown);
87
+ document.removeEventListener('keydown', handleKeyDown);
88
+ window.cancelAnimationFrame(frame);
89
+ };
90
+ }, [disabled, highlighted, normalizedOptions, onChange, open, value]);
91
+
92
+ const triggerLabel = renderValue
93
+ ? renderValue(selectedOption)
94
+ : (selectedOption ? selectedOption.label : placeholder);
95
+ const { style: triggerStyleOverride, ...triggerPropsRest } = triggerProps || {};
96
+ const { style: menuStyleOverride, ...menuPropsRest } = menuProps || {};
97
+
98
+ const triggerBaseStyle = variant === 'pill'
99
+ ? {
100
+ display: 'inline-flex',
101
+ alignItems: 'center',
102
+ justifyContent: 'space-between',
103
+ gap: '8px',
104
+ minHeight: '36px',
105
+ padding: '0 14px',
106
+ borderRadius: '999px',
107
+ border: open
108
+ ? '1px solid var(--brand)'
109
+ : '1px solid var(--control-border)',
110
+ background: open
111
+ ? 'color-mix(in srgb, var(--brand-soft) 82%, var(--control-bg))'
112
+ : 'color-mix(in srgb, var(--control-bg) 94%, var(--control-bg-muted))',
113
+ color: open ? 'var(--brand-strong)' : 'var(--text-primary)',
114
+ fontSize: '13px',
115
+ fontWeight: open ? 700 : 600,
116
+ boxShadow: open
117
+ ? '0 0 0 1px color-mix(in srgb, var(--brand) 14%, transparent), var(--shadow-control-hover)'
118
+ : 'var(--shadow-control)',
119
+ }
120
+ : {
121
+ display: 'inline-flex',
122
+ alignItems: 'center',
123
+ justifyContent: 'space-between',
124
+ gap: '10px',
125
+ minHeight: '40px',
126
+ width: width || '100%',
127
+ padding: '0 12px',
128
+ borderRadius: '12px',
129
+ border: open
130
+ ? '1px solid var(--brand)'
131
+ : '1px solid var(--control-border)',
132
+ background: 'color-mix(in srgb, var(--surface-elevated) 96%, var(--control-bg-muted))',
133
+ color: 'var(--text-primary)',
134
+ fontSize: '14px',
135
+ fontWeight: 500,
136
+ boxShadow: open
137
+ ? '0 0 0 3px color-mix(in srgb, var(--brand) 14%, transparent), var(--shadow-control-hover)'
138
+ : 'var(--shadow-control)',
139
+ };
140
+
141
+ const menuWidth = width || (variant === 'field' ? '100%' : minWidth || '168px');
142
+
143
+ return (
144
+ <div
145
+ ref={containerRef}
146
+ style={{
147
+ position: 'relative',
148
+ display: width || variant === 'field' ? 'block' : 'inline-flex',
149
+ width: width || null,
150
+ minWidth,
151
+ maxWidth,
152
+ }}
153
+ >
154
+ <button
155
+ ref={triggerRef}
156
+ type="button"
157
+ aria-haspopup="listbox"
158
+ aria-expanded={!disabled && open}
159
+ aria-label={ariaLabel}
160
+ disabled={disabled}
161
+ onClick={() => {
162
+ if (disabled) return;
163
+ setOpen((current) => !current);
164
+ }}
165
+ onKeyDown={(event) => {
166
+ if (disabled) return;
167
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
168
+ event.preventDefault();
169
+ setHighlighted(value ?? normalizedOptions[0]?.value);
170
+ setOpen(true);
171
+ }
172
+ }}
173
+ style={{
174
+ ...triggerBaseStyle,
175
+ width: triggerBaseStyle.width || width || null,
176
+ opacity: disabled ? 0.65 : 1,
177
+ cursor: disabled ? 'not-allowed' : 'pointer',
178
+ transition: 'border-color 150ms, background 150ms, color 150ms, box-shadow 150ms',
179
+ outline: 'none',
180
+ appearance: 'none',
181
+ WebkitAppearance: 'none',
182
+ ...(triggerStyleOverride || {}),
183
+ }}
184
+ {...triggerPropsRest}
185
+ >
186
+ <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'left' }}>
187
+ {triggerLabel}
188
+ </span>
189
+ <ChevronDown
190
+ size={14}
191
+ style={{
192
+ color: disabled ? 'var(--text-secondary)' : open ? 'var(--brand-strong)' : 'var(--text-secondary)',
193
+ transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
194
+ transition: 'transform 150ms, color 150ms',
195
+ flexShrink: 0,
196
+ }}
197
+ />
198
+ </button>
199
+
200
+ {!disabled && open && (
201
+ <div
202
+ role="listbox"
203
+ style={{
204
+ position: 'absolute',
205
+ top: 'calc(100% + 8px)',
206
+ left: align === 'left' ? 0 : 'auto',
207
+ right: align === 'right' ? 0 : 'auto',
208
+ width: menuWidth,
209
+ minWidth,
210
+ maxWidth,
211
+ padding: '6px',
212
+ borderRadius: '16px',
213
+ border: '1px solid var(--control-border)',
214
+ background: 'color-mix(in srgb, var(--surface-elevated) 96%, var(--control-bg-muted))',
215
+ boxShadow: 'var(--shadow-dropdown)',
216
+ display: 'grid',
217
+ gap: '4px',
218
+ zIndex: 120,
219
+ backdropFilter: 'blur(18px)',
220
+ ...(menuStyleOverride || {}),
221
+ }}
222
+ {...menuPropsRest}
223
+ >
224
+ {normalizedOptions.map((option) => {
225
+ const active = option.value === value;
226
+ const isHighlighted = option.value === highlighted;
227
+ return (
228
+ <button
229
+ key={option.value}
230
+ ref={(node) => {
231
+ if (node) optionRefs.current[option.value] = node;
232
+ else delete optionRefs.current[option.value];
233
+ }}
234
+ type="button"
235
+ role="option"
236
+ aria-selected={active}
237
+ tabIndex={active ? 0 : -1}
238
+ onMouseEnter={() => setHighlighted(option.value)}
239
+ onFocus={() => setHighlighted(option.value)}
240
+ onClick={() => {
241
+ onChange(option.value);
242
+ setOpen(false);
243
+ triggerRef.current?.focus();
244
+ }}
245
+ {...(option.buttonProps || {})}
246
+ style={{
247
+ minHeight: '38px',
248
+ padding: '8px 12px',
249
+ borderRadius: '12px',
250
+ border: 'none',
251
+ background: active
252
+ ? 'color-mix(in srgb, var(--brand-soft) 84%, var(--control-bg))'
253
+ : isHighlighted
254
+ ? 'color-mix(in srgb, var(--surface-tint) 68%, var(--control-bg))'
255
+ : 'transparent',
256
+ color: active ? 'var(--brand-strong)' : 'var(--text-primary)',
257
+ cursor: 'pointer',
258
+ fontSize: variant === 'pill' ? '13px' : '14px',
259
+ fontWeight: active ? 700 : isHighlighted ? 600 : 500,
260
+ textAlign: 'left',
261
+ display: 'flex',
262
+ alignItems: 'center',
263
+ justifyContent: 'space-between',
264
+ gap: '12px',
265
+ transition: 'background 150ms, color 150ms',
266
+ appearance: 'none',
267
+ ...(option.buttonProps?.style || {}),
268
+ }}
269
+ >
270
+ <span>{option.label}</span>
271
+ {active && <Check size={14} />}
272
+ </button>
273
+ );
274
+ })}
275
+ </div>
276
+ )}
277
+ </div>
278
+ );
279
+ }
280
+
281
+ window.DropdownSelect = DropdownSelect;
@@ -0,0 +1,104 @@
1
+ // EmailPreviewModal.jsx
2
+ function EmailPreviewModal() {
3
+ const state = window.useAppState();
4
+ const dispatch = window.useAppDispatch();
5
+ const { Mail } = lucide;
6
+
7
+ if (!state || !state.emailPreview) return null;
8
+ const { emailPreview } = state;
9
+
10
+ const close = () => dispatch({ type: 'SET_EMAIL_PREVIEW', emailPreview: null });
11
+
12
+ return React.createElement('div', {
13
+ style: {
14
+ position: 'fixed', inset: 0,
15
+ background: 'rgba(0,0,0,0.5)',
16
+ zIndex: 1002,
17
+ display: 'flex',
18
+ alignItems: 'center',
19
+ justifyContent: 'center',
20
+ padding: '16px',
21
+ }
22
+ },
23
+ React.createElement('div', {
24
+ style: {
25
+ width: '520px', maxWidth: '100%',
26
+ maxHeight: '90vh',
27
+ background: 'var(--bg-primary)',
28
+ border: '1px solid var(--border)',
29
+ borderRadius: '12px',
30
+ boxShadow: 'var(--shadow-modal)',
31
+ overflow: 'hidden',
32
+ display: 'flex',
33
+ flexDirection: 'column',
34
+ }
35
+ },
36
+ React.createElement('div', {
37
+ style: {
38
+ padding: '20px 24px 16px',
39
+ borderBottom: '1px solid var(--border)',
40
+ display: 'flex',
41
+ alignItems: 'center',
42
+ gap: '10px',
43
+ }
44
+ },
45
+ React.createElement('div', {
46
+ style: {
47
+ width: '32px', height: '32px', borderRadius: '8px',
48
+ background: 'rgba(47,129,247,0.1)',
49
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
50
+ }
51
+ }, React.createElement(Mail, { size: 16, style: { color: 'var(--brand)' } })),
52
+ React.createElement('h2', {
53
+ style: { fontSize: '16px', fontWeight: 600, color: 'var(--text-primary)', margin: 0 }
54
+ }, '模拟邮件(开发预览)')
55
+ ),
56
+ React.createElement('div', { style: { padding: '20px 24px', overflowY: 'auto', flex: 1 } },
57
+ React.createElement('div', { style: { marginBottom: '12px' } },
58
+ React.createElement('span', { style: { fontSize: '12px', color: 'var(--text-secondary)', fontWeight: 500 } }, '收件人:'),
59
+ React.createElement('span', { style: { fontSize: '14px', color: 'var(--text-primary)' } }, emailPreview.to)
60
+ ),
61
+ React.createElement('div', { style: { marginBottom: '12px' } },
62
+ React.createElement('span', { style: { fontSize: '12px', color: 'var(--text-secondary)', fontWeight: 500 } }, '主题:'),
63
+ React.createElement('span', { style: { fontSize: '14px', color: 'var(--text-primary)' } }, emailPreview.subject)
64
+ ),
65
+ React.createElement('div', null,
66
+ React.createElement('div', { style: { fontSize: '12px', color: 'var(--text-secondary)', fontWeight: 500, marginBottom: '8px' } }, '正文:'),
67
+ React.createElement('pre', {
68
+ style: {
69
+ background: 'var(--bg-secondary)',
70
+ border: '1px solid var(--border)',
71
+ borderRadius: '8px',
72
+ padding: '12px 16px',
73
+ fontSize: '13px',
74
+ color: 'var(--text-primary)',
75
+ whiteSpace: 'pre-wrap',
76
+ wordBreak: 'break-all',
77
+ fontFamily: 'monospace',
78
+ margin: 0,
79
+ }
80
+ }, emailPreview.body)
81
+ )
82
+ ),
83
+ React.createElement('div', {
84
+ style: { padding: '16px 24px', borderTop: '1px solid var(--border)' }
85
+ },
86
+ React.createElement('button', {
87
+ onClick: close,
88
+ style: {
89
+ width: '100%', padding: '8px 16px',
90
+ border: '1px solid var(--border)',
91
+ background: 'var(--bg-secondary)',
92
+ color: 'var(--text-primary)',
93
+ borderRadius: '8px',
94
+ cursor: 'pointer',
95
+ fontSize: '14px',
96
+ fontWeight: 500,
97
+ }
98
+ }, '关闭')
99
+ )
100
+ )
101
+ );
102
+ }
103
+
104
+ window.EmailPreviewModal = EmailPreviewModal;
@@ -0,0 +1,50 @@
1
+ // EmptyState.jsx
2
+ function EmptyState({ icon, title, description, action }) {
3
+ const IconComponent = typeof icon === 'string' ? lucide[icon] : null;
4
+
5
+ return React.createElement('div', {
6
+ style: {
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ padding: '60px 24px',
12
+ textAlign: 'center',
13
+ }
14
+ },
15
+ React.createElement('div', {
16
+ style: {
17
+ width: '64px', height: '64px',
18
+ borderRadius: '16px',
19
+ background: 'var(--bg-tertiary)',
20
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
21
+ marginBottom: '16px',
22
+ }
23
+ },
24
+ typeof icon === 'string' && IconComponent
25
+ ? React.createElement(IconComponent, { size: 28, style: { color: 'var(--text-secondary)' } })
26
+ : icon
27
+ ),
28
+ React.createElement('h3', {
29
+ style: { fontSize: '15px', fontWeight: 600, color: 'var(--text-primary)', margin: '0 0 8px' }
30
+ }, title),
31
+ description && React.createElement('p', {
32
+ style: { fontSize: '14px', color: 'var(--text-secondary)', margin: '0 0 16px', maxWidth: '280px', lineHeight: '1.5' }
33
+ }, description),
34
+ action && React.createElement('button', {
35
+ onClick: action.onClick,
36
+ style: {
37
+ padding: '8px 20px',
38
+ background: 'var(--brand)',
39
+ color: '#fff',
40
+ border: 'none',
41
+ borderRadius: '8px',
42
+ cursor: 'pointer',
43
+ fontSize: '14px',
44
+ fontWeight: 500,
45
+ }
46
+ }, action.label)
47
+ );
48
+ }
49
+
50
+ window.EmptyState = EmptyState;
@@ -0,0 +1,127 @@
1
+ // Modal.jsx
2
+ function Modal({
3
+ isOpen,
4
+ onClose,
5
+ title,
6
+ children,
7
+ width = '520px',
8
+ closeOnBackdrop = true,
9
+ closeOnEscape = true,
10
+ fullScreen = false,
11
+ }) {
12
+ const { useEffect } = React;
13
+ const { X } = lucide;
14
+
15
+ useEffect(() => {
16
+ if (!isOpen) return;
17
+
18
+ const modalLockCount = window.__rhModalLockCount || 0;
19
+ if (modalLockCount === 0) {
20
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
21
+ window.__rhModalBodyLockState = {
22
+ overflow: document.body.style.overflow,
23
+ paddingRight: document.body.style.paddingRight,
24
+ };
25
+ document.body.style.overflow = 'hidden';
26
+ if (scrollbarWidth > 0) {
27
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
28
+ // 固定定位的 header 相对视口,不受 body padding 影响;滚动条消失后视口变宽会导致个人菜单右移,需同步补偿
29
+ const headerShell = document.querySelector('[data-rh-layout-header-shell]');
30
+ if (headerShell) {
31
+ window.__rhModalBodyLockState.headerPaddingRight = headerShell.style.paddingRight;
32
+ headerShell.style.paddingRight = `${scrollbarWidth}px`;
33
+ }
34
+ }
35
+ }
36
+ window.__rhModalLockCount = modalLockCount + 1;
37
+
38
+ const handler = (e) => {
39
+ if (e.key === 'Escape' && closeOnEscape) {
40
+ if (document.querySelector('[role="listbox"]')) return;
41
+ onClose();
42
+ }
43
+ };
44
+ document.addEventListener('keydown', handler);
45
+
46
+ return () => {
47
+ document.removeEventListener('keydown', handler);
48
+
49
+ const nextLockCount = Math.max((window.__rhModalLockCount || 1) - 1, 0);
50
+ window.__rhModalLockCount = nextLockCount;
51
+ if (nextLockCount === 0) {
52
+ const previousState = window.__rhModalBodyLockState || {};
53
+ document.body.style.overflow = previousState.overflow || '';
54
+ document.body.style.paddingRight = previousState.paddingRight || '';
55
+ const headerShell = document.querySelector('[data-rh-layout-header-shell]');
56
+ if (headerShell && previousState.headerPaddingRight !== undefined) {
57
+ headerShell.style.paddingRight = previousState.headerPaddingRight || '';
58
+ }
59
+ delete window.__rhModalBodyLockState;
60
+ }
61
+ };
62
+ }, [closeOnEscape, isOpen, onClose]);
63
+
64
+ if (!isOpen) return null;
65
+
66
+ return React.createElement('div', {
67
+ style: {
68
+ position: 'fixed',
69
+ inset: 0,
70
+ background: 'rgba(19,34,56,0.28)',
71
+ zIndex: 1000,
72
+ display: 'flex',
73
+ alignItems: fullScreen ? 'stretch' : 'center',
74
+ justifyContent: fullScreen ? 'stretch' : 'center',
75
+ padding: fullScreen ? 0 : '16px',
76
+ },
77
+ onClick: (e) => {
78
+ if (!closeOnBackdrop) return;
79
+ if (e.target === e.currentTarget) onClose();
80
+ }
81
+ },
82
+ React.createElement('div', {
83
+ style: {
84
+ width: fullScreen ? '100%' : width,
85
+ maxWidth: '100%',
86
+ maxHeight: fullScreen ? '100vh' : '90vh',
87
+ background: 'var(--surface-elevated)',
88
+ border: '1px solid var(--border)',
89
+ borderRadius: fullScreen ? 0 : '16px',
90
+ boxShadow: 'var(--shadow-modal)',
91
+ display: 'flex',
92
+ flexDirection: 'column',
93
+ overflow: 'hidden',
94
+ },
95
+ },
96
+ // Header
97
+ React.createElement('div', {
98
+ style: {
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ justifyContent: 'space-between',
102
+ padding: '18px 22px 14px',
103
+ borderBottom: '1px solid var(--border)',
104
+ flexShrink: 0,
105
+ }
106
+ },
107
+ React.createElement('h2', {
108
+ style: { fontSize: '16px', fontWeight: 600, color: 'var(--text-primary)', margin: 0 }
109
+ }, title),
110
+ React.createElement('button', {
111
+ onClick: onClose,
112
+ style: {
113
+ background: 'var(--surface-muted)', border: '1px solid color-mix(in srgb, var(--control-border) 72%, transparent)', cursor: 'pointer',
114
+ color: 'var(--text-secondary)', padding: '4px',
115
+ borderRadius: '10px', display: 'flex', alignItems: 'center',
116
+ }
117
+ }, React.createElement(X, { size: 18 }))
118
+ ),
119
+ // Content (scrollable)
120
+ React.createElement('div', {
121
+ style: { padding: '20px 22px 22px', overflowY: 'auto', flex: 1 }
122
+ }, children)
123
+ )
124
+ );
125
+ }
126
+
127
+ window.Modal = Modal;
@@ -0,0 +1,45 @@
1
+ // PasswordStrength.jsx
2
+ function PasswordStrength({ password }) {
3
+ if (!password) return null;
4
+
5
+ let strength = 0;
6
+ let label = '';
7
+ let color = '';
8
+
9
+ const hasLetters = /[a-zA-Z]/.test(password);
10
+ const hasDigits = /[0-9]/.test(password);
11
+ const hasSpecial = /[^a-zA-Z0-9]/.test(password);
12
+ const isLongEnough = password.length >= 8;
13
+
14
+ if (hasLetters && hasDigits && hasSpecial && isLongEnough) {
15
+ strength = 3; label = '强'; color = 'var(--success)';
16
+ } else if (hasLetters && hasDigits) {
17
+ strength = 2; label = '中'; color = '#FF9500';
18
+ } else {
19
+ strength = 1; label = '弱'; color = 'var(--danger)';
20
+ }
21
+
22
+ return React.createElement('div', {
23
+ style: { marginTop: '6px' }
24
+ },
25
+ React.createElement('div', {
26
+ style: { display: 'flex', gap: '4px', marginBottom: '4px' }
27
+ },
28
+ [1, 2, 3].map(i =>
29
+ React.createElement('div', {
30
+ key: i,
31
+ style: {
32
+ flex: 1, height: '3px', borderRadius: '2px',
33
+ background: i <= strength ? color : 'var(--bg-tertiary)',
34
+ transition: 'background 200ms',
35
+ }
36
+ })
37
+ )
38
+ ),
39
+ React.createElement('span', {
40
+ style: { fontSize: '12px', color }
41
+ }, `密码强度:${label}`)
42
+ );
43
+ }
44
+
45
+ window.PasswordStrength = PasswordStrength;