@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.
- package/LICENSE +21 -0
- package/README.en.md +80 -0
- package/README.ja.md +80 -0
- package/README.md +79 -0
- package/README.zh-TW.md +80 -0
- package/bin/cli.js +10 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +59 -0
- package/dist/app.js.map +1 -0
- package/dist/db/index.js +12 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +169 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema.d.ts +743 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +88 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/i18n.js +309 -0
- package/dist/i18n.js.map +1 -0
- package/dist/plugins/admin.d.ts +4 -0
- package/dist/plugins/admin.d.ts.map +1 -0
- package/dist/plugins/admin.js +19 -0
- package/dist/plugins/admin.js.map +1 -0
- package/dist/plugins/auth.d.ts +4 -0
- package/dist/plugins/auth.d.ts.map +1 -0
- package/dist/plugins/auth.js +35 -0
- package/dist/plugins/auth.js.map +1 -0
- package/dist/routes/auth.d.ts +4 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +352 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/categories.d.ts +4 -0
- package/dist/routes/categories.d.ts.map +1 -0
- package/dist/routes/categories.js +112 -0
- package/dist/routes/categories.js.map +1 -0
- package/dist/routes/config.d.ts +4 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +227 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/resources.d.ts +4 -0
- package/dist/routes/resources.d.ts.map +1 -0
- package/dist/routes/resources.js +474 -0
- package/dist/routes/resources.js.map +1 -0
- package/dist/routes/tags.d.ts +4 -0
- package/dist/routes/tags.d.ts.map +1 -0
- package/dist/routes/tags.js +37 -0
- package/dist/routes/tags.js.map +1 -0
- package/dist/routes/users.d.ts +4 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/routes/users.js +181 -0
- package/dist/routes/users.js.map +1 -0
- package/dist/services/crypto.js +49 -0
- package/dist/services/crypto.js.map +1 -0
- package/dist/services/mail.d.ts +16 -0
- package/dist/services/mail.d.ts.map +1 -0
- package/dist/services/mail.js +33 -0
- package/dist/services/mail.js.map +1 -0
- package/dist/services/rsa.js +49 -0
- package/dist/services/rsa.js.map +1 -0
- package/dist/services/token.d.ts +9 -0
- package/dist/services/token.d.ts.map +1 -0
- package/dist/services/token.js +29 -0
- package/dist/services/token.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +73 -0
- package/public/admin/AdminCategories.jsx +310 -0
- package/public/admin/AdminConfig.jsx +254 -0
- package/public/admin/AdminEmail.jsx +279 -0
- package/public/admin/AdminTags.jsx +263 -0
- package/public/admin/AdminUsers.jsx +452 -0
- package/public/app.jsx +186 -0
- package/public/components/ConfirmDialog.jsx +78 -0
- package/public/components/DropdownSelect.jsx +281 -0
- package/public/components/EmailPreviewModal.jsx +104 -0
- package/public/components/EmptyState.jsx +50 -0
- package/public/components/Modal.jsx +127 -0
- package/public/components/PasswordStrength.jsx +45 -0
- package/public/components/Skeleton.jsx +68 -0
- package/public/components/Toast.jsx +80 -0
- package/public/components/TooltipIconButton.jsx +55 -0
- package/public/context/AppContext.jsx +314 -0
- package/public/features/BatchResourceModal.jsx +606 -0
- package/public/features/ChangePasswordModal.jsx +187 -0
- package/public/features/ProfileModal.jsx +170 -0
- package/public/features/ResourceCard.jsx +422 -0
- package/public/features/ResourceFormModal.jsx +915 -0
- package/public/features/ResourceRow.jsx +287 -0
- package/public/features/ResourceTimeline.jsx +472 -0
- package/public/hooks/useApi.jsx +26 -0
- package/public/hooks/useRouter.jsx +35 -0
- package/public/index.html +258 -0
- package/public/layout/AdminLayout.jsx +167 -0
- package/public/layout/AppLayout.jsx +119 -0
- package/public/layout/AuthLayout.jsx +503 -0
- package/public/layout/Header.jsx +543 -0
- package/public/layout/Sidebar.jsx +175 -0
- package/public/pages/AdminPage.jsx +30 -0
- package/public/pages/ForgotPasswordPage.jsx +93 -0
- package/public/pages/HomePage.jsx +2297 -0
- package/public/pages/LoginPage.jsx +191 -0
- package/public/pages/RegisterPage.jsx +137 -0
- package/public/pages/ResetPasswordPage.jsx +169 -0
- package/public/pages/SetupPage.jsx +157 -0
- package/public/utils/helpers.jsx +152 -0
- package/public/utils/i18n.jsx +1374 -0
- package/public/utils/preferences.jsx +220 -0
- package/public/utils/security.jsx +88 -0
- package/public/utils/theme.jsx +24 -0
- package/public/vendor/babel.min.js +2 -0
- package/public/vendor/lucide-react.min.js +9 -0
- package/public/vendor/react-dom.development.js +29869 -0
- 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;
|