cacode commited on
Commit
99cf942
·
verified ·
1 Parent(s): 20a9193

Upload 439 files

Browse files
src/components/admin/AdminApp.svelte CHANGED
@@ -140,7 +140,14 @@
140
  ];
141
 
142
  function cloneData<T>(value: T): T {
143
- return structuredClone(value);
 
 
 
 
 
 
 
144
  }
145
 
146
  function prettyJson(value: unknown) {
@@ -282,11 +289,13 @@
282
  };
283
  }
284
 
285
- let sessionLoading = false;
286
  let sessionRefreshing = false;
 
287
  let authenticated = false;
288
  let configured = true;
289
  let username = "";
 
290
  let activeNav: NavKey = "overview";
291
  let toast: ToastState | null = null;
292
  let loginForm = { username: "", password: "" };
@@ -430,7 +439,8 @@
430
  }
431
 
432
  async function refreshSession() {
433
- sessionRefreshing = true;
 
434
  try {
435
  const data = await api<any>("/api/admin/session");
436
  authenticated = Boolean(data.authenticated);
@@ -445,6 +455,7 @@
445
  error instanceof Error ? error.message : "后台会话检查失败",
446
  );
447
  } finally {
 
448
  sessionLoading = false;
449
  sessionRefreshing = false;
450
  }
@@ -584,12 +595,15 @@
584
 
585
  async function handleLogin(event?: SubmitEvent) {
586
  event?.preventDefault();
 
587
  try {
588
  const response = await api<any>("/api/admin/login", {
589
  method: "POST",
590
  body: JSON.stringify(loginForm),
591
  });
592
  authenticated = Boolean(response.authenticated);
 
 
593
  username = String(response.username || loginForm.username || "");
594
  loginForm.password = "";
595
  applyBuild(response);
@@ -597,6 +611,8 @@
597
  showToast("success", "登录成功");
598
  } catch (error) {
599
  showToast("error", error instanceof Error ? error.message : "\u767b\u5f55\u5931\u8d25");
 
 
600
  }
601
  }
602
 
@@ -604,6 +620,8 @@
604
  await api("/api/admin/logout", { method: "POST", body: JSON.stringify({}) });
605
  authenticated = false;
606
  username = "";
 
 
607
  showToast("info", "已退出后台");
608
  }
609
 
@@ -789,23 +807,75 @@
789
  <div class={`admin-toast ${toast.tone}`}>{toast.text}</div>
790
  {/if}
791
 
