@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,175 @@
1
+ // Sidebar.jsx
2
+ function Sidebar({ isCompact = false }) {
3
+ const state = window.useAppState();
4
+ const dispatch = window.useAppDispatch();
5
+ const [showAllTags, setShowAllTags] = React.useState(false);
6
+ const { Heart, Clock, FileText } = lucide;
7
+
8
+ if (!state) return null;
9
+ const { categories, tags, selectedCategory, selectedTags, quickAccessFilter, currentUser } = state;
10
+ const displayedTags = showAllTags ? tags : tags.slice(0, 20);
11
+
12
+ const navItemStyle = (isSelected) => ({
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ justifyContent: 'space-between',
16
+ minHeight: '36px',
17
+ borderRadius: '8px',
18
+ padding: '8px 12px',
19
+ margin: isCompact ? 0 : '2px 8px',
20
+ cursor: 'pointer',
21
+ border: 'none',
22
+ width: '100%',
23
+ textAlign: 'left',
24
+ fontSize: '14px',
25
+ background: isSelected ? 'rgba(0,113,227,0.10)' : 'none',
26
+ color: isSelected ? 'var(--brand)' : 'var(--text-primary)',
27
+ fontWeight: isSelected ? 600 : 400,
28
+ transition: 'background 150ms',
29
+ });
30
+
31
+ const sectionTitleStyle = {
32
+ fontSize: '11px',
33
+ fontWeight: 600,
34
+ textTransform: 'uppercase',
35
+ color: 'var(--text-secondary)',
36
+ letterSpacing: '0.05em',
37
+ padding: isCompact ? '12px 12px 8px' : '16px 12px 8px',
38
+ };
39
+
40
+ const quickItems = [
41
+ { key: 'favorites', label: '我的收藏', Icon: Heart },
42
+ { key: 'history', label: '最近访问', Icon: Clock },
43
+ { key: 'mine', label: '我创建的', Icon: FileText },
44
+ ];
45
+
46
+ const totalCount = (categories || []).reduce((sum, category) => sum + (category.resourceCount || 0), 0);
47
+ const allCategory = { id: null, name: '全部', color: null, resourceCount: totalCount };
48
+ const categoryList = [allCategory, ...(categories || [])];
49
+ const listLayoutStyle = {
50
+ display: 'grid',
51
+ gridTemplateColumns: isCompact ? 'repeat(auto-fit, minmax(140px, 1fr))' : '1fr',
52
+ gap: isCompact ? '6px' : 0,
53
+ padding: isCompact ? '0 8px 8px' : 0,
54
+ };
55
+
56
+ return (
57
+ <div style={{
58
+ width: isCompact ? '100%' : '220px',
59
+ flexShrink: 0,
60
+ background: 'var(--bg-secondary)',
61
+ borderRight: isCompact ? 'none' : '1px solid var(--border)',
62
+ borderBottom: isCompact ? '1px solid var(--border)' : 'none',
63
+ position: isCompact ? 'static' : 'sticky',
64
+ top: isCompact ? 'auto' : '72px',
65
+ height: isCompact ? 'auto' : 'calc(100vh - 72px)',
66
+ overflowY: 'auto',
67
+ }}>
68
+ <div style={sectionTitleStyle}>类别</div>
69
+ <div style={listLayoutStyle}>
70
+ {categoryList.map((cat) => {
71
+ const isSelected = selectedCategory === cat.id && !quickAccessFilter;
72
+ return (
73
+ <button
74
+ key={cat.id || 'all'}
75
+ style={navItemStyle(isSelected)}
76
+ onClick={() => dispatch({ type: 'SET_CATEGORY', category: cat.id })}
77
+ onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = 'var(--bg-tertiary)'; }}
78
+ onMouseLeave={(e) => { e.currentTarget.style.background = isSelected ? 'rgba(0,113,227,0.10)' : 'none'; }}
79
+ >
80
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
81
+ {cat.name}
82
+ </span>
83
+ {cat.resourceCount !== undefined && (
84
+ <span style={{
85
+ fontSize: '11px',
86
+ background: 'var(--bg-tertiary)',
87
+ borderRadius: '10px',
88
+ padding: '1px 6px',
89
+ flexShrink: 0,
90
+ marginLeft: '4px',
91
+ color: 'var(--text-secondary)',
92
+ }}>
93
+ {cat.resourceCount > 99 ? '99+' : cat.resourceCount}
94
+ </span>
95
+ )}
96
+ </button>
97
+ );
98
+ })}
99
+ </div>
100
+
101
+ <div style={{ height: '1px', background: 'var(--border)', margin: '8px 0' }} />
102
+
103
+ <div style={sectionTitleStyle}>标签</div>
104
+ <div style={{ padding: '0 8px 12px', display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
105
+ {displayedTags.map((tag) => {
106
+ const isTagSelected = (selectedTags || []).includes(tag);
107
+ return (
108
+ <button
109
+ key={tag}
110
+ onClick={() => dispatch({ type: 'TOGGLE_TAG', tag })}
111
+ style={{
112
+ fontSize: '12px',
113
+ borderRadius: '4px',
114
+ padding: '4px 8px',
115
+ border: 'none',
116
+ cursor: 'pointer',
117
+ background: isTagSelected ? 'rgba(0,113,227,0.10)' : 'var(--bg-tertiary)',
118
+ color: isTagSelected ? 'var(--brand)' : 'var(--text-secondary)',
119
+ transition: 'background 150ms',
120
+ }}
121
+ onMouseEnter={(e) => { if (!isTagSelected) e.currentTarget.style.background = 'var(--bg-primary)'; }}
122
+ onMouseLeave={(e) => { e.currentTarget.style.background = isTagSelected ? 'rgba(0,113,227,0.10)' : 'var(--bg-tertiary)'; }}
123
+ >
124
+ {tag}
125
+ </button>
126
+ );
127
+ })}
128
+
129
+ {(tags || []).length > 20 && (
130
+ <button
131
+ onClick={() => setShowAllTags((value) => !value)}
132
+ style={{
133
+ fontSize: '12px',
134
+ color: 'var(--brand)',
135
+ background: 'none',
136
+ border: 'none',
137
+ cursor: 'pointer',
138
+ padding: '4px 6px',
139
+ }}
140
+ >
141
+ {showAllTags ? '收起' : `展开更多 (${tags.length - 20})`}
142
+ </button>
143
+ )}
144
+ </div>
145
+
146
+ {currentUser && (
147
+ <>
148
+ <div style={{ height: '1px', background: 'var(--border)', margin: '8px 0' }} />
149
+ <div style={sectionTitleStyle}>快速访问</div>
150
+ <div style={listLayoutStyle}>
151
+ {quickItems.map(({ key, label, Icon }) => {
152
+ const isSelected = quickAccessFilter === key;
153
+ return (
154
+ <button
155
+ key={key}
156
+ style={navItemStyle(isSelected)}
157
+ onClick={() => dispatch({ type: 'SET_QUICK_ACCESS_FILTER', filter: isSelected ? null : key })}
158
+ onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = 'var(--bg-tertiary)'; }}
159
+ onMouseLeave={(e) => { e.currentTarget.style.background = isSelected ? 'rgba(0,113,227,0.10)' : 'none'; }}
160
+ >
161
+ <span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
162
+ <Icon size={14} style={{ color: isSelected ? 'var(--brand)' : 'var(--text-secondary)' }} />
163
+ {label}
164
+ </span>
165
+ </button>
166
+ );
167
+ })}
168
+ </div>
169
+ </>
170
+ )}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ window.Sidebar = Sidebar;
@@ -0,0 +1,30 @@
1
+ const ADMIN_TABS = ['categories', 'tags', 'users', 'config', 'email'];
2
+
3
+ function AdminPage() {
4
+ const { route, navigate } = window.useRouter();
5
+ const path = route.path || '';
6
+ const subpath = path.replace(/^\/admin\/?/, '') || 'categories';
7
+ const activeTab = ADMIN_TABS.includes(subpath) ? subpath : 'categories';
8
+
9
+ React.useEffect(() => {
10
+ if (!ADMIN_TABS.includes(subpath)) {
11
+ navigate('#/admin/categories');
12
+ }
13
+ }, [subpath, navigate]);
14
+
15
+ const tabContent = {
16
+ categories: React.createElement(window.AdminCategories, null),
17
+ tags: React.createElement(window.AdminTags, null),
18
+ users: React.createElement(window.AdminUsers, null),
19
+ config: React.createElement(window.AdminConfig, null),
20
+ email: React.createElement(window.AdminEmail, null),
21
+ };
22
+
23
+ return (
24
+ <window.AdminLayout activeTab={activeTab}>
25
+ {tabContent[activeTab] || null}
26
+ </window.AdminLayout>
27
+ );
28
+ }
29
+
30
+ window.AdminPage = AdminPage;
@@ -0,0 +1,93 @@
1
+ function ForgotPasswordPage() {
2
+ const dispatch = window.useAppDispatch();
3
+ const { navigate } = window.useRouter();
4
+ const { request } = window.useApi();
5
+ const { Loader, ArrowLeft } = lucide;
6
+
7
+ const [email, setEmail] = React.useState('');
8
+ const [loading, setLoading] = React.useState(false);
9
+ const [emailError, setEmailError] = React.useState('');
10
+
11
+ const validate = () => {
12
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return '请输入有效的邮箱地址';
13
+ return '';
14
+ };
15
+
16
+ const handleSubmit = async (e) => {
17
+ e.preventDefault();
18
+ const err = validate();
19
+ if (err) { setEmailError(err); return; }
20
+ setLoading(true);
21
+ try {
22
+ window.sessionStorage?.setItem('rh_reset_identifier', email.trim());
23
+ const { data } = await request('/api/auth/forgot-password', {
24
+ method: 'POST',
25
+ body: JSON.stringify({ email }),
26
+ });
27
+ dispatch({ type: 'ADD_TOAST', toastType: 'info', message: '若该邮箱已注册,重置链接已发送,请查收' });
28
+ if (data.emailPreview) dispatch({ type: 'SET_EMAIL_PREVIEW', emailPreview: data.emailPreview });
29
+ } finally {
30
+ setLoading(false);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <window.AuthLayout>
36
+ <div style={{ width: '400px', maxWidth: '100%' }}>
37
+ <div style={{
38
+ background: 'color-mix(in srgb, var(--surface-elevated) 94%, var(--surface-muted))', border: '1px solid color-mix(in srgb, var(--outline-strong) 84%, var(--border))',
39
+ borderRadius: '24px', padding: '36px', boxShadow: 'var(--shadow-modal)',
40
+ }}>
41
+ <div style={{ fontSize: '11px', fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--brand)', marginBottom: '10px' }}>
42
+ 账号恢复
43
+ </div>
44
+ <h1 style={{ fontSize: '20px', fontWeight: 700, color: 'var(--text-primary)', margin: '0 0 8px' }}>忘记密码</h1>
45
+ <p style={{ fontSize: '14px', color: 'var(--text-secondary)', margin: '0 0 24px', lineHeight: '1.5' }}>
46
+ 输入注册邮箱,系统将发送密码重置链接
47
+ </p>
48
+ <form onSubmit={handleSubmit} noValidate>
49
+ <div style={{ marginBottom: '16px' }}>
50
+ <label style={{ fontSize: '13px', fontWeight: 500, color: 'var(--text-primary)', display: 'block', marginBottom: '6px' }}>邮箱</label>
51
+ <input
52
+ className="rh-auth-input"
53
+ type="email" value={email} placeholder="your@example.com"
54
+ name="email"
55
+ autoComplete="email"
56
+ onChange={(e) => { setEmail(e.target.value); setEmailError(''); }}
57
+ onBlur={() => setEmailError(validate())}
58
+ disabled={loading}
59
+ style={{
60
+ width: '100%', padding: '9px 12px',
61
+ border: `1px solid ${emailError ? 'var(--danger)' : 'var(--border)'}`,
62
+ borderRadius: '8px', background: 'var(--bg-secondary)',
63
+ color: 'var(--text-primary)', fontSize: '14px',
64
+ outline: 'none', boxSizing: 'border-box',
65
+ }}
66
+ />
67
+ {emailError && <div style={{ fontSize: '12px', color: 'var(--danger)', marginTop: '4px' }}>{emailError}</div>}
68
+ </div>
69
+ <button type="submit" disabled={loading} style={{
70
+ width: '100%', padding: '10px',
71
+ background: 'var(--brand)', color: '#fff', border: 'none',
72
+ borderRadius: '8px', cursor: loading ? 'not-allowed' : 'pointer',
73
+ fontSize: '14px', fontWeight: 600, marginBottom: '16px',
74
+ opacity: loading ? 0.8 : 1,
75
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px',
76
+ }}>
77
+ {loading && <Loader size={15} />}
78
+ 发送重置链接
79
+ </button>
80
+ <div style={{ textAlign: 'center' }}>
81
+ <button type="button" onClick={() => navigate('#/login')}
82
+ style={{ background: 'none', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer', fontSize: '13px', display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
83
+ <ArrowLeft size={13} /> 返回登录
84
+ </button>
85
+ </div>
86
+ </form>
87
+ </div>
88
+ </div>
89
+ </window.AuthLayout>
90
+ );
91
+ }
92
+
93
+ window.ForgotPasswordPage = ForgotPasswordPage;