Init v3.2
commit
6e70a1d4df
|
@ -0,0 +1,14 @@
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw*
|
||||||
|
web
|
||||||
|
images
|
||||||
|
cache
|
||||||
|
go_lib
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 xusenlin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Blog
|
||||||
|
|
||||||
|
> Blog 是基于 go 语言开发的,不依赖第三方 go 库,适合用来学习和展示 markdown 文档的精美博客。
|
||||||
|
|
||||||
|
|
||||||
|
:chestnut: [JiXieShi](http://www.starss.cc/) (个人博客,正在使用)
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目背景
|
||||||
|
博客从最早的静态 Html+Css 主页到 Wordpress 、Typecho 、Hexo 、Hugo 一路尝试过来,虽然他们都是很优秀的开源项目,但是没有一款符合我内心真正的诉求。
|
||||||
|
他们有的要么太重了要么部署不方便要么对存储的文章不友好,我就放弃了之前在Typecho上的文章,同时调整分类也不那么灵活,有的迁移也不方便。
|
||||||
|
自己太懒,不想自己做文章备份,不想安装数据库,快速响应也是必须的,所以才有了用 GOLANG 来写一个简单博客的想法。
|
||||||
|
当然,自己的这个项目并不能和这些优秀的开源项目相比较,它只是符合我自己的诉求顺便用来学习GOLANG的项目。(写文章也能增加github的活跃度,逃 :))
|
||||||
|
|
||||||
|
## Blog 在做什么
|
||||||
|
|
||||||
|
|
||||||
|
简单来说,Blog 会使用 git 将你的 .md 文档仓库克隆到当前目录下, 然后并行加载和解析文章关键信息并生成内容,呈现一个博客站点。
|
||||||
|
当你向你的 git 仓库提交文章的时候,你可以设置 webHooks 来自动通知 Blog 更新同步文章,或者可以在仪表盘里手动更新文章。
|
||||||
|
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
你可以克隆当前仓库,然后在 JSON 文件里面配置好你文档的 Git 地址和 WebHook 秘钥,编译源代码并放入后台运行。
|
||||||
|
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
- 配置好 config.json 文件,详细的配置说明文件在当前仓库根目录的《配置说明.md》
|
||||||
|
- 新建一个 git 仓库,用来存放你的文章,但是对你的目录结构有一些要求
|
||||||
|
如下:
|
||||||
|
```
|
||||||
|
|-- assets //博客静态文件,存放一些图片资源,方便显示到文档里
|
||||||
|
|-- content
|
||||||
|
| |-- GOLANG //分类目录
|
||||||
|
| |-- GOLANG基础 // 子分类目录,但是在页面上不会产生分类目录
|
||||||
|
| |--- GOLANG基础语法.md
|
||||||
|
| |-- 其他分类
|
||||||
|
| |--- xxx.md
|
||||||
|
|-- extra_nav // Blog只会有 Blog Categories 两个导航,其他导航可以放到这个目录下,比如自己的简历,作品之类的
|
||||||
|
|
||||||
|
```
|
||||||
|
content目录下的一级目录代表一个分类,如果一级目录下有子级目录也不会产生新的分类,子级目录的文档也会属于一级目录的分类。
|
||||||
|
|
||||||
|
- 每一篇文章的开头可以配置一些文章的属性,具体可以看 配置说明.md
|
||||||
|
如下:
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"date":"2019.01.02 14:33",//最少需要
|
||||||
|
"tags": ["BLOG","其它tag"],//可以不填,不过最好添加一些tag,后面可以做一些好玩的东西。
|
||||||
|
"title": "文章的标题,一般不用填写,默认使用文件名",
|
||||||
|
"description": "文章描述,不填写自动取正文200个字,可以在app.json中配置",
|
||||||
|
"author": "JiXieShi", //文章作者,可以不用填写,现在也没有使用到
|
||||||
|
"musicId":"网易云歌单ID" //文章的配歌
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
我会根据关键字```json来解析,不用担心这个会显示到文章内容里面,我在解析的时候就将它去掉了。
|
||||||
|
如果不再文章前面添加这一段json也是可以的,不过我不建议这么做,因为这样就和V1.0版本没什么区别了,没有了文章属性,排序都是个问题。
|
||||||
|
最后,mac markdown 文章编辑器推荐 Typora。
|
||||||
|
|
||||||
|
> 提示:```json 前面不能有其它字符。
|
||||||
|
|
||||||
|
- 切换主题色和搜索文章在仪表盘里,访问/admin 可以查看仪表盘,
|
||||||
|
如果不想让别人知道你的仪表盘地址可以自己配置dashboard字段。
|
||||||
|
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- [x] 1.移动端更好的适配
|
||||||
|
- [x] 2.根目录可以添加其他文件生成导航
|
||||||
|
- [x] 3.仪表盘支持切换主题色
|
||||||
|
- [x] 4.仪表盘支持搜索
|
||||||
|
- [x] 5.tags的展示
|
||||||
|
- [x] 6.仪表盘添加手动更新文章功能
|
||||||
|
- [x] 7.添加webHook支持(去掉自动更新)
|
||||||
|
- [x] 8.添加评论支持(在配置里开启,所有评论都会储存在仓库的Issues)
|
||||||
|
- [x] 9.支持网易云音乐
|
||||||
|
- [x] 10.支持自定义切换主题
|
||||||
|
|
||||||
|
> 后续尽善尽美之后,我可能会提供其他漂亮的主题皮肤,也欢迎大家参与进来。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
1. 响应迅速 ---没有什么依赖,得益于GOLANG的运行速度,部署在阿里云的博客平均响应在50毫秒内。
|
||||||
|
2. 迁移方便 ---GOLANG交叉编译可以方便的发布二进制文件到不同的操作系统,执行二进制文件并克隆博客文件即可运行你的博客。
|
||||||
|
3. 小巧精美 ---非常简单的代码方便学习和改造,即使有一天你厌倦Blog,你的文章也能很好的迁移和阅读。
|
||||||
|
4. 分类调整 ---随时调整你的文章分类,你可以把一个文件夹里所有的东西移动到其他分类里的某个地方,不管有多少层级,分类发生了什么翻天覆地的变化,下一次更新就能展示。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### V3.2
|
||||||
|
* 加入Tag搜索和展示
|
||||||
|
|
||||||
|
### V3.1
|
||||||
|
* 去掉标题的.MD后缀
|
||||||
|
* 网易云音乐改为左下悬浮,同时由音乐id改为歌单id
|
||||||
|
* 增加主题配置项(ps:虽然只有一个简单主题)
|
||||||
|
|
||||||
|
### V3.0
|
||||||
|
* 完全重构,添加了短链,美化了 URL
|
||||||
|
* 自动完成克隆工作
|
||||||
|
* 添加了网易云音乐功能
|
||||||
|
* 文件自动生成导航(导航排序通过时间可自定义)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 感谢
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © Richard McRichface
|
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"blog/models"
|
||||||
|
"blog/routes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
models.CompiledContent() //克隆或者更新文章、递归生成文章、导航、短链 Map、加载模板
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
routes.InitRoute()
|
||||||
|
fmt.Printf("Version:v%v \n", config.Cfg.Version)
|
||||||
|
fmt.Printf("ListenAndServe On Port %v \n", config.Cfg.Port)
|
||||||
|
fmt.Printf("Dashboard On Path %v \n", config.Cfg.Dashboard)
|
||||||
|
fmt.Printf("UpdateArticle's GitHookUrl: %v Secret: %v \n", config.Cfg.GitHookUrl, config.Cfg.WebHookSecret)
|
||||||
|
if err := http.ListenAndServe(":"+strconv.Itoa(config.Cfg.Port), nil); err != nil {
|
||||||
|
fmt.Println("ServeErr:", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"port": 8080,
|
||||||
|
"pageSize": 3,
|
||||||
|
"descriptionLen": 200,
|
||||||
|
"author": "JiXieShi",
|
||||||
|
"icp": "",
|
||||||
|
"webHookSecret": "jixieshi",
|
||||||
|
"categoryDisplayQuantity": 6,
|
||||||
|
"utterancesRepo": "jixishi/blog_docs",
|
||||||
|
"timeLayout": "2006.01.02 15:04",
|
||||||
|
"siteName": "JiXieShi's Blog",
|
||||||
|
"htmlKeywords": "forest blog,Golang,ARM,BE6,前端,硬件",
|
||||||
|
"htmlDescription": "JiXieShi's Personal blog",
|
||||||
|
"documentGitUrl": "https://github.com/jixishi/blog_docs.git",
|
||||||
|
"themePath": "/blog",
|
||||||
|
"themeColor": "#2196f3",
|
||||||
|
"dashboard": "adminjxs",
|
||||||
|
"themeOption": ["#673ab7","#f44336","#9c27b0","#2196f3","#607d8b","#795548"]
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/utils"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
userConfig
|
||||||
|
systemConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
var Cfg Config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
Cfg.CurrentDir, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile, err := ioutil.ReadFile(Cfg.CurrentDir + "/config.json")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(configFile, &Cfg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == Cfg.Dashboard ||
|
||||||
|
!strings.HasPrefix(Cfg.Dashboard, "/") {
|
||||||
|
Cfg.Dashboard = "/admin"
|
||||||
|
}
|
||||||
|
if "" == Cfg.ThemePath ||
|
||||||
|
!strings.HasPrefix(Cfg.ThemePath, "/") {
|
||||||
|
Cfg.ThemesDir = Cfg.CurrentDir + "/themes/blog" //+ Cfg.ThemePath
|
||||||
|
} else {
|
||||||
|
Cfg.ThemesDir = Cfg.CurrentDir + "/themes" + Cfg.ThemePath
|
||||||
|
}
|
||||||
|
repoName, err := utils.GetRepoName(Cfg.DocumentGitUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Cfg.AppName = "WebServer"
|
||||||
|
Cfg.Version = 3.2
|
||||||
|
Cfg.DocumentDir = Cfg.CurrentDir + "/" + repoName
|
||||||
|
Cfg.GitHookUrl = "/api/git_push_hook"
|
||||||
|
Cfg.AppRepository = "https://gitea.starss.cc/JiXieShi/Blog"
|
||||||
|
}
|
||||||
|
|
||||||
|
func Initial() {
|
||||||
|
if _, err := exec.LookPath("git"); err != nil {
|
||||||
|
fmt.Println("请先安装git")
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if !utils.IsDir(Cfg.DocumentDir) {
|
||||||
|
fmt.Println("正在克隆文档仓库,请稍等...")
|
||||||
|
out, err := utils.RunCmdByDir(Cfg.CurrentDir, "git", "clone", Cfg.DocumentGitUrl)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(out)
|
||||||
|
} else {
|
||||||
|
out, err := utils.RunCmdByDir(Cfg.DocumentDir, "git", "pull")
|
||||||
|
fmt.Println(out)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if err := checkDocDirAndBindConfig(&Cfg); err != nil {
|
||||||
|
fmt.Println("文档缺少必要的目录")
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgDir := Cfg.CurrentDir + "/images"
|
||||||
|
if !utils.IsDir(imgDir) {
|
||||||
|
if os.Mkdir(imgDir, os.ModePerm) != nil {
|
||||||
|
panic("生成images目录失败!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDocDirAndBindConfig(cfg *Config) error {
|
||||||
|
dirs := []string{"assets", "content", "extra_nav"}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
absoluteDir := Cfg.DocumentDir + "/" + dir
|
||||||
|
if !utils.IsDir(absoluteDir) {
|
||||||
|
return errors.New("documents cannot lack " + absoluteDir + " dir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.DocumentAssetsDir = cfg.DocumentDir + "/assets"
|
||||||
|
cfg.DocumentContentDir = cfg.DocumentDir + "/content"
|
||||||
|
cfg.DocumentExtraNavDir = cfg.DocumentDir + "/extra_nav"
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
type systemConfig struct {
|
||||||
|
AppName string
|
||||||
|
Version float32
|
||||||
|
CurrentDir string
|
||||||
|
ThemesDir string
|
||||||
|
GitHookUrl string
|
||||||
|
AppRepository string
|
||||||
|
DocumentDir string
|
||||||
|
DocumentAssetsDir string
|
||||||
|
DocumentContentDir string
|
||||||
|
DocumentExtraNavDir string
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
type userConfig struct {
|
||||||
|
SiteName string `json:"siteName"`
|
||||||
|
|
||||||
|
Author string `json:"author"`
|
||||||
|
|
||||||
|
Icp string `json:"icp"`
|
||||||
|
|
||||||
|
TimeLayout string `json:"timeLayout"`
|
||||||
|
|
||||||
|
Port int `json:"port"`
|
||||||
|
|
||||||
|
WebHookSecret string `json:"webHookSecret"`
|
||||||
|
|
||||||
|
CategoryDisplayQuantity int `json:"categoryDisplayQuantity"`
|
||||||
|
|
||||||
|
UtterancesRepo string `json:"utterancesRepo"`
|
||||||
|
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
|
||||||
|
DescriptionLen int `json:"descriptionLen"`
|
||||||
|
|
||||||
|
DocumentGitUrl string `json:"documentGitUrl"`
|
||||||
|
|
||||||
|
HtmlKeywords string `json:"htmlKeywords"`
|
||||||
|
|
||||||
|
HtmlDescription string `json:"htmlDescription"`
|
||||||
|
|
||||||
|
ThemePath string `json:"themePath"`
|
||||||
|
|
||||||
|
ThemeColor string `json:"themeColor"`
|
||||||
|
|
||||||
|
ThemeOption []string `json:"themeOption"`
|
||||||
|
|
||||||
|
Dashboard string `json:"dashboard"`
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/models"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Article(w http.ResponseWriter, r *http.Request) {
|
||||||
|
articleTemplate := models.Template.Article
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
articleTemplate.WriteError(w, err)
|
||||||
|
}
|
||||||
|
key := r.Form.Get("key")
|
||||||
|
|
||||||
|
path := models.ArticleShortUrlMap[key]
|
||||||
|
|
||||||
|
articleDetail, err := models.ReadArticleDetail(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
articleTemplate.WriteError(w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
articleTemplate.WriteData(w, models.BuildViewData("Article", articleDetail))
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"blog/models"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Category(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
categoriesTemplate := models.Template.Categories
|
||||||
|
|
||||||
|
result := models.GroupByCategory(&models.ArticleList, config.Cfg.CategoryDisplayQuantity)
|
||||||
|
|
||||||
|
categoriesTemplate.WriteData(w, models.BuildViewData("Categories", result))
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"blog/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
var dashboardMsg []string
|
||||||
|
dashboardTemplate := models.Template.Dashboard
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
dashboardTemplate.WriteError(w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
index, err := strconv.Atoi(r.Form.Get("theme"))
|
||||||
|
if err == nil && index < len(config.Cfg.ThemeOption) {
|
||||||
|
config.Cfg.ThemeColor = config.Cfg.ThemeOption[index]
|
||||||
|
dashboardMsg = append(dashboardMsg, "颜色切换成功!")
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.Form.Get("action")
|
||||||
|
if "updateArticle" == action {
|
||||||
|
models.CompiledContent()
|
||||||
|
dashboardMsg = append(dashboardMsg, "文章更新成功!")
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboardTemplate.WriteData(w, models.BuildViewData("Dashboard", map[string]interface{}{
|
||||||
|
"msg": dashboardMsg,
|
||||||
|
}))
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/models"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExtraNav(w http.ResponseWriter, r *http.Request) {
|
||||||
|
extraNavTemplate := models.Template.ExtraNav
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
extraNavTemplate.WriteError(w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := r.Form.Get("name")
|
||||||
|
for _, nav := range models.Navigation {
|
||||||
|
if nav.Title == name {
|
||||||
|
articleDetail, err := models.ReadArticleDetail(nav.Path)
|
||||||
|
if err != nil {
|
||||||
|
extraNavTemplate.WriteError(w, err)
|
||||||
|
}
|
||||||
|
extraNavTemplate.WriteData(w, models.BuildViewData(nav.Title, articleDetail))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"blog/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Index(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
indexTemplate := models.Template.Index
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
indexTemplate.WriteError(w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(r.Form.Get("page"))
|
||||||
|
if err != nil {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
articles := models.ArticleList
|
||||||
|
|
||||||
|
search := r.Form.Get("search")
|
||||||
|
category := r.Form.Get("category")
|
||||||
|
tag := r.Form.Get("tag")
|
||||||
|
|
||||||
|
if search != "" || category != "" || tag != "" {
|
||||||
|
articles = models.ArticleSearch(&articles, search, category, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := models.Pagination(&articles, page, config.Cfg.PageSize)
|
||||||
|
|
||||||
|
indexTemplate.WriteData(w, models.BuildViewData("Blog", result))
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/models"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GithubHook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
//err := r.ParseForm()
|
||||||
|
//if err != nil {
|
||||||
|
// SedResponse(w, err.Error())
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//if "" == config.Cfg.WebHookSecret || "push" != r.Header.Get("x-github-event") {
|
||||||
|
// SedResponse(w, "No Configuration WebHookSecret Or Not Pushing Events")
|
||||||
|
// log.Println("No Configuration WebHookSecret Or Not Pushing Events")
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//sign := r.Header.Get("X-Hub-Signature")
|
||||||
|
//
|
||||||
|
//bodyContent, err := ioutil.ReadAll(r.Body)
|
||||||
|
//
|
||||||
|
//if err != nil {
|
||||||
|
// SedResponse(w, err.Error())
|
||||||
|
// log.Println("WebHook err:" + err.Error())
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//if err = r.Body.Close(); err != nil {
|
||||||
|
// SedResponse(w, err.Error())
|
||||||
|
// log.Println("WebHook err:" + err.Error())
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//mac := hmac.New(sha1.New, []byte(config.Cfg.WebHookSecret))
|
||||||
|
//mac.Write(bodyContent)
|
||||||
|
//expectedHash := "sha1=" + hex.EncodeToString(mac.Sum(nil))
|
||||||
|
//
|
||||||
|
//if sign != expectedHash {
|
||||||
|
// SedResponse(w, "WebHook err:Signature does not match")
|
||||||
|
// log.Println("WebHook err:Signature does not match")
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
|
||||||
|
SedResponse(w, "ok")
|
||||||
|
|
||||||
|
models.CompiledContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SedResponse(w http.ResponseWriter, msg string) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
_, err := w.Write([]byte(`{"msg": "` + msg + `"}`))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||||
|
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
@ -0,0 +1,238 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"blog/utils"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Time time.Time
|
||||||
|
|
||||||
|
type Article struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Date Time `json:"date"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
MusicId string `json:"musicId"`
|
||||||
|
Path string
|
||||||
|
ShortUrl string
|
||||||
|
Category string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Articles []Article
|
||||||
|
|
||||||
|
type ArticleDetail struct {
|
||||||
|
Article
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
func initArticlesAndImages(dir string) (Articles, map[string]string, error) {
|
||||||
|
var articles Articles
|
||||||
|
shortUrlMap := make(map[string]string)
|
||||||
|
|
||||||
|
articles, err := RecursiveReadArticles(dir)
|
||||||
|
if err != nil {
|
||||||
|
return articles, shortUrlMap, err
|
||||||
|
}
|
||||||
|
sort.Sort(articles)
|
||||||
|
for i := len(articles) - 1; i >= 0; i-- {
|
||||||
|
//这里必须使用倒序的方式生成 shortUrl,因为如果有相同的文章标题,
|
||||||
|
// 倒序会将最老的文章优先生成shortUrl,保证和之前的 shortUrl一样
|
||||||
|
article := articles[i]
|
||||||
|
keyword := utils.GenerateShortUrl(article.Title, func(url, keyword string) bool {
|
||||||
|
//保证 keyword 唯一
|
||||||
|
_, ok := shortUrlMap[keyword]
|
||||||
|
return !ok
|
||||||
|
})
|
||||||
|
articles[i].ShortUrl = keyword
|
||||||
|
shortUrlMap[keyword] = article.Path
|
||||||
|
}
|
||||||
|
return articles, shortUrlMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArticleSearch(articles *Articles, search string, category string, tag string) Articles {
|
||||||
|
|
||||||
|
var articleList Articles
|
||||||
|
for _, article := range *articles {
|
||||||
|
|
||||||
|
pass := true
|
||||||
|
|
||||||
|
if search != "" && strings.Index(article.Title, search) == -1 {
|
||||||
|
pass = false
|
||||||
|
}
|
||||||
|
if category != "" && strings.Index(article.Category, category) == -1 {
|
||||||
|
pass = false
|
||||||
|
}
|
||||||
|
if tag != "" {
|
||||||
|
pass = false
|
||||||
|
for _, tagx := range article.Tags {
|
||||||
|
if strings.Index(tagx, tag) != -1 {
|
||||||
|
pass = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pass {
|
||||||
|
articleList = append(articleList, article)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return articleList
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecursiveReadArticles(dir string) (Articles, error) {
|
||||||
|
|
||||||
|
var articles Articles
|
||||||
|
|
||||||
|
dirInfo, err := os.Stat(dir)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return articles, err
|
||||||
|
}
|
||||||
|
if !dirInfo.IsDir() {
|
||||||
|
return articles, errors.New("目标不是一个目录")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileOrDir, err := ioutil.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return articles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fileInfo := range fileOrDir {
|
||||||
|
name := fileInfo.Name()
|
||||||
|
path := dir + "/" + name
|
||||||
|
upperName := strings.ToUpper(name)
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
subArticles, err := RecursiveReadArticles(path)
|
||||||
|
if err != nil {
|
||||||
|
return articles, err
|
||||||
|
}
|
||||||
|
articles = append(articles, subArticles...)
|
||||||
|
} else if strings.HasSuffix(upperName, ".MD") {
|
||||||
|
article, err := ReadArticle(path)
|
||||||
|
if err != nil {
|
||||||
|
return articles, err
|
||||||
|
}
|
||||||
|
articles = append(articles, article)
|
||||||
|
} else if strings.HasSuffix(upperName, ".PNG") ||
|
||||||
|
strings.HasSuffix(upperName, ".GIF") ||
|
||||||
|
strings.HasSuffix(upperName, ".JPG") {
|
||||||
|
|
||||||
|
dst := config.Cfg.CurrentDir + "/images/" + name
|
||||||
|
fmt.Println(utils.IsFile(dst))
|
||||||
|
if !utils.IsFile(dst) {
|
||||||
|
_, _ = utils.CopyFile(path, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return articles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadArticle(path string) (Article, error) {
|
||||||
|
article, _, err := readMarkdown(path)
|
||||||
|
if err != nil {
|
||||||
|
return article, err
|
||||||
|
}
|
||||||
|
return article, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadArticleDetail(path string) (ArticleDetail, error) {
|
||||||
|
_, articleDetail, err := readMarkdown(path)
|
||||||
|
if err != nil {
|
||||||
|
return articleDetail, err
|
||||||
|
}
|
||||||
|
return articleDetail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMarkdown(path string) (Article, ArticleDetail, error) {
|
||||||
|
var article Article
|
||||||
|
var articleDetail ArticleDetail
|
||||||
|
mdFile, err := os.Stat(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return article, articleDetail, err
|
||||||
|
}
|
||||||
|
if mdFile.IsDir() {
|
||||||
|
return article, articleDetail, errors.New("this path is Dir")
|
||||||
|
}
|
||||||
|
markdown, err := ioutil.ReadFile(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return article, articleDetail, err
|
||||||
|
}
|
||||||
|
markdown = bytes.TrimSpace(markdown)
|
||||||
|
|
||||||
|
article.Path = path
|
||||||
|
article.Category = GetCategoryName(path)
|
||||||
|
article.Title = strings.TrimSuffix(strings.ToUpper(mdFile.Name()), ".MD")
|
||||||
|
article.Date = Time(mdFile.ModTime())
|
||||||
|
|
||||||
|
if !bytes.HasPrefix(markdown, []byte("```json")) {
|
||||||
|
article.Description = cropDesc(markdown)
|
||||||
|
articleDetail.Article = article
|
||||||
|
articleDetail.Body = string(markdown)
|
||||||
|
return article, articleDetail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown = bytes.Replace(markdown, []byte("```json"), []byte(""), 1)
|
||||||
|
markdownArrInfo := bytes.SplitN(markdown, []byte("```"), 2)
|
||||||
|
|
||||||
|
article.Description = cropDesc(markdownArrInfo[1])
|
||||||
|
|
||||||
|
if err := json.Unmarshal(bytes.TrimSpace(markdownArrInfo[0]), &article); err != nil {
|
||||||
|
article.Title = "文章[" + article.Title + "]解析 JSON 出错,请检查。"
|
||||||
|
article.Description = err.Error()
|
||||||
|
return article, articleDetail, nil
|
||||||
|
}
|
||||||
|
article.Path = path
|
||||||
|
article.Title = strings.ToUpper(article.Title)
|
||||||
|
|
||||||
|
articleDetail.Article = article
|
||||||
|
articleDetail.Body = string(markdownArrInfo[1])
|
||||||
|
return article, articleDetail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cropDesc(c []byte) string {
|
||||||
|
content := []rune(string(c))
|
||||||
|
contentLen := len(content)
|
||||||
|
|
||||||
|
if contentLen <= config.Cfg.DescriptionLen {
|
||||||
|
return string(content[0:contentLen])
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(content[0:config.Cfg.DescriptionLen])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) UnmarshalJSON(b []byte) error {
|
||||||
|
date, err := time.ParseInLocation(`"`+config.Cfg.TimeLayout+`"`, string(b), time.Local)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*t = Time(date)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
|
return []byte(t.Format(`"` + config.Cfg.TimeLayout + `"`)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) Format(layout string) string {
|
||||||
|
return time.Time(t).Format(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Articles) Len() int { return len(a) }
|
||||||
|
|
||||||
|
func (a Articles) Less(i, j int) bool { return time.Time(a[i].Date).After(time.Time(a[j].Date)) }
|
||||||
|
|
||||||
|
func (a Articles) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
@ -0,0 +1,69 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
Name string
|
||||||
|
Quantity int
|
||||||
|
Articles Articles
|
||||||
|
}
|
||||||
|
type Categories []Category
|
||||||
|
|
||||||
|
func GetCategoryName(path string) string {
|
||||||
|
var categoryName string
|
||||||
|
newPath := strings.Replace(path, config.Cfg.DocumentContentDir+"/", "", 1)
|
||||||
|
|
||||||
|
if strings.Index(newPath, "/") == -1 { //文件在根目录下(content/)没有分类名称
|
||||||
|
categoryName = "未分类"
|
||||||
|
} else {
|
||||||
|
categoryName = strings.Split(newPath, "/")[0]
|
||||||
|
}
|
||||||
|
return categoryName
|
||||||
|
}
|
||||||
|
|
||||||
|
func GroupByCategory(articles *Articles, articleQuantity int) Categories {
|
||||||
|
|
||||||
|
var categories Categories
|
||||||
|
categoryMap := make(map[string]Articles)
|
||||||
|
|
||||||
|
for _, article := range *articles {
|
||||||
|
|
||||||
|
_, existedCategory := categoryMap[article.Category]
|
||||||
|
if existedCategory {
|
||||||
|
categoryMap[article.Category] = append(categoryMap[article.Category], article)
|
||||||
|
} else {
|
||||||
|
categoryMap[article.Category] = Articles{article}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for categoryName, articles := range categoryMap {
|
||||||
|
articleLen := len(articles)
|
||||||
|
|
||||||
|
var articleList Articles
|
||||||
|
if articleQuantity <= 0 {
|
||||||
|
articleList = articles
|
||||||
|
} else {
|
||||||
|
if articleQuantity > articleLen {
|
||||||
|
articleList = articles[0:articleLen]
|
||||||
|
} else {
|
||||||
|
articleList = articles[0:articleQuantity]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories = append(categories, Category{
|
||||||
|
Name: categoryName,
|
||||||
|
Quantity: articleLen,
|
||||||
|
Articles: articleList,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Sort(categories)
|
||||||
|
return categories
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Categories) Len() int { return len(c) }
|
||||||
|
|
||||||
|
func (c Categories) Less(i, j int) bool { return c[i].Quantity > c[j].Quantity }
|
||||||
|
|
||||||
|
func (c Categories) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
|
@ -0,0 +1,50 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Navigation Navs
|
||||||
|
var ArticleList Articles
|
||||||
|
var ArticleShortUrlMap map[string]string //用来保证文章 shortUrl 唯一和快速定位文章
|
||||||
|
var Template HtmlTemplate
|
||||||
|
|
||||||
|
func CompiledContent() {
|
||||||
|
config.Initial() //克隆或者更新文档库
|
||||||
|
//下面是对内容的生成
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
var err error
|
||||||
|
//导航
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
Navigation, err = initExtraNav(config.Cfg.DocumentExtraNavDir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
//加载html模板
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
Template, err = initHtmlTemplate(config.Cfg.ThemesDir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
//文章
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
ArticleList, ArticleShortUrlMap, err = initArticlesAndImages(config.Cfg.DocumentContentDir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
//启用并发比之前节约4倍左右的时间
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Nav struct {
|
||||||
|
Title string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
type Navs []Nav
|
||||||
|
|
||||||
|
func initExtraNav(dir string) (Navs, error) {
|
||||||
|
|
||||||
|
var navigation Navs
|
||||||
|
var extraNav Articles
|
||||||
|
|
||||||
|
extraNav, err := RecursiveReadArticles(dir)
|
||||||
|
if err != nil {
|
||||||
|
return navigation, err
|
||||||
|
}
|
||||||
|
sort.Sort(extraNav)
|
||||||
|
|
||||||
|
for _, article := range extraNav {
|
||||||
|
title := strings.Title(strings.ToLower(article.Title))
|
||||||
|
navigation = append(navigation, Nav{Title: title, Path: article.Path})
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigation, nil
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TemplatePointer struct {
|
||||||
|
*template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtmlTemplate struct {
|
||||||
|
Article TemplatePointer
|
||||||
|
Categories TemplatePointer
|
||||||
|
Dashboard TemplatePointer
|
||||||
|
ExtraNav TemplatePointer
|
||||||
|
Index TemplatePointer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TemplatePointer) WriteData(w io.Writer, data interface{}) {
|
||||||
|
|
||||||
|
err := t.Execute(w, data)
|
||||||
|
if err != nil {
|
||||||
|
if _, e := w.Write([]byte(err.Error())); e != nil {
|
||||||
|
fmt.Println(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TemplatePointer) WriteError(w io.Writer, err error) {
|
||||||
|
if _, e := w.Write([]byte(err.Error())); e != nil {
|
||||||
|
fmt.Println(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildViewData(title string, data interface{}) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"Title": title,
|
||||||
|
"Data": data,
|
||||||
|
"Config": config.Cfg,
|
||||||
|
"Navs": Navigation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initHtmlTemplate(viewDir string) (HtmlTemplate, error) {
|
||||||
|
var htmlTemplate HtmlTemplate
|
||||||
|
|
||||||
|
tp, err := readHtmlTemplate(
|
||||||
|
[]string{"index", "extraNav", "dashboard", "categories", "article"},
|
||||||
|
viewDir)
|
||||||
|
if err != nil {
|
||||||
|
return htmlTemplate, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlTemplate.Index = tp[0]
|
||||||
|
htmlTemplate.ExtraNav = tp[1]
|
||||||
|
htmlTemplate.Dashboard = tp[2]
|
||||||
|
htmlTemplate.Categories = tp[3]
|
||||||
|
htmlTemplate.Article = tp[4]
|
||||||
|
|
||||||
|
return htmlTemplate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SpreadDigit(n int) []int {
|
||||||
|
var r []int
|
||||||
|
for i := 1; i <= n; i++ {
|
||||||
|
r = append(r, i)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func readHtmlTemplate(htmlFileName []string, viewDir string) ([]TemplatePointer, error) {
|
||||||
|
var htmlTemplate []TemplatePointer
|
||||||
|
|
||||||
|
head := viewDir + "/layouts/head.html"
|
||||||
|
footer := viewDir + "/layouts/footer.html"
|
||||||
|
|
||||||
|
for _, name := range htmlFileName {
|
||||||
|
|
||||||
|
tp, err := template.New(name+".html").
|
||||||
|
Funcs(template.FuncMap{"SpreadDigit": SpreadDigit}).
|
||||||
|
ParseFiles(viewDir+"/"+name+".html", head, footer)
|
||||||
|
if err != nil {
|
||||||
|
return htmlTemplate, err
|
||||||
|
}
|
||||||
|
htmlTemplate = append(htmlTemplate, TemplatePointer{tp})
|
||||||
|
}
|
||||||
|
return htmlTemplate, nil
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PageResult struct {
|
||||||
|
List Articles `json:"list"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
TotalPage int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pagination(articles *Articles, page int, pageSize int) PageResult {
|
||||||
|
|
||||||
|
articleLen := len(*articles)
|
||||||
|
totalPage := int(math.Floor(float64(articleLen / pageSize)))
|
||||||
|
|
||||||
|
if (articleLen % pageSize) != 0 {
|
||||||
|
totalPage++
|
||||||
|
}
|
||||||
|
result := PageResult{
|
||||||
|
Total: articleLen,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
TotalPage: totalPage,
|
||||||
|
}
|
||||||
|
if page < 1 {
|
||||||
|
result.Page = 1
|
||||||
|
}
|
||||||
|
if page > result.TotalPage {
|
||||||
|
result.Page = result.TotalPage
|
||||||
|
}
|
||||||
|
|
||||||
|
if articleLen <= result.PageSize {
|
||||||
|
result.List = (*articles)[0:articleLen]
|
||||||
|
} else {
|
||||||
|
startNum := (result.Page - 1) * result.PageSize
|
||||||
|
endNum := startNum + result.PageSize
|
||||||
|
if endNum > articleLen {
|
||||||
|
endNum = articleLen
|
||||||
|
}
|
||||||
|
result.List = (*articles)[startNum:endNum]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/config"
|
||||||
|
"blog/controller"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitRoute() {
|
||||||
|
|
||||||
|
http.HandleFunc("/", controller.Index)
|
||||||
|
http.HandleFunc("/blog", controller.Index)
|
||||||
|
http.HandleFunc("/categories", controller.Category)
|
||||||
|
http.HandleFunc("/article", controller.Article)
|
||||||
|
http.HandleFunc("/extra-nav", controller.ExtraNav)
|
||||||
|
|
||||||
|
http.HandleFunc(config.Cfg.GitHookUrl, controller.GithubHook)
|
||||||
|
http.HandleFunc(config.Cfg.Dashboard, controller.Dashboard)
|
||||||
|
|
||||||
|
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(config.Cfg.ThemesDir+"/public"))))
|
||||||
|
http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir(config.Cfg.DocumentAssetsDir))))
|
||||||
|
http.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.Dir(config.Cfg.CurrentDir+"/images"))))
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
<div class="sub-title">- {{ .Title }} -</div>
|
||||||
|
|
||||||
|
<h1 style="text-align: center">{{ .Data.Title }}</h1>
|
||||||
|
|
||||||
|
<div class="article-info">
|
||||||
|
<span>
|
||||||
|
<img class="icon" src="/public/img/folder.svg" alt="">
|
||||||
|
<span>
|
||||||
|
分类于
|
||||||
|
<a class="category" href="/?category={{ .Data.Category }}">{{ .Data.Category }}</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="divider-line"></span>
|
||||||
|
<span>
|
||||||
|
<img class="icon" src="/public/img/date.svg" alt="">
|
||||||
|
<span>发表于{{ .Data.Date.Format "2006-01-02 15:04" }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="divider-line"></span>
|
||||||
|
<span>
|
||||||
|
<img class="icon" src="/public/img/tag.svg" alt="">
|
||||||
|
<span>Tags:
|
||||||
|
{{ range .Data.Tags }}
|
||||||
|
<a class="category" href="/?tag={{.}}">{{.}}</a>
|
||||||
|
{{ end }}</span>
|
||||||
|
</span>
|
||||||
|
<!-- <span id="busuanzi_container_page_pv">-->
|
||||||
|
<!-- 已浏览<span id="busuanzi_value_page_pv"></span>次-->
|
||||||
|
<!-- </span>-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if ne .Data.MusicId "" }}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/meting@1.2.0/dist/Meting.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/kaygb/kaygb@master/js/v3.js"></script>
|
||||||
|
<script src="https://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/kaygb/kaygb@master/layer/layer.js"></script>
|
||||||
|
<div id="aplayer" class="aplayer" data-order="random" data-id="{{ .Data.MusicId }}" data-server="netease"
|
||||||
|
data-type="playlist" data-fixed="true" data-autoplay="true" data-volume="0.8"></div>
|
||||||
|
<span class="pln"><span class="pln"> </span></span>
|
||||||
|
{{ end }}
|
||||||
|
<div id="article" class="markdown">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('article').innerHTML = marked({{ .Data.Body }});
|
||||||
|
</script>
|
||||||
|
<script src="/public/js/prism.js"></script>
|
||||||
|
{{if ne .Config.UtterancesRepo ""}}
|
||||||
|
<script src="https://utteranc.es/client.js" repo="{{ .Config.UtterancesRepo }}" issue-term="[{{ .Data.Title }}]"
|
||||||
|
theme="github-light" crossorigin="anonymous" async>
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
{{template "footer" .}}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
<div class="sub-title">- {{ .Title }} -</div>
|
||||||
|
<div class="categories">
|
||||||
|
{{range .Data}}
|
||||||
|
<div class="categories-card">
|
||||||
|
<a href="/?category={{ .Name }}">
|
||||||
|
<h3><img class="icon" src="/public/img/folder.svg" alt=""> {{ .Name }} <sub>({{ .Quantity }})</sub></h3>
|
||||||
|
</a>
|
||||||
|
<article class="categories-article">
|
||||||
|
{{range .Articles}}
|
||||||
|
<a href="/article?key={{ .ShortUrl }}">{{ .Title }}</a>
|
||||||
|
{{end}}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{template "footer" .}}
|
|
@ -0,0 +1,45 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
<div class="sub-title">- {{ .Title }} -</div>
|
||||||
|
<p>Search:</p>
|
||||||
|
<div class="item-content">
|
||||||
|
<div class="search-box">
|
||||||
|
<input id="search-input" class="search-input" type="text">
|
||||||
|
<img onclick="searchArticle()" class="search-icon" src="/public/img/search.svg" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>Theme:</p>
|
||||||
|
<div class="item-content">
|
||||||
|
<ul class="colors">
|
||||||
|
{{range $index, $color := .Config.ThemeOption }}
|
||||||
|
<li onclick="selectColor('{{ $index }}')" class="{{ if eq $color $.Config.ThemeColor }}active{{end}}" style="background-color: {{ $color }}"></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p>Action:</p>
|
||||||
|
<div class="item-content">
|
||||||
|
<div class="action">
|
||||||
|
<a href="{{ .Config.DashboardEntrance }}?action=updateArticle">更新文章</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ if .Data.msg }}
|
||||||
|
<div id="action-msg" class="action-msg">
|
||||||
|
{{range $msg := .Data.msg }}
|
||||||
|
<span>{{ $msg }}</span>
|
||||||
|
{{ end }}
|
||||||
|
<span class="close" onclick="document.getElementById('action-msg').remove()">x</span>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<span class="action-tip">提示:更新文章会执行git pull命令,和你的仓库网络有关,等待时间可能会稍长。</span>
|
||||||
|
<script>
|
||||||
|
function selectColor(index) {
|
||||||
|
window.location.href = '{{ .Config.DashboardEntrance }}?theme=' + index
|
||||||
|
}
|
||||||
|
function searchArticle() {
|
||||||
|
var searchKey = document.getElementById('search-input').value;
|
||||||
|
searchKey = searchKey.replace(/^\s+|\s+$/g,"")
|
||||||
|
if("" === searchKey){return}
|
||||||
|
|
||||||
|
window.location.href = '/?search=' + searchKey
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{template "footer" .}}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
<div class="sub-title">- {{ .Title }} -</div>
|
||||||
|
<div id="about"></div>
|
||||||
|
<script>
|
||||||
|
document.getElementById('about').innerHTML = marked({{ .Data.Body }});
|
||||||
|
</script>
|
||||||
|
{{template "footer" .}}
|
|
@ -0,0 +1,55 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
<div class="sub-title">- {{ .Title }} -</div>
|
||||||
|
|
||||||
|
<ul class="articles">
|
||||||
|
{{range .Data.List }}
|
||||||
|
<li>
|
||||||
|
<h2>
|
||||||
|
<a class="title" href="/article?key={{ .ShortUrl }}">{{ .Title }}</a>
|
||||||
|
</h2>
|
||||||
|
<div class="article-info">
|
||||||
|
{{ if ne .Category "" }}
|
||||||
|
<span>
|
||||||
|
<img class="icon" src="/public/img/folder.svg" alt="">
|
||||||
|
<span>
|
||||||
|
分类于
|
||||||
|
<a class="category" href="/?category={{ .Category }}">{{ .Category }}</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="divider-line"></span>
|
||||||
|
{{ end }}
|
||||||
|
<span>
|
||||||
|
<img class="icon" src="/public/img/date.svg" alt="">
|
||||||
|
<span>发表于{{ .Date.Format "2006-01-02 15:04" }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="description"> {{ .Description }}...</div>
|
||||||
|
<a class="read-all" href="/article?key={{ .ShortUrl }}" rel="contents">
|
||||||
|
阅读全文 »
|
||||||
|
</a>
|
||||||
|
<div class="article-eof"></div>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="pagination">
|
||||||
|
{{ range $page := SpreadDigit .Data.TotalPage }}
|
||||||
|
<li
|
||||||
|
class="{{ if eq $page $.Data.Page }}active{{end}}"
|
||||||
|
{{ if ne $page $.Data.Page }}onclick="goPage({{$page}})"{{end}}
|
||||||
|
>
|
||||||
|
<a href="javascript:;">{{ $page }}</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
<li style="color: #bfbfbf;">
|
||||||
|
[{{ .Data.Total }}]
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<script>
|
||||||
|
var urlParams = currentUrlToParams();
|
||||||
|
function goPage(page) {
|
||||||
|
urlParams.page = page
|
||||||
|
window.location.href = "/" + obj2StrParams(urlParams)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{template "footer" .}}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{{define "footer"}}
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<span>{{ .Config.Icp }}</span><span class="footer-divider"> | </span>
|
||||||
|
<span>
|
||||||
|
© 2018 - 2022 {{ .Config.Author }}
|
||||||
|
Powered By <a href="{{ .Config.AppRepository }}" target="_blank"> {{ .Config.AppName }} </a>
|
||||||
|
</span>
|
||||||
|
<span id="busuanzi_container_site_pv" style='display:none'>
|
||||||
|
总访问量<span id="busuanzi_value_site_pv"></span>次
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end}}
|
|
@ -0,0 +1,69 @@
|
||||||
|
{{define "header"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-cn">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="shortcut icon" href="public/img/favicon.ico" type="image/x-icon">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="Keywords" content="{{ .Config.HtmlKeywords }}"/>
|
||||||
|
<meta name="description" content="{{ .Config.HtmlDescription }}"/>
|
||||||
|
<title>{{ .Title }} - {{ .Config.SiteName }}</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: {
|
||||||
|
{
|
||||||
|
.Config.ThemeColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="/public/css/app.css">
|
||||||
|
<link href="/public/css/prism.css" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css">
|
||||||
|
<script src="/public/js/marked.min.js"></script>
|
||||||
|
<script async src="https://busuanzi.icodeq.com/busuanzi.pure.mini.js"></script>
|
||||||
|
<script>
|
||||||
|
function obj2StrParams(obj, firstStr = '?') {
|
||||||
|
let params = firstStr;
|
||||||
|
for (let p in obj) {
|
||||||
|
let isFirst = (params === '?');
|
||||||
|
let qz = isFirst ? '' : (params === '&' ? '' : '&');
|
||||||
|
params += (qz + p + '=' + obj[p])
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentUrlToParams(key = null) {
|
||||||
|
let paramsUrl = (window.location.href).split('?');
|
||||||
|
|
||||||
|
if (paramsUrl.length < 2) return key ? null : {};
|
||||||
|
let paramsArr = paramsUrl[1].split('&');
|
||||||
|
let paramsData = {}
|
||||||
|
|
||||||
|
paramsArr.forEach(r => {
|
||||||
|
let data = r.split('=')
|
||||||
|
paramsData[data[0]] = data[1]
|
||||||
|
})
|
||||||
|
if (key) return paramsData.hasOwnProperty(key) ? paramsData[key] : null;
|
||||||
|
return paramsData;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="theme{{ .Config.ThemeColor }}">
|
||||||
|
<nav class="head">
|
||||||
|
<div class="container head-content">
|
||||||
|
<div class="logo">{{ .Config.SiteName }}</div>
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/blog">Blog</a>
|
||||||
|
<a href="/categories">Categories</a>
|
||||||
|
{{range $nav := .Navs }}
|
||||||
|
<a href="/extra-nav?name={{ $nav.Title }}">{{ $nav.Title }}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="post-warp">
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,477 @@
|
||||||
|
|
||||||
|
*{
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
font-family: josefin sans,-apple-system,BlinkMacSystemFont,helvetica neue,pingfang sc,hiragino sans gb,STHeiti,microsoft yahei,wenquanyi micro hei,Arial,Verdana,sans-serif;
|
||||||
|
}
|
||||||
|
html::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-thumb {
|
||||||
|
height: 40px;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #ddd
|
||||||
|
}
|
||||||
|
pre{
|
||||||
|
padding: 1em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
background: #f5f2f0;
|
||||||
|
}
|
||||||
|
body{
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a{
|
||||||
|
color: #161209;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color .2s ease,border-color .46s ease,background .46s ease,opacity .46s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover{
|
||||||
|
color: #673ab7;
|
||||||
|
color: var(--primary,#673ab7);
|
||||||
|
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote{
|
||||||
|
text-indent: 2em;
|
||||||
|
position: relative;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
blockquote::before{
|
||||||
|
position: absolute;
|
||||||
|
left: -32px;
|
||||||
|
top: -10px;
|
||||||
|
content: '“';
|
||||||
|
font-family: fantasy;
|
||||||
|
font-size: 40px;
|
||||||
|
color: #673ab7;
|
||||||
|
color: var(--primary,#673ab7);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 860px){
|
||||||
|
body{
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: josefin sans;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.eot);
|
||||||
|
src: local("Josefin Sans"),local("JosefinSans-Normal"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.eot?#iefix) format("embedded-opentype"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.woff2) format("woff2"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.woff) format("woff"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.ttf) format("truetype"),url(//lib.baomitu.com/fonts/josefin-sans/josefin-sans-regular.svg#JosefinSans) format("svg")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: auto;
|
||||||
|
max-width: 1200px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.head{
|
||||||
|
width: 100%;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.head-content{
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 860px){
|
||||||
|
.head-content{
|
||||||
|
padding-top: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a{
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 82px;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 860px){
|
||||||
|
.sub-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.post-warp{
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 780px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
padding: 46px 0;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 860px){
|
||||||
|
.post-warp{
|
||||||
|
min-height: calc(100vh - 132px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdow img{
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.footer{
|
||||||
|
color: #999;
|
||||||
|
display: flex;
|
||||||
|
height: 54px;
|
||||||
|
line-height: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.footer-divider{
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 860px){
|
||||||
|
.footer{
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.footer span{
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.footer-divider{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a{
|
||||||
|
color: #999;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
pre{
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
hr{
|
||||||
|
border: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
hr:after{
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
margin: 30px auto;
|
||||||
|
width: 10%;
|
||||||
|
height: 1px;
|
||||||
|
background: #ccc;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*Categories*/
|
||||||
|
|
||||||
|
.categories{
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
.categories-card{
|
||||||
|
width: 50%;
|
||||||
|
margin-bottom: 46px;
|
||||||
|
min-height: 250px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 860px){
|
||||||
|
.categories-card{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icon{
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
.categories-article{
|
||||||
|
text-indent: 2em;
|
||||||
|
}
|
||||||
|
.categories-article a{
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Pagination*/
|
||||||
|
|
||||||
|
.pagination{
|
||||||
|
display: flex;
|
||||||
|
margin: 60px 0;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.pagination li{
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0 10px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
.pagination li a {
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
color: #bfbfbf;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
.pagination li a:hover,.pagination .active a{
|
||||||
|
border-bottom: 3px solid #673ab7;
|
||||||
|
border-bottom: 3px solid var(--primary,#673ab7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Index*/
|
||||||
|
.articles{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
.articles li{
|
||||||
|
padding-bottom: 8px;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articles li:after{
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
margin: 60px auto;
|
||||||
|
width: 10%;
|
||||||
|
height: 1px;
|
||||||
|
background: #ccc;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articles li:last-child:after{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.read-all{
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
background: #673ab7;
|
||||||
|
border: 2px solid #673ab7;
|
||||||
|
background: var(--primary,#673ab7);
|
||||||
|
border: 2px solid var(--primary,#673ab7);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition-property: background-color;
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-all:hover{
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articles li .title{
|
||||||
|
color: #555;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.articles li .title:hover{
|
||||||
|
color: #673ab7;
|
||||||
|
color: var(--primary,#673ab7);
|
||||||
|
}
|
||||||
|
.article-info{
|
||||||
|
margin:20px 0 60px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-flow: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-info img{
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
.article-img img{
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 200px;
|
||||||
|
border-radius:3%;
|
||||||
|
object-fit:cover;
|
||||||
|
}
|
||||||
|
.article-info .divider-line{
|
||||||
|
width: 2px;
|
||||||
|
height: 16px;
|
||||||
|
background: #999;
|
||||||
|
margin: 0 14px;
|
||||||
|
}
|
||||||
|
.article{
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.articles .description{
|
||||||
|
color: #555;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 2;
|
||||||
|
text-indent: 2em;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 860px){
|
||||||
|
.articles .description{
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub{
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*dashboard*/
|
||||||
|
.item-content{
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.search-box{
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 2px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid #673ab7;
|
||||||
|
border: 1px solid var(--primary,#673ab7);
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
.search-input{
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.search-input:focus,.search-input:active{
|
||||||
|
border: none;
|
||||||
|
outline:none;
|
||||||
|
}
|
||||||
|
.search-icon{
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.colors{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colors li{
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 10px 20px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.colors li.active:before{
|
||||||
|
content: '';
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.action{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.action a{
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: .8;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #673ab7;
|
||||||
|
background-color: var(--primary,#673ab7);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.action a:hover{
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 4px rgba(0,0,0,.2);
|
||||||
|
}
|
||||||
|
.action-tip{
|
||||||
|
font-size: 14px;
|
||||||
|
color: #673ab7;
|
||||||
|
color: var(--primary,#673ab7);
|
||||||
|
}
|
||||||
|
.action-msg{
|
||||||
|
z-index: 1400;
|
||||||
|
position: fixed;
|
||||||
|
top: 24px;
|
||||||
|
left: auto;
|
||||||
|
right: 24px;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 1;
|
||||||
|
min-width: 288px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #673ab7;
|
||||||
|
background-color: var(--primary,#673ab7);
|
||||||
|
box-shadow: 0 3px 5px -1px rgba(0,0,0,0.2), 0 6px 10px 0 rgba(0,0,0,0.14), 0 1px 18px 0 rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
.action-msg .close{
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: auto;
|
||||||
|
font-size: 16px;
|
||||||
|
right: 24px;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* PrismJS 1.28.0
|
||||||
|
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+c+cpp+cmake+go+java+json+json5+kotlin+lua+markup-templating+nginx+php+python+verilog+yaml&plugins=line-highlight+show-language+toolbar+copy-to-clipboard */
|
||||||
|
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
|
||||||
|
pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}
|
||||||
|
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
|
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1556811784413" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3571" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M508.34940208 792.26591763c-22.11718637 0-41.00975882-7.11308325-56.67650371-21.35017245-15.66674489-14.23102103-25.65006222-35.48046222-29.95116562-63.7422554l-58.0600415 4.9152c3.68579318 37.8919443 18.47993837 68.30307555 44.38850372 91.23703467 25.90613808 22.94002725 59.33677985 34.4064 100.29920711 34.4064 49.96634548 0 89.39353125-18.22507615 118.26942103-54.68008297 23.75558637-29.69144889 35.63459318-64.91704889 35.63459319-105.67437273 0-42.80107615-13.51740682-77.82035911-40.54979319-105.06027616-27.03238637-27.23263525-60.21059318-40.85562785-99.53097956-40.85562784-29.69751703 0-58.05882785 9.06338608-85.09121422 27.9025588L461.34916741 437.96859259H640.64474075v-52.18607407H417.11236741l-43.62027616 231.3505754 51.91543467 6.7744806c8.18835911-12.90209659 19.45448297-23.38542933 33.79230341-31.47912534 14.3317523-8.08520059 30.41113125-12.12901452 48.22721422-12.12901451 28.672 0 51.86324859 9.11799941 69.57981393 27.3406483 17.71171082 18.22993067 26.57242075 43.11540622 26.57242074 74.65035851 0 33.17699318-9.21508978 59.80281363-27.64648297 79.87139319-18.43260682 20.07100682-40.96242725 30.10408297-67.58339317 30.10408296z" fill="#999"></path><path d="M894.81762133 50.82074075H131.55259733C85.31694933 50.82074075 47.17985185 88.05732503 47.17985185 134.29418667v756.1337363C47.17985185 936.66478459 85.31694933 974.39288889 131.55259733 974.39288889h763.41551408C941.20497303 974.39288889 978.03377778 936.66478459 978.03377778 890.42792297V134.29418667c0-46.23686163-36.98050845-83.47344592-83.21615645-83.47344592zM732.27377778 117.57037037c30.16233718 0 54.61333333 24.45099615 54.61333333 54.61333333s-24.45099615 54.61333333-54.61333333 54.61333333-54.61333333-24.45099615-54.61333333-54.61333333 24.45099615-54.61333333 54.61333333-54.61333333z m-439.33392593 0c30.16233718 0 54.61333333 24.45099615 54.61333333 54.61333333s-24.45099615 54.61333333-54.61333333 54.61333333-54.61333333-24.45099615-54.61333333-54.61333333 24.45099615-54.61333333 54.61333333-54.61333333zM905.216 890.42792297c0 6.01110755-4.23556741 11.14718815-10.24667497 11.14718814H131.55259733c-6.01110755 0-11.5549677-5.13486697-11.5549677-11.14718814V276.55585185h785.21837037v613.87207112z" fill="#999"></path></svg>
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1558276399452" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2757" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M511.939185 874.030645C312.344934 874.030645 149.969355 711.655066 149.969355 511.939185c0-199.594251 162.375579-361.96983 361.96983-361.96983 199.594251 0 361.96983 162.375579 361.96983 361.96983 0.12163 199.715881-162.253949 362.09146-361.96983 362.09146z m0-677.963654C337.76553 196.066991 195.945362 337.76553 195.945362 512.060815s141.698539 315.993823 315.993823 315.993823S827.933009 686.356099 827.933009 512.060815 686.23447 196.066991 511.939185 196.066991z" fill="#5B5C5C" p-id="2758"></path><path d="M554.144673 333.265233c-17.636299-35.759116-28.461337-70.666825-33.934672-90.978976V498.681554c9.000594 3.40563 15.325336 11.919705 15.325336 22.014966 0 13.014372-10.581779 23.596152-23.596152 23.596151s-23.596152-10.581779-23.596151-23.596151c0-10.581779 7.05452-19.460744 16.663261-22.379855V242.042998c-5.473334 20.190521-16.298373 55.21986-34.056301 91.222235-27.73156 56.071267-62.274379 105.452904-62.274379 105.452905L430.93384 741.089441s21.041929 23.474522 81.613493 23.474521c60.693194 0 81.613493-23.474522 81.613494-23.474521l22.136595-302.371303c0.12163 0-34.42119-49.381637-62.152749-105.452905z" fill="#5B5C5C" p-id="2759"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.333333 938.666667H170.666667c-72.533333 0-128-55.466667-128-128V213.333333c0-72.533333 55.466667-128 128-128h213.333333c12.8 0 25.6 8.533333 34.133333 17.066667L490.666667 213.333333H853.333333c72.533333 0 128 55.466667 128 128v469.333334c0 72.533333-55.466667 128-128 128zM170.666667 170.666667c-25.6 0-42.666667 17.066667-42.666667 42.666666v597.333334c0 25.6 17.066667 42.666667 42.666667 42.666666h682.666666c25.6 0 42.666667-17.066667 42.666667-42.666666V341.333333c0-25.6-17.066667-42.666667-42.666667-42.666666h-384c-12.8 0-25.6-8.533333-34.133333-17.066667L362.666667 170.666667H170.666667z" fill="#999"></path></svg>
|
After Width: | Height: | Size: 922 B |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566293108350" class="icon" viewBox="0 0 1030 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2103" width="48.28125" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M738.618409 646.405992c96.606263 96.606263 192.944918 192.944918 290.889218 290.621611-31.310063 29.169204-62.352519 57.803193-93.662582 86.972397-93.662582-93.662582-190.001237-190.001237-286.875107-286.875107-104.099269 71.451169-215.691545 95.535833-336.917687 66.901844-96.87387-22.746627-175.015224-75.732887-233.621239-156.282708-120.690927-165.648966-98.747122-390.439162 42.81718-530.130212 149.860131-147.719272 377.861615-153.339027 534.947145-33.450922C814.08369 205.389036 876.436208 448.644141 738.618409 646.405992zM728.984544 407.700212C728.984544 230.008915 585.814598 86.036146 408.658514 86.036146 231.502431 86.036146 86.994448 230.276522 86.994448 407.164998c0 178.226513 143.972768 322.466888 321.664066 321.664066C587.152634 728.293849 728.984544 585.926725 728.984544 407.700212z" p-id="2104"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1711012507479" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1625" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M648.32 715.072l-124.992 125.056a224 224 0 0 1-316.8 0l-22.656-22.656a224 224 0 0 1 0-316.8L308.48 376.064l63.488 63.232-124.736 124.8a134.4 134.4 0 0 0-6.528 183.04l6.528 7.04 22.656 22.592a134.4 134.4 0 0 0 183.04 6.528l7.04-6.528 124.928-124.992 63.36 63.296z m-44.288-295.04a44.8 44.8 0 0 1 0 63.36L484.736 602.56a44.8 44.8 0 1 1-63.36-63.36l119.296-119.232a44.8 44.8 0 0 1 63.36 0z m236.032-213.504a224 224 0 0 1 0 316.8l-123.84 123.84-63.488-63.296 123.968-123.904a134.4 134.4 0 0 0 6.592-183.04l-6.592-7.04-22.592-22.592a134.4 134.4 0 0 0-183.04-6.592l-7.04 6.592-124.224 124.16-63.488-63.232 124.352-124.288a224 224 0 0 1 316.8 0l22.592 22.592z" p-id="1626"></path></svg>
|
After Width: | Height: | Size: 1012 B |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,15 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunCmdByDir(dir string, cmdName string, arg ...string) (string, error) {
|
||||||
|
cmd := exec.Command(cmdName, arg ...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/utils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunCmdByDir(t *testing.T) {
|
||||||
|
_, err := utils.RunCmdByDir("./", "ping", "127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("run cmd error", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsDir(name string) bool {
|
||||||
|
if info, err := os.Stat(name); err == nil {
|
||||||
|
return info.IsDir()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func IsFile(filename string) bool {
|
||||||
|
existed := true
|
||||||
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||||
|
existed = false
|
||||||
|
}
|
||||||
|
return existed
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeDir(dir string) error {
|
||||||
|
if !IsDir(dir) {
|
||||||
|
return os.MkdirAll(dir, os.ModePerm)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveDir(dir string) error {
|
||||||
|
|
||||||
|
if !IsDir(dir) {
|
||||||
|
return errors.New("cannot delete without directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.RemoveAll(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyFile(src, dst string) (int64, error) {
|
||||||
|
sourceFileStat, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sourceFileStat.Mode().IsRegular() {
|
||||||
|
return 0, fmt.Errorf("%s is not a regular file", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
source, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer source.Close()
|
||||||
|
|
||||||
|
destination, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer destination.Close()
|
||||||
|
nBytes, err := io.Copy(destination, source)
|
||||||
|
return nBytes, err
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//通过git url 返回仓库的名字
|
||||||
|
func GetRepoName(gitUrl string) (string, error) {
|
||||||
|
|
||||||
|
if !strings.HasSuffix(gitUrl, ".git") {
|
||||||
|
return "", errors.New("git URL must end with .git!")
|
||||||
|
}
|
||||||
|
|
||||||
|
noSuffixUrl := strings.TrimSuffix(gitUrl, ".git")
|
||||||
|
urlArr := strings.Split(noSuffixUrl, "/")
|
||||||
|
|
||||||
|
return urlArr[ len(urlArr)-1 ], nil
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/utils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetRepoName(t *testing.T) {
|
||||||
|
url := "http://jsit205.vaiwan.cn/JiXieShi/blog.git"
|
||||||
|
name, err := utils.GetRepoName(url)
|
||||||
|
if err != nil || name != "blog" {
|
||||||
|
t.Error("repository name error")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const charset = "A0a12B3b4CDc56Ede7FGf8Hg9IhJKiLjkMNlmOPnQRopqrSstTuvUVwxWXyYzZ"
|
||||||
|
|
||||||
|
func generateCharset(url, hexMd5 string, len, sectionNum int, cb func(url, keyword string) bool) string {
|
||||||
|
for i := 0; i < sectionNum; i++ {
|
||||||
|
sectionHex := hexMd5[i*8 : 8+i*8]
|
||||||
|
bits, _ := strconv.ParseUint(sectionHex, 16, 32)
|
||||||
|
bits = bits & 0x3FFFFFFF
|
||||||
|
keyword := ""
|
||||||
|
for j := 0; j < len; j++ {
|
||||||
|
idx := bits & 0x3D
|
||||||
|
keyword = keyword + string(charset[idx])
|
||||||
|
bits = bits >> 5
|
||||||
|
}
|
||||||
|
if cb(url, keyword) {
|
||||||
|
return keyword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 起初生成6位的短码,当四组6位短码都重复时,再生成8位的短码,因此总共会有8个短码供你选择。
|
||||||
|
|
||||||
|
func GenerateShortUrl(url string, cb func(url, keyword string) bool) string {
|
||||||
|
if url == "" || cb == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
hexMd5 := fmt.Sprintf("%x", md5.Sum([]byte(url)))
|
||||||
|
sections := len(hexMd5) / 8
|
||||||
|
|
||||||
|
keyword := generateCharset(url, hexMd5, 6, sections, cb)
|
||||||
|
if keyword == "" {
|
||||||
|
return generateCharset(url, hexMd5, 8, sections, cb)
|
||||||
|
}
|
||||||
|
return keyword
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blog/utils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateShortUrl(t *testing.T) {
|
||||||
|
url := "http://jsit205.vaiwan.cn/JiXieShi/blog.git"
|
||||||
|
shortUrl := utils.GenerateShortUrl(url, func(url, keyword string) bool { return true })
|
||||||
|
if shortUrl != "ItMIz7" {
|
||||||
|
t.Error("generate short URL error")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
## config.json 配置说明
|
||||||
|
|
||||||
|
- "port": 服务器监听的端口号,
|
||||||
|
- "pageSize": 首页每一页的文章数量,
|
||||||
|
- "descriptionLen": 文章没有配置description字段时,默认取文章内容多少个字作为描述,
|
||||||
|
- "author": 博客作者,网站底部展示,
|
||||||
|
- "icp": 网站的备案号,
|
||||||
|
- "webHookSecret": 博客文章更新勾子的密钥,这里要和你在仓库设置的密钥一样,
|
||||||
|
- "categoryDisplayQuantity": 在分类页面下,每个分类下最多展示多少篇文章,
|
||||||
|
- "utterancesRepo": 是否开启utterances评论,留空没有评论,否则填写评论存储的仓库name/repo,
|
||||||
|
- "timeLayout": 解析时间的格式,保持和你文章里面的date字段一样,除非了解Golang的时间解析,否则不要修改,
|
||||||
|
- "siteName": 网站的名字,
|
||||||
|
- "documentGitUrl": 你文章的git地址,应用会把文章克隆在当前目录下,必须公开并且以.git结尾,
|
||||||
|
- "htmlKeywords": 网页里面的htmlKeywords,
|
||||||
|
- "htmlDescription": 网页里面的htmlDescription,
|
||||||
|
- "themePath": 主题路径,留空使用/blog,
|
||||||
|
- "themeColor": 博客的主题颜色,
|
||||||
|
- "dashboardEntrance": 网站仪表盘的访问路径,留空使用/admin,
|
||||||
|
- "themeOption": 网站可选择的主题颜色
|
||||||
|
|
||||||
|
|
||||||
|
## MD 文章支持的字段
|
||||||
|
|
||||||
|
- "title": 文章标题,不填使用文件名,
|
||||||
|
- "date": 文章日期,排序使用(导航文章使用这个来排序),
|
||||||
|
- "description": 文章描述
|
||||||
|
- "tags": 文章 tag
|
||||||
|
- "author": 文章作者
|
||||||
|
- "musicId": 网易云的歌单ID
|
||||||
|
|
||||||
|
> 文章的这些字段全部可以为空,但是没有日期会默认使用文件生成的日期,那样每次迁移文档时间都是不可控的。
|
||||||
|
|
||||||
|
## 其他
|
||||||
|
开启utterances评论之后,utterancesRepo指向的仓库必须是公开并且可以被评论的,具体使用请访问 https://utteranc.es
|
||||||
|
webHook的地址: 你的域名/api/git_push_hook
|
Loading…
Reference in New Issue