792
- {#if !configured}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
  <div class="card-base admin-panel admin-empty">
794
  <h2>后台尚未启用</h2>
795
  <p>请在 Hugging Face Space 中配置环境变量 <code>ADMIN</code> 和 <code>PASSWORD</code> 后再访问管理页。</p>
796
  </div>
797
  {:else if !authenticated}
798
- <form class="card-base admin-login admin-login-stage" method="post" action={`${entryPath}/login`} on:submit|preventDefault={handleLogin}>
799
- <div class="admin-login-badge">Firefly Admin</div>
800
- <h2>登录后台</h2>
801
- <p>使用 Space 环境变量中的管理员账号和密码进入管理页面。</p>
802
- <input class="admin-input" name="username" autocomplete="username" placeholder="管理员账号" bind:value={loginForm.username} />
803
- <input class="admin-input" name="password" autocomplete="current-password" type="password" placeholder="密码" bind:value={loginForm.password} />
804
- <button class="btn-regular admin-button primary" type="submit">&#30331;&#24405;</button>
805
- {#if sessionRefreshing || sessionLoading}
806
- <p class="admin-login-status">正在检查当前后台会话...</p>
807
- {/if}
808
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  {:else}
810
  <div class="admin-workbench">
811
  <aside class="admin-sidebar">
@@ -1005,6 +1075,63 @@
1005
  .admin-panel, .admin-login {
1006
  padding: 1.2rem;
1007
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
  .admin-login {
1009
  max-width: 30rem;
1010
  margin: 0 auto;
@@ -1012,6 +1139,43 @@
1012
  flex-direction: column;
1013
  gap: 0.9rem;
1014
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1015
  .admin-login-stage {
1016
  position: relative;
1017
  z-index: 6;
@@ -1048,6 +1212,7 @@
1048
  .admin-quick-actions { display: flex; gap: 0.6rem; align-items: center; }
1049
  .admin-quick-actions.wrap { flex-wrap: wrap; }
1050
  .admin-button { min-height: 2.7rem; padding: 0.65rem 1rem; border-radius: 0.95rem; }
 
1051
  .admin-button.ghost { border: 1px solid var(--line-divider); }
1052
  .admin-button.danger { color: oklch(0.62 0.2 25); }
1053
  .admin-nav-button, .editor-item, .overview-item {
@@ -1098,8 +1263,25 @@
1098
  .build-log { max-height: 22rem; overflow: auto; }
1099
  .danger-text { color: oklch(0.62 0.2 25); }
1100
  .admin-empty { padding: 2rem; text-align: center; }
 
 
 
 
 
 
 
 
1101
  @media (max-width: 1200px) {
1102
- .admin-workbench, .two-col, .metrics-grid, .asset-grid, .editor-layout, .posts-layout { grid-template-columns: 1fr; }
 
 
 
 
 
 
 
 
 
1103
  }
1104
  @media (max-width: 800px) {
1105
  .admin-login {
 
140
  ];
141
 
142
  function cloneData<T>(value: T): T {
143
+ if (value === null || value === undefined || typeof value !== "object") {
144
+ return value;
145
+ }
146
+ try {
147
+ return structuredClone(value);
148
+ } catch {
149
+ return JSON.parse(JSON.stringify(value)) as T;
150
+ }
151
  }
152
 
153
  function prettyJson(value: unknown) {
 
289
  };
290
  }
291
 
292
+ let sessionLoading = true;
293
  let sessionRefreshing = false;
294
+ let sessionResolved = false;
295
  let authenticated = false;
296
  let configured = true;
297
  let username = "";
298
+ let loginSubmitting = false;
299
  let activeNav: NavKey = "overview";
300
  let toast: ToastState | null = null;
301
  let loginForm = { username: "", password: "" };
 
439
  }
440
 
441
  async function refreshSession() {
442
+ sessionLoading = true;
443
+ sessionRefreshing = sessionResolved;
444
  try {
445
  const data = await api<any>("/api/admin/session");
446
  authenticated = Boolean(data.authenticated);
 
455
  error instanceof Error ? error.message : "后台会话检查失败",
456
  );
457
  } finally {
458
+ sessionResolved = true;
459
  sessionLoading = false;
460
  sessionRefreshing = false;
461
  }
 
595
 
596
  async function handleLogin(event?: SubmitEvent) {
597
  event?.preventDefault();
598
+ loginSubmitting = true;
599
  try {
600
  const response = await api<any>("/api/admin/login", {
601
  method: "POST",
602
  body: JSON.stringify(loginForm),
603
  });
604
  authenticated = Boolean(response.authenticated);
605
+ configured = response.configured !== false;
606
+ sessionResolved = true;
607
  username = String(response.username || loginForm.username || "");
608
  loginForm.password = "";
609
  applyBuild(response);
 
611
  showToast("success", "登录成功");
612
  } catch (error) {
613
  showToast("error", error instanceof Error ? error.message : "\u767b\u5f55\u5931\u8d25");
614
+ } finally {
615
+ loginSubmitting = false;
616
  }
617
  }
618
 
 
620
  await api("/api/admin/logout", { method: "POST", body: JSON.stringify({}) });
621
  authenticated = false;
622
  username = "";
623
+ sessionResolved = true;
624
+ loginForm.password = "";
625
  showToast("info", "已退出后台");
626
  }
627
 
 
807
  <div class={`admin-toast ${toast.tone}`}>{toast.text}</div>
808
  {/if}
809
 
810
+ {#if !sessionResolved}
811
+ <section class="admin-auth-shell admin-auth-shell-pending">
812
+ <div class="card-base admin-panel admin-auth-copy">
813
+ <div class="admin-login-badge">Firefly Admin</div>
814
+ <div class="admin-auth-intro">
815
+ <h2>正在连接后台</h2>
816
+ <p>正在检查管理员会话与后台启用状态,确认完成后会显示登录入口或直接进入内容管理台。</p>
817
+ </div>
818
+ <div class="admin-loading-stack" aria-hidden="true">
819
+ <div class="admin-loading-line"></div>
820
+ <div class="admin-loading-line short"></div>
821
+ <div class="admin-loading-line medium"></div>
822
+ </div>
823
+ </div>
824
+ <div class="card-base admin-login admin-login-stage admin-login-pending">
825
+ <div class="admin-login-badge">会话检查中</div>
826
+ <h2>后台准备中</h2>
827
+ <p class="admin-login-status">正在从当前 Space 读取后台环境变量并确认登录状态。</p>
828
+ </div>
829
+ </section>
830
+ {:else if !configured}
831
  <div class="card-base admin-panel admin-empty">
832
  <h2>后台尚未启用</h2>
833
  <p>请在 Hugging Face Space 中配置环境变量 <code>ADMIN</code> 和 <code>PASSWORD</code> 后再访问管理页。</p>
834
  </div>
835
  {:else if !authenticated}
836
+ <section class="admin-auth-shell">
837
+ <div class="card-base admin-panel admin-auth-copy">
838
+ <div class="admin-login-badge">Firefly Admin</div>
839
+ <div class="admin-auth-intro">
840
+ <h2>后台管理入口</h2>
841
+ <p>在这里统一管理博客标题、介绍、导航、侧栏、文章、图片素材与构建状态,界面风格与前台博客保持一致。</p>
842
+ </div>
843
+ <div class="admin-auth-facts">
844
+ <div class="admin-auth-fact">
845
+ <strong>账号来源</strong>
846
+ <span><code>ADMIN</code></span>
847
+ </div>
848
+ <div class="admin-auth-fact">
849
+ <strong>密码来源</strong>
850
+ <span><code>PASSWORD</code></span>
851
+ </div>
852
+ <div class="admin-auth-fact">
853
+ <strong>内容存储</strong>
854
+ <span>当前版本保存到容器本地目录</span>
855
+ </div>
856
+ </div>
857
+ </div>
858
+ <form class="card-base admin-login admin-login-stage" method="post" action={`${entryPath}/login`} on:submit|preventDefault={handleLogin}>
859
+ <div class="admin-login-badge">管理员登录</div>
860
+ <h2>输入管理员凭证</h2>
861
+ <p>登录成功后将进入内容工作台,可继续管理站点配置、文章与图片资源。</p>
862
+ <label class="admin-field">
863
+ <span>管理员账号</span>
864
+ <input class="admin-input" name="username" autocomplete="username" autocapitalize="none" placeholder="管理员账号" bind:value={loginForm.username} required />
865
+ </label>
866
+ <label class="admin-field">
867
+ <span>登录密码</span>
868
+ <input class="admin-input" name="password" autocomplete="current-password" type="password" placeholder="密码" bind:value={loginForm.password} required />
869
+ </label>
870
+ <button class="btn-regular admin-button primary" type="submit" disabled={loginSubmitting}>
871
+ {loginSubmitting ? "登录中..." : "登录后台"}
872
+ </button>
873
+ <p class="admin-login-status">后台凭证直接读取自 Hugging Face Space 环境变量,不写入前端代码。</p>
874
+ {#if sessionRefreshing || sessionLoading}
875
+ <p class="admin-login-status">正在刷新当前后台会话...</p>
876
+ {/if}
877
+ </form>
878
+ </section>
879
  {:else}
880
  <div class="admin-workbench">
881
  <aside class="admin-sidebar">
 
1075
  .admin-panel, .admin-login {
1076
  padding: 1.2rem;
1077
  }
1078
+ .admin-auth-shell {
1079
+ display: grid;
1080
+ grid-template-columns: minmax(0, 1.15fr) minmax(22rem, 0.85fr);
1081
+ gap: 1rem;
1082
+ align-items: stretch;
1083
+ }
1084
+ .admin-auth-copy {
1085
+ position: relative;
1086
+ overflow: hidden;
1087
+ display: flex;
1088
+ flex-direction: column;
1089
+ justify-content: space-between;
1090
+ gap: 1.4rem;
1091
+ min-height: 22rem;
1092
+ background:
1093
+ radial-gradient(circle at top right, rgb(255 255 255 / 0.18), transparent 32%),
1094
+ linear-gradient(145deg, color-mix(in oklch, var(--card-bg) 78%, white 22%), color-mix(in oklch, var(--card-bg) 92%, var(--btn-card-bg-hover) 8%));
1095
+ border: 1px solid color-mix(in oklch, var(--line-divider) 72%, white 28%);
1096
+ box-shadow: 0 28px 80px rgb(8 20 28 / 0.12);
1097
+ }
1098
+ .admin-auth-intro {
1099
+ display: flex;
1100
+ flex-direction: column;
1101
+ gap: 0.75rem;
1102
+ }
1103
+ .admin-auth-intro h2 {
1104
+ margin: 0;
1105
+ font-size: clamp(2rem, 4vw, 3rem);
1106
+ line-height: 1.08;
1107
+ }
1108
+ .admin-auth-intro p {
1109
+ max-width: 40rem;
1110
+ }
1111
+ .admin-auth-facts {
1112
+ display: grid;
1113
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1114
+ gap: 0.8rem;
1115
+ }
1116
+ .admin-auth-fact {
1117
+ display: flex;
1118
+ flex-direction: column;
1119
+ gap: 0.35rem;
1120
+ padding: 0.9rem;
1121
+ border-radius: 1rem;
1122
+ border: 1px solid color-mix(in oklch, var(--line-divider) 74%, white 26%);
1123
+ background: color-mix(in oklch, var(--card-bg) 82%, white 18%);
1124
+ }
1125
+ .admin-auth-fact strong {
1126
+ font-size: 0.95rem;
1127
+ }
1128
+ .admin-auth-fact span {
1129
+ font-size: 0.92rem;
1130
+ color: rgba(0,0,0,.72);
1131
+ }
1132
+ :root.dark .admin-auth-fact span {
1133
+ color: rgba(255,255,255,.72);
1134
+ }
1135
  .admin-login {
1136
  max-width: 30rem;
1137
  margin: 0 auto;
 
1139
  flex-direction: column;
1140
  gap: 0.9rem;
1141
  }
1142
+ .admin-auth-shell .admin-login {
1143
+ max-width: none;
1144
+ width: 100%;
1145
+ margin: 0;
1146
+ justify-content: center;
1147
+ }
1148
+ .admin-field {
1149
+ display: flex;
1150
+ flex-direction: column;
1151
+ gap: 0.4rem;
1152
+ }
1153
+ .admin-field > span {
1154
+ font-size: 0.88rem;
1155
+ font-weight: 700;
1156
+ }
1157
+ .admin-login-pending {
1158
+ min-height: 22rem;
1159
+ justify-content: center;
1160
+ }
1161
+ .admin-loading-stack {
1162
+ display: flex;
1163
+ flex-direction: column;
1164
+ gap: 0.75rem;
1165
+ }
1166
+ .admin-loading-line {
1167
+ height: 0.9rem;
1168
+ border-radius: 999px;
1169
+ background: linear-gradient(90deg, rgb(255 255 255 / 0.28), rgb(255 255 255 / 0.62), rgb(255 255 255 / 0.22));
1170
+ background-size: 200% 100%;
1171
+ animation: adminPulse 1.8s ease-in-out infinite;
1172
+ }
1173
+ .admin-loading-line.short {
1174
+ width: 48%;
1175
+ }
1176
+ .admin-loading-line.medium {
1177
+ width: 70%;
1178
+ }
1179
  .admin-login-stage {
1180
  position: relative;
1181
  z-index: 6;
 
1212
  .admin-quick-actions { display: flex; gap: 0.6rem; align-items: center; }
1213
  .admin-quick-actions.wrap { flex-wrap: wrap; }
1214
  .admin-button { min-height: 2.7rem; padding: 0.65rem 1rem; border-radius: 0.95rem; }
1215
+ .admin-button[disabled] { opacity: 0.7; cursor: wait; }
1216
  .admin-button.ghost { border: 1px solid var(--line-divider); }
1217
  .admin-button.danger { color: oklch(0.62 0.2 25); }
1218
  .admin-nav-button, .editor-item, .overview-item {
 
1263
  .build-log { max-height: 22rem; overflow: auto; }
1264
  .danger-text { color: oklch(0.62 0.2 25); }
1265
  .admin-empty { padding: 2rem; text-align: center; }
1266
+ @keyframes adminPulse {
1267
+ 0% {
1268
+ background-position: 0% 50%;
1269
+ }
1270
+ 100% {
1271
+ background-position: 100% 50%;
1272
+ }
1273
+ }
1274
  @media (max-width: 1200px) {
1275
+ .admin-auth-shell,
1276
+ .admin-auth-facts,
1277
+ .admin-workbench,
1278
+ .two-col,
1279
+ .metrics-grid,
1280
+ .asset-grid,
1281
+ .editor-layout,
1282
+ .posts-layout {
1283
+ grid-template-columns: 1fr;
1284
+ }
1285
  }
1286
  @media (max-width: 800px) {
1287
  .admin-login {
src/layouts/AdminLayout.astro CHANGED
@@ -78,8 +78,8 @@ const isWallpaperSwitchable = backgroundWallpaper.switchable ?? true;
78
  <div class="admin-hero-fallback absolute inset-0" />
79
  )}
80
  <div class="absolute inset-0 admin-hero-overlay" />
81
- <div class="relative admin-hero-grid h-full gap-5 p-6 text-white md:p-8">
82
- <div class="admin-hero-copy flex flex-col justify-end gap-4">
83
  <div class="inline-flex w-fit items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm font-medium backdrop-blur-md">
84
  <span class="h-2.5 w-2.5 rounded-full bg-[oklch(0.75_0.14_var(--hue))]" />
85
  Firefly Admin
@@ -93,27 +93,13 @@ const isWallpaperSwitchable = backgroundWallpaper.switchable ?? true;
93
  </p>
94
  </div>
95
  </div>
96
- <div class="admin-hero-side">
97
- <div class="admin-hero-chip">
98
- <span>管理入口</span>
99
- <strong>/admin</strong>
100
- </div>
101
- <div class="admin-hero-chip">
102
- <span>登录方式</span>
103
- <strong>ADMIN / PASSWORD</strong>
104
- </div>
105
- <div class="admin-hero-chip">
106
- <span>内容存储</span>
107
- <strong>容器本地目录</strong>
108
- </div>
109
- </div>
110
  </div>
111
  </div>
112
  </div>
113
  </div>
114
  </section>
115
 
116
- <main class="relative z-30 -mt-14 w-full px-2 pb-10 pt-0 md:-mt-18 md:px-4">
117
  <div class="mx-auto max-w-(--page-width)">
118
  <slot />
119
  </div>
@@ -133,7 +119,7 @@ const isWallpaperSwitchable = backgroundWallpaper.switchable ?? true;
133
 
134
  .admin-hero-frame {
135
  position: relative;
136
- height: clamp(18rem, 34vw, 25rem);
137
  }
138
 
139
  .admin-hero-media {
@@ -158,57 +144,18 @@ const isWallpaperSwitchable = backgroundWallpaper.switchable ?? true;
158
  }
159
 
160
  .admin-hero-grid {
161
- display: grid;
162
- align-items: end;
163
- }
164
-
165
- .admin-hero-side {
166
- display: grid;
167
- gap: 0.75rem;
168
- align-self: stretch;
169
- }
170
-
171
- .admin-hero-chip {
172
  display: flex;
173
- flex-direction: column;
174
- gap: 0.2rem;
175
- border: 1px solid rgb(255 255 255 / 0.14);
176
- border-radius: 1rem;
177
- background: rgb(255 255 255 / 0.1);
178
- backdrop-filter: blur(18px);
179
- padding: 0.9rem 1rem;
180
- }
181
-
182
- .admin-hero-chip span {
183
- font-size: 0.8rem;
184
- color: rgb(255 255 255 / 0.72);
185
  }
186
 
187
- .admin-hero-chip strong {
188
- font-size: 0.98rem;
189
- letter-spacing: 0.02em;
190
- }
191
-
192
- @media (min-width: 960px) {
193
- .admin-hero-grid {
194
- grid-template-columns: minmax(0, 1.35fr) minmax(16rem, 0.75fr);
195
- }
196
  }
197
 
198
  @media (max-width: 959px) {
199
  .admin-hero-frame {
200
  height: auto;
201
- min-height: 17rem;
202
- }
203
-
204
- .admin-hero-side {
205
- grid-template-columns: repeat(3, minmax(0, 1fr));
206
- }
207
- }
208
-
209
- @media (max-width: 720px) {
210
- .admin-hero-side {
211
- grid-template-columns: 1fr;
212
  }
213
  }
214
  </style>
 
78
  <div class="admin-hero-fallback absolute inset-0" />
79
  )}
80
  <div class="absolute inset-0 admin-hero-overlay" />
81
+ <div class="absolute inset-0 admin-hero-grid gap-5 p-6 text-white md:p-8">
82
+ <div class="admin-hero-copy flex max-w-3xl flex-col gap-4">
83
  <div class="inline-flex w-fit items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm font-medium backdrop-blur-md">
84
  <span class="h-2.5 w-2.5 rounded-full bg-[oklch(0.75_0.14_var(--hue))]" />
85
  Firefly Admin
 
93
  </p>
94
  </div>
95
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </div>
97
  </div>
98
  </div>
99
  </div>
100
  </section>
101
 
102
+ <main class="relative z-30 -mt-5 w-full px-2 pb-10 pt-0 md:-mt-6 md:px-4">
103
  <div class="mx-auto max-w-(--page-width)">
104
  <slot />
105
  </div>
 
119
 
120
  .admin-hero-frame {
121
  position: relative;
122
+ height: clamp(15rem, 26vw, 20rem);
123
  }
124
 
125
  .admin-hero-media {
 
144
  }
145
 
146
  .admin-hero-grid {
 
 
 
 
 
 
 
 
 
 
 
147
  display: flex;
148
+ align-items: flex-start;
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
 
151
+ .admin-hero-copy {
152
+ padding-top: clamp(0.5rem, 2vw, 1.25rem);
 
 
 
 
 
 
 
153
  }
154
 
155
  @media (max-width: 959px) {
156
  .admin-hero-frame {
157
  height: auto;
158
+ min-height: 14rem;
 
 
 
 
 
 
 
 
 
 
159
  }
160
  }
161
  </style>