Upload 439 files
Browse files- CONTRIBUTING.md +20 -0
- README.zh.md +301 -0
- _frontmatter.json +67 -0
- biome.json +64 -0
- server/index.mjs +50 -4
- src/components/admin/AdminApp.svelte +52 -19
- src/pages/admin/index.astro +1 -1
- src/pages/cmsadmin/index.astro +1 -1
- storage/admin/config.json +5 -0
- vercel.json +39 -0
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing
|
| 2 |
+
|
| 3 |
+
Thank you for your interest in contributing!
|
| 4 |
+
|
| 5 |
+
## Before You Start
|
| 6 |
+
|
| 7 |
+
If you plan to make major changes (especially new features or design changes), please open an issue or discussion before starting work. This helps ensure your effort aligns with the project's direction.
|
| 8 |
+
|
| 9 |
+
## Submitting Code
|
| 10 |
+
|
| 11 |
+
Please keep each pull request focused on a single purpose. Avoid mixing unrelated changes in one PR, as this can make reviewing and merging code more difficult.
|
| 12 |
+
|
| 13 |
+
Please use the [Conventional Commits](https://www.conventionalcommits.org/) format for your commit messages whenever possible. This keeps our history clear and consistent.
|
| 14 |
+
|
| 15 |
+
Before submitting code, please run the appropriate commands to check for errors and format your code.
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
pnpm check
|
| 19 |
+
pnpm format
|
| 20 |
+
```
|
README.zh.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<img src="./docs/images/1131.png" width = "350" height = "500" alt="Firefly" align=right />
|
| 3 |
+
|
| 4 |
+
<div align="center">
|
| 5 |
+
|
| 6 |
+
# 流萤 / Firefly
|
| 7 |
+
> 一款清新美观的 Astro 静态博客主题模板
|
| 8 |
+
>
|
| 9 |
+
> 
|
| 10 |
+

|
| 11 |
+

|
| 12 |
+

|
| 13 |
+
>
|
| 14 |
+
> [](https://github.com/CuteLeaf/Firefly/stargazers)
|
| 15 |
+
[](https://github.com/CuteLeaf/Firefly/network/members)
|
| 16 |
+
[](https://github.com/CuteLeaf/Firefly/issues)
|
| 17 |
+
>
|
| 18 |
+
> [](https://ko-fi.com/Z8Z41NQALY)
|
| 19 |
+
>
|
| 20 |
+
> 
|
| 21 |
+
[](https://deepwiki.com/CuteLeaf/Firefly)
|
| 22 |
+
[](https://afdian.com/a/cuteleaf)
|
| 23 |
+
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
📖 README:
|
| 29 |
+
**[简体中文](README.zh.md)** | **[繁體中文](docs/README.zh-TW.md)** | **[English](README.md)** | **[日本語](docs/README.ja.md)** | **[Русский](docs/README.ru.md)**
|
| 30 |
+
|
| 31 |
+
🚀 快速指南:
|
| 32 |
+
[**🖥️在线预览**](https://firefly.cuteleaf.cn/) /
|
| 33 |
+
[**📝使用文档**](https://docs-firefly.cuteleaf.cn/) /
|
| 34 |
+
[**🍀我的博客**](https://blog.cuteleaf.cn)
|
| 35 |
+
|
| 36 |
+
⚡ 静态站点生成: 基于Astro的超快加载速度和SEO优化
|
| 37 |
+
|
| 38 |
+
🎨 现代化设计: 简洁美观的界面,支持自定义主题色
|
| 39 |
+
|
| 40 |
+
📱 移动友好: 完美的响应式体验,移动端专项优化
|
| 41 |
+
|
| 42 |
+
🔧 高度可配置: 大部分功能模块均可通过配置文件自定义
|
| 43 |
+
|
| 44 |
+
<img alt="firefly" src="./docs/images/1.webp" />
|
| 45 |
+
<img alt="Lighthouse" src="./docs/images/Lighthouse.png" />
|
| 46 |
+
|
| 47 |
+
>[!TIP]
|
| 48 |
+
>
|
| 49 |
+
>Firefly 是一款基于 Astro 框架和 Fuwari 模板开发的清新美观且现代化个人博客主题模板,专为技术爱好者和内容创作者设计。该主题融合了现代 Web 技术栈,提供了丰富的功能模块和高度可定制的界面,让您能够轻松打造出专业且美观的个人博客网站。
|
| 50 |
+
>
|
| 51 |
+
>在重要的布局上,Firefly 创新性地增加了左右双侧边栏、文章网格(多列)布局、瀑布流布局,增加了站点统计、日历组件、文章目录等小组件,让侧边栏更加丰富,同时也保留了原版 fuwari 的布局,可根据自己的喜好在配置文件中自由切换。
|
| 52 |
+
>
|
| 53 |
+
>**更多布局配置及演示请查看:[Firefly 布局系统详解](https://firefly.cuteleaf.cn/posts/firefly-layout-system/)**
|
| 54 |
+
>
|
| 55 |
+
>Firefly 支持i18n多语言切换,但除了简体中文,其他语言均为AI翻译转换,如有错误,欢迎提交 [Pull Request](https://github.com/CuteLeaf/Firefly/pulls) 修正。
|
| 56 |
+
|
| 57 |
+
## ✨ 功能特性
|
| 58 |
+
|
| 59 |
+
### 核心功能
|
| 60 |
+
|
| 61 |
+
- [x] **Astro + Tailwind CSS** - 基于现代技术栈的超快静态站点生成
|
| 62 |
+
- [x] **流畅动画** - Swup 页面过渡动画,提供丝滑的浏览体验
|
| 63 |
+
- [x] **响应式设计** - 完美适配桌面端、平板和移动设备
|
| 64 |
+
- [x] **多语言支持** - i18n 国际化,支持简体中文、繁体中文、英文、日文、俄语
|
| 65 |
+
- [x] **全文搜索** - 基于 Pagefind 的客户端搜索,支持文章内容索引
|
| 66 |
+
|
| 67 |
+
### 个性化
|
| 68 |
+
- [x] **动态侧边栏** - 支持配置单侧边栏、双侧边栏
|
| 69 |
+
- [x] **文章布局** - 支持配置(单列)列表、网格(多列/瀑布流)布局
|
| 70 |
+
- [x] **字体管理** - 支持自定义字体,丰富的字体选择器
|
| 71 |
+
- [x] **页脚配置** - HTML 内容注入,完全自定义
|
| 72 |
+
- [x] **亮暗色模式** - 支持亮色/暗色/跟随系统三种模式
|
| 73 |
+
- [x] **导航栏自定义** - Logo、标题、链接全面自定义
|
| 74 |
+
- [x] **壁纸模式切换** - 横幅壁纸、全屏透明壁纸、纯色背景
|
| 75 |
+
- [x] **主题色自定义** - 360° 色相调节
|
| 76 |
+
|
| 77 |
+
### 页面组件
|
| 78 |
+
|
| 79 |
+
- [x] **留言板** - 支持留言页面
|
| 80 |
+
- [x] **公告栏** - 支持侧边栏公告提示
|
| 81 |
+
- [x] **看板娘** - 支持 Spine 和 Live2D 两种动画引擎
|
| 82 |
+
- [x] **站点统计** - 显示文章、分类、标签数目、文章总字数等数据
|
| 83 |
+
- [x] **站点日历** - 显示当月日历,以及当月的发布文章
|
| 84 |
+
- [x] **赞助页面** - 赞助链接跳转、收款码展示、赞助者列表、文章内赞助按钮
|
| 85 |
+
- [x] **分享海报** - 支持生成精美的文章分享海报
|
| 86 |
+
- [x] **樱花特效** - 支持樱花特效,全屏樱花效果
|
| 87 |
+
- [x] **友情链接** - 精美的友情链接展示页面
|
| 88 |
+
- [x] **广告组件** - 支持自定义侧边栏广告内容
|
| 89 |
+
- [x] **番组计划** - 基于 Bangumi API 的追番和游戏等记录展示
|
| 90 |
+
- [x] **评论系统** - 集成 Twikoo、Waline、Giscus、Disqus、Artalk 评论系统
|
| 91 |
+
- [x] **访问量统计** - 支持调用 Waline、Twikoo 自带的访问量追踪
|
| 92 |
+
- [x] **音乐播放器** - Material Design 3 设计风格的音乐播放器
|
| 93 |
+
|
| 94 |
+
### 内容增强
|
| 95 |
+
- [x] **图片灯箱** - Fancybox 图片预览功能
|
| 96 |
+
- [x] **浮动目录** - 动态显示文章目录,支持锚点跳转,在侧边栏目录隐藏后显示
|
| 97 |
+
- [x] **邮箱保护** - 让自动化爬虫程序无法直接爬到邮箱地址,被垃圾邮件骚扰
|
| 98 |
+
- [x] **侧边栏目录** - 动态显示文章目录,支持锚点跳转
|
| 99 |
+
- [x] **增强代码块** - 基于 Expressive Code,支持代码折叠、行号、语言标识
|
| 100 |
+
- [x] **数学公式支持** - KaTeX 渲染引擎,支持行内和块级公式
|
| 101 |
+
- [x] **文章随机封面图** - 支持通过 API 获取随机封面图
|
| 102 |
+
- [x] **Markdown扩展** - 更多的 Markdown 扩展语法
|
| 103 |
+
|
| 104 |
+
### SEO
|
| 105 |
+
- [x] **SEO 优化** - 完整的 meta 标签和结构化数据
|
| 106 |
+
- [x] **RSS 订阅** - 自动生成 RSS Feed
|
| 107 |
+
- [x] **站点地图** - 自动生成 XML Sitemap,支持页面过滤配置
|
| 108 |
+
- [x] **统计分析** - 集成 Google Analytics、Microsoft Clarity
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
如果你有好用的功能和优化,请提交 [Pull Request](https://github.com/CuteLeaf/Firefly/pulls)
|
| 112 |
+
|
| 113 |
+
## 🚀 快速开始
|
| 114 |
+
|
| 115 |
+
### 环境要求
|
| 116 |
+
|
| 117 |
+
- Node.js ≤ 22
|
| 118 |
+
- pnpm ≤ 9
|
| 119 |
+
|
| 120 |
+
### 本地开发部署
|
| 121 |
+
|
| 122 |
+
1. **克隆仓库:**
|
| 123 |
+
```bash
|
| 124 |
+
git clone https://github.com/Cuteleaf/Firefly.git
|
| 125 |
+
cd Firefly
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
**先 [Fork](https://github.com/CuteLeaf/Firefly/fork) 到自己仓库在克隆(推荐),记得先点 Star 在 Fork 哦!**
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
git clone https://github.com/you-github-name/Firefly.git
|
| 132 |
+
cd Firefly
|
| 133 |
+
```
|
| 134 |
+
3. **安装依赖:**
|
| 135 |
+
```bash
|
| 136 |
+
# 如果没有安装 pnpm,先安装
|
| 137 |
+
npm install -g pnpm
|
| 138 |
+
|
| 139 |
+
# 安装项目依赖
|
| 140 |
+
pnpm install
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
4. **配置博客:**
|
| 144 |
+
- 编辑 `src/config/` 目录下的配置文件自定义博客设置
|
| 145 |
+
|
| 146 |
+
5. **启动开发服务器:**
|
| 147 |
+
```bash
|
| 148 |
+
pnpm dev
|
| 149 |
+
```
|
| 150 |
+
博客将在 `http://localhost:4321` 可用
|
| 151 |
+
|
| 152 |
+
### 平台托管部署
|
| 153 |
+
- **参考[官方指南](https://docs.astro.build/zh-cn/guides/deploy/)将博客部署至 Vercel, Netlify, GitHub Pages, Cloudflare Pages, EdgeOne Pages 等。**
|
| 154 |
+
|
| 155 |
+
框架预设: `Astro`
|
| 156 |
+
|
| 157 |
+
根目录: `./`
|
| 158 |
+
|
| 159 |
+
输出目录: `dist`
|
| 160 |
+
|
| 161 |
+
构建命令: `pnpm run build`
|
| 162 |
+
|
| 163 |
+
安装命令: `pnpm install`
|
| 164 |
+
|
| 165 |
+
## 📖 配置说明
|
| 166 |
+
|
| 167 |
+
> 📚 **详细配置文档**: 查看 [Firefly使用文档](https://docs-firefly.cuteleaf.cn/) 获取完整的配置指南
|
| 168 |
+
|
| 169 |
+
### 设置网站语言
|
| 170 |
+
|
| 171 |
+
要设置博客的默认语言,请编辑 `src/config/siteConfig.ts` 文件:
|
| 172 |
+
|
| 173 |
+
```typescript
|
| 174 |
+
// 定义站点语言
|
| 175 |
+
const SITE_LANG = "zh_CN";
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
**支持的语言代码:**
|
| 179 |
+
- `zh_CN` - 简体中文
|
| 180 |
+
- `zh_TW` - 繁体中文
|
| 181 |
+
- `en` - 英文
|
| 182 |
+
- `ja` - 日文
|
| 183 |
+
- `ru` - 俄文
|
| 184 |
+
|
| 185 |
+
### 配置文件结构
|
| 186 |
+
|
| 187 |
+
```
|
| 188 |
+
src/
|
| 189 |
+
├── config/
|
| 190 |
+
│ ├── index.ts # 配置索引文件
|
| 191 |
+
│ ├── siteConfig.ts # 站点基础配置
|
| 192 |
+
│ ├── backgroundWallpaper.ts # 背景壁纸配置
|
| 193 |
+
│ ├── profileConfig.ts # 用户资料配置
|
| 194 |
+
│ ├── commentConfig.ts # 评论系统配置
|
| 195 |
+
│ ├── announcementConfig.ts # 公告配置
|
| 196 |
+
│ ├── licenseConfig.ts # 许可证配置
|
| 197 |
+
│ ├── footerConfig.ts # 页脚配置
|
| 198 |
+
│ ├── FooterConfig.html # 页脚HTML内容
|
| 199 |
+
│ ├── expressiveCodeConfig.ts # 代码高亮配置
|
| 200 |
+
│ ├── sakuraConfig.ts # 樱花特效配置
|
| 201 |
+
│ ├── fontConfig.ts # 字体配置
|
| 202 |
+
│ ├── sidebarConfig.ts # 侧边栏布局配置
|
| 203 |
+
│ ├── navBarConfig.ts # 导航栏配置
|
| 204 |
+
│ ├── musicConfig.ts # 音乐播放器配置
|
| 205 |
+
│ ├── pioConfig.ts # 看板娘配置
|
| 206 |
+
│ ├── adConfig.ts # 广告配置
|
| 207 |
+
│ ├── friendsConfig.ts # 友链配置
|
| 208 |
+
│ ├── sponsorConfig.ts # 赞助配置
|
| 209 |
+
│ └── coverImageConfig.ts # 文章封面图配置
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
## ⚙️ 文章 Frontmatter
|
| 213 |
+
|
| 214 |
+
```yaml
|
| 215 |
+
---
|
| 216 |
+
title: My First Blog Post
|
| 217 |
+
published: 2023-09-09
|
| 218 |
+
description: This is the first post of my new Astro blog.
|
| 219 |
+
image: ./cover.jpg # 或使用 "api" 来启用随机封面图
|
| 220 |
+
tags: [Foo, Bar]
|
| 221 |
+
category: Front-end
|
| 222 |
+
draft: false
|
| 223 |
+
lang: zh-CN # 仅当文章语言与 `siteConfig.ts` 中的网站语言不同时需要设置
|
| 224 |
+
pinned: false # 置顶
|
| 225 |
+
comment: true # 是否允许评论
|
| 226 |
+
---
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
## 🧩 Markdown 扩展语法
|
| 230 |
+
|
| 231 |
+
除了 Astro 默认支持的 [GitHub Flavored Markdown](https://github.github.com/gfm/) 之外,还包含了一些额外的 Markdown 功能:
|
| 232 |
+
|
| 233 |
+
- 提醒块(Admonitions) - 支持 GitHub, Obsidian, VitePress 三种风格主题配置 ([预览和用法](https://firefly.cuteleaf.cn/posts/markdown-extended/))
|
| 234 |
+
- GitHub 仓库卡片 ([预览和用法](https://firefly.cuteleaf.cn/posts/markdown-extended/))
|
| 235 |
+
- 基于 Expressive Code 的增强代码块 ([预览](http://firefly.cuteleaf.cn/posts/code-examples/) / [文档](https://expressive-code.com/))
|
| 236 |
+
|
| 237 |
+
## 🧞 指令
|
| 238 |
+
|
| 239 |
+
下列指令均需要在项目根目录执行:
|
| 240 |
+
|
| 241 |
+
| Command | Action |
|
| 242 |
+
|:---------------------------|:----------------------------------------------------|
|
| 243 |
+
| `pnpm install` | 安装依赖 |
|
| 244 |
+
| `pnpm dev` | 在 `localhost:4321` 启动本地开发服务器 |
|
| 245 |
+
| `pnpm build` | 构建网站至 `./dist/` |
|
| 246 |
+
| `pnpm preview` | 本地预览已构建的网站 |
|
| 247 |
+
| `pnpm check` | 检查代码中的错误 |
|
| 248 |
+
| `pnpm format` | 使用Biome格式化您的代码 |
|
| 249 |
+
| `pnpm new-post <filename>` | 创建新文章 |
|
| 250 |
+
| `pnpm astro ...` | 执行 `astro add`, `astro check` 等指令 |
|
| 251 |
+
| `pnpm astro --help` | 显示 Astro CLI 帮助 |
|
| 252 |
+
|
| 253 |
+
## 🙏 致谢
|
| 254 |
+
|
| 255 |
+
- 非常感谢 [saicaca](https://github.com/saicaca) 开发的 [fuwari](https://github.com/saicaca/fuwari) 模板,Firefly 就是基于这个模板二次开发
|
| 256 |
+
- 参考了博主 [霞葉](https://kasuha.com) 分享的 [Bangumi 收藏展示](https://kasuha.com/posts/fuwari-enhance-ep2/) 和 [邮箱保护/图片标题](https://kasuha.com/posts/fuwari-enhance-ep1/) 方案
|
| 257 |
+
- 参考了 [Mizuki](https://github.com/matsuzaka-yuki/Mizuki) 的横幅标题/多级菜单导航栏/樱花特效/KaTeX/Fancybox方案
|
| 258 |
+
- 使用了 [Astro](https://astro.build) 和 [Tailwind CSS](https://tailwindcss.com) 构建
|
| 259 |
+
- 使用了b站up [公公的日常](https://space.bilibili.com/3546750017080050) 提供的Q版 `流萤` 看板娘切片数据模型
|
| 260 |
+
- 图标来自 [Iconify](https://iconify.design/)
|
| 261 |
+
- 流萤部分相关图片素材版权归游戏 [《崩坏:星穹铁道》](https://sr.mihoyo.com/) 开发商 [米哈游](https://www.mihoyo.com/) 所有
|
| 262 |
+
|
| 263 |
+
## 📝 许可协议
|
| 264 |
+
|
| 265 |
+
本项目遵循 [MIT license](https://mit-license.org/) 开源协议,详细查看 [LICENSE](./LICENSE) 文件
|
| 266 |
+
|
| 267 |
+
最初 Fork 自 [saicaca/fuwari](https://github.com/saicaca/fuwari),感谢原作者的贡献,原项目采用 [MIT license](https://mit-license.org/)
|
| 268 |
+
|
| 269 |
+
**版权声明:**
|
| 270 |
+
- Copyright (c) 2024 [saicaca](https://github.com/saicaca) - [fuwari](https://github.com/saicaca/fuwari)
|
| 271 |
+
- Copyright (c) 2025 [CuteLeaf](https://github.com/CuteLeaf) - [Firefly](https://github.com/CuteLeaf/Firefly)
|
| 272 |
+
|
| 273 |
+
根据 MIT 开源协议,你可以自由使用、修改、分发代码,但需保留上述版权声明。
|
| 274 |
+
|
| 275 |
+
## 🍀 贡献者
|
| 276 |
+
|
| 277 |
+
感谢以下贡献者对本项目做出的贡献,如有问题或建议,请提交 [Issue](https://github.com/CuteLeaf/Firefly/issues) 或 [Pull Request](https://github.com/CuteLeaf/Firefly/pulls)。
|
| 278 |
+
|
| 279 |
+
><a href="https://github.com/CuteLeaf/Firefly/graphs/contributors">
|
| 280 |
+
> <img src="https://contrib.rocks/image?repo=CuteLeaf/Firefly" />
|
| 281 |
+
></a>
|
| 282 |
+
|
| 283 |
+
感谢以下贡献者对原项目 [fuwari](https://github.com/saicaca/fuwari) 做出的贡献,为本项目奠定了基础。
|
| 284 |
+
|
| 285 |
+
><a href="https://github.com/saicaca/fuwari/graphs/contributors">
|
| 286 |
+
> <img src="https://contrib.rocks/image?repo=saicaca/fuwari" />
|
| 287 |
+
></a>
|
| 288 |
+
|
| 289 |
+
## ⭐ Star History
|
| 290 |
+
|
| 291 |
+
[](https://star-history.com/#CuteLeaf/Firefly&Date)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
| 295 |
+
<!-- prettier-ignore-start -->
|
| 296 |
+
<!-- markdownlint-disable -->
|
| 297 |
+
|
| 298 |
+
<!-- markdownlint-restore -->
|
| 299 |
+
<!-- prettier-ignore-end -->
|
| 300 |
+
|
| 301 |
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
_frontmatter.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://frontmatter.codes/frontmatter.schema.json",
|
| 3 |
+
"frontMatter.framework.id": "astro",
|
| 4 |
+
"frontMatter.preview.host": "http://localhost:4321",
|
| 5 |
+
"frontMatter.content.publicFolder": "public",
|
| 6 |
+
"frontMatter.content.pageFolders": [
|
| 7 |
+
{
|
| 8 |
+
"title": "posts",
|
| 9 |
+
"path": "[[workspace]]/src/content/posts"
|
| 10 |
+
}
|
| 11 |
+
],
|
| 12 |
+
"frontMatter.taxonomy.contentTypes": [
|
| 13 |
+
{
|
| 14 |
+
"name": "default",
|
| 15 |
+
"pageBundle": true,
|
| 16 |
+
"previewPath": "'blog'",
|
| 17 |
+
"filePrefix": null,
|
| 18 |
+
"clearEmpty": true,
|
| 19 |
+
"fields": [
|
| 20 |
+
{
|
| 21 |
+
"title": "title",
|
| 22 |
+
"name": "title",
|
| 23 |
+
"type": "string",
|
| 24 |
+
"single": true
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"title": "description",
|
| 28 |
+
"name": "description",
|
| 29 |
+
"type": "string"
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"title": "published",
|
| 33 |
+
"name": "published",
|
| 34 |
+
"type": "datetime",
|
| 35 |
+
"default": "{{now}}",
|
| 36 |
+
"isPublishDate": true
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"title": "preview",
|
| 40 |
+
"name": "image",
|
| 41 |
+
"type": "image",
|
| 42 |
+
"isPreviewImage": true
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"title": "tags",
|
| 46 |
+
"name": "tags",
|
| 47 |
+
"type": "list"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"title": "category",
|
| 51 |
+
"name": "category",
|
| 52 |
+
"type": "string"
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"title": "draft",
|
| 56 |
+
"name": "draft",
|
| 57 |
+
"type": "boolean"
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"title": "language",
|
| 61 |
+
"name": "language",
|
| 62 |
+
"type": "string"
|
| 63 |
+
}
|
| 64 |
+
]
|
| 65 |
+
}
|
| 66 |
+
]
|
| 67 |
+
}
|
biome.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
|
| 3 |
+
"vcs": {
|
| 4 |
+
"enabled": false,
|
| 5 |
+
"clientKind": "git",
|
| 6 |
+
"useIgnoreFile": false
|
| 7 |
+
},
|
| 8 |
+
"files": {
|
| 9 |
+
"ignoreUnknown": false,
|
| 10 |
+
"includes": [
|
| 11 |
+
"**",
|
| 12 |
+
"!**/src/**/*.css",
|
| 13 |
+
"!**/src/public/**/*",
|
| 14 |
+
"!**/dist/**/*",
|
| 15 |
+
"!**/node_modules/**/*",
|
| 16 |
+
"!**/src/constants/icons.ts"
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
"formatter": {
|
| 20 |
+
"enabled": true,
|
| 21 |
+
"indentStyle": "tab"
|
| 22 |
+
},
|
| 23 |
+
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
| 24 |
+
"linter": {
|
| 25 |
+
"enabled": true,
|
| 26 |
+
"rules": {
|
| 27 |
+
"recommended": true,
|
| 28 |
+
"style": {
|
| 29 |
+
"noParameterAssign": "error",
|
| 30 |
+
"useAsConstAssertion": "error",
|
| 31 |
+
"useDefaultParameterLast": "error",
|
| 32 |
+
"useEnumInitializers": "error",
|
| 33 |
+
"useSelfClosingElements": "error",
|
| 34 |
+
"useSingleVarDeclarator": "error",
|
| 35 |
+
"noUnusedTemplateLiteral": "error",
|
| 36 |
+
"useNumberNamespace": "error",
|
| 37 |
+
"noInferrableTypes": "error",
|
| 38 |
+
"noUselessElse": "error"
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
"javascript": {
|
| 43 |
+
"formatter": {
|
| 44 |
+
"quoteStyle": "double"
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
"overrides": [
|
| 48 |
+
{
|
| 49 |
+
"includes": ["**/*.svelte", "**/*.astro", "**/*.vue"],
|
| 50 |
+
"linter": {
|
| 51 |
+
"rules": {
|
| 52 |
+
"style": {
|
| 53 |
+
"useConst": "off",
|
| 54 |
+
"useImportType": "off"
|
| 55 |
+
},
|
| 56 |
+
"correctness": {
|
| 57 |
+
"noUnusedVariables": "off",
|
| 58 |
+
"noUnusedImports": "off"
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
]
|
| 64 |
+
}
|
server/index.mjs
CHANGED
|
@@ -150,6 +150,13 @@ function respondError(res, statusCode, message, details = undefined) {
|
|
| 150 |
});
|
| 151 |
}
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
function parseCookies(req) {
|
| 154 |
const header = req.headers.cookie || "";
|
| 155 |
return header
|
|
@@ -218,6 +225,12 @@ async function readJsonBody(req) {
|
|
| 218 |
return raw ? JSON.parse(raw) : {};
|
| 219 |
}
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
async function statSafe(filePath) {
|
| 222 |
try {
|
| 223 |
return await fs.stat(filePath);
|
|
@@ -462,6 +475,36 @@ function isAdminConfigured() {
|
|
| 462 |
return Boolean(process.env.ADMIN && process.env.PASSWORD);
|
| 463 |
}
|
| 464 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
async function walkFiles(dirPath) {
|
| 466 |
const stats = await statSafe(dirPath);
|
| 467 |
if (!stats?.isDirectory()) {
|
|
@@ -841,10 +884,7 @@ async function handleApiRequest(req, res, url) {
|
|
| 841 |
if (payload.username !== process.env.ADMIN || payload.password !== process.env.PASSWORD) {
|
| 842 |
return respondError(res, 401, "Invalid username or password");
|
| 843 |
}
|
| 844 |
-
|
| 845 |
-
setCookie(res, SESSION_COOKIE, token, {
|
| 846 |
-
maxAge: Math.floor(SESSION_TTL_MS / 1000),
|
| 847 |
-
});
|
| 848 |
return respondJson(res, 200, {
|
| 849 |
success: true,
|
| 850 |
authenticated: true,
|
|
@@ -1092,6 +1132,12 @@ async function handleRequest(req, res) {
|
|
| 1092 |
if (url.pathname.startsWith("/api/")) {
|
| 1093 |
return await handleApiRequest(req, res, url);
|
| 1094 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
if (url.pathname.startsWith(PUBLIC_MEDIA_ROUTE)) {
|
| 1096 |
const filePath = await resolveSourceStaticFile(
|
| 1097 |
PUBLIC_ADMIN_ASSETS_DIR,
|
|
|
|
| 150 |
});
|
| 151 |
}
|
| 152 |
|
| 153 |
+
function redirect(res, location, statusCode = 302) {
|
| 154 |
+
respond(res, statusCode, "", {
|
| 155 |
+
"Cache-Control": "no-store",
|
| 156 |
+
Location: location,
|
| 157 |
+
});
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
function parseCookies(req) {
|
| 161 |
const header = req.headers.cookie || "";
|
| 162 |
return header
|
|
|
|
| 225 |
return raw ? JSON.parse(raw) : {};
|
| 226 |
}
|
| 227 |
|
| 228 |
+
async function readFormBody(req) {
|
| 229 |
+
const raw = await readRequestBody(req);
|
| 230 |
+
const params = new URLSearchParams(raw);
|
| 231 |
+
return Object.fromEntries(params.entries());
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
async function statSafe(filePath) {
|
| 235 |
try {
|
| 236 |
return await fs.stat(filePath);
|
|
|
|
| 475 |
return Boolean(process.env.ADMIN && process.env.PASSWORD);
|
| 476 |
}
|
| 477 |
|
| 478 |
+
function completeAdminLogin(res, username) {
|
| 479 |
+
const token = createSession(String(username));
|
| 480 |
+
setCookie(res, SESSION_COOKIE, token, {
|
| 481 |
+
maxAge: Math.floor(SESSION_TTL_MS / 1000),
|
| 482 |
+
});
|
| 483 |
+
return token;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
function getAdminEntryPath(pathname = "/admin/login") {
|
| 487 |
+
return pathname.startsWith("/cmsadmin") ? "/cmsadmin/" : "/admin/";
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
async function handleAdminFormLogin(req, res, url) {
|
| 491 |
+
const entryPath = getAdminEntryPath(url.pathname);
|
| 492 |
+
if (!isAdminConfigured()) {
|
| 493 |
+
return redirect(res, entryPath + "?login=disabled");
|
| 494 |
+
}
|
| 495 |
+
const contentType = String(req.headers["content-type"] || "");
|
| 496 |
+
const payload = contentType.includes("application/json")
|
| 497 |
+
? await readJsonBody(req)
|
| 498 |
+
: await readFormBody(req);
|
| 499 |
+
const username = String(payload.username || "");
|
| 500 |
+
const password = String(payload.password || "");
|
| 501 |
+
if (username !== process.env.ADMIN || password !== process.env.PASSWORD) {
|
| 502 |
+
return redirect(res, entryPath + "?login=invalid");
|
| 503 |
+
}
|
| 504 |
+
completeAdminLogin(res, username);
|
| 505 |
+
return redirect(res, entryPath);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
async function walkFiles(dirPath) {
|
| 509 |
const stats = await statSafe(dirPath);
|
| 510 |
if (!stats?.isDirectory()) {
|
|
|
|
| 884 |
if (payload.username !== process.env.ADMIN || payload.password !== process.env.PASSWORD) {
|
| 885 |
return respondError(res, 401, "Invalid username or password");
|
| 886 |
}
|
| 887 |
+
completeAdminLogin(res, payload.username);
|
|
|
|
|
|
|
|
|
|
| 888 |
return respondJson(res, 200, {
|
| 889 |
success: true,
|
| 890 |
authenticated: true,
|
|
|
|
| 1132 |
if (url.pathname.startsWith("/api/")) {
|
| 1133 |
return await handleApiRequest(req, res, url);
|
| 1134 |
}
|
| 1135 |
+
if (
|
| 1136 |
+
(url.pathname === "/admin/login" || url.pathname === "/cmsadmin/login") &&
|
| 1137 |
+
req.method === "POST"
|
| 1138 |
+
) {
|
| 1139 |
+
return await handleAdminFormLogin(req, res, url);
|
| 1140 |
+
}
|
| 1141 |
if (url.pathname.startsWith(PUBLIC_MEDIA_ROUTE)) {
|
| 1142 |
const filePath = await resolveSourceStaticFile(
|
| 1143 |
PUBLIC_ADMIN_ASSETS_DIR,
|
src/components/admin/AdminApp.svelte
CHANGED
|
@@ -125,6 +125,7 @@
|
|
| 125 |
| "sakura";
|
| 126 |
|
| 127 |
export let initialSnapshot: AdminSnapshot;
|
|
|
|
| 128 |
|
| 129 |
const navItems: Array<{ key: NavKey; label: string; hint: string }> = [
|
| 130 |
{ key: "overview", label: "总览", hint: "状态、统计与快捷操作" },
|
|
@@ -281,7 +282,8 @@
|
|
| 281 |
};
|
| 282 |
}
|
| 283 |
|
| 284 |
-
let sessionLoading =
|
|
|
|
| 285 |
let authenticated = false;
|
| 286 |
let configured = true;
|
| 287 |
let username = "";
|
|
@@ -428,12 +430,24 @@
|
|
| 428 |
}
|
| 429 |
|
| 430 |
async function refreshSession() {
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
}
|
| 438 |
|
| 439 |
async function loadBuild() {
|
|
@@ -552,7 +566,24 @@
|
|
| 552 |
syncEditors();
|
| 553 |
}
|
| 554 |
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
try {
|
| 557 |
const response = await api<any>("/api/admin/login", {
|
| 558 |
method: "POST",
|
|
@@ -565,7 +596,7 @@
|
|
| 565 |
await loadAll();
|
| 566 |
showToast("success", "登录成功");
|
| 567 |
} catch (error) {
|
| 568 |
-
showToast("error", error instanceof Error ? error.message : "
|
| 569 |
}
|
| 570 |
}
|
| 571 |
|
|
@@ -742,6 +773,7 @@
|
|
| 742 |
}, 3000);
|
| 743 |
|
| 744 |
(async () => {
|
|
|
|
| 745 |
await refreshSession();
|
| 746 |
if (authenticated) {
|
| 747 |
await loadAll();
|
|
@@ -757,22 +789,23 @@
|
|
| 757 |
<div class={`admin-toast ${toast.tone}`}>{toast.text}</div>
|
| 758 |
{/if}
|
| 759 |
|
| 760 |
-
{#if
|
| 761 |
-
<div class="card-base admin-panel admin-empty">正在连接后台服务...</div>
|
| 762 |
-
{:else if !configured}
|
| 763 |
<div class="card-base admin-panel admin-empty">
|
| 764 |
<h2>后台尚未启用</h2>
|
| 765 |
<p>请在 Hugging Face Space 中配置环境变量 <code>ADMIN</code> 和 <code>PASSWORD</code> 后再访问管理页。</p>
|
| 766 |
</div>
|
| 767 |
{:else if !authenticated}
|
| 768 |
-
<
|
| 769 |
<div class="admin-login-badge">Firefly Admin</div>
|
| 770 |
<h2>登录后台</h2>
|
| 771 |
<p>使用 Space 环境变量中的管理员账号和密码进入管理页面。</p>
|
| 772 |
-
<input class="admin-input" placeholder="管理员账号" bind:value={loginForm.username} />
|
| 773 |
-
<input class="admin-input" type="password" placeholder="密码" bind:value={loginForm.password} />
|
| 774 |
-
<button class="btn-regular admin-button primary"
|
| 775 |
-
|
|
|
|
|
|
|
|
|
|
| 776 |
{:else}
|
| 777 |
<div class="admin-workbench">
|
| 778 |
<aside class="admin-sidebar">
|
|
@@ -1002,8 +1035,8 @@
|
|
| 1002 |
.admin-toast.error { background: color-mix(in oklch, oklch(0.68 0.18 25) 16%, var(--card-bg) 84%); }
|
| 1003 |
.admin-toast.info { background: color-mix(in oklch, var(--btn-regular-bg-hover) 35%, var(--card-bg) 65%); }
|
| 1004 |
.admin-sidebar-card h3, .admin-panel h3, .admin-panel h4, .admin-login h2 { margin: 0; }
|
| 1005 |
-
.admin-sidebar-card p, .admin-panel p, .admin-login p { margin: 0; color: rgba(0,0,0,.72); }
|
| 1006 |
-
:root.dark .admin-sidebar-card p, :root.dark .admin-panel p, :root.dark .admin-login p { color: rgba(255,255,255,.72); }
|
| 1007 |
.admin-quick-actions { display: flex; gap: 0.6rem; align-items: center; }
|
| 1008 |
.admin-quick-actions.wrap { flex-wrap: wrap; }
|
| 1009 |
.admin-button { min-height: 2.7rem; padding: 0.65rem 1rem; border-radius: 0.95rem; }
|
|
|
|
| 125 |
| "sakura";
|
| 126 |
|
| 127 |
export let initialSnapshot: AdminSnapshot;
|
| 128 |
+
export let entryPath = "/admin";
|
| 129 |
|
| 130 |
const navItems: Array<{ key: NavKey; label: string; hint: string }> = [
|
| 131 |
{ key: "overview", label: "总览", hint: "状态、统计与快捷操作" },
|
|
|
|
| 282 |
};
|
| 283 |
}
|
| 284 |
|
| 285 |
+
let sessionLoading = false;
|
| 286 |
+
let sessionRefreshing = false;
|
| 287 |
let authenticated = false;
|
| 288 |
let configured = true;
|
| 289 |
let username = "";
|
|
|
|
| 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);
|
| 437 |
+
configured = data.configured !== false;
|
| 438 |
+
username = String(data.username || "");
|
| 439 |
+
if (data.build) applyBuild(data);
|
| 440 |
+
} catch (error) {
|
| 441 |
+
authenticated = false;
|
| 442 |
+
configured = true;
|
| 443 |
+
showToast(
|
| 444 |
+
"error",
|
| 445 |
+
error instanceof Error ? error.message : "后台会话检查失败",
|
| 446 |
+
);
|
| 447 |
+
} finally {
|
| 448 |
+
sessionLoading = false;
|
| 449 |
+
sessionRefreshing = false;
|
| 450 |
+
}
|
| 451 |
}
|
| 452 |
|
| 453 |
async function loadBuild() {
|
|
|
|
| 566 |
syncEditors();
|
| 567 |
}
|
| 568 |
|
| 569 |
+
function applyLoginSearchFeedback() {
|
| 570 |
+
const currentUrl = new URL(window.location.href);
|
| 571 |
+
const loginStatus = currentUrl.searchParams.get("login");
|
| 572 |
+
if (loginStatus === "invalid") {
|
| 573 |
+
showToast("error", "账号或密码错误,请重新输入");
|
| 574 |
+
} else if (loginStatus === "disabled") {
|
| 575 |
+
showToast("error", "后台尚未启用,请先配置 ADMIN 和 PASSWORD");
|
| 576 |
+
}
|
| 577 |
+
if (!loginStatus) {
|
| 578 |
+
return;
|
| 579 |
+
}
|
| 580 |
+
currentUrl.searchParams.delete("login");
|
| 581 |
+
const nextPath = currentUrl.pathname + currentUrl.search + currentUrl.hash;
|
| 582 |
+
window.history.replaceState({}, "", nextPath || currentUrl.pathname);
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
async function handleLogin(event?: SubmitEvent) {
|
| 586 |
+
event?.preventDefault();
|
| 587 |
try {
|
| 588 |
const response = await api<any>("/api/admin/login", {
|
| 589 |
method: "POST",
|
|
|
|
| 596 |
await loadAll();
|
| 597 |
showToast("success", "登录成功");
|
| 598 |
} catch (error) {
|
| 599 |
+
showToast("error", error instanceof Error ? error.message : "\u767b\u5f55\u5931\u8d25");
|
| 600 |
}
|
| 601 |
}
|
| 602 |
|
|
|
|
| 773 |
}, 3000);
|
| 774 |
|
| 775 |
(async () => {
|
| 776 |
+
applyLoginSearchFeedback();
|
| 777 |
await refreshSession();
|
| 778 |
if (authenticated) {
|
| 779 |
await loadAll();
|
|
|
|
| 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" 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">登录</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">
|
|
|
|
| 1035 |
.admin-toast.error { background: color-mix(in oklch, oklch(0.68 0.18 25) 16%, var(--card-bg) 84%); }
|
| 1036 |
.admin-toast.info { background: color-mix(in oklch, var(--btn-regular-bg-hover) 35%, var(--card-bg) 65%); }
|
| 1037 |
.admin-sidebar-card h3, .admin-panel h3, .admin-panel h4, .admin-login h2 { margin: 0; }
|
| 1038 |
+
.admin-sidebar-card p, .admin-panel p, .admin-login p, .admin-login-status { margin: 0; color: rgba(0,0,0,.72); }
|
| 1039 |
+
:root.dark .admin-sidebar-card p, :root.dark .admin-panel p, :root.dark .admin-login p, :root.dark .admin-login-status { color: rgba(255,255,255,.72); }
|
| 1040 |
.admin-quick-actions { display: flex; gap: 0.6rem; align-items: center; }
|
| 1041 |
.admin-quick-actions.wrap { flex-wrap: wrap; }
|
| 1042 |
.admin-button { min-height: 2.7rem; padding: 0.65rem 1rem; border-radius: 0.95rem; }
|
src/pages/admin/index.astro
CHANGED
|
@@ -11,5 +11,5 @@ const initialSnapshot = getAdminSnapshot();
|
|
| 11 |
|
| 12 |
<AdminLayout title="博客后台管理" description="Firefly 博客后台管理面板">
|
| 13 |
<KatexManager slot="head" />
|
| 14 |
-
<AdminApp client:load initialSnapshot={initialSnapshot} />
|
| 15 |
</AdminLayout>
|
|
|
|
| 11 |
|
| 12 |
<AdminLayout title="博客后台管理" description="Firefly 博客后台管理面板">
|
| 13 |
<KatexManager slot="head" />
|
| 14 |
+
<AdminApp client:load initialSnapshot={initialSnapshot} entryPath="/admin" />
|
| 15 |
</AdminLayout>
|
src/pages/cmsadmin/index.astro
CHANGED
|
@@ -11,5 +11,5 @@ const initialSnapshot = getAdminSnapshot();
|
|
| 11 |
|
| 12 |
<AdminLayout title="博客后台管理" description="Firefly 博客后台管理面板">
|
| 13 |
<KatexManager slot="head" />
|
| 14 |
-
<AdminApp client:load initialSnapshot={initialSnapshot} />
|
| 15 |
</AdminLayout>
|
|
|
|
| 11 |
|
| 12 |
<AdminLayout title="博客后台管理" description="Firefly 博客后台管理面板">
|
| 13 |
<KatexManager slot="head" />
|
| 14 |
+
<AdminApp client:load initialSnapshot={initialSnapshot} entryPath="/cmsadmin" />
|
| 15 |
</AdminLayout>
|
storage/admin/config.json
CHANGED
|
@@ -165,6 +165,11 @@
|
|
| 165 |
"preset": "about"
|
| 166 |
}
|
| 167 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
]
|
| 170 |
},
|
|
|
|
| 165 |
"preset": "about"
|
| 166 |
}
|
| 167 |
]
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"name": "Admin",
|
| 171 |
+
"url": "/admin",
|
| 172 |
+
"external": false
|
| 173 |
}
|
| 174 |
]
|
| 175 |
},
|
vercel.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"buildCommand": "pnpm build",
|
| 3 |
+
"outputDirectory": "dist",
|
| 4 |
+
"installCommand": "pnpm install",
|
| 5 |
+
"framework": "astro",
|
| 6 |
+
"headers": [
|
| 7 |
+
{
|
| 8 |
+
"source": "/(.*)",
|
| 9 |
+
"headers": [
|
| 10 |
+
{
|
| 11 |
+
"key": "X-Content-Type-Options",
|
| 12 |
+
"value": "nosniff"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"key": "X-Frame-Options",
|
| 16 |
+
"value": "DENY"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"key": "X-XSS-Protection",
|
| 20 |
+
"value": "1; mode=block"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"key": "Referrer-Policy",
|
| 24 |
+
"value": "strict-origin-when-cross-origin"
|
| 25 |
+
}
|
| 26 |
+
]
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"source": "/_astro/(.*)",
|
| 30 |
+
"headers": [
|
| 31 |
+
{
|
| 32 |
+
"key": "Cache-Control",
|
| 33 |
+
"value": "public, max-age=31536000, immutable"
|
| 34 |
+
}
|
| 35 |
+
]
|
| 36 |
+
}
|
| 37 |
+
],
|
| 38 |
+
"cleanUrls": true
|
| 39 |
+
}
|