Repository: getgridea/gridea
Branch: master
Commit: 48e4ab573bfa
Files: 206
Total size: 508.3 KB
Directory structure:
gitextract_69c_nu5k/
├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .github/
│ └── ISSUE_TEMPLATE/
│ ├── bug_report.md
│ ├── feature_request.md
│ └── question.md
├── .gitignore
├── .npmrc
├── CHANGELOG.md
├── LICENSE
├── README-ru.md
├── README-zh_CN.md
├── README-zh_TW.md
├── README.md
├── babel.config.js
├── package.json
├── postcss.config.js
├── public/
│ ├── app-icons/
│ │ └── gridea.icns
│ ├── default-files/
│ │ ├── config/
│ │ │ ├── posts.json
│ │ │ ├── setting.json
│ │ │ └── theme.json
│ │ ├── post-images/
│ │ │ └── .gitkeep
│ │ ├── posts/
│ │ │ ├── about.md
│ │ │ └── hello-gridea.md
│ │ ├── static/
│ │ │ └── 404.html
│ │ └── themes/
│ │ ├── fly/
│ │ │ ├── assets/
│ │ │ │ └── styles/
│ │ │ │ ├── _blocks/
│ │ │ │ │ ├── archives.less
│ │ │ │ │ ├── fonts.less
│ │ │ │ │ ├── footer.less
│ │ │ │ │ ├── header.less
│ │ │ │ │ ├── link.less
│ │ │ │ │ ├── list.less
│ │ │ │ │ ├── pagination.less
│ │ │ │ │ ├── post.less
│ │ │ │ │ ├── tag.less
│ │ │ │ │ └── tags.less
│ │ │ │ ├── _core/
│ │ │ │ │ ├── base.less
│ │ │ │ │ ├── colors.less
│ │ │ │ │ └── github.less
│ │ │ │ └── main.less
│ │ │ ├── config.json
│ │ │ ├── style-override.js
│ │ │ └── templates/
│ │ │ ├── archives.ejs
│ │ │ ├── includes/
│ │ │ │ ├── footer.ejs
│ │ │ │ ├── head.ejs
│ │ │ │ ├── header.ejs
│ │ │ │ ├── post-list-archives.ejs
│ │ │ │ ├── post-list.ejs
│ │ │ │ └── scripts.ejs
│ │ │ ├── index.ejs
│ │ │ ├── post.ejs
│ │ │ ├── tag.ejs
│ │ │ └── tags.ejs
│ │ ├── notes/
│ │ │ ├── assets/
│ │ │ │ └── styles/
│ │ │ │ ├── abstracts/
│ │ │ │ │ └── varibles.less
│ │ │ │ ├── components/
│ │ │ │ │ ├── about.less
│ │ │ │ │ ├── archives.less
│ │ │ │ │ ├── footer.less
│ │ │ │ │ ├── header.less
│ │ │ │ │ ├── home.less
│ │ │ │ │ ├── post.less
│ │ │ │ │ ├── tag.less
│ │ │ │ │ └── tags.less
│ │ │ │ ├── lib/
│ │ │ │ │ ├── colors.less
│ │ │ │ │ ├── github.less
│ │ │ │ │ └── modern-normalize.less
│ │ │ │ └── main.less
│ │ │ ├── config.json
│ │ │ ├── style-override.js
│ │ │ └── templates/
│ │ │ ├── archives.ejs
│ │ │ ├── includes/
│ │ │ │ ├── disqus.ejs
│ │ │ │ ├── footer.ejs
│ │ │ │ ├── gitalk.ejs
│ │ │ │ ├── head.ejs
│ │ │ │ ├── header.ejs
│ │ │ │ ├── pagination.ejs
│ │ │ │ ├── post-list-archives.ejs
│ │ │ │ └── post-list.ejs
│ │ │ ├── index.ejs
│ │ │ ├── post.ejs
│ │ │ ├── tag.ejs
│ │ │ └── tags.ejs
│ │ ├── paper/
│ │ │ ├── assets/
│ │ │ │ └── styles/
│ │ │ │ ├── _core/
│ │ │ │ │ ├── base.less
│ │ │ │ │ ├── colors.less
│ │ │ │ │ └── github.less
│ │ │ │ └── main.less
│ │ │ ├── config.json
│ │ │ ├── style-override.js
│ │ │ └── templates/
│ │ │ ├── _blocks/
│ │ │ │ ├── head.ejs
│ │ │ │ ├── header.ejs
│ │ │ │ ├── pagination.ejs
│ │ │ │ ├── post-list.ejs
│ │ │ │ ├── scripts.ejs
│ │ │ │ └── sidebar.ejs
│ │ │ ├── archives.ejs
│ │ │ ├── index.ejs
│ │ │ ├── post.ejs
│ │ │ ├── tag.ejs
│ │ │ └── tags.ejs
│ │ └── simple/
│ │ ├── assets/
│ │ │ └── styles/
│ │ │ ├── _core/
│ │ │ │ ├── a11y-dark.less
│ │ │ │ ├── atom-one-dark.less
│ │ │ │ ├── base.less
│ │ │ │ ├── colors.less
│ │ │ │ └── github.less
│ │ │ └── main.less
│ │ ├── config.json
│ │ ├── style-override.js
│ │ └── templates/
│ │ ├── _blocks/
│ │ │ ├── head.ejs
│ │ │ ├── pagination.ejs
│ │ │ ├── scripts.ejs
│ │ │ └── sidebar.ejs
│ │ ├── archives.ejs
│ │ ├── index.ejs
│ │ ├── post.ejs
│ │ ├── tag.ejs
│ │ └── tags.ejs
│ └── index.html
├── src/
│ ├── App.vue
│ ├── assets/
│ │ ├── locales-menu.ts
│ │ ├── locales.ts
│ │ └── styles/
│ │ ├── custom.less
│ │ ├── main.less
│ │ ├── tailwind.css
│ │ ├── var.less
│ │ └── zwicon.less
│ ├── background.ts
│ ├── components/
│ │ ├── AppSystem/
│ │ │ ├── Index.vue
│ │ │ └── includes/
│ │ │ ├── LanguageSetting.vue
│ │ │ ├── SourceFolderSetting.vue
│ │ │ └── Version.vue
│ │ ├── ColorCard/
│ │ │ └── Index.vue
│ │ ├── EmojiCard/
│ │ │ └── Index.vue
│ │ ├── FooterBox/
│ │ │ └── Index.vue
│ │ ├── Main.vue
│ │ ├── MonacoMarkdownEditor/
│ │ │ ├── Index.vue
│ │ │ └── theme.js
│ │ └── PostsCard/
│ │ └── Index.vue
│ ├── helpers/
│ │ ├── analytics.ts
│ │ ├── constants.ts
│ │ ├── content-helper.ts
│ │ ├── enums.ts
│ │ ├── shortcut-keys.ts
│ │ ├── slug.ts
│ │ ├── utils.ts
│ │ ├── vee-validate.ts
│ │ └── words-count.ts
│ ├── interfaces/
│ │ ├── menu.ts
│ │ ├── post.ts
│ │ ├── setting.ts
│ │ ├── snackbar.ts
│ │ ├── tag.ts
│ │ └── theme.ts
│ ├── main.ts
│ ├── router.ts
│ ├── server/
│ │ ├── app.ts
│ │ ├── deploy.ts
│ │ ├── events/
│ │ │ ├── deploy.ts
│ │ │ ├── index.ts
│ │ │ ├── menu.ts
│ │ │ ├── post.ts
│ │ │ ├── renderer.ts
│ │ │ ├── setting.ts
│ │ │ ├── site.ts
│ │ │ ├── tag.ts
│ │ │ └── theme.ts
│ │ ├── interfaces/
│ │ │ ├── application.ts
│ │ │ ├── menu.ts
│ │ │ ├── post.ts
│ │ │ ├── renderer.ts
│ │ │ ├── setting.ts
│ │ │ ├── tag.ts
│ │ │ └── theme.ts
│ │ ├── menus.ts
│ │ ├── model.ts
│ │ ├── plugins/
│ │ │ ├── deploys/
│ │ │ │ ├── gitproxy.ts
│ │ │ │ ├── netlify.ts
│ │ │ │ └── sftp.ts
│ │ │ └── markdown.ts
│ │ ├── posts.ts
│ │ ├── renderer.ts
│ │ ├── setting.ts
│ │ ├── tags.ts
│ │ └── theme.ts
│ ├── server.ts
│ ├── shims-tsx.d.ts
│ ├── shims-vue.d.ts
│ ├── shims.d.ts
│ ├── store/
│ │ ├── index.ts
│ │ └── modules/
│ │ └── site.ts
│ ├── views/
│ │ ├── article/
│ │ │ ├── ArticleUpdate.vue
│ │ │ └── Articles.vue
│ │ ├── loading/
│ │ │ └── Index.vue
│ │ ├── menu/
│ │ │ └── Index.vue
│ │ ├── setting/
│ │ │ ├── Index.vue
│ │ │ └── includes/
│ │ │ ├── BasicSetting.vue
│ │ │ ├── CommentSetting.vue
│ │ │ ├── DisqusSetting.vue
│ │ │ └── GitalkSetting.vue
│ │ ├── tags/
│ │ │ └── Index.vue
│ │ └── theme/
│ │ ├── Index.vue
│ │ └── includes/
│ │ ├── AvatarSetting.vue
│ │ ├── BasicSetting.vue
│ │ ├── CustomSetting.vue
│ │ └── FaviconSetting.vue
│ └── vue-bus.ts
├── tailwind.config.js
├── tsconfig.json
└── vue.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .browserslistrc
================================================
> 1%
last 2 versions
not ie <= 8
================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[*.{js,ts}]
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2
================================================
FILE: .eslintrc.js
================================================
module.exports = {
root: true,
env: {
node: true,
},
extends: ['plugin:vue/essential', '@vue/airbnb', '@vue/typescript'],
rules: {
// js 和 ts 不需要检查 import 的文件后缀
'import/extensions': [
'error',
'always',
{
js: 'never',
ts: 'never',
},
],
'no-restricted-syntax': [
'error',
'WithStatement',
'BinaryExpression[operator=\'in\']',
],
// 可以 debugger
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
// 不要分号
semi: [2, 'never'],
// 全部单引号
quotes: [2, 'single'],
// 对象缩写
'object-shorthand': 0,
// 可以使用 console
'no-console': 0,
// 允许使用匿名函数
'func-names': 0,
// 允许属性的 key 值加引号
'quote-props': 0,
// 允许对函数的参数赋值
'no-param-reassign': 0,
// 函数的参数可以不使用
'no-unused-vars': 0,
// 不用强制 export default
'import/prefer-default-export': 0,
// 不禁止箭头函数直接return对象
'arrow-body-style': 0,
// 允许空行
'no-trailing-spaces': ['error', { skipBlankLines: true }],
// 允许short circuit evaluations
'no-unused-expressions': [
'error',
{ allowShortCircuit: true, allowTernary: true },
],
// 最长字符
'max-len': ['error', { code: 1500 }],
'vue/no-parsing-error': [
2,
{
'invalid-first-character-of-tag-name': false,
},
],
// no-plusplus
'no-plusplus': 0,
'class-methods-use-this': 0,
'no-irregular-whitespace': 0,
'consistent-return': 0,
'import/no-extraneous-dependencies': 0,
'global-require': 0,
'no-continue': 0,
'linebreak-style': 0,
},
parserOptions: {
parser: '@typescript-eslint/parser',
},
}
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug Report
about: 事情不像预期的那样工作吗?
title: ''
labels: 'bug'
assignees: ''
---
## 我的环境
| 名称 | 值 |
| ------- | ---- |
| 操作系统 | |
| 软件版本 | |
| 主题名称 | |
---
## 期望行为
## 当前行为
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature Request
about: 想让我们为 Gridea 增加什么功能吗?
title: 'feat: '
labels: 'Feature Request'
assignees: ''
---
## 概述
## 动机
## 详细解释
================================================
FILE: .github/ISSUE_TEMPLATE/question.md
================================================
---
name: Question
about: 对 Gridea 有任何问题吗?
title: ''
labels: 'question'
assignees: ''
---
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
#Electron-builder output
/dist_electron
================================================
FILE: .npmrc
================================================
registry=https://registry.npmjs.org
================================================
FILE: CHANGELOG.md
================================================
## v0.9.1
`2020-01-01`
- 修复首次安装使用 Gridea,无法预览 BUG
## v0.9.0
`2019-12-28`
**注意:若你之前有手动放置文件或文件夹到构建后文件夹(output 文件夹)本次升级有非兼容升级,请看下升级说明,若没有,可直接升级啦**
升级说明:
需手动复制到博客源文件夹的 static 文件夹(若无此文件夹可新建一个,新版使用时会自动生成,有静态文件需求的可放在此文件夹,构建时会直接复制到 output 文件夹)
## 新增
- 新增 SFTP 部署
- 新增文章置顶功能
- 自定义配置支持图片类型和数组类型,增加文章数据卡片类型
- 自定义归档路径前缀
- 文章页与标签页支持精简 URL 与默认 URL
- 新增菜单拖动排序
- 支持自定义模板渲染,具体可见[Gridea 文档](https://gridea.dev/docs)
## 修复
- 修复更改文章 URL 大小写会删除文章的 bug
- 修复编辑器超出一屏后回车不会滚动的 bug
- 修复 Linux(Ubuntu)初始化配置时,检测远程链接失败的 BUG
## 优化
- 文章内图片支持懒加载(基于 chrome 的lazy loading)
- 升级 Electron 至 7.x
- 标签页支持渲染列表隐藏文章(break change)
- 增加预览唤出快捷键(Ctrl + P)
- 优化编辑器 Windows 下字体显示
- 增加渲染过程错误日志弹窗
- 增加应用内通知系统
## v0.8.3
`2019-09-23`
「全新体验,助你妙笔生花」
## 新增
- 全新的内置编辑器,全新的编辑体验与快捷键
- 增加 Emoji 输入面板与文章信息统计
- 增加 `isHomepage`字段,赋予主题开发更多可能性
- Markdown 渲染支持 `Emoji` 与 `implict-figures`
- 增加 GA 统计,为了更好的优化产品
- 新上架一款主题 「Tech」
## 修复
- 修复文章编辑页返回未保存提示,内容不丢失
- 修复创建文章时报 URL 冲突 BUG
## 优化
- 升级 Electron 版本到 6.0+,更快了
- 更改本地预览为 Server 模式
- 提升了阅读时间的准确度
- 更多细节优化,等你发现
## v0.8.2
`2019-07-15`
## 新增
- 文章编辑页增加 `Command/Ctrl + S` 快捷键,快速保存文章
- 增加繁体中文显示 (thanks @joinmouse )
- Makrdown 渲染支持 `Mark`、`Sup`、`Abbr`、`Footnote`
- 侧边栏新增直达站点按钮
- 增加上一篇文章渲染字段支持 `post.prevPost`
- 文章增加 Reading Time(阅读时间,例如 `9 min read`) 字段 `post.stats`
- Markdown 渲染支持设置图片大小,例如:``
## 优化
- 更新 Electron 版本到 5.0.6,提升应用内菜单切换流畅性,更快了
- 优化编辑器**编辑体验**与应用内预览样式(**强烈推荐**)
- 新增文章编辑时可新建标签
- 提升编辑器的安全性
- Notes 主题显示优化与增加目录显示(**需手动更新主题**)
- 应用菜单多语言支持与优化
## v0.8.1
`2019-05-26`
### 新增
- 增加 **Linux 版**
- 增加 RSS 支持,并在默认主题中增加链接(⚠️**需手动更新主题**,或在原主题中添加链接为 `href="<%= themeConfig.domain %>/atom.xml"`)
- 增加 **Task lists** 渲染支持
- 增加**编辑器粘贴剪切板中图片**功能
- 增加 [主题市场](https://gridea.dev/themes/) 与 [主题开发样板](https://github.com/getgridea/gridea-theme-starter),欢迎参与主题开发
### 修复
- 修复文章标题包含特殊字符时渲染 BUG
- 修复 KaTeX 公式渲染 BUG(⚠️**需手动更新主题**,若不更新主题,文章内使用 KaTeX 可能会造成文章公式显示异常)
- 修复文章详情页 `site.posts` 包含隐藏文章 BUG
- 修复应用内预览文章,点击链接为应用内打开 BUG
- 修复编辑文章时,更改文章标题,文章 URL 自动变化 bug
### 优化
- 编辑器菜单栏置顶优化
- 优化安装包体积,减小 **25%**
> **提示:**
> 手动更新主题,需将`源文件夹/themes` 文件夹里的旧主题删除,重启应用即可
> (源文件夹默认为:`~/Documents/Gridea`,也可在应用内:**系统 -> 源文件夹** 进行查看)
## v0.8.0
`2019-04-14`
### 新增
- 全新品牌名 **Gridea**,更易读!
- 全新 LOGO,更好记!thanks @Leohuaji
- 全新视觉,更简约!
- 新增主题自定义配置功能,你可以使用主题提供的社交、谷歌统计、自定义样式...等自定义配置,当然你也可以开发新的主题,提供更有趣的自定义配置,尽情发挥你的想象(⚠️**需手动更新主题**)
- 新增一款主题 「Paper」,英文手写体风格,欢迎体验
- 封面图支持外链
- 文章支持 `@[toc]` 显示文章目录,同时增加 `post.toc` 字段供主题显示
- [**KaTeX**](https://katex.org/) 公式支持
- 全新项目主页,欢迎 [访问](https://gridea.dev)
### 修复
- 修复基本配置未填写时,点击检测远程链接导致后续操作失效 BUG
- 修复文章 URL 中含有 `/` 导致新建文章保存失败 BUG #29
- 修复 Favicon 和 Avatar 图片缓存问题 #24
- 修复更改源文件夹,新文件夹为空时 BUG
### 优化
- 固定文章编辑页附属信息栏,长文章编辑体验增强
- 内置主题细节优化,适配主题自定义配置功能
> **提示:**
> 若您是老用户,安装新版之后,需将 `源文件夹/themes` 文件夹里的旧主题删除,重启应用,方可使用内置主题的自定义配置功能
> (源文件夹默认为:旧:`~/Documents/hve-notes`,新:`~/Documents/Gridea`)
## v0.7.7
`2019-03-01`
- 🛠 修复了应用中非第一页文章删除时 BUG
## v0.7.6
`2019-02-27`
### 新增
- 🔥 Windows 安装支持**自定义安装路径**
- 🔥 增加**远程连接检测**功能
- 🔥 新增一款主题: **Simple**
- 🔥 增加**自定义源文件夹**,可利用 OneDrive、百度网盘、iCloud、Dropbox 等进行多设备同步
- 🧩 增加文章目录(TOC)能力(数据支持,还需主题支持)
### 修复
- 🛠 文章时间渲染错误
- 🛠 所有文章隐藏渲染错误
- 🛠 部署除 master 分支外分支不成功问题
- 🛠 标签带空格使用时渲染错误
- 🛠 文章编辑全屏时无法点击输入菜单栏的 BUG
- 🛠 文章快捷输入链接菜单BUG
### 优化
- 🌟 摘要分隔符判断逻辑 <!-- more --> 单独行为摘要渲染分割符
- 🌟 调整应用部分 UI
- 🌟 增加应用退出快捷键
- 🌟 增加同步和检测远程连接时开发者工具控制台错误信息显示,更精准的定位同步问题
## v0.7.5 - 🎈 新春快乐 🏮 🧨 🧧
`2019-01-30`
- 🌈 重写应用 UI
- 🔥 增加**归档页**渲染支持和**每页归档数**自定义功能
- 🔥 增加**标签页**渲染支持
- 🔥 增加文章 Link 和标签 Link 设置**默认生成方式**和**自定义**支持
- 🔥 增加**列表隐藏文章**功能,用于创建特殊页面使用(例如,**关于**页)
- 🔥 增加**显示日期格式自定义**功能
- 🌟 优化默认主题「notes、fly」,支持归档页和标签页显示(️️⚡️ 需手动更新)
- 🌟 更新文档
> 注:
> - 更新主题,需将 `~/Documents/hve-notes/themes` 文件夹中主题删除,重新启动应用即可
## v0.7.0
`2019-01-20`
- 🔥 增加多语言支持:English、简体中文(默认)
- 🔥 增加多平台部署支持:Github Pages、Coding Pages
- 🔥 增加多评论系统支持:Gitalk、DisqusJS(不兼容更新 ⚡️)
- 🌟 全新的文章编辑页,专注写作
- 🌟 优化多处交互体验
- 🌟 完善文档,更强大的主题开发能力
- 🌟 优化默认主题「notes、fly」:多评论支持及 UI 优化
> 注:
> - 更新主题,需将 `~/Documents/hve-notes/themes` 文件夹中对应主题删除,重新启动应用即可
> - ⚡️ 若之前有设置过 Gitalk 配置,此版本更新后需重新设置一下(原 Gitalk 配置信息可见`~/Documents/hve-notes/config/setting.json`)
## v0.6.4
- 新增一款主题:「fly」,优化默认主题UI:「notes」 (若想更新 notes 主题,已安装旧版本用户需将旧版本应用文件夹 `~/Documents/hve-notes` 删除,安装新版应用即可(记得备份文件哦!))
- 新增头像设置功能
- 调整多处 UI
- 完善主题开发文档
## v0.6.3
- 🐛 更改构建配置,修复了应用初始化文件夹的 BUG(使用前,需要手动删除文档(Documents)目录下的 hve-notes 文件夹,然后打开应用即可正常初始化)
- 修复 material icons load bug,感谢 @rosuH
- 简单优化 favicon 页面 UI
- 添加发布前配置简单校验
- 🙏 感谢支持
## v0.6.2
- 优化默认主题 title、link 等
- 新增版本更新提醒
- 新增 CNAME 配置
- 新增 favicon 配置
- 新增 Gitalk 配置
- 修复 Windows 下文章更新 bug
## v0.6.1
- 增加文章删除功能
- 更新 footer 信息
- 更改顶部窗口控制方式
- 更改默认打开窗口大小
- windows版 和 mac 版同步发布 ✌
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 EryouHao
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.
================================================
FILE: README-ru.md
================================================
<% let years = []; posts.forEach((item) => { const year = item.date.substring(0, 4); if (!years.includes(year)) { years.push(year); } }); %>
文章归档
<% years.forEach(function(year) { %>
<%- year %>
<% posts.forEach(function(post) { %>
<%if (post.date.indexOf(year) !== -1) { %>
<%= post.title %>
<% } %>
<% }); %>
<% }); %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/paper/templates/index.ejs
================================================
<%- include('./_blocks/head', { siteTitle: themeConfig.siteName }) %>
<%- include('./_blocks/header') %>
<%- include('./_blocks/post-list') %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/paper/templates/post.ejs
================================================
<%- include('./_blocks/head', { siteTitle: `${post.title} | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/header') %>
<%= post.title %>
<%= post.dateFormat %>
<% post.tags.forEach(function(tag, tagIndex) { %>
<%= tag.name %>
<% }); %>
<% if (post.feature) { %>
<% } %>
<%- post.content %>
<% if (post.nextPost && !post.hideInList) { %>
<%= site.customConfig.nextArticleText || '下一篇' %>
<%= post.nextPost.title %>
<% } %>
<% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %>
<% if (commentSetting.commentPlatform === 'gitalk') { %>
<% } %>
<% if (commentSetting.commentPlatform === 'disqus') { %>
<% } %>
<% } %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/paper/templates/tag.ejs
================================================
<%- include('./_blocks/head', { siteTitle: `${tag.name} | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/header') %>
标签: <%= tag.name %>
<%- include('./_blocks/post-list') %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/paper/templates/tags.ejs
================================================
<%- include('./_blocks/head', { siteTitle: `标签列表 | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/header') %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/simple/assets/styles/_core/a11y-dark.less
================================================
/* a11y-dark theme */
/* Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css */
/* @author: ericwbailey */
/* Comment */
.hljs-comment,
.hljs-quote {
color: #d4d0ab;
}
/* Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #ffa07a;
}
/* Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5ab35;
}
/* Yellow */
.hljs-attribute {
color: #ffd700;
}
/* Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #abe338;
}
/* Blue */
.hljs-title,
.hljs-section {
color: #00e0e0;
}
/* Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #dcc6e0;
}
.hljs {
display: block;
overflow-x: auto;
background: #2b2b2b;
color: #f8f8f2;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
@media screen and (-ms-high-contrast: active) {
.hljs-addition,
.hljs-attribute,
.hljs-built_in,
.hljs-builtin-name,
.hljs-bullet,
.hljs-comment,
.hljs-link,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-params,
.hljs-string,
.hljs-symbol,
.hljs-type,
.hljs-quote {
color: highlight;
}
.hljs-keyword,
.hljs-selector-tag {
font-weight: bold;
}
}
================================================
FILE: public/default-files/themes/simple/assets/styles/_core/atom-one-dark.less
================================================
/*
Atom One Dark by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
base: #282c34
mono-1: #abb2bf
mono-2: #818896
mono-3: #5c6370
hue-1: #56b6c2
hue-2: #61aeee
hue-3: #c678dd
hue-4: #98c379
hue-5: #e06c75
hue-5-2: #be5046
hue-6: #d19a66
hue-6-2: #e6c07b
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #abb2bf;
background: #282c34;
}
.hljs-comment,
.hljs-quote {
color: #5c6370;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c678dd;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #98c379;
}
.hljs-built_in,
.hljs-class .hljs-title {
color: #e6c07b;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #d19a66;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}
================================================
FILE: public/default-files/themes/simple/assets/styles/_core/base.less
================================================
/*! modern-normalize | MIT License | https://github.com/sindresorhus/modern-normalize */
/* Document
========================================================================== */
/**
* Use a better box model (opinionated).
*/
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
/**
* Use a more readable tab size (opinionated).
*/
:root {
-moz-tab-size: 4;
tab-size: 4;
}
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
font-size: 14px;
}
/**
* Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
*/
body {
font-family:
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
Helvetica,
Arial,
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol';
}
/* Grouping content
========================================================================== */
/**
* Add the correct height in Firefox.
*/
hr {
height: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr[title] {
text-decoration: underline dotted;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Remove the inheritance of text transform in Edge and Firefox.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers.
*/
legend {
padding: 0;
}
/**
* Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/**
* Correct the cursor style of increment and decrement buttons in Safari.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
================================================
FILE: public/default-files/themes/simple/assets/styles/_core/colors.less
================================================
//
//
// 𝗖 𝗢 𝗟 𝗢 𝗥
// v 1.6.3
//
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// General
// ───────────────────────────────────
@oc-white: #ffffff;
@oc-black: #000000;
// Gray
// ───────────────────────────────────
@oc-gray-list: #f8f9fa, #f1f3f5, #e9ecef, #dee2e6, #ced4da, #adb5bd, #868e96, #495057, #343a40, #212529;
@oc-gray-0: extract(@oc-gray-list, 1);
@oc-gray-1: extract(@oc-gray-list, 2);
@oc-gray-2: extract(@oc-gray-list, 3);
@oc-gray-3: extract(@oc-gray-list, 4);
@oc-gray-4: extract(@oc-gray-list, 5);
@oc-gray-5: extract(@oc-gray-list, 6);
@oc-gray-6: extract(@oc-gray-list, 7);
@oc-gray-7: extract(@oc-gray-list, 8);
@oc-gray-8: extract(@oc-gray-list, 9);
@oc-gray-9: extract(@oc-gray-list, 10);
// Red
// ───────────────────────────────────
@oc-red-list: #fff5f5, #ffe3e3, #ffc9c9, #ffa8a8, #ff8787, #ff6b6b, #fa5252, #f03e3e, #e03131, #c92a2a;
@oc-red-0: extract(@oc-red-list, 1);
@oc-red-1: extract(@oc-red-list, 2);
@oc-red-2: extract(@oc-red-list, 3);
@oc-red-3: extract(@oc-red-list, 4);
@oc-red-4: extract(@oc-red-list, 5);
@oc-red-5: extract(@oc-red-list, 6);
@oc-red-6: extract(@oc-red-list, 7);
@oc-red-7: extract(@oc-red-list, 8);
@oc-red-8: extract(@oc-red-list, 9);
@oc-red-9: extract(@oc-red-list, 10);
// Pink
// ───────────────────────────────────
@oc-pink-list: #fff0f6, #ffdeeb, #fcc2d7, #faa2c1, #f783ac, #f06595, #e64980, #d6336c, #c2255c, #a61e4d;
@oc-pink-0: extract(@oc-pink-list, 1);
@oc-pink-1: extract(@oc-pink-list, 2);
@oc-pink-2: extract(@oc-pink-list, 3);
@oc-pink-3: extract(@oc-pink-list, 4);
@oc-pink-4: extract(@oc-pink-list, 5);
@oc-pink-5: extract(@oc-pink-list, 6);
@oc-pink-6: extract(@oc-pink-list, 7);
@oc-pink-7: extract(@oc-pink-list, 8);
@oc-pink-8: extract(@oc-pink-list, 9);
@oc-pink-9: extract(@oc-pink-list, 10);
// Grape
// ───────────────────────────────────
@oc-grape-list: #f8f0fc, #f3d9fa, #eebefa, #e599f7, #da77f2, #cc5de8, #be4bdb, #ae3ec9, #9c36b5, #862e9c;
@oc-grape-0: extract(@oc-grape-list, 1);
@oc-grape-1: extract(@oc-grape-list, 2);
@oc-grape-2: extract(@oc-grape-list, 3);
@oc-grape-3: extract(@oc-grape-list, 4);
@oc-grape-4: extract(@oc-grape-list, 5);
@oc-grape-5: extract(@oc-grape-list, 6);
@oc-grape-6: extract(@oc-grape-list, 7);
@oc-grape-7: extract(@oc-grape-list, 8);
@oc-grape-8: extract(@oc-grape-list, 9);
@oc-grape-9: extract(@oc-grape-list, 10);
// Violet
// ───────────────────────────────────
@oc-violet-list: #f3f0ff, #e5dbff, #d0bfff, #b197fc, #9775fa, #845ef7, #7950f2, #7048e8, #6741d9, #5f3dc4;
@oc-violet-0: extract(@oc-violet-list, 1);
@oc-violet-1: extract(@oc-violet-list, 2);
@oc-violet-2: extract(@oc-violet-list, 3);
@oc-violet-3: extract(@oc-violet-list, 4);
@oc-violet-4: extract(@oc-violet-list, 5);
@oc-violet-5: extract(@oc-violet-list, 6);
@oc-violet-6: extract(@oc-violet-list, 7);
@oc-violet-7: extract(@oc-violet-list, 8);
@oc-violet-8: extract(@oc-violet-list, 9);
@oc-violet-9: extract(@oc-violet-list, 10);
// Indigo
// ───────────────────────────────────
@oc-indigo-list: #edf2ff, #dbe4ff, #bac8ff, #91a7ff, #748ffc, #5c7cfa, #4c6ef5, #4263eb, #3b5bdb, #364fc7;
@oc-indigo-0: extract(@oc-indigo-list, 1);
@oc-indigo-1: extract(@oc-indigo-list, 2);
@oc-indigo-2: extract(@oc-indigo-list, 3);
@oc-indigo-3: extract(@oc-indigo-list, 4);
@oc-indigo-4: extract(@oc-indigo-list, 5);
@oc-indigo-5: extract(@oc-indigo-list, 6);
@oc-indigo-6: extract(@oc-indigo-list, 7);
@oc-indigo-7: extract(@oc-indigo-list, 8);
@oc-indigo-8: extract(@oc-indigo-list, 9);
@oc-indigo-9: extract(@oc-indigo-list, 10);
// Blue
// ───────────────────────────────────
@oc-blue-list: #e7f5ff, #d0ebff, #a5d8ff, #74c0fc, #4dabf7, #339af0, #228be6, #1c7ed6, #1971c2, #1864ab;
@oc-blue-0: extract(@oc-blue-list, 1);
@oc-blue-1: extract(@oc-blue-list, 2);
@oc-blue-2: extract(@oc-blue-list, 3);
@oc-blue-3: extract(@oc-blue-list, 4);
@oc-blue-4: extract(@oc-blue-list, 5);
@oc-blue-5: extract(@oc-blue-list, 6);
@oc-blue-6: extract(@oc-blue-list, 7);
@oc-blue-7: extract(@oc-blue-list, 8);
@oc-blue-8: extract(@oc-blue-list, 9);
@oc-blue-9: extract(@oc-blue-list, 10);
// Cyan
// ───────────────────────────────────
@oc-cyan-list: #e3fafc, #c5f6fa, #99e9f2, #66d9e8, #3bc9db, #22b8cf, #15aabf, #1098ad, #0c8599, #0b7285;
@oc-cyan-0: extract(@oc-cyan-list, 1);
@oc-cyan-1: extract(@oc-cyan-list, 2);
@oc-cyan-2: extract(@oc-cyan-list, 3);
@oc-cyan-3: extract(@oc-cyan-list, 4);
@oc-cyan-4: extract(@oc-cyan-list, 5);
@oc-cyan-5: extract(@oc-cyan-list, 6);
@oc-cyan-6: extract(@oc-cyan-list, 7);
@oc-cyan-7: extract(@oc-cyan-list, 8);
@oc-cyan-8: extract(@oc-cyan-list, 9);
@oc-cyan-9: extract(@oc-cyan-list, 10);
// Teal
// ───────────────────────────────────
@oc-teal-list: #e6fcf5, #c3fae8, #96f2d7, #63e6be, #38d9a9, #20c997, #12b886, #0ca678, #099268, #087f5b;
@oc-teal-0: extract(@oc-teal-list, 1);
@oc-teal-1: extract(@oc-teal-list, 2);
@oc-teal-2: extract(@oc-teal-list, 3);
@oc-teal-3: extract(@oc-teal-list, 4);
@oc-teal-4: extract(@oc-teal-list, 5);
@oc-teal-5: extract(@oc-teal-list, 6);
@oc-teal-6: extract(@oc-teal-list, 7);
@oc-teal-7: extract(@oc-teal-list, 8);
@oc-teal-8: extract(@oc-teal-list, 9);
@oc-teal-9: extract(@oc-teal-list, 10);
// Green
// ───────────────────────────────────
@oc-green-list: #ebfbee, #d3f9d8, #b2f2bb, #8ce99a, #69db7c, #51cf66, #40c057, #37b24d, #2f9e44, #2b8a3e;
@oc-green-0: extract(@oc-green-list, 1);
@oc-green-1: extract(@oc-green-list, 2);
@oc-green-2: extract(@oc-green-list, 3);
@oc-green-3: extract(@oc-green-list, 4);
@oc-green-4: extract(@oc-green-list, 5);
@oc-green-5: extract(@oc-green-list, 6);
@oc-green-6: extract(@oc-green-list, 7);
@oc-green-7: extract(@oc-green-list, 8);
@oc-green-8: extract(@oc-green-list, 9);
@oc-green-9: extract(@oc-green-list, 10);
// Lime
// ───────────────────────────────────
@oc-lime-list: #f4fce3, #e9fac8, #d8f5a2, #c0eb75, #a9e34b, #94d82d, #82c91e, #74b816, #66a80f, #5c940d;
@oc-lime-0: extract(@oc-lime-list, 1);
@oc-lime-1: extract(@oc-lime-list, 2);
@oc-lime-2: extract(@oc-lime-list, 3);
@oc-lime-3: extract(@oc-lime-list, 4);
@oc-lime-4: extract(@oc-lime-list, 5);
@oc-lime-5: extract(@oc-lime-list, 6);
@oc-lime-6: extract(@oc-lime-list, 7);
@oc-lime-7: extract(@oc-lime-list, 8);
@oc-lime-8: extract(@oc-lime-list, 9);
@oc-lime-9: extract(@oc-lime-list, 10);
// Yellow
// ───────────────────────────────────
@oc-yellow-list: #fff9db, #fff3bf, #ffec99, #ffe066, #ffd43b, #fcc419, #fab005, #f59f00, #f08c00, #e67700;
@oc-yellow-0: extract(@oc-yellow-list, 1);
@oc-yellow-1: extract(@oc-yellow-list, 2);
@oc-yellow-2: extract(@oc-yellow-list, 3);
@oc-yellow-3: extract(@oc-yellow-list, 4);
@oc-yellow-4: extract(@oc-yellow-list, 5);
@oc-yellow-5: extract(@oc-yellow-list, 6);
@oc-yellow-6: extract(@oc-yellow-list, 7);
@oc-yellow-7: extract(@oc-yellow-list, 8);
@oc-yellow-8: extract(@oc-yellow-list, 9);
@oc-yellow-9: extract(@oc-yellow-list, 10);
// Orange
// ───────────────────────────────────
@oc-orange-list: #fff4e6, #ffe8cc, #ffd8a8, #ffc078, #ffa94d, #ff922b, #fd7e14, #f76707, #e8590c, #d9480f;
@oc-orange-0: extract(@oc-orange-list, 1);
@oc-orange-1: extract(@oc-orange-list, 2);
@oc-orange-2: extract(@oc-orange-list, 3);
@oc-orange-3: extract(@oc-orange-list, 4);
@oc-orange-4: extract(@oc-orange-list, 5);
@oc-orange-5: extract(@oc-orange-list, 6);
@oc-orange-6: extract(@oc-orange-list, 7);
@oc-orange-7: extract(@oc-orange-list, 8);
@oc-orange-8: extract(@oc-orange-list, 9);
@oc-orange-9: extract(@oc-orange-list, 10);
================================================
FILE: public/default-files/themes/simple/assets/styles/_core/github.less
================================================
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
================================================
FILE: public/default-files/themes/simple/assets/styles/main.less
================================================
@import "_core/colors";
@import "_core/base";
@import "_core/a11y-dark";
body {
background: @oc-gray-0;
color: @oc-gray-7;
font-size: 16px;
}
a {
text-decoration: none;
color: @oc-gray-7;
}
.sidebar {
width: 320px;
position: fixed;
left: 0;
top: 0;
bottom: 0;
background-color: #7c8280;
background-size: cover;
background-position: center;
background-image: url('../media/images/sidebar-bg.jpg');
display: flex;
flex-direction: column;
overflow-y: scroll;
.menu-btn {
display: none;
}
.top-container {
text-align: center;
padding: 48px 16px;
flex: 1;
.site-logo {
width: 80px;
height: 80px;
border-radius: 50%;
border: 2px solid #F1F3F5;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
}
.site-title {
font-size: 24px;
padding: 32px 0;
color: @oc-gray-3;
}
.site-nav {
display: block;
padding: 8px 16px;
margin: 16px 0;
color: @oc-gray-3;
transition: all 0.3s;
&:hover {
color: @oc-gray-0;
}
}
}
.bottom-container {
padding: 24px 16px;
color: @oc-gray-3;
font-size: 12px;
.site-description {
padding: 16px 0;
}
a {
color: #fff;
}
}
}
.main-container {
margin-left: 320px;
}
.content-container {
max-width: 1064px;
margin: 0 auto;
padding: 48px 32px;
}
.post-item {
display: flex;
padding-bottom: 32px;
margin-bottom: 32px;
border-bottom: 1px solid @oc-gray-4;
&:last-of-type {
border-bottom: none;
}
.left {
flex: 1;
.post-title {
font-size: 24px;
transition: all 0.3s;
&:hover {
color: @oc-gray-9;
}
}
.post-date {
font-size: 18px;
padding: 24px 0;
color: @oc-gray-5;
}
.post-abstract {
line-height: 24px;
font-size: 18px;
color: @oc-gray-6;
}
}
.right {
flex-shrink: 0;
margin-left: 24px;
width: 38.2%;
.feature-container {
padding-top: 56.25%;
background-size: cover;
background-position: center;
border-radius: 3px;
box-shadow: 0 2px 5px rgba(0,0,25,0.1), 0 5px 75px 1px rgba(0,0,50,0.2);
}
}
}
.pagination-container {
display: flex;
justify-content: space-between;
.prev, .next {
display: inline-block;
padding: 8px 16px;
background: #fff;
border: 1px solid @oc-gray-1;
border-radius: 2px;
transition: all 0.3s;
&:hover {
transform: translateY(-3px);
border: 1px solid @oc-gray-3;
}
}
}
.post-detail {
max-width: 720px;
margin: 0 auto;
.feature-container {
padding-top: 56.25%;
background-size: cover;
background-position: center;
border-radius: 3px;
box-shadow: 0 2px 5px rgba(0,0,25,0.1), 0 5px 75px 1px rgba(0,0,50,0.2);
margin-bottom: 24px;
}
.post-title {
font-size: 40px;
}
.post-date {
font-size: 18px;
padding: 24px 0;
color: @oc-gray-5;
}
.post-content {
h1, h2, h3, h4, h5, h6 {
margin: 16px 0;
color: @oc-gray-8;
}
a {
color: @oc-indigo-6;
border-bottom: 1px dotted @oc-indigo-6;
transition: all 0.3s;
&:hover {
color: @oc-indigo-8;
border-bottom: 1px dotted @oc-indigo-8;
}
}
img {
display: block;
box-shadow: 0 2px 5px rgba(0,0,25,0.1), 0 5px 75px 1px rgba(0,0,50,0.2);
max-width: 100%;
border-radius: 2px;
margin: 24px auto;
}
p {
line-height: 1.725;
margin-bottom: 24px;
font-size: 18px;
color: @oc-gray-7;
}
p, ul, ol {
code {
padding: 0 3px;
margin: 0 2px;
// background: rgba(195,195,195,0.41);
background: @oc-gray-2;
font-size: 0.9em;
border-radius: 2px;
border: 1px solid @oc-gray-3;
display: inline-block;
line-height: 1.5;
color: @oc-gray-6;
}
}
blockquote {
background: @oc-gray-2;
padding: 16px;
border-left: 3px solid @oc-violet-7;
border-radius: 2px;
margin-bottom: 16px;
p {
color: @oc-gray-6;
margin-bottom: 0;
}
}
pre {
margin-bottom: 16px;
code {
font-size: 14px;
font-family: 'Source Code Pro', Consolas, Menlo, Monaco, 'Courier New', monospace;
padding: 2em 1em 1em;
border-radius: 5px;
line-height: 1.375;
position: relative;
background: @oc-gray-8;
color: @oc-gray-1;
display: block;
&:after {
content: 'CODE';
display: block;
position: absolute;
left: 8px;
top: 4px;
font-size: 14px;
font-weight: bold;
color: @oc-gray-7;
}
}
}
table {
border-collapse: collapse;
margin: 1rem 0;
display: block;
overflow-x: auto;
}
tr {
border-top: 1px solid #dfe2e5;
}
td, th {
border: 1px solid #dfe2e5;
padding: .6em 1em;
}
ul, ol {
color: var(--c-base-blacklight);
padding-left: 24px;
line-height: 1.725;
margin-bottom: 16px;
}
strong {
font-weight: bolder;
}
em {
color: @oc-gray-6;
}
hr {
height: 0;
border: 2px solid #efefef;
margin-bottom: 24px;
}
}
}
.tag {
display: inline-block;
font-size: 14px;
padding: 8px 16px;
border-radius: 16px;
background: @oc-gray-2;
color: @oc-gray-6;
margin: 16px 16px 16px 0;
transition: all 0.3s;
&:hover {
background: @oc-gray-3;
color: @oc-gray-7;
transform: translateY(-3px);
}
}
.next-post {
border-top: 1px solid @oc-gray-4;
border-bottom: 1px solid @oc-gray-4;
padding: 24px 0;
margin: 32px 0;
.post-title {
font-size: 24px;
}
.next {
color: @oc-gray-4;
margin-bottom: 16px;
}
}
.archives-title, .tag-list-title, .current-tag {
color: @oc-gray-7;
padding-bottom: 48px;
font-size: 32px;
}
.archives-container {
padding-bottom: 32px;
.year {
font-size: 16px;
padding-bottom: 16px;
border-bottom: 1px solid @oc-gray-4;
margin: 16px 0;
color: @oc-gray-6;
}
.post {
padding-bottom: 16px;
.post-title {
font-size: 18px;
transition: all 0.3s;
&:hover {
color: @oc-gray-9;
}
}
}
}
// Mobile ----------------------- //
@media (max-width: 800px) {
.sidebar {
position: relative;
width: 100% !important;
height: 80px;
overflow: hidden;
transition: height 0.382s ease-in-out;
&.full-height {
height: 100vh;
}
.sidebar-content {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.top-header-container {
display: flex;
justify-content: space-between;
margin-top: 16px;
.menu-btn {
display: block;
position: relative;
width: 48px;
height: 48px;
.line {
width: 32px;
height: 2px;
background: @oc-gray-2;
border-radius: 2px;
position: absolute;
right: 0;
top: 23px;
}
&:before, &:after {
content: '';
display: block;
width: 32px;
height: 2px;
background: @oc-gray-2;
border-radius: 2px;
position: absolute;
right: 0;
}
&:before {
top: 12px;
}
&:after {
bottom: 12px;
}
}
}
.top-container {
text-align: left;
padding: 0 16px;
.site-title-container {
display: flex;
align-items: center;
}
.site-logo {
width: 48px;
height: 48px;
}
.site-title {
display: inline;
padding: 0 8px;
font-size: 18px;
}
}
}
.main-container {
margin-left: 0 !important;;
}
.content-container {
padding: 32px 16px;
}
.post-item {
flex-direction: column-reverse;
padding-bottom: 16px;
margin-bottom: 16x;
.right {
width: 100%;
margin-left: 0;
margin-bottom: 16px;
}
.left {
.post-date {
font-size: 16px;
padding: 16px 0;
}
.post-abstract {
font-size: 16px;
}
}
}
.pagination-container {
.prev, .next {
&:hover {
transform: translateY(0px);
}
}
}
.post-detail {
.post-title {
font-size: 28px;
}
.post-date {
font-size: 16px;
padding: 16px 0;
}
.feature-container {
margin-bottom: 16px;
}
.post-content {
p {
font-size: 16px;
}
}
}
.next-post {
margin: 24px 0;
padding: 16px 0;
}
.archives-title, .tag-list-title, .current-tag {
font-size: 28px;
padding-bottom: 32px;
}
.tag {
margin: 8px 8px 8px 0;
&:hover {
transform: translateY(0px);
}
}
}
.social-container {
.social-link {
color: #dee2e6;
font-size: 16px;
margin: 4px 8px;
}
}
================================================
FILE: public/default-files/themes/simple/config.json
================================================
{
"name": "Simple",
"version": "1.1.0",
"author": "EryouHao",
"customConfig": [
{
"name": "sidebarWidth",
"label": "菜单栏宽度",
"group": "布局",
"value": "320px",
"type": "input",
"note": "可填像素类型(如:320px)或百分比类型(如:38.2%)"
},
{
"name": "featureBorderRadius",
"label": "封面图圆角",
"group": "布局",
"value": "3px",
"type": "input",
"note": "像素类型(如:3px)"
},
{
"name": "menuColor",
"label": "菜单颜色",
"group": "颜色",
"value": "#dee2e6",
"type": "input",
"card": "color",
"note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))"
},
{
"name": "contentBgColor",
"label": "内容区背景色",
"group": "颜色",
"value": "#f8f9fa",
"type": "input",
"card": "color",
"note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))"
},
{
"name": "renderKaTeX",
"label": "是否渲染 KaTeX 公式",
"group": "渲染",
"value": false,
"type": "switch"
},
{
"name": "renderCode",
"label": "是否渲染代码高亮",
"group": "渲染",
"value": false,
"type": "switch"
},
{
"name": "github",
"label": "Github",
"group": "社交",
"value": "",
"type": "input",
"note": "链接地址"
},
{
"name": "twitter",
"label": "Twitter",
"group": "社交",
"value": "",
"type": "input",
"note": "链接地址"
},
{
"name": "weibo",
"label": "微博",
"group": "社交",
"value": "",
"type": "input",
"note": "链接地址"
},
{
"name": "zhihu",
"label": "知乎",
"group": "社交",
"value": "",
"type": "input",
"note": "链接地址"
},
{
"name": "facebook",
"label": "Facebook",
"group": "社交",
"value": "",
"type": "input",
"note": "链接地址"
},
{
"name": "customCss",
"label": "自定义CSS",
"group": "自定义样式",
"value": "",
"type": "textarea",
"note": ""
},
{
"name": "ga",
"label": "跟踪 ID",
"group": "谷歌统计",
"value": "",
"type": "input",
"note": "UA-xxxxxxxxx-x"
}
]
}
================================================
FILE: public/default-files/themes/simple/style-override.js
================================================
const generateOverride = (params = {}) => {
let result = ''
// 侧边栏宽度 - sidebarWidth
if (params.sidebarWidth && params.sidebarWidth !== '320px') {
result += `
.sidebar {
width: ${params.sidebarWidth};
}
.main-container {
margin-left: ${params.sidebarWidth};
}
`
}
// 菜单颜色 - menuColor
if (params.menuColor && params.menuColor !== '#dee2e6') {
result += `
.sidebar .top-container .site-nav {
color: ${params.menuColor};
}
`
}
// 封面图圆角 - featureBorderRadius
if (params.featureBorderRadius && params.featureBorderRadius !== '3px') {
result += `
.post-item .right .feature-container {
border-radius: ${params.featureBorderRadius};
}
`
}
// 内容区背景色 - contentBgColor
if (params.contentBgColor && params.contentBgColor !== '#f8f9fa') {
result += `
body {
background: ${params.contentBgColor};
}
`
}
if (params.customCss) {
result += `
${params.customCss}
`
}
console.log('result', result)
return result
}
module.exports = generateOverride
================================================
FILE: public/default-files/themes/simple/templates/_blocks/head.ejs
================================================
<%= siteTitle %>
<% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %>
<% if (commentSetting.commentPlatform === 'gitalk') { %>
<% } %>
<% if (commentSetting.commentPlatform === 'disqus') { %>
<% } %>
<% } %>
<% if (site.customConfig.ga) { %>
<% } %>
================================================
FILE: public/default-files/themes/simple/templates/_blocks/pagination.ejs
================================================
================================================
FILE: public/default-files/themes/simple/templates/_blocks/scripts.ejs
================================================
<% if (site.customConfig.renderCode) { %>
<% } %>
<% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %>
<% if (commentSetting.commentPlatform === 'gitalk') { %>
<% } %>
<% if (commentSetting.commentPlatform === 'disqus') { %>
<% } %>
<% } %>
================================================
FILE: public/default-files/themes/simple/templates/_blocks/sidebar.ejs
================================================
================================================
FILE: public/default-files/themes/simple/templates/archives.ejs
================================================
<%- include('./_blocks/head', { siteTitle: `文章归档 | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/sidebar') %>
<% let years = []; posts.forEach((item) => { const year = item.date.substring(0, 4); if (!years.includes(year)) { years.push(year); } }); %>
文章归档
<% years.forEach(function(year) { %>
<%- year %>
<% posts.forEach(function(post) { %>
<%if (post.date.indexOf(year) !== -1) { %>
<%= post.title %>
<% } %>
<% }); %>
<% }); %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/simple/templates/index.ejs
================================================
<%- include('./_blocks/head', { siteTitle: themeConfig.siteName }) %>
<% if (site.customConfig.renderKaTeX) { %>
<% } %>
<%- include('./_blocks/sidebar') %>
<% posts.forEach(function(post) { %>
<% if (themeConfig.showFeatureImage && post.feature) { %>
<% } %>
<% }); %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/simple/templates/post.ejs
================================================
<%- include('./_blocks/head', { siteTitle: `${post.title} | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/sidebar') %>
<%= post.title %>
<%= post.dateFormat %>
<% if (post.feature) { %>
<% } %>
<%- post.content %>
<% if (post.tags.length > 0) { %>
<% } %>
<% if (post.nextPost && !post.hideInList) { %>
<% } %>
<% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %>
<% if (commentSetting.commentPlatform === 'gitalk') { %>
<% } %>
<% if (commentSetting.commentPlatform === 'disqus') { %>
<% } %>
<% } %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/simple/templates/tag.ejs
================================================
<%- include('./_blocks/head', { siteTitle: `${tag.name} | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/sidebar') %>
标签: <%= tag.name %>
<% posts.forEach(function(post) { %>
<% if (themeConfig.showFeatureImage && post.feature) { %>
<% } %>
<% }); %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/default-files/themes/simple/templates/tags.ejs
================================================
<%- include('./_blocks/head', { siteTitle: `标签列表 | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %>
================================================
FILE: public/index.html
================================================
Gridea
We're sorry but Gridea doesn't work properly without JavaScript enabled. Please enable it to continue.
================================================
FILE: src/App.vue
================================================
================================================
FILE: src/assets/locales-menu.ts
================================================
const messages: any = {
'zh-CN': {
edit: '编辑',
help: '帮助',
save: '保存',
undo: '撤销',
redo: '重做',
cut: '剪切',
copy: '复制',
paste: '粘贴',
delete: '删除',
selectall: '全选',
toggledevtools: '开发者工具',
close: '关闭',
quit: '退出',
},
'zh-TW': {
edit: '编辑',
help: '帮助',
save: '保存',
undo: '撤銷',
redo: '重做',
cut: '剪切',
copy: '複製',
paste: '粘貼',
delete: '刪除',
selectall: '全選',
toggledevtools: '開發者工具',
close: '關閉',
quit: '退出',
},
'en': {
edit: 'Edit',
help: 'Help',
save: 'Save',
undo: 'Undo',
redo: 'Redo',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
delete: 'Delete',
selectall: 'Select All',
toggledevtools: 'Toogle Developer Tools',
close: 'Close Window',
quit: 'Quit',
},
'fr-FR': {
edit: 'Éditer',
help: 'Aide',
save: 'Enregistrer',
undo: 'Annuler',
redo: 'Refaire',
cut: 'Couper',
copy: 'Copier',
paste: 'Coller',
delete: 'Supprimer',
selectall: 'Tout sélectionner',
toggledevtools: 'Toogle Developer Tools',
close: 'Fermer',
quit: 'Quitter',
},
'ru': {
edit: 'Редактировать',
help: 'Помощь',
save: 'Сохранить',
undo: 'Отменить',
redo: 'Повторить',
cut: 'Вырезать',
copy: 'Копировать',
paste: 'Вставить',
delete: 'Удалить',
selectall: 'Выделить всё',
toggledevtools: 'Инструменты разработчика',
close: 'Закрыть окно',
quit: 'Выход',
},
'ja-JP': {
edit: '編集',
help: 'ヘルプ',
save: 'セーブ',
undo: '取り消す',
redo: 'やり直す',
cut: 'カット',
copy: 'コピー',
paste: 'ペースト',
delete: '削除',
selectall: '全て選択',
toggledevtools: '開発者ツール',
close: '閉じる',
quit: '終了',
}
}
export default messages
================================================
FILE: src/assets/locales.ts
================================================
const message = {
zhHans: {
preview: '预 览',
syncSite: '同 步',
newVersion: '有新版本',
article: '文 章',
menu: '菜 单',
tag: '标 签',
theme: '主 题',
remote: '远 程',
system: '系 统',
renderSuccess: '渲染完毕,快去预览吧!',
renderError: '渲染失败,请检查 hosts 文件中 127.0.0.1 是否指向 localhost。确认配置正确后,尝试重启应用。',
syncWarning: '必须完成配置才能同步哦!',
syncSuccess: '同步成功啦!',
syncError1: '同步遇到了错误,请查阅',
syncError2: '来寻找解决方案',
newVersionTips: '有新版本发布,快去下载新版本吧!',
newArticle: '新文章',
publish: '发布',
published: '已发布',
draft: '草稿',
title: '标题',
status: '状态',
createAt: '创建时间',
actions: '操作',
deleteWarning: '删除后不可撤销,你确定要删除吗?',
warning: '警告',
articleDelete: '文章已删除',
cancel: '取 消',
select: '选 择',
featureImage: '封面图',
saveDraft: '存草稿',
save: '保 存',
newMenu: '新菜单',
name: '名称',
openType: '打开方式',
link: 'Link',
menuSuccess: '菜单已保存',
menuDelete: '菜单已删除',
draftSuccess: '已存为草稿',
saveSuccess: '已保存',
newTag: '新标签',
tagName: '标签名',
selectTheme: '选择主题',
siteName: '网站名称',
siteDescription: '网站描述',
footerInfo: '底部信息',
isShowFeatureImage: '显示封面图',
articlesPerPage: '每页文章数',
archivesPerPage: '每页归档数',
basicSetting: '基础配置',
commentSetting: '评论配置',
faviconSetting: '网页图标',
avatarSetting: '头像配置',
domain: '域 名',
repository: '仓库名称',
branch: '分 支',
username: '仓库用户名',
email: '邮 箱',
isShowComment: '是否显示评论',
domainShouldStartsWithWarn: '域名应以 \'https://\' 或 \'http://\' 开头',
basicSettingSuccess: '基础配置已保存',
commentSettingSuccess: '评论配置已保存',
faviconSettingSuccess: '网页图标已保存',
avatarSettingSuccess: '头像配置已保存',
saved: '已保存',
syncing: '同步中,请耐心等待...',
articleDefault: '文章 URL 默认格式',
tagDefault: '标签 URL 默认格式',
hideInList: '列表中隐藏',
dateFormat: '日期格式',
htmlSupport: '支持 HTML',
change: '更 换',
editorTip: '你可以插入单独行的 为摘要分隔标识(此行之前内容为摘要)',
saveError: '保存失败',
privateKeyTip: '请填写绝对路径,例如:/home/username/.ssh/id_rsa',
remotePathTip: '请填写绝对路径,例如:/home/username/www/',
testConnection: '检测远程连接',
connectSuccess: '远程连接成功',
connectFailed: '远程连接失败,请检查仓库、用户名和令牌设置',
sourceFolder: '站点源文件路径',
language: '语 言',
inConfig: '配置中',
searchArticle: '搜索文章',
deleteSelected: '已选',
inputContent: '输入内容',
postUrlRepeatTip: '文章的 URL 与其他文章重复',
postUrlIncludeTip: 'URL 不可包含 /',
onlyPicDrag: '仅支持图片拖拽',
themeConfigSaved: '主题配置已保存',
reset: '重置',
reseted: '已重置',
noCustomConfigTip: '当前主题暂无自定义配置',
customConfig: '自定义配置',
moreThemes: '更多主题',
postSettings: '文章设置',
back: '返回',
savedIn: '保存于',
or: '或',
starSupport: 'Star 支持作者!',
showAllPost: '显示全文',
showAbstract: '仅显示摘要',
unsavedWarning: '你将丢失所有的未保存的更改,是否继续?',
noSaveAndBack: '继续',
insertImage: '插入图片',
insertMore: '插入摘要分隔符',
writingIn: '写作于',
words: '字 数',
readingTime: '阅读时间',
version: '版本',
token: '令 牌',
tokenUsername: '令牌用户名',
platform: '平 台',
topArticles: '置顶文章',
default: '默认',
external: '外链',
pathContainHttps: '路径必须包含 http 或 https',
articleUrlPath: '文章 URL 路径',
concise: '精简',
tagUrlPath: '标签 URL 路径',
archivePathPrefix: '归档路径前缀',
showFullText: '显示全文',
showAbstractOnly: '仅显示摘要',
numberArticlesRSS: 'RSS/Feed 文章数量',
Proxy: 'HTTP代理',
ProxyAddress: '地址',
ProxyPort: '端口',
},
zh_TW: {
preview: '預 覽',
syncSite: '同 步',
newVersion: '有新版本',
article: '文 章',
menu: '菜 單',
tag: '標 簽',
theme: '主 題',
remote: '遠 程',
system: '系 統',
renderSuccess: '渲染完毕,快去预览吧!',
renderError: '渲染失敗,請檢查 hosts 文件中 127.0.0.1 是否指向 localhost。確認配置正確後,嘗試重啟應用。',
syncWarning: '必須完成配置才能同步哦!',
syncSuccess: '同步成功啦!',
syncError1: '同步遇到了错误,请查閱',
syncError2: '來尋找解決方案',
newVersionTips: '有新版本發布,快去下載新版本吧!',
newArticle: '新文章',
publish: '發布',
published: '已發布',
draft: '草稿',
title: '標題',
status: '狀態',
createAt: '創建時間',
actions: '操作',
deleteWarning: '刪除後不可撤銷,你確定要刪除嗎?',
warning: '警告',
articleDelete: '文章已刪除',
cancel: '取 消',
select: '選 擇',
featureImage: '封面圖',
saveDraft: '存草稿',
save: '保 存',
newMenu: '新菜單',
name: '名稱',
openType: '打開方式',
link: 'Link',
menuSuccess: '菜單已保存',
menuDelete: '菜單已刪除',
draftSuccess: '已存為草稿',
saveSuccess: '已保存',
newTag: '新標籤',
tagName: '標籤名',
selectTheme: '選擇主題',
siteName: '網站名稱',
siteDescription: '網站描述',
footerInfo: '底部信息',
isShowFeatureImage: '顯示封面圖',
articlesPerPage: '每頁文章數',
archivesPerPage: '每頁歸檔數',
basicSetting: '基礎配置',
commentSetting: '評論配置',
faviconSetting: '網頁圖標',
avatarSetting: '頭像配置',
domain: '域 名',
repository: '倉庫名稱',
branch: '分 支',
username: '倉庫用戶名',
email: '郵 箱',
isShowComment: '是否顯示評論',
domainShouldStartsWithWarn: '域名應以 \'https://\' 或 \'http://\' 開頭',
basicSettingSuccess: '基礎配置已保存',
commentSettingSuccess: '評論配置已保存',
faviconSettingSuccess: '網頁圖標配置已保存',
avatarSettingSuccess: '頭像配置已保存',
saved: '已保存',
syncing: '同步中,請耐心等待...',
articleDefault: '文章 URL 默認格式',
tagDefault: '標籤 URL 默認格式',
hideInList: '列表中隱藏',
dateFormat: '日期格式',
htmlSupport: '支持 HTML',
change: '更 換',
editorTip: '你可以插入單獨行的 為摘要分隔標識(此行之前內容為摘要)',
saveError: '保存失敗',
privateKeyTip: '請填寫絕對路徑,例如:/home/username/.ssh/id_rsa',
remotePathTip: '請填寫絕對路徑,例如:/home/username/www/',
testConnection: '檢測遠程連接',
connectSuccess: '遠程連接成功',
connectFailed: '遠程連接失敗,請檢查倉庫、用戶名和令牌設置',
sourceFolder: '站点源文件路径',
language: '語 言',
inConfig: '配置中',
searchArticle: '搜索文章',
deleteSelected: '已選',
inputContent: '輸入內容',
postUrlRepeatTip: '文章的 URL 與其他文章重複',
postUrlIncludeTip: 'URL 不可包含 /',
onlyPicDrag: '僅支持圖片拖拽',
themeConfigSaved: '主題配置已保存',
reset: '重置',
reseted: '已重置',
noCustomConfigTip: '當前主題暫無自定義配置',
customConfig: '自定義配置',
moreThemes: '更多主題',
postSettings: '文章設置',
back: '返回',
savedIn: '保存於',
or: '或',
starSupport: 'Star 支持作者!',
showAllPost: '顯示全文',
showAbstract: '僅顯示摘要',
unsavedWarning: '你將丟失所有的未保存的更改,是否繼續?',
noSaveAndBack: '繼續',
insertImage: '插入圖片',
insertMore: '插入摘要分隔符',
writingIn: '寫作於',
words: '字 數',
readingTime: '閱讀時間',
version: '版本',
token: '令 牌',
tokenUsername: '令牌用户名',
platform: '平 臺',
topArticles: '置顶文章',
default: '默认',
external: '外链',
pathContainHttps: '路径必须包含 http 或 https',
articleUrlPath: '文章 URL 路径',
concise: '精简',
tagUrlPath: '标签 URL 路径',
archivePathPrefix: '归档路径前缀',
showFullText: '显示全文',
showAbstractOnly: '仅显示摘要',
numberArticlesRSS: 'RSS/Feed 文章数量',
Proxy: 'HTTP代理',
ProxyAddress: '地址',
ProxyPort: '端口',
},
en: {
preview: 'Preview',
syncSite: 'Sync Website',
newVersion: 'New version',
article: 'Article',
menu: 'Menu',
tag: 'Tag',
theme: 'Theme',
remote: 'Server',
system: 'System',
renderSuccess: 'Congratulations, the rendering is complete, go ahead and preview.',
renderError: 'Rendering failed, please check whether 127.0.0.1 in the hosts file points to localhost. After confirming that the configuration is correct, try to restart the application.',
syncWarning: 'You must complete the configuration to synchronize!',
syncSuccess: 'Synchronization succeeded!',
syncError1: 'Sorry, synchronization encountered an error, please refer to',
syncError2: 'to find a solution',
newVersionTips: 'There is a new version released, go and download the new version!',
newArticle: 'New',
publish: 'Publish',
published: 'Published',
draft: 'Draft',
title: 'Title',
status: 'Status',
createAt: 'Create Time',
actions: 'Actions',
deleteWarning: 'After deletion, it cannot be revoked. Are you sure you want to delete it?',
warning: 'Warning',
articleDelete: 'Article deleted',
cancel: 'Cancel',
select: 'Select',
featureImage: 'Feature Image',
saveDraft: 'Save as Draft',
save: 'Save',
newMenu: 'New',
name: 'Name',
openType: 'Open Type',
link: 'Link',
menuSuccess: 'Menu saved',
menuDelete: 'Menu deleted',
draftSuccess: 'Saved as draft',
saveSuccess: 'Saved successfully',
newTag: 'New',
tagName: 'Tag Name',
selectTheme: 'Select Theme',
siteName: 'Site Name',
siteDescription: 'Site Description',
footerInfo: 'Footer Information',
isShowFeatureImage: 'Feature image',
articlesPerPage: 'Articles per page',
archivesPerPage: 'Archives per page',
basicSetting: 'Basic settings',
commentSetting: 'Comment settings',
faviconSetting: 'Favicon setting',
avatarSetting: 'Avatar setting',
domain: 'Domain',
repository: 'Repository Name',
branch: 'Branch',
username: 'Branch Username',
email: 'Email',
isShowComment: 'Show comments',
domainShouldStartsWithWarn: 'Domain should starts with \'https://\' or \'http://\' ',
basicSettingSuccess: 'The basic setting saved',
commentSettingSuccess: 'The comment setting saved',
faviconSettingSuccess: 'The favicon setting saved',
avatarSettingSuccess: 'The avatar setting saved',
saved: 'Saved',
syncing: 'Synchronizing, please wait...',
articleDefault: 'Article URL Default',
tagDefault: 'Tag URL Default',
hideInList: 'Hide in list',
dateFormat: 'Date format',
htmlSupport: 'HTML is supported in this field',
change: 'Change',
editorTip: 'You can insert a separate line is the abstract separator identifier ( the content before this line is the abstract)',
saveError: 'Save failed',
privateKeyTip: 'Please fill in the absolute path, for example: /home/username/.ssh/id_rsa',
remotePathTip: 'Please fill in the absolute path, for example::/home/username/www/',
testConnection: 'Test Connection',
connectSuccess: 'Remote connection succeeded',
connectFailed: 'Remote connection failed, please check repository, username and token settings',
sourceFolder: 'Site source file path',
language: 'Language',
inConfig: 'In configuration',
searchArticle: 'Articles search',
deleteSelected: 'Selected',
inputContent: 'Input content',
postUrlRepeatTip: 'The URL of the article is duplicated with other articles.',
postUrlIncludeTip: 'URL cannot contain /',
onlyPicDrag: 'Only picture dragging is supported',
themeConfigSaved: 'The theme configuration has been saved',
reset: 'Reset',
reseted: 'Reset',
noCustomConfigTip: 'There is no custom configuration for the theme',
customConfig: 'Custom configuration',
moreThemes: 'More Themes',
postSettings: 'Post Settings',
back: 'Back',
savedIn: 'Saved in',
or: 'or',
starSupport: 'Give us a star!',
showAllPost: 'Show full content',
showAbstract: 'Show abstract only',
unsavedWarning: 'You will lose all unsaved changes, do you want to continue?',
noSaveAndBack: 'Continue',
insertImage: 'Insert image',
insertMore: 'Insert summary separator',
writingIn: 'Writing in',
words: 'Words',
readingTime: 'Reading time',
version: 'Version',
token: 'Token',
tokenUsername: 'Token Username',
platform: 'Platform',
topArticles: 'Top articles',
default: 'Default',
external: 'External',
pathContainHttps: 'The path must contain either http or https',
articleUrlPath: 'Article URL path',
concise: 'concise',
tagUrlPath: 'Tag URL path',
archivePathPrefix: 'Archive path prefix',
showFullText: 'Show full text',
showAbstractOnly: 'Show abstract only',
numberArticlesRSS: 'Number articles RSS/Feed',
Proxy: 'HTTP Proxy',
ProxyAddress: 'Proxy Address',
ProxyPort: 'Proxy Port',
},
fr_FR: {
preview: 'Aperçu',
syncSite: 'Synchroniser',
newVersion: 'Nouvelle version',
article: 'Article',
menu: 'Menu',
tag: 'Tag',
theme: 'Thème',
remote: 'Serveur',
system: 'Système',
renderSuccess: 'Félicitations, le rendu est terminé et regardez en avant-première.',
renderError: 'Le rendu a échoué, veuillez vérifier si 127.0.0.1 dans le fichier hosts pointe vers localhost. Après avoir vérifié que la configuration est correcte, essayez de redémarrer l\'application.',
syncWarning: 'Vous devez compléter la configuration pour synchroniser !',
syncSuccess: 'La synchronisation a réussi !',
syncError1: 'Désolé, la synchronisation a rencontré une erreur, veuillez vous référer à',
syncError2: 'pour trouver une solution',
newVersionTips: 'Une nouvelle version est disponible, téléchargez la nouvelle version!',
newArticle: 'Nouveau',
publish: 'Publier',
published: 'Publié',
draft: 'Brouillon',
title: 'Titre',
status: 'Status',
createAt: 'Heure de création',
actions: 'Actions',
deleteWarning: 'Après la suppression, celui-ci ne peut plus être révoqué. Êtes-vous sûr de vouloir supprimer ?',
warning: 'Attention',
articleDelete: 'Article supprimé',
cancel: 'Annuler',
select: 'Selectionner',
featureImage: 'Image de fond',
saveDraft: 'Enregristrer en brouillon',
save: 'Enregistrer',
newMenu: 'Nouveau',
name: 'Nom',
openType: 'type d\'ouverture',
link: 'Lien',
menuSuccess: 'Menu enregistré',
menuDelete: 'Menu supprimé',
draftSuccess: 'Enregistré en brouillon',
saveSuccess: 'Sauvegardé avec succès',
newTag: 'Nouveau tag',
tagName: 'Nom du tag',
selectTheme: 'Sélectionnez un thème',
siteName: 'Nom du site',
siteDescription: 'Description du site',
footerInfo: 'Informations sur le Footer',
isShowFeatureImage: 'Image de fond',
articlesPerPage: 'Articles par page',
archivesPerPage: 'Archives par page',
basicSetting: 'Paramètres de base',
commentSetting: 'Paramétrage des commentaires',
faviconSetting: 'Paramètres du Favicon',
avatarSetting: 'Paramètres de l\'avatar',
domain: 'Domaine',
repository: 'Nom du repository',
branch: 'Branche',
username: 'Nom d\'utilisateur',
email: 'Email',
isShowComment: 'Afficher les commentaires',
domainShouldStartsWithWarn: 'Le domaine doit commencer par \'https://\' or \'http://\' ',
basicSettingSuccess: 'Les réglages de base sont enregistrés',
commentSettingSuccess: 'Les réglages des commentaires sont enregistrés',
faviconSettingSuccess: 'Les réglages du favicon sont enregistrés',
avatarSettingSuccess: 'Les réglages de l\'avatar sont enregistrés',
saved: 'Enregistré',
syncing: 'Synchronisation, veuillez patienter...',
articleDefault: 'URL de l\'article par défaut',
tagDefault: 'URL de tag par défault',
hideInList: 'Cacher dans la liste',
dateFormat: 'Format de la date',
htmlSupport: 'Gestion du HTML',
change: 'Changer',
editorTip: 'Vous pouvez insérer une ligne séparée c\'est un identifiant pour séparer le résumé (le contenu avant cette ligne est le résumé)',
saveError: 'Enregistrement échoué',
privateKeyTip: 'Veuillez indiquer le chemin absolu, par exemple: /home/username/.ssh/id_rsa',
remotePathTip: 'Veuillez indiquer le chemin absolu, par exemple: /home/username/www/',
testConnection: 'Test de connexion',
connectSuccess: 'Connexion à distance a réussi',
connectFailed: 'La connexion à distance a échoué, veuillez vérifier les paramètres du référentiel, du nom d\'utilisateur et du token',
sourceFolder: 'Chemin d\'accès au fichier source du site',
language: 'Langue',
inConfig: 'En configuration',
searchArticle: 'Rechercher des articles',
deleteSelected: 'Sélectionné',
inputContent: 'Saisie du contenu',
postUrlRepeatTip: 'L\'URL de l\'article est dupliquée avec d\'autres articles.',
postUrlIncludeTip: 'L\'URL ne peut pas contenir /',
onlyPicDrag: 'Seul le dragage d\'images est autorisé',
themeConfigSaved: 'La configuration du thème a été sauvegardée',
reset: 'Réinitialiser',
reseted: 'Réinitialiser',
noCustomConfigTip: 'Il n\'y a pas de configuration personnalisée pour le thème',
customConfig: 'Configuration personnalisée',
moreThemes: 'Autres thèmes',
postSettings: 'Paramètres des postes',
back: 'Retour',
savedIn: 'Sauvegardé dans',
or: 'ou',
starSupport: 'Donnez-nous une étoile !',
showAllPost: 'Afficher tout les postes',
showAbstract: 'Afficher uniquement le résumé',
unsavedWarning: 'Vous allez perdre tous les changements non sauvegardés, voulez-vous continuer ?',
noSaveAndBack: 'Continuer',
insertImage: 'Insérer une image',
insertMore: 'Insérer un séparateur de résumé',
writingIn: 'Ecrire en',
words: 'Mots',
readingTime: 'Temps de lecture',
version: 'Version',
token: 'Token',
tokenUsername: 'Token Username',
platform: 'Plate-forme',
topArticles: 'Articles en tête',
default: 'Par défaut',
external: 'Externe',
pathContainHttps: 'L\'URL doit contenir soit \'http\' ou \'https\'',
articleUrlPath: 'URL des articles',
concise: 'simplifié',
tagUrlPath: 'URL des tags',
archivePathPrefix: 'Préfix du chemin des Archives',
showFullText: 'Tout afficher',
showAbstractOnly: 'Afficher seulement le résumé',
numberArticlesRSS: 'Nombre d\'articles RSS/Feed',
Proxy: 'HTTP Proxy',
ProxyAddress: 'Proxy Address',
ProxyPort: 'Proxy Port',
},
ru: {
preview: 'Предпросмотр',
syncSite: 'Синхронизировать сайт',
newVersion: 'Новая версия',
article: 'Запись',
menu: 'Меню',
tag: 'Тег',
theme: 'Тема',
remote: 'Сервер',
system: 'Система',
renderSuccess: 'Поздравляем, рендеринг завершен, переходите к предпросмотру.',
renderError: 'Не удалось выполнить рендеринг, пожалуйста, проверьте, указывает ли 127.0.0.1 в файле hosts на localhost. После подтверждения правильности настроек попробуйте перезапустить приложение.',
syncWarning: 'Для синхронизации следует завершить настройку!',
syncSuccess: 'Синхронизация выполнена успешно!',
syncError1: 'Извините, при синхронизации произошла ошибка, пожалуйста, обратитесь к',
syncError2: 'чтобы найти решение',
newVersionTips: 'Выпущена новая версия, пожалуйста, зайдите и скачайте новую версию!',
newArticle: 'Новая запись',
publish: 'Опубликовать',
published: 'Опубликовано',
draft: 'Черновик',
title: 'Заголовок',
status: 'Статус',
createAt: 'Дата и время создания',
actions: 'Действия',
deleteWarning: 'После удаления вернуть всё назад не получится. Вы уверены, что хотите удалить?',
warning: 'Предупреждение',
articleDelete: 'Запись удалена',
cancel: 'Отмена',
select: 'Выбрать',
featureImage: 'Изображение записи',
saveDraft: 'Сохранить как Черновик',
save: 'Сохранить',
newMenu: 'Добавить',
name: 'Название',
openType: 'Тип ссылки',
link: 'Ссылка',
menuSuccess: 'Меню сохранено',
menuDelete: 'Меню удалено',
draftSuccess: 'Сохранено как Черновик',
saveSuccess: 'Успешно сохранено',
newTag: 'Добавить тег',
tagName: 'Название тега',
selectTheme: 'Выберите Тему',
siteName: 'Название сайта',
siteDescription: 'Описание сайта',
footerInfo: 'Информация в подвале сайта',
isShowFeatureImage: 'Изображения записей',
articlesPerPage: 'Записей на странице',
archivesPerPage: 'Архивных записей на странице',
basicSetting: 'Основные настройки',
commentSetting: 'Настройки комментариев',
faviconSetting: 'Настройки иконки сайта',
avatarSetting: 'Настройки аватара',
domain: 'Домен',
repository: 'Название репозитория',
branch: 'Ветка',
username: 'Имя пользователя ветки',
email: 'Email',
isShowComment: 'Показывать комментарии',
domainShouldStartsWithWarn: 'Домен должен начинаться с \'https://\' или \'http://\' ',
basicSettingSuccess: 'Основные настройки сохранены',
commentSettingSuccess: 'Настройки комментариев сохранены',
faviconSettingSuccess: 'Новая иконка сайта сохранена',
avatarSettingSuccess: 'Новый аватар сохранён',
saved: 'Сохранено',
syncing: 'Идёт синхронизация, пожалуйста подождите...',
articleDefault: 'URL записи по умолчанию',
tagDefault: 'URL тега по умолчанию',
hideInList: 'Скрыть из списка записей',
dateFormat: 'Формат даты',
htmlSupport: 'В этом поле поддерживается HTML',
change: 'Изменить',
editorTip: 'Вы можете вставить отдельную строку с тегом — это тег разделителя (содержимое перед этой строкой будет отображаться на странице со списком записей)',
saveError: 'Ошибка сохранения',
privateKeyTip: 'Пожалуйста, введите абсолютный путь, например: /home/username/.ssh/id_rsa',
remotePathTip: 'Пожалуйста, введите абсолютный путь, например: /home/username/www/',
testConnection: 'Проверка соединения',
connectSuccess: 'Удалённое подключение выполнено успешно',
connectFailed: 'Ошибка при удалённом подключении. Пожалуйста, проверьте настройки репозитория, имени пользователя и токена.',
sourceFolder: 'Путь к исходным файлам сайта',
language: 'Язык',
inConfig: 'Применение настроек',
searchArticle: 'Поиск записей',
deleteSelected: 'Выбранное',
inputContent: 'Введите содержимое',
postUrlRepeatTip: 'URL-адрес записи в точности повторяет URL-адрес других записей.',
postUrlIncludeTip: 'URL-адрес не может содержать /',
onlyPicDrag: 'Поддерживается только перетаскивание изображений',
themeConfigSaved: 'Настройки темы были сохранены',
reset: 'Сбросить',
reseted: 'Сброшено',
noCustomConfigTip: 'Пользовательских настроек для этой темы не предусмотрено',
customConfig: 'Пользовательские настройки',
moreThemes: 'Ещё больше тем',
postSettings: 'Настройки записи',
back: 'Назад',
savedIn: 'Сохранено в',
or: 'или',
starSupport: 'Оцените нас!',
showAllPost: 'Показывать содержимое записей полностью',
showAbstract: 'Показывать краткое описание записей',
unsavedWarning: 'Вы потеряете все несохраненные изменения, хотите продолжить?',
noSaveAndBack: 'Продолжить',
insertImage: 'Вставить изображение',
insertMore: 'Вставить разделитель',
writingIn: 'Создано с помощью',
words: 'Слов(а)',
readingTime: 'Время чтения',
version: 'Версия',
token: 'Токен',
tokenUsername: 'Имя пользователя токена',
platform: 'Платформа',
topArticles: 'Закрепить запись',
default: 'По умолчанию',
external: 'Ссылка на изображение',
pathContainHttps: 'Ссылка должна содержать либо http, либо https',
articleUrlPath: 'URL-адрес записи',
concise: 'Компактный вид',
tagUrlPath: 'URL-адрес тега',
archivePathPrefix: 'Префикс для архива',
showFullText: 'Отображать содержимое полностью',
showAbstractOnly: 'Отображать краткое содержимое',
numberArticlesRSS: 'Количество записей в RSS/Feed',
Proxy: 'HTTP Прокси',
ProxyAddress: 'Прокси адрес',
ProxyPort: 'Прокси порт',
},
ja_JP: {
preview: 'プレビュー',
syncSite: 'サイトを同期化',
newVersion: '新しいバージョン',
article: '文書',
menu: 'メニュー',
tag: 'タグ',
theme: 'テーマ',
remote: 'リモート',
system: 'システム',
renderSuccess: 'レンダリングは完成しました、プレビューしてください。',
renderError: 'レンダリングに失敗しました。hostsファイルの127.0.0.1がlocalhostを指しているかどうかを確認してください。構成が正しいことを確認した後、アプリケーションを再起動してみてください。',
syncWarning: 'サイトを同期化する前に、システムの配置を完成してください!',
syncSuccess: 'サイトの同期化は完成しました!',
syncError1: '同期化の途中でエラーが発生しました、ご確認ください。',
syncError2: '解決策を見つかりましょう',
newVersionTips: '新しいバージョンがリリースしました、アップデートしましょう!',
newArticle: '新規文書',
publish: '公開',
published: '公開済み',
draft: '下書き',
title: 'タイトル',
status: 'ステータス',
createAt: '作成日',
actions: '操作',
deleteWarning: '完全に削除してもよろしいですか?',
warning: 'ウォーニング',
articleDelete: '文書を削除しました。',
cancel: '取り消す',
select: '選択',
featureImage: '表紙イメージ',
saveDraft: '下書き保存',
save: 'セーブ',
newMenu: '新規メニュー',
name: '名前',
openType: '開く方式',
link: 'リンク',
menuSuccess: 'メニューを保存しました',
menuDelete: 'メニューを削除しました',
draftSuccess: '下書きを保存しました',
saveSuccess: 'セーブしました',
newTag: '新規タグ',
tagName: 'タグ名',
selectTheme: 'テーマ選択',
siteName: 'サイト名',
siteDescription: 'サイト紹介',
footerInfo: 'フッター',
isShowFeatureImage: '表紙イメージを表示します',
articlesPerPage: '毎ページの文書数',
archivesPerPage: '毎ページのアーカイブ数',
basicSetting: 'システム設定',
commentSetting: 'コメント設定',
faviconSetting: 'タブアイコン',
avatarSetting: 'ユーザーアイコン',
domain: 'ドメイン名',
repository: 'レポジトリ名',
branch: 'ブランチ',
username: 'ユーザー名',
email: 'メールアドレス',
isShowComment: 'コメント表示',
domainShouldStartsWithWarn: 'ドメイン名は必ず \'https://\' 或 \'http://\' で始まります',
basicSettingSuccess: 'システム設定を保存しました',
commentSettingSuccess: 'コメント設定を保存しました',
faviconSettingSuccess: 'タブアイコンを保存しました',
avatarSettingSuccess: 'ユーザーアイコンを保存しました',
saved: '保存しました',
syncing: '同期中、しばらくお待ちください…',
articleDefault: '文書URLのデフォルトフォーマット',
tagDefault: 'タグURLのデフォルトフォーマット',
hideInList: 'リストに隠す',
dateFormat: '日付フォーマット',
htmlSupport: 'HTMLサポート',
change: '更新',
editorTip: '文書の後ろに一行の を追加したら、この部分を摘要としてを表示できます。',
saveError: '保存失敗しました',
privateKeyTip: '絶対パスを記入してください、例えば:「/home/username/.ssh/id_rsa」',
remotePathTip: '絶対パスを記入してください、例えば:「/home/username/www/」',
testConnection: '接続をテストしています',
connectSuccess: '接続は成功しました',
connectFailed: '接続が失敗しました。リポジトリ名、ユーザー名とトークンを確認してください。',
sourceFolder: 'サイトのソースファイルのパス',
language: '言語',
inConfig: '配置中',
searchArticle: '文書検索',
deleteSelected: '選択済み',
inputContent: '内容を入力してください',
postUrlRepeatTip: '文書のURLは他の文書と重複しました',
postUrlIncludeTip: 'URLの中には / を含めません',
onlyPicDrag: 'イメージをドラッグのみです',
themeConfigSaved: 'テームの設定を保存しました',
reset: 'リセット',
reseted: 'リセット完成しました',
noCustomConfigTip: '現在のテーマはカスタム設定はありません',
customConfig: 'カスタム設定',
moreThemes: 'テーマストアー',
postSettings: '文書設定',
back: '戻る',
savedIn: '上書き保存',
or: 'または',
starSupport: 'Starで開発者をサポートします',
showAllPost: '全ての文書を表示します',
showAbstract: '摘要のみを表示します',
unsavedWarning: '未保存の変更内容を全て削除します、よろしいですか?',
noSaveAndBack: 'つづく',
insertImage: 'イマージを挿入します',
insertMore: '摘要標記を挿入します',
writingIn: '作成地',
words: '字数',
readingTime: '読む時間',
version: 'バージョン',
token: 'トークン',
tokenUsername: 'トークンのユーザー名',
platform: 'プラットホーム',
topArticles: '固定文書',
default: 'デフォルト',
external: '外部リンク',
pathContainHttps: 'パスは必ずhttpまたはhttpsを含めます',
articleUrlPath: '文書のURLのパス',
concise: '簡素化',
tagUrlPath: 'タグURLのパス',
archivePathPrefix: 'アーカイブのパスの接頭辞',
showFullText: '全文を表示します',
showAbstractOnly: '摘要のみを表示します',
numberArticlesRSS: 'RSS/Feed 文書数量',
Proxy: 'HTTPプロキシ',
ProxyAddress: 'プロキシアドレス',
ProxyPort: 'プロキシポート',
}
}
export default message
================================================
FILE: src/assets/styles/custom.less
================================================
.ant-table {
background: #fff;
color: #555;
}
.ant-table-tbody {
.ant-table-row:nth-of-type(2n) {
background: #fafafa;
}
}
.ant-table-tbody > tr > td {
border-bottom: 0;
}
.ant-table-thead > tr > th {
background: #ffffff;
color: #1b1b18b0;
font-weight: normal;
}
.ant-table-thead > tr.ant-table-row-hover:not(.ant-table-expanded-row) > td, .ant-table-tbody > tr.ant-table-row-hover:not(.ant-table-expanded-row) > td, .ant-table-thead > tr:hover:not(.ant-table-expanded-row) > td, .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td {
background: transparent;
}
.ant-table-thead > tr > th, .ant-table-tbody > tr > td {
padding: 12px 12px;
}
.ant-table-tbody > tr.ant-table-row-selected td {
background: transparent;
}
.tool-container {
padding: 0px 16px 7px;
margin-bottom: 16px;
position: fixed;
top: 8px;
left: 200px;
right: 0px;
z-index: 1;
background: #fff;
border-bottom: 1px solid #e8e8e88a;
-webkit-app-region: drag;
.btn {
margin-left: 16px;
-webkit-app-region: no-drag;
}
.op-btn {
height: 30px;
line-height: 30px;
padding: 0 16px;
font-size: 18px;
border-radius: 20px;
margin-left: 8px;
outline: none;
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
i {
font-weight: bold;
}
&:hover {
background: #efefef;
color: #515457;
}
&:focus {
background: #efefef;
}
&.save-btn {
color: #38A169;
&:hover {
background: #9AE6B4;
color: #22543D;
}
&:focus {
background: #68D391;
}
}
}
}
.content-container {
margin-top: 48px;
}
.ant-input, .ant-input-group-addon, .ant-select-selection, .ant-input-number-input, .ant-input-number {
background: #FAFAFA;
border-color: #EAEAEA;
&:focus {
box-shadow: none;
background: #fff;
border-color: #999;
}
}
.ant-input-number-focused {
box-shadow: none;
}
.ant-select-dropdown-menu-item-active, .ant-select-dropdown-menu-item:hover {
background: #f7f7f7;
}
.ant-btn {
font-size: 13px;
box-shadow: none;
[class^='zwicon-']:not(:last-child) {
font-size: 16px;
margin-right: 8px;
}
}
.ant-btn-primary {
box-shadow: none;
text-shadow: none;
}
.ant-menu-inline > .ant-menu-item {
height: 36px;
line-height: 36px;
}
.ant-menu-item, .ant-menu-submenu-title {
transition: none;
}
.ant-menu-item:active, .ant-menu-submenu-title:active {
background: inherit;
}
.ant-pagination-prev .ant-pagination-item-link, .ant-pagination-next .ant-pagination-item-link, .ant-pagination-item {
border: none;
&:hover {
background: #fafafa;
}
}
.ant-pagination-item-active {
&, &:hover {
background: #eaeaea;
}
}
.ant-menu.ant-menu-dark .ant-menu-item-selected, .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected {
position: relative;
}
.ant-checkbox-inner:after {
left: 3.571429px;
}
.ant-checkbox-checked .ant-checkbox-inner {
background-color: #fff;
&:after {
border-color: @primary-color;
}
}
.ant-tabs-nav {
.ant-tabs-tab {
padding: 4px 8px;
margin-bottom: 8px;
color: #b1b1b1;
border-radius: 6px;
transition: all 0.3s;
&:hover {
background: #efefef;
}
}
.ant-tabs-tab-active {
color: #1b1b18;
}
}
.ant-modal-confirm .ant-modal-body {
padding: 24px 16px 16px;
}
.tip-text {
background: #fafafa;
padding: 4px 8px;
border-radius: 2px;
line-height: 1.25;
font-size: 12px;
color: #6669;
}
.ant-tabs-ink-bar {
height: 0;
}
.ant-message {
z-index: 3010;
top: 8px;
font-size: 12px;
}
.ant-message-notice-content {
padding: 8px 16px;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)!important;
background: #000;
color: #fafafa;
}
.ant-drawer-close-x {
width: 32px;
height: 32px;
line-height: 32px;
border-radius: 50%;
background: #f3f7f9;
margin: 12px;
font-size: 14px;
display: inline-flex;
justify-content: center;
align-items: center;
}
.ant-modal-close {
&:focus {
outline: none;
}
}
.anticon {
vertical-align: unset;
}
.ant-drawer-header {
border-bottom: none;
}
.ant-form-item-control {
padding-right: 2px;
}
.ant-menu-inline .ant-menu-item, .ant-menu-inline .ant-menu-submenu-title {
width: 90%;
margin-left: 5%;
border-radius: 6px;
color: #666;
}
.menu-tab {
.ant-tabs-top-bar {
position: fixed;
top: 8px;
left: 200px;
right: 0;
background: #fff;
z-index: 100;
padding-left: 24px;
}
.ant-tabs-content {
padding-top: 44px;
}
}
.ant-tabs-bar {
border-bottom: 1px solid #e8e8e88a;
}
.ant-tag {
border: none;
line-height: 22px;
}
.ant-notification-notice {
padding: 8px 12px;
box-shadow: 0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05)!important;
}
.ant-notification-notice-description {
font-size: 12px;
line-height: 16px;
}
.ant-notification-notice-with-icon {
.ant-notification-notice-message,
.ant-notification-notice-description {
margin-left: 40px;
}
}
.ant-notification-notice-close {
right: 4px;
top: 2px;
}
.ant-modal-content {
border-radius: 10px;
overflow: hidden;
box-shadow: 0px 20px 100px 0px rgba(0,0,0,0.15);
}
.ant-modal-header {
border-bottom-color: #fafafa;
}
.ant-form-explain, .ant-form-extra {
font-size: 12px;
}
.ant-tooltip {
font-size: 12px;
}
.ant-tooltip-arrow,
.ant-popover-arrow {
display: none;
}
.ant-tooltip-inner {
padding: 4px 8px;
min-height: auto;
}
.ant-drawer-mask, .ant-modal-mask {
backdrop-filter: saturate(180%) blur(20px);
background-color: rgba(242,242,242,0.5);
}
.ant-drawer.ant-drawer-open .ant-drawer-mask {
opacity: 1;
}
.ant-drawer-right.ant-drawer-open .ant-drawer-content-wrapper {
box-shadow: 0px 20px 100px 0px rgba(0,0,0,0.15);
}
.ant-popover-inner {
border: 1px solid #eee;
box-shadow: 0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06)!important;
}
::selection {
background: #bed6fb;
color: @primary-color;
}
.ant-radio-inner:after {
width: 14px;
height: 14px;
left: 0;
top: 0;
}
.ant-radio-wrapper {
padding: 2px 0 2px 8px;
border-radius: 2px;
&:hover {
transition: all 0.3s;
background: #efefef;
}
}
.ant-slider-rail, .ant-slider-track, .ant-slider-step {
height: 2px;
top: 5px;
}
.ant-drawer-close:focus {
outline: none;
}
================================================
FILE: src/assets/styles/main.less
================================================
// @import "./custom.less";
@import "~ant-design-vue/dist/antd.less";
@import "./var.less";
@import "./custom.less";
@import "./zwicon.less";
================================================
FILE: src/assets/styles/tailwind.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
.transition {
transition: all .1s ease-in !important;
}
@variants responsive, hover {
.translate-r-2px {
transform: translateX(2px) !important;
}
.transition-fast {
transition: all .2s ease !important;
}
}
================================================
FILE: src/assets/styles/var.less
================================================
@primary-color: #1b1b18;
@primary-bg: #f7f6f6;
@danger-color: #fa5252;
@border-radius-base : 6px;
@link-color: #1a5ccf;
@border-color: #e8e8e88a;
@btn-default-border: #eaeaea;
@label-color: #1b1b18;
@font-family: PingFang SC,-apple-system,SF UI Text,Lucida Grande,STheiti,Microsoft YaHei,sans-serif;
@input-hover-border-color: #999;
================================================
FILE: src/assets/styles/zwicon.less
================================================
@font-face {
font-family: 'zwicon';
src: url('../fonts/zwicon.eot?k483k8');
src: url('../fonts/zwicon.eot?k483k8#iefix') format('embedded-opentype'),
url('../fonts/zwicon.ttf?k483k8') format('truetype'),
url('../fonts/zwicon.woff?k483k8') format('woff'),
url('../fonts/zwicon.svg?k483k8#zwicon') format('svg');
font-weight: normal;
font-style: normal;
}
i {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'zwicon' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.zwicon-align-bottom:before {
content: "\e900";
}
.zwicon-align-h:before {
content: "\e901";
}
.zwicon-align-left:before {
content: "\e902";
}
.zwicon-align-right:before {
content: "\e903";
}
.zwicon-align-top:before {
content: "\e904";
}
.zwicon-align-v:before {
content: "\e905";
}
.zwicon-distribute-h:before {
content: "\e906";
}
.zwicon-distribute-v:before {
content: "\e907";
}
.zwicon-arrow-bottom-left:before {
content: "\e908";
}
.zwicon-arrow-bottom-right:before {
content: "\e909";
}
.zwicon-arrow-circle-down:before {
content: "\e90a";
}
.zwicon-arrow-circle-left:before {
content: "\e90b";
}
.zwicon-arrow-circle-right:before {
content: "\e90c";
}
.zwicon-arrow-circle-up:before {
content: "\e90d";
}
.zwicon-arrow-down:before {
content: "\e90e";
}
.zwicon-arrow-left:before {
content: "\e90f";
}
.zwicon-arrow-right:before {
content: "\e910";
}
.zwicon-arrow-square-down:before {
content: "\e911";
}
.zwicon-arrow-square-left:before {
content: "\e912";
}
.zwicon-arrow-square-right:before {
content: "\e913";
}
.zwicon-arrow-square-up:before {
content: "\e914";
}
.zwicon-arrow-to-top:before {
content: "\e915";
}
.zwicon-arrow-top-left:before {
content: "\e916";
}
.zwicon-arrow-top-right:before {
content: "\e917";
}
.zwicon-arrow-up:before {
content: "\e918";
}
.zwicon-back:before {
content: "\e919";
}
.zwicon-chevron-double-down:before {
content: "\e91a";
}
.zwicon-chevron-double-left:before {
content: "\e91b";
}
.zwicon-chevron-double-right:before {
content: "\e91c";
}
.zwicon-chevron-double-up:before {
content: "\e91d";
}
.zwicon-chevron-down:before {
content: "\e91e";
}
.zwicon-chevron-left:before {
content: "\e91f";
}
.zwicon-chevron-right:before {
content: "\e920";
}
.zwicon-chevron-up:before {
content: "\e921";
}
.zwicon-collapse-alt:before {
content: "\e922";
}
.zwicon-collapse-alt2:before {
content: "\e923";
}
.zwicon-collapse-down:before {
content: "\e924";
}
.zwicon-collapse-left:before {
content: "\e925";
}
.zwicon-collapse-right:before {
content: "\e926";
}
.zwicon-collapse-up:before {
content: "\e927";
}
.zwicon-collapse:before {
content: "\e928";
}
.zwicon-continue:before {
content: "\e929";
}
.zwicon-expand-alt:before {
content: "\e92a";
}
.zwicon-expand-alt2:before {
content: "\e92b";
}
.zwicon-expand-down:before {
content: "\e92c";
}
.zwicon-expand-h:before {
content: "\e92d";
}
.zwicon-expand-left:before {
content: "\e92e";
}
.zwicon-expand-right:before {
content: "\e92f";
}
.zwicon-expand-up:before {
content: "\e930";
}
.zwicon-expand-v:before {
content: "\e931";
}
.zwicon-expand:before {
content: "\e932";
}
.zwicon-loop:before {
content: "\e933";
}
.zwicon-prioritize-down:before {
content: "\e934";
}
.zwicon-prioritize-up:before {
content: "\e935";
}
.zwicon-redo:before {
content: "\e936";
}
.zwicon-refresh-double:before {
content: "\e937";
}
.zwicon-refresh-left:before {
content: "\e938";
}
.zwicon-refresh-right:before {
content: "\e939";
}
.zwicon-restart:before {
content: "\e93a";
}
.zwicon-split-h:before {
content: "\e93b";
}
.zwicon-split-v:before {
content: "\e93c";
}
.zwicon-undo:before {
content: "\e93d";
}
.zwicon-cell-border-bottom:before {
content: "\e93e";
}
.zwicon-cell-border-full:before {
content: "\e93f";
}
.zwicon-cell-border-left:before {
content: "\e940";
}
.zwicon-cell-border-right:before {
content: "\e941";
}
.zwicon-cell-border-top:before {
content: "\e942";
}
.zwicon-cell-empty:before {
content: "\e943";
}
.zwicon-cell-full:before {
content: "\e944";
}
.zwicon-cell-split-h:before {
content: "\e945";
}
.zwicon-cell-split-v:before {
content: "\e946";
}
.zwicon-cell-split:before {
content: "\e947";
}
.zwicon-archive:before {
content: "\e948";
}
.zwicon-document:before {
content: "\e949";
}
.zwicon-folder-delete:before {
content: "\e94a";
}
.zwicon-folder-minus:before {
content: "\e94b";
}
.zwicon-folder-open:before {
content: "\e94c";
}
.zwicon-folder-plus:before {
content: "\e94d";
}
.zwicon-folder:before {
content: "\e94e";
}
.zwicon-note:before {
content: "\e94f";
}
.zwicon-notebook:before {
content: "\e950";
}
.zwicon-script:before {
content: "\e951";
}
.zwicon-sticker:before {
content: "\e952";
}
.zwicon-sticky-notes:before {
content: "\e953";
}
.zwicon-tray-delete:before {
content: "\e954";
}
.zwicon-tray-empty:before {
content: "\e955";
}
.zwicon-tray-export:before {
content: "\e956";
}
.zwicon-tray-import:before {
content: "\e957";
}
.zwicon-tray-minus:before {
content: "\e958";
}
.zwicon-tray-plus:before {
content: "\e959";
}
.zwicon-tray-stack:before {
content: "\e95a";
}
.zwicon-tray:before {
content: "\e95b";
}
.zwicon-artboard:before {
content: "\e95c";
}
.zwicon-brush:before {
content: "\e95d";
}
.zwicon-clipboard:before {
content: "\e95e";
}
.zwicon-copy-alt:before {
content: "\e95f";
}
.zwicon-copy:before {
content: "\e960";
}
.zwicon-crop:before {
content: "\e961";
}
.zwicon-cut-alt:before {
content: "\e962";
}
.zwicon-cut:before {
content: "\e963";
}
.zwicon-drafting-compass:before {
content: "\e964";
}
.zwicon-duplicate-alt:before {
content: "\e965";
}
.zwicon-duplicate:before {
content: "\e966";
}
.zwicon-eraser:before {
content: "\e967";
}
.zwicon-eye-dropper:before {
content: "\e968";
}
.zwicon-group:before {
content: "\e969";
}
.zwicon-layer-stack:before {
content: "\e96a";
}
.zwicon-magic-wand:before {
content: "\e96b";
}
.zwicon-marker:before {
content: "\e96c";
}
.zwicon-paint-bucket:before {
content: "\e96d";
}
.zwicon-paint-roller:before {
content: "\e96e";
}
.zwicon-palette:before {
content: "\e96f";
}
.zwicon-paste-alt:before {
content: "\e970";
}
.zwicon-paste:before {
content: "\e971";
}
.zwicon-pen-circle:before {
content: "\e972";
}
.zwicon-pen:before {
content: "\e973";
}
.zwicon-pencil-circle:before {
content: "\e974";
}
.zwicon-pencil:before {
content: "\e975";
}
.zwicon-ruler-combined:before {
content: "\e976";
}
.zwicon-ruler-h:before {
content: "\e977";
}
.zwicon-ruler-v:before {
content: "\e978";
}
.zwicon-stamp:before {
content: "\e979";
}
.zwicon-table:before {
content: "\e97a";
}
.zwicon-activity:before {
content: "\e97b";
}
.zwicon-android:before {
content: "\e97c";
}
.zwicon-apple:before {
content: "\e97d";
}
.zwicon-bolt:before {
content: "\e97e";
}
.zwicon-cloud-download:before {
content: "\e97f";
}
.zwicon-cloud-minus:before {
content: "\e980";
}
.zwicon-cloud-plus:before {
content: "\e981";
}
.zwicon-cloud-upload:before {
content: "\e982";
}
.zwicon-code:before {
content: "\e983";
}
.zwicon-command:before {
content: "\e984";
}
.zwicon-database:before {
content: "\e985";
}
.zwicon-deploy:before {
content: "\e986";
}
.zwicon-git-branch:before {
content: "\e987";
}
.zwicon-git-commit:before {
content: "\e988";
}
.zwicon-git-fork:before {
content: "\e989";
}
.zwicon-git-merge:before {
content: "\e98a";
}
.zwicon-git-pull:before {
content: "\e98b";
}
.zwicon-ios:before {
content: "\e98c";
}
.zwicon-lan-connection:before {
content: "\e98d";
}
.zwicon-lan-error:before {
content: "\e98e";
}
.zwicon-lan:before {
content: "\e98f";
}
.zwicon-osx:before {
content: "\e990";
}
.zwicon-repository:before {
content: "\e991";
}
.zwicon-web:before {
content: "\e992";
}
.zwicon-window-delete:before {
content: "\e993";
}
.zwicon-window-minus:before {
content: "\e994";
}
.zwicon-window-plus:before {
content: "\e995";
}
.zwicon-window:before {
content: "\e996";
}
.zwicon-windows:before {
content: "\e997";
}
.zwicon-airpods-alt:before {
content: "\e998";
}
.zwicon-airpods:before {
content: "\e999";
}
.zwicon-apple-watch-smile:before {
content: "\e99a";
}
.zwicon-apple-watch-time:before {
content: "\e99b";
}
.zwicon-apple-watch:before {
content: "\e99c";
}
.zwicon-cable-hdmi:before {
content: "\e99d";
}
.zwicon-cable-jack:before {
content: "\e99e";
}
.zwicon-cable-lan:before {
content: "\e99f";
}
.zwicon-cable-lightning:before {
content: "\e9a0";
}
.zwicon-cable-magsafe:before {
content: "\e9a1";
}
.zwicon-cable-usb:before {
content: "\e9a2";
}
.zwicon-cardboard-vr:before {
content: "\e9a3";
}
.zwicon-controller-alt:before {
content: "\e9a4";
}
.zwicon-controller:before {
content: "\e9a5";
}
.zwicon-desktop:before {
content: "\e9a6";
}
.zwicon-devices:before {
content: "\e9a7";
}
.zwicon-floppy:before {
content: "\e9a8";
}
.zwicon-gameboy:before {
content: "\e9a9";
}
.zwicon-hard-drive:before {
content: "\e9aa";
}
.zwicon-headphone:before {
content: "\e9ab";
}
.zwicon-imac:before {
content: "\e9ac";
}
.zwicon-ipad-h:before {
content: "\e9ad";
}
.zwicon-ipad:before {
content: "\e9ae";
}
.zwicon-iphone-h:before {
content: "\e9af";
}
.zwicon-iphone-x-h:before {
content: "\e9b0";
}
.zwicon-iphone-x:before {
content: "\e9b1";
}
.zwicon-iphone:before {
content: "\e9b2";
}
.zwicon-keyboard:before {
content: "\e9b3";
}
.zwicon-laptop:before {
content: "\e9b4";
}
.zwicon-mac-pro:before {
content: "\e9b5";
}
.zwicon-macbook-pro:before {
content: "\e9b6";
}
.zwicon-memory-card:before {
content: "\e9b7";
}
.zwicon-mouse:before {
content: "\e9b8";
}
.zwicon-phone-andorid-h:before {
content: "\e9b9";
}
.zwicon-phone-andorid:before {
content: "\e9ba";
}
.zwicon-phone-holding-double:before {
content: "\e9bb";
}
.zwicon-phone-holding:before {
content: "\e9bc";
}
.zwicon-plug:before {
content: "\e9bd";
}
.zwicon-printer:before {
content: "\e9be";
}
.zwicon-server-stack:before {
content: "\e9bf";
}
.zwicon-smart-glasses:before {
content: "\e9c0";
}
.zwicon-smart-tv:before {
content: "\e9c1";
}
.zwicon-smart-watch-time:before {
content: "\e9c2";
}
.zwicon-smart-watch:before {
content: "\e9c3";
}
.zwicon-tablet-h:before {
content: "\e9c4";
}
.zwicon-tablet:before {
content: "\e9c5";
}
.zwicon-terminal:before {
content: "\e9c6";
}
.zwicon-virtual-reality:before {
content: "\e9c7";
}
.zwicon-voice-assistant:before {
content: "\e9c8";
}
.zwicon-edit-circle:before {
content: "\e9c9";
}
.zwicon-edit-pencil:before {
content: "\e9ca";
}
.zwicon-edit-square-feather:before {
content: "\e9cb";
}
.zwicon-edit-square:before {
content: "\e9cc";
}
.zwicon-file-archive:before {
content: "\e9cd";
}
.zwicon-file-audio:before {
content: "\e9ce";
}
.zwicon-file-cloud:before {
content: "\e9cf";
}
.zwicon-file-download:before {
content: "\e9d0";
}
.zwicon-file-empty:before {
content: "\e9d1";
}
.zwicon-file-export:before {
content: "\e9d2";
}
.zwicon-file-font:before {
content: "\e9d3";
}
.zwicon-file-graphic:before {
content: "\e9d4";
}
.zwicon-file-image:before {
content: "\e9d5";
}
.zwicon-file-import:before {
content: "\e9d6";
}
.zwicon-file-pdf:before {
content: "\e9d7";
}
.zwicon-file-search:before {
content: "\e9d8";
}
.zwicon-file-sketch:before {
content: "\e9d9";
}
.zwicon-file-table:before {
content: "\e9da";
}
.zwicon-file-upload:before {
content: "\e9db";
}
.zwicon-file-vector:before {
content: "\e9dc";
}
.zwicon-file-video:before {
content: "\e9dd";
}
.zwicon-filter-alt:before {
content: "\e9de";
}
.zwicon-filter:before {
content: "\e9df";
}
.zwicon-slider-circle-h:before {
content: "\e9e0";
}
.zwicon-slider-circle-v:before {
content: "\e9e1";
}
.zwicon-slider-rectangle-h:before {
content: "\e9e2";
}
.zwicon-slider-rectangle-v:before {
content: "\e9e3";
}
.zwicon-sort-alphabetic-down:before {
content: "\e9e4";
}
.zwicon-sort-alphabetic-up:before {
content: "\e9e5";
}
.zwicon-sort-amount-down:before {
content: "\e9e6";
}
.zwicon-sort-amount-up:before {
content: "\e9e7";
}
.zwicon-sort-numeric-down:before {
content: "\e9e8";
}
.zwicon-sort-numeric-up:before {
content: "\e9e9";
}
.zwicon-toggle-switch:before {
content: "\e9ea";
}
.zwicon-bar-code-scan:before {
content: "\e9eb";
}
.zwicon-bar-code:before {
content: "\e9ec";
}
.zwicon-bid:before {
content: "\e9ed";
}
.zwicon-bill:before {
content: "\e9ee";
}
.zwicon-bitcoin-sign:before {
content: "\e9ef";
}
.zwicon-bull-horn:before {
content: "\e9f0";
}
.zwicon-coin:before {
content: "\e9f1";
}
.zwicon-credit-card:before {
content: "\e9f2";
}
.zwicon-diamond:before {
content: "\e9f3";
}
.zwicon-dollar-sign:before {
content: "\e9f4";
}
.zwicon-euro-sign:before {
content: "\e9f5";
}
.zwicon-hammer:before {
content: "\e9f6";
}
.zwicon-line-chart:before {
content: "\e9f7";
}
.zwicon-lira-sign:before {
content: "\e9f8";
}
.zwicon-money-bill:before {
content: "\e9f9";
}
.zwicon-money-stack:before {
content: "\e9fa";
}
.zwicon-package:before {
content: "\e9fb";
}
.zwicon-piggy-bank:before {
content: "\e9fc";
}
.zwicon-pound-sign:before {
content: "\e9fd";
}
.zwicon-price-tag:before {
content: "\e9fe";
}
.zwicon-qr-code-scan:before {
content: "\e9ff";
}
.zwicon-qr-code:before {
content: "\ea00";
}
.zwicon-receipt:before {
content: "\ea01";
}
.zwicon-rubel-sign:before {
content: "\ea02";
}
.zwicon-rupee-sign:before {
content: "\ea03";
}
.zwicon-sale-badge:before {
content: "\ea04";
}
.zwicon-shopping-bag-alt:before {
content: "\ea05";
}
.zwicon-shopping-bag:before {
content: "\ea06";
}
.zwicon-shopping-cart:before {
content: "\ea07";
}
.zwicon-store:before {
content: "\ea08";
}
.zwicon-wallet:before {
content: "\ea09";
}
.zwicon-won-sign:before {
content: "\ea0a";
}
.zwicon-yen-sign:before {
content: "\ea0b";
}
.zwicon-flip-left-alt:before {
content: "\ea0c";
}
.zwicon-flip-left:before {
content: "\ea0d";
}
.zwicon-flip-right-alt:before {
content: "\ea0e";
}
.zwicon-flip-right:before {
content: "\ea0f";
}
.zwicon-double-tap-two:before {
content: "\ea10";
}
.zwicon-double-tap:before {
content: "\ea11";
}
.zwicon-drag:before {
content: "\ea12";
}
.zwicon-flick-left-two:before {
content: "\ea13";
}
.zwicon-flick-left:before {
content: "\ea14";
}
.zwicon-flick-right-two:before {
content: "\ea15";
}
.zwicon-flick-right:before {
content: "\ea16";
}
.zwicon-horns:before {
content: "\ea17";
}
.zwicon-pinch:before {
content: "\ea18";
}
.zwicon-point:before {
content: "\ea19";
}
.zwicon-press:before {
content: "\ea1a";
}
.zwicon-scroll-down-two:before {
content: "\ea1b";
}
.zwicon-scroll-down:before {
content: "\ea1c";
}
.zwicon-scroll-h-two:before {
content: "\ea1d";
}
.zwicon-scroll-h:before {
content: "\ea1e";
}
.zwicon-scroll-up-two:before {
content: "\ea1f";
}
.zwicon-scroll-up:before {
content: "\ea20";
}
.zwicon-scroll-v-two:before {
content: "\ea21";
}
.zwicon-scroll-v:before {
content: "\ea22";
}
.zwicon-shaka:before {
content: "\ea23";
}
.zwicon-spread:before {
content: "\ea24";
}
.zwicon-tap-two:before {
content: "\ea25";
}
.zwicon-tap:before {
content: "\ea26";
}
.zwicon-two-drag:before {
content: "\ea27";
}
.zwicon-add-item-alt:before {
content: "\ea28";
}
.zwicon-add-item:before {
content: "\ea29";
}
.zwicon-add-note:before {
content: "\ea2a";
}
.zwicon-add-to-list:before {
content: "\ea2b";
}
.zwicon-at:before {
content: "\ea2c";
}
.zwicon-attach-document:before {
content: "\ea2d";
}
.zwicon-paperclip:before {
content: "\ea2e";
}
.zwicon-battery-full:before {
content: "\ea30";
}
.zwicon-battery-low:before {
content: "\ea31";
}
.zwicon-battery-mid:before {
content: "\ea32";
}
.zwicon-battery-v:before {
content: "\ea33";
}
.zwicon-bell-alt-ring:before {
content: "\ea34";
}
.zwicon-bell-alt:before {
content: "\ea35";
}
.zwicon-bell-slash:before {
content: "\ea36";
}
.zwicon-bell-snooze:before {
content: "\ea37";
}
.zwicon-bell:before {
content: "\ea38";
}
.zwicon-block:before {
content: "\ea39";
}
.zwicon-book-alt:before {
content: "\ea3a";
}
.zwicon-book:before {
content: "\ea3b";
}
.zwicon-bookmark:before {
content: "\ea3c";
}
.zwicon-briefcase:before {
content: "\ea3d";
}
.zwicon-calendar-day:before {
content: "\ea3e";
}
.zwicon-calendar-month:before {
content: "\ea3f";
}
.zwicon-calendar-never:before {
content: "\ea40";
}
.zwicon-calendar-week:before {
content: "\ea41";
}
.zwicon-calendar:before {
content: "\ea42";
}
.zwicon-call-in:before {
content: "\ea43";
}
.zwicon-call-out:before {
content: "\ea44";
}
.zwicon-chat:before {
content: "\ea45";
}
.zwicon-checkmark-circle:before {
content: "\ea46";
}
.zwicon-checkmark-square:before {
content: "\ea47";
}
.zwicon-checkmark:before {
content: "\ea48";
}
.zwicon-clock:before {
content: "\ea49";
}
.zwicon-close-circle:before {
content: "\ea4a";
}
.zwicon-close-square:before {
content: "\ea4b";
}
.zwicon-close:before {
content: "\ea4c";
}
.zwicon-cog:before {
content: "\ea4d";
}
.zwicon-comment:before {
content: "\ea4e";
}
.zwicon-compass:before {
content: "\ea4f";
}
.zwicon-delete:before {
content: "\ea50";
}
.zwicon-download:before {
content: "\ea51";
}
.zwicon-earth-alt:before {
content: "\ea52";
}
.zwicon-earth:before {
content: "\ea53";
}
.zwicon-exclamation-triangle:before {
content: "\ea54";
}
.zwicon-exclamation-mark:before {
content: "\ea2f";
}
.zwicon-export:before {
content: "\ea55";
}
.zwicon-eye-slash:before {
content: "\ea56";
}
.zwicon-eye:before {
content: "\ea57";
}
.zwicon-face-id:before {
content: "\ea58";
}
.zwicon-flag:before {
content: "\ea59";
}
.zwicon-grid:before {
content: "\ea5a";
}
.zwicon-hamburger-menu:before {
content: "\ea5b";
}
.zwicon-heart:before {
content: "\ea5c";
}
.zwicon-home:before {
content: "\ea5d";
}
.zwicon-import:before {
content: "\ea5e";
}
.zwicon-info-circle:before {
content: "\ea5f";
}
.zwicon-lifebelt:before {
content: "\ea60";
}
.zwicon-link:before {
content: "\ea61";
}
.zwicon-lock-alt:before {
content: "\ea62";
}
.zwicon-lock:before {
content: "\ea63";
}
.zwicon-mail:before {
content: "\ea64";
}
.zwicon-map-marker:before {
content: "\ea65";
}
.zwicon-minus-circle:before {
content: "\ea66";
}
.zwicon-minus-square:before {
content: "\ea67";
}
.zwicon-minus:before {
content: "\ea68";
}
.zwicon-more-h:before {
content: "\ea69";
}
.zwicon-more-v:before {
content: "\ea6a";
}
.zwicon-my-location:before {
content: "\ea6b";
}
.zwicon-password:before {
content: "\ea6c";
}
.zwicon-phone:before {
content: "\ea6d";
}
.zwicon-pin:before {
content: "\ea6e";
}
.zwicon-plus-circle:before {
content: "\ea6f";
}
.zwicon-plus-square:before {
content: "\ea70";
}
.zwicon-plus:before {
content: "\ea71";
}
.zwicon-search:before {
content: "\ea72";
}
.zwicon-send:before {
content: "\ea73";
}
.zwicon-share:before {
content: "\ea74";
}
.zwicon-shortcut:before {
content: "\ea75";
}
.zwicon-sign-in:before {
content: "\ea76";
}
.zwicon-sign-out:before {
content: "\ea77";
}
.zwicon-thumbs-down:before {
content: "\ea78";
}
.zwicon-thumbs-up:before {
content: "\ea79";
}
.zwicon-trash:before {
content: "\ea7a";
}
.zwicon-unlink:before {
content: "\ea7b";
}
.zwicon-upload:before {
content: "\ea7c";
}
.zwicon-user-circle:before {
content: "\ea7d";
}
.zwicon-user-delete:before {
content: "\ea7e";
}
.zwicon-user-follow:before {
content: "\ea7f";
}
.zwicon-user-minus:before {
content: "\ea80";
}
.zwicon-user-plus:before {
content: "\ea81";
}
.zwicon-user:before {
content: "\ea82";
}
.zwicon-users:before {
content: "\ea83";
}
.zwicon-history:before {
content: "\ea84";
}
.zwicon-task:before {
content: "\ea85";
}
.zwicon-bottom-bar:before {
content: "\ea86";
}
.zwicon-content-left:before {
content: "\ea87";
}
.zwicon-content-right:before {
content: "\ea88";
}
.zwicon-desktop-1:before {
content: "\ea89";
}
.zwicon-desktop-2:before {
content: "\ea8a";
}
.zwicon-desktop-3:before {
content: "\ea8b";
}
.zwicon-half-h:before {
content: "\ea8c";
}
.zwicon-half-v:before {
content: "\ea8d";
}
.zwicon-layout-1:before {
content: "\ea8e";
}
.zwicon-layout-2:before {
content: "\ea8f";
}
.zwicon-layout-3:before {
content: "\ea90";
}
.zwicon-layout-4:before {
content: "\ea91";
}
.zwicon-layout-5:before {
content: "\ea92";
}
.zwicon-left-bar:before {
content: "\ea93";
}
.zwicon-margin:before {
content: "\ea94";
}
.zwicon-right-bar:before {
content: "\ea95";
}
.zwicon-sidebar:before {
content: "\ea96";
}
.zwicon-three-h:before {
content: "\ea97";
}
.zwicon-three-v:before {
content: "\ea98";
}
.zwicon-to-bottom:before {
content: "\ea99";
}
.zwicon-to-left:before {
content: "\ea9a";
}
.zwicon-to-right:before {
content: "\ea9b";
}
.zwicon-to-top:before {
content: "\ea9c";
}
.zwicon-top-bar:before {
content: "\ea9d";
}
.zwicon-airplay:before {
content: "\ea9e";
}
.zwicon-broadcast:before {
content: "\ea9f";
}
.zwicon-camera-alt:before {
content: "\eaa0";
}
.zwicon-camera-alt2:before {
content: "\eaa1";
}
.zwicon-camera:before {
content: "\eaa2";
}
.zwicon-cast:before {
content: "\eaa3";
}
.zwicon-collapse-wide:before {
content: "\eaa4";
}
.zwicon-collapse1:before {
content: "\eaa5";
}
.zwicon-disk:before {
content: "\eaa6";
}
.zwicon-expand-wide:before {
content: "\eaa7";
}
.zwicon-expand1:before {
content: "\eaa8";
}
.zwicon-film-alt:before {
content: "\eaa9";
}
.zwicon-film-play:before {
content: "\eaaa";
}
.zwicon-film:before {
content: "\eaab";
}
.zwicon-image-circle:before {
content: "\eaac";
}
.zwicon-image-gallery:before {
content: "\eaad";
}
.zwicon-image-wide:before {
content: "\eaae";
}
.zwicon-image:before {
content: "\eaaf";
}
.zwicon-microphone-mute:before {
content: "\eab0";
}
.zwicon-microphone:before {
content: "\eab1";
}
.zwicon-next-alt:before {
content: "\eab2";
}
.zwicon-next:before {
content: "\eab3";
}
.zwicon-panorama-h:before {
content: "\eab4";
}
.zwicon-pause-alt:before {
content: "\eab5";
}
.zwicon-pause:before {
content: "\eab6";
}
.zwicon-play-alt:before {
content: "\eab7";
}
.zwicon-play:before {
content: "\eab8";
}
.zwicon-previous-alt:before {
content: "\eab9";
}
.zwicon-previous:before {
content: "\eaba";
}
.zwicon-shuffle:before {
content: "\eabb";
}
.zwicon-video-alt:before {
content: "\eabc";
}
.zwicon-video-camera:before {
content: "\eabd";
}
.zwicon-video:before {
content: "\eabe";
}
.zwicon-volume-low:before {
content: "\eabf";
}
.zwicon-volume-max:before {
content: "\eac0";
}
.zwicon-volume-mid:before {
content: "\eac1";
}
.zwicon-volume-min:before {
content: "\eac2";
}
.zwicon-wide-angle:before {
content: "\eac3";
}
.zwicon-exclude:before {
content: "\eac4";
}
.zwicon-flatten:before {
content: "\eac5";
}
.zwicon-intersect:before {
content: "\eac6";
}
.zwicon-substract-back:before {
content: "\eac7";
}
.zwicon-substract-front:before {
content: "\eac8";
}
.zwicon-unite:before {
content: "\eac9";
}
.zwicon-height:before {
content: "\eaca";
}
.zwicon-resize-alt:before {
content: "\eacb";
}
.zwicon-resize:before {
content: "\eacc";
}
.zwicon-scale-down:before {
content: "\eacd";
}
.zwicon-scale-up:before {
content: "\eace";
}
.zwicon-scale:before {
content: "\eacf";
}
.zwicon-width:before {
content: "\ead0";
}
.zwicon-rotate-axis-x:before {
content: "\ead1";
}
.zwicon-rotate-axis-xy:before {
content: "\ead2";
}
.zwicon-rotate-axis-y:before {
content: "\ead3";
}
.zwicon-rotate-left:before {
content: "\ead4";
}
.zwicon-rotate-right:before {
content: "\ead5";
}
.zwicon-rotate-shape:before {
content: "\ead6";
}
.zwicon-cursor-square:before {
content: "\ead7";
}
.zwicon-cursor:before {
content: "\ead8";
}
.zwicon-select-cursor:before {
content: "\ead9";
}
.zwicon-select:before {
content: "\eada";
}
.zwicon-shape-circle:before {
content: "\eadb";
}
.zwicon-shape-cone:before {
content: "\eadc";
}
.zwicon-shape-cube:before {
content: "\eadd";
}
.zwicon-shape-cylinder:before {
content: "\eade";
}
.zwicon-shape-octagonal:before {
content: "\eadf";
}
.zwicon-shape-polygon:before {
content: "\eae0";
}
.zwicon-shape-sphere:before {
content: "\eae1";
}
.zwicon-shape-square:before {
content: "\eae2";
}
.zwicon-laugh:before {
content: "\eae3";
}
.zwicon-neutral:before {
content: "\eae4";
}
.zwicon-sad:before {
content: "\eae5";
}
.zwicon-smile:before {
content: "\eae6";
}
.zwicon-bold:before {
content: "\eae7";
}
.zwicon-draw-text-field:before {
content: "\eae8";
}
.zwicon-font-height:before {
content: "\eae9";
}
.zwicon-font-size:before {
content: "\eaea";
}
.zwicon-font-width:before {
content: "\eaeb";
}
.zwicon-font:before {
content: "\eaec";
}
.zwicon-heading:before {
content: "\eaed";
}
.zwicon-indent-left-alt:before {
content: "\eaee";
}
.zwicon-indent-left:before {
content: "\eaef";
}
.zwicon-indent-right-alt:before {
content: "\eaf0";
}
.zwicon-indent-right:before {
content: "\eaf1";
}
.zwicon-italic:before {
content: "\eaf2";
}
.zwicon-list-bullet:before {
content: "\eaf3";
}
.zwicon-list-number:before {
content: "\eaf4";
}
.zwicon-outdent-left:before {
content: "\eaf5";
}
.zwicon-outdent-right:before {
content: "\eaf6";
}
.zwicon-paragraph:before {
content: "\eaf7";
}
.zwicon-text-align-center:before {
content: "\eaf8";
}
.zwicon-text-align-justify:before {
content: "\eaf9";
}
.zwicon-text-align-left:before {
content: "\eafa";
}
.zwicon-text-align-right:before {
content: "\eafb";
}
.zwicon-text-cursor:before {
content: "\eafc";
}
.zwicon-text-decoration:before {
content: "\eafd";
}
.zwicon-text-field:before {
content: "\eafe";
}
.zwicon-text:before {
content: "\eaff";
}
.zwicon-underline:before {
content: "\eb00";
}
.zwicon-wrap-img-left:before {
content: "\eb01";
}
.zwicon-wrap-img-right:before {
content: "\eb02";
}
.zwicon-wrap-left:before {
content: "\eb03";
}
.zwicon-wrap-right:before {
content: "\eb04";
}
.zwicon-transform-left:before {
content: "\eb05";
}
.zwicon-transform-right:before {
content: "\eb06";
}
.zwicon-ab-testing:before {
content: "\eb07";
}
.zwicon-agile:before {
content: "\eb08";
}
.zwicon-backlog:before {
content: "\eb09";
}
.zwicon-design-studio:before {
content: "\eb0a";
}
.zwicon-design-validation:before {
content: "\eb0b";
}
.zwicon-information-architecture:before {
content: "\eb0c";
}
.zwicon-interview:before {
content: "\eb0d";
}
.zwicon-kanban-board:before {
content: "\eb0e";
}
.zwicon-lego-serious-play:before {
content: "\eb0f";
}
.zwicon-paper-prototype:before {
content: "\eb10";
}
.zwicon-persona:before {
content: "\eb11";
}
.zwicon-prototype-mobile:before {
content: "\eb12";
}
.zwicon-prototype:before {
content: "\eb13";
}
.zwicon-responsive:before {
content: "\eb14";
}
.zwicon-screen-flow:before {
content: "\eb15";
}
.zwicon-stand-up:before {
content: "\eb16";
}
.zwicon-sticky-notes1:before {
content: "\eb17";
}
.zwicon-usability:before {
content: "\eb18";
}
.zwicon-user-flow:before {
content: "\eb19";
}
.zwicon-user-interview:before {
content: "\eb1a";
}
.zwicon-user-journey:before {
content: "\eb1b";
}
.zwicon-cloud:before {
content: "\eb1c";
}
.zwicon-cloudy-day:before {
content: "\eb1d";
}
.zwicon-cloudy-night:before {
content: "\eb1e";
}
.zwicon-heavy-rain-day:before {
content: "\eb1f";
}
.zwicon-heavy-rain-night:before {
content: "\eb20";
}
.zwicon-heavy-rain:before {
content: "\eb21";
}
.zwicon-heavy-wind:before {
content: "\eb22";
}
.zwicon-mild-rain-day:before {
content: "\eb23";
}
.zwicon-mild-rain-night:before {
content: "\eb24";
}
.zwicon-mild-rain:before {
content: "\eb25";
}
.zwicon-moon:before {
content: "\eb26";
}
.zwicon-rain-day:before {
content: "\eb27";
}
.zwicon-rain-night:before {
content: "\eb28";
}
.zwicon-rain:before {
content: "\eb29";
}
.zwicon-snow-day:before {
content: "\eb2a";
}
.zwicon-snow-night:before {
content: "\eb2b";
}
.zwicon-snow:before {
content: "\eb2c";
}
.zwicon-storm-day:before {
content: "\eb2d";
}
.zwicon-storm-night:before {
content: "\eb2e";
}
.zwicon-storm:before {
content: "\eb2f";
}
.zwicon-sun:before {
content: "\eb30";
}
.zwicon-temperature:before {
content: "\eb31";
}
.zwicon-wind-alt:before {
content: "\eb32";
}
.zwicon-wind-cloudy-day:before {
content: "\eb33";
}
.zwicon-wind-cloudy-night:before {
content: "\eb34";
}
.zwicon-wind-cloudy:before {
content: "\eb35";
}
.zwicon-wind:before {
content: "\eb36";
}
================================================
FILE: src/background.ts
================================================
import {
app, protocol, BrowserWindow, Menu, shell,
} from 'electron'
import {
createProtocol,
} from 'vue-cli-plugin-electron-builder/lib'
import { autoUpdater } from 'electron-updater'
import { init } from '@sentry/electron/dist/main'
import App from './server/app'
import messages from './assets/locales-menu'
import initServer from './server'
init({ dsn: 'https://6a6dacc57a6a4e27a88eb31596c152f8@sentry.io/1887150' })
const isDevelopment = process.env.NODE_ENV !== 'production'
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win: any
let menu: Menu
let httpServer: any
// Standard scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }])
function createWindow() {
// Create the browser window.
const winOption: any = {
width: 1200,
height: 800,
minHeight: 642,
minWidth: 1000,
webPreferences: {
webSecurity: false, // FIXED: Not allowed to load local resource
nodeIntegration: true,
enableRemoteModule: true, // FIXED: 兼容 electron@11.0.1
},
// frame: false, // 去除默认窗口栏
titleBarStyle: 'hiddenInset' as ('hidden' | 'default' | 'hiddenInset' | 'customButtonsOnHover' | undefined),
}
if (process.platform !== 'darwin') {
winOption.icon = `${__dirname}/app-icons/gridea.png`
}
win = new BrowserWindow(winOption)
win.setTitle('Gridea')
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string)
if (!process.env.IS_TEST) { win.webContents.openDevTools() }
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
autoUpdater.checkForUpdatesAndNotify()
}
win.on('closed', () => {
win = null
})
const locale: string = app.getLocale() || 'zh-CN'
const menuLabels = messages[locale] || messages['zh-CN']
// menu
const template: any = [
{
label: menuLabels.edit,
submenu: [
{
label: menuLabels.save,
accelerator: 'CmdOrCtrl+S',
click: () => {
win.webContents.send('click-menu-save')
},
},
{ type: 'separator' },
{ role: 'undo', label: menuLabels.undo },
{ role: 'redo', label: menuLabels.redo },
{ type: 'separator' },
{ role: 'cut', label: menuLabels.cut },
{ role: 'copy', label: menuLabels.copy },
{ role: 'paste', label: menuLabels.paste },
{ role: 'delete', label: menuLabels.delete },
{ role: 'selectall', label: menuLabels.selectall },
{ role: 'toggledevtools', label: menuLabels.toggledevtools },
{ type: 'separator' },
{ role: 'close', label: menuLabels.close },
{ role: 'quit', label: menuLabels.quit },
],
},
{
role: 'windowMenu',
},
{
role: menuLabels.help,
submenu: [
{
label: 'Learn More',
click() { shell.openExternal('https://github.com/getgridea/gridea') },
},
],
},
]
menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
const s = initServer()
httpServer = s.server
const setting = {
mainWindow: win,
app,
baseDir: __dirname,
previewServer: s.app,
}
// Init app
const appInstance = new App(setting)
console.log('Main process runing...', appInstance.appDir) // DELETE ME
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
httpServer && httpServer.close()
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
// if (isDevelopment && !process.env.IS_TEST) {
// // Install Vue Devtools
// await installVueDevtools()
// }
createWindow()
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}
// ipcMain.on('min-window', () => {
// if (win) {
// win.minimize()
// }
// })
// ipcMain.on('max-window', () => {
// if (win) {
// if (win.isMaximized()) {
// win.unmaximize()
// } else {
// win.maximize()
// }
// }
// })
// ipcMain.on('close-window', () => {
// if (win) {
// win.close()
// }
// })
/**
* Auto Updater
*
* Uncomment the following code below and install `electron-updater` to
* support auto updating. Code Signing with a valid certificate is required.
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating
*/
/*
import { autoUpdater } from 'electron-updater'
autoUpdater.on('update-downloaded', () => {
autoUpdater.quitAndInstall()
})
app.on('ready', () => {
if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
})
*/
================================================
FILE: src/components/AppSystem/Index.vue
================================================
================================================
FILE: src/components/AppSystem/includes/LanguageSetting.vue
================================================
{{ $t('language') }}
简体中文
English
繁體中文
Français
日本語
{{ $t('save') }}
================================================
FILE: src/components/AppSystem/includes/SourceFolderSetting.vue
================================================
{{ $t('sourceFolder') }}
{{ $t('save') }}
================================================
FILE: src/components/AppSystem/includes/Version.vue
================================================
{{ $t('version') }}
{{ version }}
Gridea
================================================
FILE: src/components/ColorCard/Index.vue
================================================
================================================
FILE: src/components/EmojiCard/Index.vue
================================================
================================================
FILE: src/components/FooterBox/Index.vue
================================================
================================================
FILE: src/components/Main.vue
================================================
{{ $t('preview') }}
{{ $t('syncSite') }}
🙁 {{ $t('syncError1') }} FAQ {{ $t('or') }} Issues {{ $t('syncError2') }}
{{ newVersion }}
{{ log.type }}
{{ log.message }}
================================================
FILE: src/components/MonacoMarkdownEditor/Index.vue
================================================
================================================
FILE: src/components/MonacoMarkdownEditor/theme.js
================================================
export default {
'base': 'vs',
'inherit': true,
'rules': [
{
'foreground': '999999',
'token': 'comment',
},
{
'foreground': 'e88501',
'token': 'string',
},
{
'foreground': '999999',
'token': 'string.link',
},
{
'foreground': '999999',
'token': 'variable.source',
},
{
'foreground': '4C51BF',
'token': 'variable',
},
{
'foreground': '2B6CB0',
'token': 'markup.list',
},
{
'foreground': '2B6CB0',
'token': 'markup.underline.link',
},
{
'foreground': '46a609',
'token': 'constant.numeric',
},
{
'foreground': '39946a',
'token': 'constant.language',
},
{
'foreground': 'b7791f',
'token': 'keyword',
},
{
'fontStyle': 'bold',
'token': 'markup.heading',
},
{
'fontStyle': 'bold',
'token': 'markup.bold',
},
{
'fontStyle': 'italic',
'token': 'markup.italic',
},
// ie bold/italic/heading/list marks
{
'foreground': '999999',
'token': 'punctuation.definition.constant.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.bold.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.italic.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.heading.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.heading.begin.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.heading.end.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.heading.setext.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.list_item.markdown',
},
{
'foreground': '999999',
'token': 'markup.list.numbered.bullet.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.bold.begin.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.bold.end.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.italic.begin.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.italic.end.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.variable.begin.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.variable.end.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.link.begin.markdown',
},
{
'foreground': '999999',
'token': 'punctuation.definition.link.end.markdown',
},
{
'foreground': 'b7791f',
'token': 'support.constant.property-value',
},
{
'foreground': 'b7791f',
'token': 'constant.other.color',
},
{
'foreground': '96dc5f',
'token': 'keyword.other.unit',
},
{
'foreground': '484848',
'token': 'keyword.operator',
},
{
'foreground': 'c52727',
'token': 'storage',
},
{
'foreground': '858585',
'token': 'entity.other.inherited-class',
},
{
'foreground': '606060',
'token': 'entity.name.tag',
},
{
'foreground': 'bf78cc',
'token': 'constant.character.entity',
},
{
'foreground': 'bf78cc',
'token': 'support.class.js',
},
{
'foreground': '606060',
'token': 'entity.other.attribute-name',
},
{
'foreground': 'c52727',
'token': 'meta.selector.css',
},
{
'foreground': 'c52727',
'token': 'entity.name.tag.css',
},
{
'foreground': 'c52727',
'token': 'entity.other.attribute-name.id.css',
},
{
'foreground': 'c52727',
'token': 'entity.other.attribute-name.class.css',
},
{
'foreground': '484848',
'token': 'meta.property-name.css',
},
{
'foreground': 'c52727',
'token': 'support.function',
},
{
'background': 'ff002a',
'token': 'invalid',
},
{
'foreground': 'c52727',
'token': 'punctuation.section.embedded',
},
{
'foreground': '606060',
'token': 'punctuation.definition.tag',
},
{
'foreground': 'bf78cc',
'token': 'constant.other.color.rgb-value.css',
},
{
'foreground': 'bf78cc',
'token': 'support.constant.property-value.css',
},
],
'colors': {
'editor.foreground': '#333333',
'editor.background': '#FFFFFF',
'editor.selectionBackground': '#BDD5FC',
'editor.lineHighlightBackground': '#FFFBD1',
'editorCursor.foreground': '#000000',
'editorWhitespace.foreground': '#BFBFBF',
'textLink.foreground': '#666',
},
}
================================================
FILE: src/components/PostsCard/Index.vue
================================================
================================================
FILE: src/helpers/analytics.ts
================================================
import GA from 'electron-google-analytics'
import macaddress from 'macaddress'
import * as pkg from '../../package.json'
const isDevelopment = process.env.NODE_ENV !== 'production'
interface EvOptions {
evLabel?: any
evValue?: any
}
const hostname = 'http://client.gridea.dev'
class Analytics {
private readonly ga: any
private clientId: any
constructor() {
this.ga = new GA('UA-113307620-4', { debug: isDevelopment })
this.ga.set('version', (pkg as any).version)
}
public getClientId(callback: any) {
if (this.clientId) {
callback(this.clientId)
}
macaddress.one((err: any, mac: string) => {
this.clientId = mac
callback(mac)
})
}
public async pageView(url: string, title?: string) {
this.getClientId(async (clientId: any) => {
try {
await this.ga.pageview(hostname, url, title, 1, clientId)
} catch (e) {
console.error(e)
}
})
}
public async event(evCategory: string, evAction: string, options: EvOptions) {
this.getClientId(async (clientId: any) => {
try {
await this.ga.event(evCategory, evAction, {
...options,
clientID: clientId,
})
} catch (e) {
console.error(e)
}
})
}
public async exception(exDesc: string, exFatal: any) {
this.getClientId(async (clientId: any) => {
try {
await this.ga.exception(exDesc, exFatal)
} catch (e) {
console.error(e)
}
})
}
}
export default new Analytics()
================================================
FILE: src/helpers/constants.ts
================================================
export const UrlFormats = [
{
text: 'Slug',
value: 'SLUG',
},
{
text: 'Short ID',
value: 'SHORT_ID',
},
]
export const DEFAULT_POST_PAGE_SIZE = 10
export const DEFAULT_ARCHIVES_PAGE_SIZE = 50
export const DEFAULT_FEED_COUNT = 10
export const DEFAULT_ARCHIVES_PATH = 'archives'
export const DEFAULT_POST_PATH = 'post'
export const DEFAULT_TAG_PATH = 'tag'
================================================
FILE: src/helpers/content-helper.ts
================================================
export default class ContentHelper {
localReg: RegExp
domainReg: RegExp
featureDomainReg: RegExp
featureLocalReg: RegExp
constructor() {
this.localReg = /\(file.*\/post-images\//g
this.domainReg = /\(.*\/post-images\//g
this.featureDomainReg = /\.*\/post-images\//g
this.featureLocalReg = /file.*\/post-images\//g
}
/**
* 将文章中本地图片路径,变更为线上路径
* @param content 内容
* @param domainPath 线上路径
*/
changeImageUrlLocalToDomain(content: string, domainPath: string) {
domainPath = domainPath.replace(/\\/g, '/')
return content.replace(this.localReg, `(${domainPath}/post-images/`)
}
/**
* 将文章中线上图片路径,变更为本地路径
* @param content 内容
* @param localPath 本地路径
*/
changeImageUrlDomainToLocal(content: string, localPath: string) {
localPath = localPath.replace(/\\/g, '/')
return content.replace(this.domainReg, `(file://${localPath}/post-images/`)
}
/**
* 将 feature 图片路径,变更为本地路径
*/
changeFeatureImageUrlDomainToLocal(content: string, localPath: string) {
return content.replace(this.featureDomainReg, `file://${localPath}/post-images/`)
}
/**
* 将 feature 本地图片路径,变更为线上路径
*/
changeFeatureImageUrlLocalToDomain(content: string, domainPath: string) {
let url = content.replace(this.featureLocalReg, `${domainPath}/post-images/`)
url = url.replace(/\\/g, '/')
return url
}
}
================================================
FILE: src/helpers/enums.ts
================================================
export enum MenuTypes {
Internal = 'Internal',
External = 'External',
}
export enum UrlFormats {
Slug = 'SLUG',
ShortId = 'SHORT_ID',
}
================================================
FILE: src/helpers/shortcut-keys.ts
================================================
export default [
{
name: '编辑',
list: [
{
title: '保存文章',
keyboard: ['⌘', 'S'],
},
{
title: '剪切',
keyboard: ['⌘', 'X'],
},
{
title: '复制',
keyboard: ['⌘', 'C'],
},
{
title: '粘贴',
keyboard: ['⌘', 'V'],
},
],
},
{
name: 'Markdown',
list: [
{
title: '标题降级 (# -)',
keyboard: ['⌃', '⇧', '['],
},
{
title: '标题升级 (# +)',
keyboard: ['⌃', '⇧', ']'],
},
{
title: '加粗 (**)',
keyboard: ['⌘', 'B'],
},
{
title: '行内 Code(`)',
keyboard: ['⌘', '`'],
},
{
title: '斜体 (*)',
keyboard: ['⌘', 'I'],
},
{
title: '列表 (-)',
keyboard: ['⌘', 'L'],
},
{
title: 'LaTeX ($)',
keyboard: ['⌘', 'M'],
},
{
title: 'LaTeX ($$)',
keyboard: ['⇧', '⌘', 'M'],
},
{
title: '删除线 (~~)',
keyboard: ['⌥', 'S'],
},
],
},
{
name: '其他',
list: [
{
title: '格式化文档',
keyboard: ['⇧', '⌥', 'F'],
},
],
},
]
================================================
FILE: src/helpers/slug.ts
================================================
/* tslint:disable */
const { transliterate } = require('transliteration')
const slug = require('slug')
/*
* Custom mode of rfc3986 without unicode symbols
*/
slug.defaults.modes['rfc3986-non-unicode'] = {
replacement: '-', // replace spaces with replacement
symbols: false, // replace unicode symbols or not
remove: /[.]/g, // (optional) regex to remove characters
lower: true, // result in lower case
charmap: slug.charmap, // replace special characters
multicharmap: slug.multicharmap, // replace multi-characters
}
slug.defaults.modes['rfc3986-non-unicode-with-dots'] = {
replacement: '-', // replace spaces with replacement
symbols: false, // replace unicode symbols or not
lower: true, // result in lower case
charmap: slug.charmap, // replace special characters
multicharmap: slug.multicharmap, // replace multi-characters
}
slug.defaults.modes['rfc3986-non-unicode-with-dots-no-lower'] = {
replacement: '-', // replace spaces with replacement
symbols: false, // replace unicode symbols or not
lower: false, // result in lower case
charmap: slug.charmap, // replace special characters
multicharmap: slug.multicharmap, // replace multi-characters
}
slug.defaults.mode = 'rfc3986-non-unicode'
/**
* Slugify 文本
* @param textToSlugify 待 slugify 的文本
* @param filenameMode
* @param saveLowerChars
*/
function createSlug(textToSlugify: any, filenameMode = false, saveLowerChars = false) {
textToSlugify = transliterate(textToSlugify)
if (!filenameMode) {
if (saveLowerChars) {
slug.defaults.mode = 'rfc3986-non-unicode-with-dots-no-lower'
}
textToSlugify = slug(textToSlugify)
slug.defaults.mode = 'rfc3986-non-unicode'
} else {
slug.defaults.mode = 'rfc3986-non-unicode-with-dots'
textToSlugify = slug(textToSlugify)
slug.defaults.mode = 'rfc3986-non-unicode'
}
return textToSlugify
}
export default createSlug
================================================
FILE: src/helpers/utils.ts
================================================
import markdown from '../server/plugins/markdown'
/**
* Add single-quoted to string type field, in order to be compatible with many special characters
* eg. true, false, 1, [, ], {, }, ,, #, <, >, @,
*/
export function formatYamlString(string: any) {
return string.replace(/'/g, '\'\'')
}
export const formatThemeCustomConfigToRender = (config: any, currentThemeConfig: any) => {
for (const configItem of currentThemeConfig) {
const configValue = config[configItem.name]
if (configItem.type === 'markdown') {
if (!configValue) continue
config[configItem.name] = markdown.render(configValue)
} else if (configItem.type === 'array' && configValue) {
for (let arrItemIndex = 0; arrItemIndex < configValue.length; arrItemIndex += 1) {
const foundConfigItem = currentThemeConfig.find((i: any) => i.name === configItem.name)
const arrayItemKeys = Object.keys(configValue[arrItemIndex])
for (let keyIndex = 0; keyIndex < arrayItemKeys.length; keyIndex += 1) {
const key = arrayItemKeys[keyIndex]
const foundMarkdownField = foundConfigItem.arrayItems.find((i: any) => i.name === key && i.type === 'markdown')
if (foundMarkdownField) {
const fieldValue = configValue[arrItemIndex][key]
if (!fieldValue) continue
configValue[arrItemIndex][key] = markdown.render(fieldValue)
}
}
}
}
}
return config
}
================================================
FILE: src/helpers/vee-validate.ts
================================================
import { required } from 'vee-validate/dist/rules'
import { extend } from 'vee-validate'
extend('required', {
...required,
message: '此项是必填项',
})
================================================
FILE: src/helpers/words-count.ts
================================================
import striptags from 'striptags'
const CN_PATTERN = /[\u4E00-\u9FA5]/g
const EN_PATTERN = /[a-zA-Z0-9_\u0392-\u03c9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+|\w+/g
function countContent(content: any): [number, number] {
if (typeof content !== 'string') {
throw new Error('[word-counter] content must be string type')
}
let cn = 0
let en = 0
if (content.length > 0) {
content = striptags(content)
cn = (content.match(CN_PATTERN) || []).length
en = (content.replace(CN_PATTERN, '').match(EN_PATTERN) || []).length
}
return [cn, en]
}
export function wordCount(content?: any, transformFn?: (count: number) => any): any {
const [cn, en] = countContent(content)
const count = cn + en
if (typeof transformFn === 'function') {
return transformFn(count)
}
return count
}
interface TimeConfig {
cn?: number
en?: number
}
export function timeCalc(content?: any, { cn = 300, en = 160 }: TimeConfig = {}): {
minius: number,
second: number,
} {
const [cnCount, enCount] = countContent(content)
const minius = cnCount / cn + enCount / en
return {
minius: minius === 0 ? 0 : Math.ceil(minius),
second: Math.floor(minius * 60),
}
}
================================================
FILE: src/interfaces/menu.ts
================================================
export interface IMenu {
name: string
openType: string
link: string
}
================================================
FILE: src/interfaces/post.ts
================================================
export interface IPostData {
title: string
date: string
published: boolean
hideInList: boolean
tags?: []
feature: string
isTop: boolean
}
export interface IPost {
content: string
data: IPostData
fileName: string
}
================================================
FILE: src/interfaces/setting.ts
================================================
export interface ISetting {
platform: 'github' | 'coding' | 'sftp' | 'gitee' | 'netlify'
domain: string
repository: string
branch: string
username: string
email: string
tokenUsername: string
token: string
cname: string
port: string
server: string
password: string
privateKey: string
remotePath: string
proxyPath: string
proxyPort: string
enabledProxy: 'direct' | 'proxy'
netlifyAccessToken: string
netlifySiteId: string
[index: string]: string
}
export interface IDisqusSetting {
api: string
apikey: string
shortname: string
}
export interface IGitalkSetting {
clientId: string
clientSecret: string
repository: string
owner: string
}
export interface ICommentSetting {
commentPlatform: string
showComment: boolean
disqusSetting: IDisqusSetting
gitalkSetting: IGitalkSetting
}
================================================
FILE: src/interfaces/snackbar.ts
================================================
export default interface ISnackbar {
/**
* 通知颜色:success, info, error 或其他颜色
* default: success
*/
color?: string
snackbar?: boolean
message?: string
bottom?: boolean
}
================================================
FILE: src/interfaces/tag.ts
================================================
export interface ITag {
name: string
used: boolean
slug?: string
}
================================================
FILE: src/interfaces/theme.ts
================================================
export interface ITheme {
themeName: string
postPageSize: number
archivesPageSize: number
siteName: string
siteDescription: string
footerInfo: string
showFeatureImage: boolean
postUrlFormat: string
tagUrlFormat: string
dateFormat: string
feedFullText: boolean
feedCount: number
archivesPath: string
postPath: string
tagPath: string
}
================================================
FILE: src/main.ts
================================================
import Vue from 'vue'
import moment from 'moment'
import Antd from 'ant-design-vue'
import 'remixicon/fonts/remixicon.css'
import 'katex/dist/katex.min.css'
import '@fontsource/noto-serif/index.css'
import '@/assets/styles/tailwind.css'
import '@/assets/styles/main.less'
import VueI18n from 'vue-i18n'
import Prism from 'prismjs'
import VueShortkey from 'vue-shortkey'
import { remote } from 'electron'
import * as Sentry from '@sentry/electron'
import locale from './assets/locales'
import App from './App.vue'
import router from './router'
import store from './store/index'
import VueBus from './vue-bus'
import ga from './helpers/analytics'
import './helpers/vee-validate'
ga.event('Client', 'show', {
evLabel: 'startup',
})
Sentry.init({ dsn: 'https://6a6dacc57a6a4e27a88eb31596c152f8@sentry.io/1887150' })
const defaultLocale = ({
'zh-CN': 'zhHans',
'zh-TW': 'zh_TW',
'en-US': 'en',
} as any)[remote.app.getLocale() || 'zh-CN']
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: localStorage.getItem('language') || defaultLocale,
messages: locale as any,
silentTranslationWarn: true,
})
Prism.highlightAll()
Vue.use(Antd)
Vue.use(VueBus)
Vue.use(VueShortkey)
Vue.prototype.$moment = moment
Vue.config.productionTip = false
new Vue({
router,
store,
i18n,
render: h => h(App),
mounted() {
router.push('/')
},
}).$mount('#app')
================================================
FILE: src/router.ts
================================================
import Vue from 'vue'
import Router from 'vue-router'
import macaddress from 'macaddress'
import Main from './components/Main.vue'
// import ArticleUpdate from './views/article/ArticleUpdate.vue'
import Articles from './views/article/Articles.vue'
import Menu from './views/menu/Index.vue'
import Tags from './views/tags/Index.vue'
import Theme from './views/theme/Index.vue'
import Setting from './views/setting/Index.vue'
import Loading from './views/loading/Index.vue'
import ga from './helpers/analytics'
Vue.use(Router)
const router = new Router({
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'main',
component: Main,
children: [
{
path: '/articles',
name: 'articles',
component: Articles,
},
{
path: '/menu',
name: 'menu',
component: Menu,
},
{
path: '/tags',
name: 'tags',
component: Tags,
},
{
path: '/theme',
name: 'theme',
component: Theme,
},
{
path: '/setting',
name: 'setting',
component: Setting,
},
{
path: '/loading',
name: 'loading',
component: Loading,
},
{
path: '*',
redirect: '/articles',
},
],
},
],
})
router.afterEach((to, from) => {
ga.pageView(to.fullPath, to.name)
})
export default router
================================================
FILE: src/server/app.ts
================================================
import { BrowserWindow, app } from 'electron'
import * as fse from 'fs-extra'
import * as path from 'path'
import express from 'express'
import EventClasses from './events/index'
import Posts from './posts'
import Tags from './tags'
import Menus from './menus'
import Theme from './theme'
import Renderer from './renderer'
import Setting from './setting'
import Deploy from './deploy'
import { IApplicationDb, IApplicationSetting } from './interfaces/application'
import {
DEFAULT_POST_PAGE_SIZE, DEFAULT_ARCHIVES_PAGE_SIZE, DEFAULT_FEED_COUNT, DEFAULT_ARCHIVES_PATH, DEFAULT_POST_PATH, DEFAULT_TAG_PATH,
} from '../helpers/constants'
// eslint-disable-next-line
declare const __static: string
export default class App {
mainWindow: BrowserWindow
app: any
baseDir: string
appDir: string
previewServer: any
db: IApplicationDb
buildDir: string
constructor(setting: IApplicationSetting) {
this.mainWindow = setting.mainWindow
this.app = setting.app
this.baseDir = setting.baseDir
this.appDir = path.join(this.app.getPath('documents'), 'gridea')
this.previewServer = setting.previewServer
this.buildDir = path.join(this.app.getPath('home'), '.gridea', 'output')
this.db = {
posts: [],
tags: [],
menus: [],
themeConfig: {
themeName: '',
postPageSize: DEFAULT_POST_PAGE_SIZE,
archivesPageSize: DEFAULT_ARCHIVES_PAGE_SIZE,
siteName: '',
siteDescription: '',
footerInfo: 'Powered by Gridea',
showFeatureImage: true,
domain: '',
postUrlFormat: 'SLUG',
tagUrlFormat: 'SLUG',
dateFormat: 'YYYY-MM-DD',
feedFullText: true,
feedCount: DEFAULT_FEED_COUNT,
archivesPath: DEFAULT_ARCHIVES_PATH,
postPath: DEFAULT_POST_PATH,
tagPath: DEFAULT_TAG_PATH,
},
themeCustomConfig: {},
currentThemeConfig: [],
themes: [],
setting: {
platform: 'github',
domain: '',
repository: '',
branch: '',
username: '',
email: '',
tokenUsername: '',
token: '',
cname: '',
port: '22',
server: '',
password: '',
privateKey: '',
remotePath: '',
proxyPath: '',
proxyPort: '',
enabledProxy: 'direct',
netlifySiteId: '',
netlifyAccessToken: '',
},
commentSetting: {
showComment: false,
commentPlatform: 'gitalk',
gitalkSetting: {
clientId: '',
clientSecret: '',
repository: '',
owner: '',
},
disqusSetting: {
api: '',
apikey: '',
shortname: '',
},
},
}
this.checkDir()
}
/**
* Load site config and data
*/
public async loadSite() {
const postsInstance = new Posts(this)
const posts = await postsInstance.list()
const tagsInstance = new Tags(this)
const tags = await tagsInstance.list()
const menusInstance = new Menus(this)
const menus = await menusInstance.list()
const themeInstance = new Theme(this)
const themeConfig = await themeInstance.getThemeConfig()
const themes = await themeInstance.getThemeList()
const themeCustomConfig = await themeInstance.getThemeCustomConfig()
const currentThemeConfig = await themeInstance.getCurrentThemeCustomConfig()
const settingInstance = new Setting(this)
const setting = await settingInstance.getSetting()
const commentSetting = await settingInstance.getCommentSetting()
const deployInstance = new Deploy(this)
this.db = {
posts,
tags,
menus,
themeConfig: Object.assign(this.db.themeConfig, themeConfig),
themeCustomConfig,
currentThemeConfig,
themes,
setting,
commentSetting: commentSetting || this.db.commentSetting,
}
this.updateStaticServer()
this.initEvents()
return {
...this.db,
currentThemeConfig,
appDir: this.appDir,
mainWindow: this.mainWindow,
}
}
public renderHtml() {
const renderer = new Renderer(this)
console.log(renderer)
// renderer.renderPostList()
}
public async saveSourceFolderSetting(sourceFolderPath: string = '') {
try {
const appConfigFolder = path.join(this.app.getPath('home'), '.gridea')
const appConfigPath = path.join(appConfigFolder, 'config.json')
const jsonString = `{"sourceFolder": "${sourceFolderPath || this.appDir}"}`
fse.writeFileSync(appConfigPath, jsonString)
const appConfig = fse.readJsonSync(appConfigPath)
this.appDir = appConfig.sourceFolder
this.updateStaticServer()
this.checkDir()
return true
} catch (e) {
console.log(e)
return false
}
}
/**
* Check if the hve-next folder exists, if it does not exist, it is initialized
*/
private async checkDir() {
// Check if there is a .hve-notes folder, if it exists, load it, otherwise use the default configuration.
const appConfigFolderOld = path.join(this.app.getPath('home'), '.hve-notes') // < 0.7.7
const appConfigFolder = path.join(this.app.getPath('home'), '.gridea')
const appConfigPath = path.join(appConfigFolder, 'config.json')
let defaultAppDir = path.join(this.app.getPath('documents'), 'Gridea')
defaultAppDir = defaultAppDir.replace(/\\/g, '/')
try {
// if exist `.hve-notes` config folder, change folder name to `.gridea`
try {
if (!fse.pathExistsSync(appConfigFolder) && fse.pathExistsSync(appConfigFolderOld)) {
fse.renameSync(appConfigFolderOld, appConfigFolder)
}
} catch (e) {
console.log('Rename Error: ', e)
}
if (!fse.pathExistsSync(appConfigFolder)) {
fse.mkdirSync(appConfigFolder)
const jsonString = `{"sourceFolder": "${defaultAppDir}"}`
fse.writeFileSync(appConfigPath, jsonString)
}
const buildDir = path.join(appConfigFolder, 'output')
if (!fse.pathExistsSync(buildDir)) {
fse.mkdirSync(buildDir)
}
const appConfig = fse.readJsonSync(appConfigPath)
this.appDir = appConfig.sourceFolder
// Site folder exists
if (fse.pathExistsSync(this.appDir)) {
// Check if the `images`, `config`, 'output', `post-images`, 'posts', 'themes', 'static' folder exists, if it does not exist, copy it from default-files
['images', 'config', 'post-images', 'posts', 'themes', 'static'].forEach((folder: string) => {
const folderPath = path.join(this.appDir, folder)
if (!fse.pathExistsSync(folderPath)) {
fse.copySync(
path.join(__static, 'default-files', folder),
folderPath,
)
}
})
// Check default theme folder if includes [notes、fly、simple、paper] themes
this.checkTheme('notes')
this.checkTheme('fly')
this.checkTheme('simple')
this.checkTheme('paper')
// move output/favicon.ico to Gridea/favicon.ico
const outputFavicon = path.join(this.buildDir, 'favicon.ico')
const sourceFavicon = path.join(this.appDir, 'favicon.ico')
const existFaviconOutput = fse.pathExistsSync(outputFavicon)
const existFaviconSource = fse.pathExistsSync(sourceFavicon)
if (existFaviconOutput && !existFaviconSource) {
fse.moveSync(outputFavicon, sourceFavicon)
}
return
}
// Site folder not exists
this.appDir = defaultAppDir
const jsonString = `{"sourceFolder": "${defaultAppDir}"}`
fse.writeFileSync(appConfigPath, jsonString)
fse.mkdirSync(this.appDir)
fse.copySync(
path.join(__static, 'default-files'),
path.join(this.appDir),
)
} catch (e) {
console.log('Error', e)
} finally {
this.initEvents()
}
}
/**
* Check whether the theme is included, and if not, initialize one copy of the theme within the application.
*/
private checkTheme(themeName: string): void {
const themePath = path.join(this.appDir, 'themes', themeName)
if (!fse.pathExistsSync(themePath)) {
fse.copySync(
path.join(__static, 'default-files', 'themes', themeName),
themePath,
)
}
}
private updateStaticServer(): void {
function removeMiddleware(route: any, i: number, routes: any) {
if (route.handle.name === 'serveStatic') {
routes.splice(i, 1)
console.log('Preview server: Removed old static route')
}
}
const routers = this.previewServer._router // eslint-disable-line no-underscore-dangle
if (routers) {
const routesStack = routers.stack
routesStack.forEach(removeMiddleware)
}
this.previewServer.use(express.static(`${this.buildDir}`))
console.log(`Preview server: Static dir change to ${this.buildDir}`)
}
private initEvents(): void {
const {
SiteEvents,
PostEvents,
TagEvents,
MenuEvents,
ThemeEvents,
RendererEvents,
SettingEvents,
DeployEvents,
} = EventClasses
const site = new SiteEvents(this)
const post = new PostEvents(this)
const tag = new TagEvents(this)
const menu = new MenuEvents(this)
const theme = new ThemeEvents(this)
const renderer = new RendererEvents(this)
const setting = new SettingEvents(this)
const deploy = new DeployEvents(this)
}
}
================================================
FILE: src/server/deploy.ts
================================================
import fs from 'fs'
import moment from 'moment'
// @ts-ignore
import Model from './model'
import GitProxy from './plugins/deploys/gitproxy'
const git = require('isomorphic-git')
export default class Deploy extends Model {
outputDir: string = this.buildDir
remoteUrl = ''
platformAddress = ''
http = new GitProxy(this)
constructor(appInstance: any) {
super(appInstance)
const { setting } = this.db
this.platformAddress = ({
github: 'github.com',
coding: 'e.coding.net',
gitee: 'gitee.com',
} as any)[setting.platform || 'github']
const preUrl = ({
github: `${setting.username}:${setting.token}`,
coding: `${setting.tokenUsername}:${setting.token}`,
gitee: `${setting.username}:${setting.token}`,
} as any)[setting.platform || 'github']
this.remoteUrl = `https://${preUrl}@${this.platformAddress}/${setting.username}/${setting.repository}.git`
}
/**
* Check whether the remote connection is normal
*/
async remoteDetect() {
const result = {
success: true,
message: [''],
}
try {
const { setting } = this.db
let isRepo = false
try {
await git.currentBranch({ fs, dir: this.outputDir })
isRepo = true
} catch (e) {
console.log('Not a repo', e.message)
}
if (!setting.username || !setting.repository || !setting.token) {
return {
success: false,
message: 'Username、repository、token is required',
}
}
if (!isRepo) {
await git.init({ fs, dir: this.outputDir })
await git.setConfig({
fs,
dir: this.outputDir,
path: 'user.name',
value: setting.username,
})
await git.setConfig({
fs,
dir: this.outputDir,
path: 'user.email',
value: setting.email,
})
}
await git.addRemote({
fs, dir: this.outputDir, remote: 'origin', url: this.remoteUrl, force: true,
})
const info = await git.getRemoteInfo({
http: this.http,
url: this.remoteUrl,
})
console.log('info', info)
result.message = info.capabilities
} catch (e) {
console.log('Test Remote Error: ', e)
result.success = false
result.message = e.message
}
return result
}
async publish() {
await this.remoteDetect()
this.db.themeConfig.domain = this.db.setting.domain
let result = {
success: true,
message: '',
localBranchs: {},
}
let isRepo = false
try {
await git.currentBranch({ fs, dir: this.outputDir })
isRepo = true
} catch (e) {
console.log('Not a repo', e.message)
}
if (isRepo) {
result = await this.commonPush()
} else {
// result = await this.firstPush()
}
return result
}
async commonPush() {
console.log('common push')
const { setting } = this.db
const localBranchs = {}
try {
const statusSummary = await git.status({ fs, dir: this.outputDir, filepath: '.' })
console.log('statusSummary', statusSummary)
await git.addRemote({
fs, dir: this.outputDir, remote: 'origin', url: this.remoteUrl, force: true,
})
if (statusSummary !== 'unmodified') {
await git.add({ fs, dir: this.outputDir, filepath: '.' })
await git.commit({
fs,
dir: this.outputDir,
message: `update from gridea: ${moment().format('YYYY-MM-DD HH:mm:ss')}`,
})
}
await this.checkCurrentBranch()
const pushRes = await git.push({
fs,
http: this.http,
dir: this.outputDir,
remote: 'origin',
ref: setting.branch,
force: true,
})
console.log('pushRes', pushRes)
return {
success: true,
data: pushRes,
message: '',
localBranchs,
}
} catch (e) {
console.log(e)
return {
success: false,
message: e.message,
data: localBranchs,
localBranchs,
}
}
}
/**
* Check whether the branch needs to be switched,
* FIXME: if branch is change, then the fist push is not work. so need to push again.
*/
async checkCurrentBranch() {
const { setting } = this.db
const currentBranch = await git.currentBranch({ fs, dir: this.outputDir, fullname: false })
const localBranches = await git.listBranches({ fs, dir: this.outputDir })
if (currentBranch !== setting.branch) {
if (!localBranches.includes(setting.branch)) {
await git.branch({ fs, dir: this.outputDir, ref: setting.branch })
}
await git.checkout({ fs, dir: this.outputDir, ref: setting.branch })
}
}
}
================================================
FILE: src/server/events/deploy.ts
================================================
import { ipcMain, IpcMainEvent } from 'electron'
import Deploy from '../deploy'
import Renderer from '../renderer'
import SftpDeploy from '../plugins/deploys/sftp'
import NetlifyDeploy from '../plugins/deploys/netlify'
export default class DeployEvents {
constructor(appInstance: any) {
const { platform } = appInstance.db.setting
const deploy = new Deploy(appInstance)
const sftp = new SftpDeploy(appInstance)
const renderer = new Renderer(appInstance)
const netlify = new NetlifyDeploy(appInstance)
ipcMain.removeAllListeners('site-publish')
ipcMain.removeAllListeners('site-published')
ipcMain.removeAllListeners('remote-detect')
ipcMain.removeAllListeners('remote-detected')
ipcMain.on('site-publish', async (event: IpcMainEvent, params: any) => {
console.log(platform)
const client = ({
'github': deploy,
'coding': deploy,
'gitee': deploy,
'sftp': sftp,
'netlify': netlify,
} as any)[platform]
// render
renderer.db.themeConfig.domain = renderer.db.setting.domain
await renderer.renderAll()
// publish
const result = await client.publish()
event.sender.send('site-published', result)
})
ipcMain.on('remote-detect', async (event: IpcMainEvent, params: any) => {
const client = ({
'github': deploy,
'coding': deploy,
'gitee': deploy,
'sftp': sftp,
'netlify': netlify,
} as any)[platform]
const result = await client.remoteDetect()
event.sender.send('remote-detected', result)
})
}
}
================================================
FILE: src/server/events/index.ts
================================================
import SiteEvents from './site'
import PostEvents from './post'
import TagEvents from './tag'
import MenuEvents from './menu'
import ThemeEvents from './theme'
import RendererEvents from './renderer'
import SettingEvents from './setting'
import DeployEvents from './deploy'
export default {
SiteEvents,
PostEvents,
TagEvents,
MenuEvents,
ThemeEvents,
RendererEvents,
SettingEvents,
DeployEvents,
}
================================================
FILE: src/server/events/menu.ts
================================================
import { ipcMain, IpcMainEvent } from 'electron'
import Menus from '../menus'
import { IMenu } from '../interfaces/menu'
export default class MenuEvents {
constructor(appInstance: any) {
const menus = new Menus(appInstance)
ipcMain.removeAllListeners('menu-delete')
ipcMain.removeAllListeners('menu-deleted')
ipcMain.removeAllListeners('menu-save')
ipcMain.removeAllListeners('menu-saved')
ipcMain.removeAllListeners('menu-sort')
ipcMain.removeAllListeners('menu-sorted')
ipcMain.on('menu-delete', async (event: IpcMainEvent, menuName: string) => {
const data = await menus.deleteMenu(menuName)
event.sender.send('menu-deleted', data)
})
ipcMain.on('menu-save', async (event: IpcMainEvent, menu: IMenu) => {
const data = await menus.saveMenu(menu)
event.sender.send('menu-saved', data)
})
ipcMain.on('menu-sort', async (event: IpcMainEvent, menuList: IMenu[]) => {
const data = await menus.saveMenus(menuList)
event.sender.send('menu-sorted', data)
})
}
}
================================================
FILE: src/server/events/post.ts
================================================
import { ipcMain, IpcMainEvent } from 'electron'
import { IPost, IPostDb } from '../interfaces/post'
import Posts from '../posts'
export default class PostEvents {
constructor(appInstance: any) {
ipcMain.removeAllListeners('app-post-create')
ipcMain.removeAllListeners('app-post-created')
ipcMain.removeAllListeners('app-post-delete')
ipcMain.removeAllListeners('app-post-deleted')
ipcMain.removeAllListeners('app-post-list-delete')
ipcMain.removeAllListeners('app-post-list-deleted')
ipcMain.removeAllListeners('image-upload')
ipcMain.removeAllListeners('image-uploaded')
const posts = new Posts(appInstance)
ipcMain.on('app-post-create', async (event: IpcMainEvent, post: IPost) => {
const data = await posts.savePostToFile(post)
event.sender.send('app-post-created', data)
})
ipcMain.on('app-post-delete', async (event: IpcMainEvent, post: IPostDb) => {
const data = await posts.deletePost(post)
event.sender.send('app-post-deleted', data)
})
ipcMain.on('app-post-list-delete', async (event: IpcMainEvent, postList: IPostDb[]) => {
let data: any = false
for (const post of postList) {
data = posts.deletePost(post)
}
event.sender.send('app-post-list-deleted', data)
})
ipcMain.on('image-upload', async (event: IpcMainEvent, files: any[]) => {
console.log('执行了上传图片', files)
const data = await posts.uploadImages(files)
event.sender.send('image-uploaded', data)
})
}
}
================================================
FILE: src/server/events/renderer.ts
================================================
import { ipcMain, IpcMainEvent } from 'electron'
import Renderer from '../renderer'
export default class RendererEvents {
constructor(appInstance: any) {
const renderer = new Renderer(appInstance)
ipcMain.removeAllListeners('html-render')
ipcMain.removeAllListeners('html-rendered')
ipcMain.on('html-render', async (event: IpcMainEvent, params: any) => {
if (renderer.db.themeConfig.themeName) {
await renderer.preview()
}
event.sender.send('html-rendered', null)
})
}
}
================================================
FILE: src/server/events/setting.ts
================================================
import { ipcMain, IpcMainEvent } from 'electron'
import Setting from '../setting'
import { ICommentSetting } from '../interfaces/setting'
import { ISetting } from '../../interfaces/setting'
export default class SettingEvents {
constructor(appInstance: any) {
const settingInstance = new Setting(appInstance)
ipcMain.removeAllListeners('setting-save')
ipcMain.removeAllListeners('setting-saved')
ipcMain.removeAllListeners('comment-setting-save')
ipcMain.removeAllListeners('comment-setting-saved')
ipcMain.removeAllListeners('favicon-upload')
ipcMain.removeAllListeners('favicon-uploaded')
ipcMain.removeAllListeners('avatar-upload')
ipcMain.removeAllListeners('avatar-uploaded')
ipcMain.on('setting-save', async (event: IpcMainEvent, setting: ISetting) => {
const data = await settingInstance.saveSetting(setting)
event.sender.send('setting-saved', data)
})
ipcMain.on('comment-setting-save', async (event: IpcMainEvent, setting: ICommentSetting) => {
const data = await settingInstance.saveCommentSetting(setting)
event.sender.send('comment-setting-saved', data)
})
ipcMain.on('favicon-upload', async (event: IpcMainEvent, filePath: string) => {
console.log('执行了上传图片', filePath)
const data = await settingInstance.uploadFavicon(filePath)
event.sender.send('favicon-uploaded', data)
})
ipcMain.on('avatar-upload', async (event: IpcMainEvent, filePath: string) => {
console.log('执行了上传头像', filePath)
const data = await settingInstance.uploadAvatar(filePath)
event.sender.send('avatar-uploaded', data)
})
}
}
================================================
FILE: src/server/events/site.ts
================================================
import { ipcMain, IpcMainEvent } from 'electron'
export default class SiteEvents {
constructor(appInstance: any) {
/**
* load site config and data
*/
ipcMain.removeAllListeners('app-site-reload')
ipcMain.removeAllListeners('app-site-loaded')
ipcMain.removeAllListeners('app-source-folder-setting')
ipcMain.removeAllListeners('app-source-folder-set')
ipcMain.removeAllListeners('app-preview-server-port-get')
ipcMain.removeAllListeners('app-preview-server-port-got')
ipcMain.on('app-site-reload', async (event: IpcMainEvent, params: any) => {
const result = await appInstance.loadSite()
event.sender.send('app-site-loaded', result)
})
ipcMain.on('app-source-folder-setting', async (event: IpcMainEvent, params: string) => {
const result = await appInstance.saveSourceFolderSetting(params)
event.sender.send('app-source-folder-set', result)
})
ipcMain.on('app-preview-server-port-get', async (event: IpcMainEvent, params: string) => {
const port = await appInstance.previewServer.get('port')
event.sender.send('app-preview-server-port-got', port)
})
}
}
================================================
FILE: src/server/events/tag.ts
================================================
import { ipcMain, IpcMainEvent } from 'electron'
import Tags from '../tags'
import { ITag } from '../interfaces/tag'
export default class TagEvents {
constructor(appInstance: any) {
const tags = new Tags(appInstance)
ipcMain.removeAllListeners('tag-delete')
ipcMain.removeAllListeners('tag-deleted')
ipcMain.removeAllListeners('tag-save')
ipcMain.removeAllListeners('tag-saved')
ipcMain.on('tag-delete', async (event: IpcMainEvent, tagName: string) => {
const data = await tags.deleteTag(tagName)
event.sender.send('tag-deleted', data)
})
ipcMain.on('tag-save', async (event: IpcMainEvent, tag: ITag) => {
const data = await tags.saveTag(tag)
event.sender.send('tag-saved', data)
})
}
}
================================================
FILE: src/server/events/theme.ts
================================================
import { ipcMain, IpcMainEvent } from 'electron'
import { ITheme } from '../interfaces/theme'
import Theme from '../theme'
export default class ThemeEvents {
constructor(appInstance: any) {
const theme = new Theme(appInstance)
ipcMain.removeAllListeners('theme-save')
ipcMain.removeAllListeners('theme-saved')
ipcMain.removeAllListeners('theme-custom-config-save')
ipcMain.removeAllListeners('theme-custom-config-saved')
ipcMain.on('theme-save', async (event: IpcMainEvent, themeConfig: ITheme) => {
const config = await theme.saveThemeConfig(themeConfig)
event.sender.send('theme-saved', config)
})
ipcMain.on('theme-custom-config-save', async (event: IpcMainEvent, config: any) => {
const result = await theme.saveThemeCustomConfig(config)
event.sender.send('theme-custom-config-saved', result)
})
}
}
================================================
FILE: src/server/interfaces/application.ts
================================================
import { BrowserWindow } from 'electron'
import { IPostDb } from './post'
import { ITag } from './tag'
import { ITheme } from './theme'
import { IMenu } from './menu'
import { ICommentSetting } from './setting'
import { ISetting } from '../../interfaces/setting'
export interface IApplicationSetting {
mainWindow: BrowserWindow
app: any
baseDir: string
previewServer: any
}
export interface IApplicationDb {
posts: IPostDb[]
tags: ITag[]
menus: IMenu[]
themeConfig: ITheme
themeCustomConfig: any
themes: any[]
setting: ISetting
commentSetting: ICommentSetting
currentThemeConfig: any[]
}
export interface IApplication {
mainWindow: BrowserWindow
app: any
baseDir: string
appDir: string
buildDir: string
db: IApplicationDb
}
================================================
FILE: src/server/interfaces/menu.ts
================================================
export interface IMenu {
name: string
index?: number
openType: string
link: string
}
================================================
FILE: src/server/interfaces/post.ts
================================================
import { ITag } from './tag'
export interface IPost {
title: string
fileName: string
tags: string[]
date: string
content: string
published: boolean
hideInList: boolean
isTop: boolean
featureImage: {
name?: string,
path?: string,
type?: string,
}
/** 外链封面图 */
featureImagePath: string
deleteFileName?: string
}
export interface IPostData {
title: string
date: string
published: boolean
hideInList: boolean
isTop: boolean
tags?: []
feature: string
}
export interface IPostDb {
content: string
abstract: string,
data: IPostData
fileName: string
}
export interface ITagRenderData extends ITag {
link: string
}
export interface ISiteTagsData extends ITagRenderData {
count: number
}
export interface IStats {
text: string
minutes: number
time: number
words: number
}
export interface IPostRenderData {
content: string
fileName: string
abstract: string
title: string
tags: ITagRenderData[]
date: string
dateFormat: string
feature: string
link: string
hideInList: boolean
isTop: boolean
toc?: any
stats: IStats
nextPost?: IPostRenderData
prevPost?: IPostRenderData
description: string
}
================================================
FILE: src/server/interfaces/renderer.ts
================================================
export interface IPagination {
prev: string,
next: string,
}
================================================
FILE: src/server/interfaces/setting.ts
================================================
export interface IDisqusSetting {
api: string
apikey: string
shortname: string
}
export interface IGitalkSetting {
clientId: string
clientSecret: string
repository: string
owner: string
}
export interface ICommentSetting {
commentPlatform: string
showComment: boolean
disqusSetting: IDisqusSetting
gitalkSetting: IGitalkSetting
}
================================================
FILE: src/server/interfaces/tag.ts
================================================
export interface ITag {
name: string
used: boolean
slug?: string
index?: number
}
================================================
FILE: src/server/interfaces/theme.ts
================================================
export interface ITheme {
themeName: string
postPageSize: number
archivesPageSize: number
siteName: string
siteDescription: string
footerInfo: string
showFeatureImage: boolean
domain: string
postUrlFormat: string
tagUrlFormat: string
dateFormat: string
feedFullText: boolean
feedCount: number
archivesPath: string
postPath: string
tagPath: string
}
================================================
FILE: src/server/menus.ts
================================================
import Model from './model'
import { IMenu } from './interfaces/menu'
export default class Menus extends Model {
list() {
const menus = this.$posts.get('menus').value()
return menus
}
public async saveMenu(menu: IMenu) {
const menus = await this.$posts.get('menus').value()
if (typeof menu.index === 'number') {
const { index } = menu
delete menu.index
menus[index] = menu
} else {
delete menu.index
menus.push(menu)
}
await this.$posts.set('menus', menus).write()
return menus
}
public async saveMenus(menus: IMenu[]) {
await this.$posts.set('menus', menus).write()
return menus
}
public async deleteMenu(menuValue: string) {
const menu = await this.$posts.get('menus').remove({ name: menuValue }).write()
return menu
}
}
================================================
FILE: src/server/model.ts
================================================
import path from 'path'
import low from 'lowdb'
import FileSync from 'lowdb/adapters/FileSync'
import { BrowserWindow } from 'electron'
import { IApplicationDb, IApplication } from './interfaces/application'
export default class Model {
appDir: string
buildDir: string
$setting: any
$posts: any
$theme: any
db: IApplicationDb
mainWindow: BrowserWindow
constructor(appInstance: IApplication) {
this.appDir = appInstance.appDir
this.buildDir = appInstance.buildDir
this.db = appInstance.db
this.mainWindow = appInstance.mainWindow
this.initDataStore()
}
private initDataStore(): void {
const settingAdapter = new FileSync(path.join(this.appDir, 'config/setting.json'))
const setting = low(settingAdapter)
this.$setting = setting
const postsAdapter = new FileSync(path.join(this.appDir, 'config/posts.json'))
const posts = low(postsAdapter)
this.$posts = posts
const themeAdapter = new FileSync(path.join(this.appDir, 'config/theme.json'))
const theme = low(themeAdapter)
this.$theme = theme
}
}
================================================
FILE: src/server/plugins/deploys/gitproxy.ts
================================================
import { IApplicationDb } from '../../interfaces/application'
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent')
const get = require('simple-get')
export default class GitProxy {
db: IApplicationDb
constructor(appInstance: any) {
this.db = appInstance.db
// console.log('instance git proxy',this.db.setting)
}
public async request({
url,
method,
headers,
body,
}: {
url: any;
method: any;
headers: any;
body: any;
}) {
const { setting } = this.db
body = await this.mergeBuffers(body)
const proxy = url.startsWith('https:')
? { Agent: HttpsProxyAgent }
: { Agent: HttpProxyAgent }
const agent = setting.enabledProxy === 'proxy'
? new proxy.Agent({
proxy: `http://${setting.proxyPath}:${setting.proxyPort}`,
})
: undefined
// const agent = new proxy.Agent({ proxy: 'http://127.0.0.1:1081' })
return new Promise((resolve, reject) => get(
{
url,
method,
agent,
headers,
body,
},
(err: any, res: any) => (err ? reject(err) : resolve(this.transformResponse(res))),
))
}
private async mergeBuffers(data: any[] | Uint8Array) {
if (!Array.isArray(data)) return data
if (data.length === 1 && data[0] instanceof Buffer) return data[0]
const buffers = []
let offset = 0
let size = 0
for await (const chunk of data) {
buffers.push(chunk)
size += chunk.byteLength
}
data = new Uint8Array(size)
for (const buffer of buffers) {
data.set(buffer, offset)
offset += buffer.byteLength
}
return Buffer.from(data.buffer)
}
private async transformResponse(res: {
url: any;
method: any;
statusCode: any;
statusMessage: any;
headers: any;
}) {
const {
url, method, statusCode, statusMessage, headers,
} = res
return {
url,
method,
statusCode,
statusMessage,
headers,
body: res,
}
}
}
================================================
FILE: src/server/plugins/deploys/netlify.ts
================================================
import fs from 'fs'
import path from 'path'
import axios from 'axios'
import normalizePath from 'normalize-path'
import crypto from 'crypto'
import util from 'util'
import Model from '../../model'
import { IApplication } from '../../interfaces/application'
const asyncReadFile = util.promisify(fs.readFile)
export default class NetlifyApi extends Model {
private apiUrl: string
private accessToken: string
private siteId: string
private inputDir: string
constructor(appInstance: IApplication) {
super(appInstance)
this.apiUrl = 'https://api.netlify.com/api/v1/'
this.accessToken = appInstance.db.setting.netlifyAccessToken
this.siteId = appInstance.db.setting.netlifySiteId
this.inputDir = appInstance.buildDir
}
async request(method: 'GET' | 'PUT' | 'POST', endpoint: string, data?: any) {
const endpointUrl = this.apiUrl + endpoint.replace(':site_id', this.siteId)
const { setting } = this.db
const proxy = setting.enabledProxy === 'proxy' ? {
host: setting.proxyPath,
port: Number(setting.proxyPort),
} : undefined
return axios(
endpointUrl,
{
method,
headers: {
'User-Agent': 'Gridea',
'Authorization': `Bearer ${this.accessToken}`,
},
data,
proxy,
},
)
}
async remoteDetect() {
try {
const res = await this.request('GET', 'sites/:site_id/')
if (res.status === 200) {
return {
success: true,
message: res.data,
}
}
return {
success: false,
message: res.data,
}
} catch (e) {
return {
success: false,
message: e,
}
}
}
async publish() {
const result = {
success: true,
message: '同步成功',
data: null,
}
try {
const localFilesList = await this.prepareLocalFilesList()
const deployData = await this.request('POST', 'sites/:site_id/deploys', localFilesList)
const deployId = deployData.data.id
const hashOfFilesToUpload = deployData.data.required
const filesToUpload = this.getFilesToUpload(localFilesList, hashOfFilesToUpload)
for (let i = 0; i < filesToUpload.length; i += 1) {
const filePath = filesToUpload[i]
try {
// eslint-disable-next-line no-await-in-loop
const res = await this.uploadFile(filePath, deployId)
if (res.status === 422) {
return Promise.reject(res)
}
} catch (e) {
try {
// eslint-disable-next-line no-await-in-loop
const res = await this.uploadFile(filePath, deployId)
if (res.status === 422) {
return Promise.reject(res)
}
} catch (error) {
return Promise.reject(error)
}
}
}
return result
} catch (e) {
result.success = false
result.message = `[Server] 同步失败: ${e.message}`
}
}
async prepareLocalFilesList() {
const tempFileList: any = this.readDirRecursiveSync(this.inputDir)
const fileList: any = {}
for (const filePath of tempFileList) {
if (fs.lstatSync(path.join(this.inputDir, filePath)).isDirectory()) {
continue
}
// eslint-disable-next-line no-await-in-loop
const fileHash = await this.getFileHash(path.join(this.inputDir, filePath))
const fileKey = `/${filePath}`.replace(/\/\//gmi, '/')
fileList[fileKey] = fileHash
}
return Promise.resolve({ files: fileList })
}
readDirRecursiveSync(dir: string, fileList?: any) {
const files = fs.readdirSync(dir)
fileList = fileList || []
files.forEach((file) => {
if (this.fileIsDirectory(dir, file)) {
fileList = this.readDirRecursiveSync(path.join(dir, file), fileList)
return
}
if (this.fileIsNotExcluded(file)) {
fileList.push(this.getFilePath(dir, file))
}
})
return fileList
}
fileIsDirectory(dir: string, file: string) {
return fs.statSync(path.join(dir, file)).isDirectory()
}
fileIsNotExcluded(file: string) {
return file.indexOf('.') !== 0 || file === '.htaccess' || file === '_redirects'
}
getFilePath(dir: string, file: string, includeInputDir = false) {
if (!includeInputDir) {
dir = dir.replace(this.inputDir, '')
}
return normalizePath(path.join(dir, file))
}
getFileHash(fileName: string) {
return new Promise((resolve, reject) => {
const shaSumCalculator = crypto.createHash('sha1')
try {
const fileStream = fs.createReadStream(fileName)
fileStream.on('data', fileContentChunk => shaSumCalculator.update(fileContentChunk))
fileStream.on('end', () => resolve(shaSumCalculator.digest('hex')))
} catch (e) {
return reject(e)
}
})
}
getFilesToUpload(filesList: any, hashesToUpload: any) {
const filePaths = Object.keys(filesList.files)
const filesToUpload = []
const foundedHashes = []
for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i]
if (hashesToUpload.indexOf(filesList.files[filePath]) > -1) {
filesToUpload.push(filePath.replace(/\/\//gmi, '/'))
foundedHashes.push(filesList.files[filePath])
}
}
return filesToUpload
}
async uploadFile(filePath: any, deployID: any) {
const endpointUrl = `${this.apiUrl}deploys/${deployID}/files${filePath}`
const fullFilePath = this.getFilePath(this.inputDir, filePath, true)
const fileContent = await asyncReadFile(fullFilePath)
return axios(endpointUrl, {
method: 'PUT',
headers: {
'User-Agent': 'Gridea',
'Content-Type': 'application/octet-stream',
'Authorization': `Bearer ${this.accessToken}`,
},
data: fileContent,
})
}
}
================================================
FILE: src/server/plugins/deploys/sftp.ts
================================================
import * as fse from 'fs-extra'
// import * as fs from 'fs'
import path from 'path'
import SftpClient from 'ssh2-sftp-client'
import NodeSsh from 'node-ssh'
import normalizePath from 'normalize-path'
import Model from '../../model'
type sftpConnectConfig = {
host: string;
port: number;
type?: string;
username: string;
password?: string;
privateKey?: string | Buffer;
}
export default class SftpDeploy extends Model {
// connect: SftpClient
constructor(appInstance: any) {
super(appInstance)
// this.connect = new SftpClient()
console.log('instance sftp deploy')
}
async remoteDetect() {
const result = {
success: true,
message: '',
}
const client = new SftpClient()
const { setting } = this.db
const connectConfig: sftpConnectConfig = {
host: setting.server,
port: Number(setting.port),
username: setting.username,
}
if (setting.privateKey) {
try {
connectConfig.privateKey = fse.readFileSync(setting.privateKey)
} catch (e) {
console.error('SFTP Test Remote Error: ', e.message)
result.success = false
result.message = e.message
return result
}
} else {
connectConfig.password = setting.password
}
const testFilename = 'gridea.txt'
const localTestFilePath = normalizePath(path.join(this.appDir, testFilename))
const remoteTestFilePath = normalizePath(path.join(setting.remotePath, testFilename))
try {
await client.connect(connectConfig)
await client.list('/')
try {
fse.writeFileSync(localTestFilePath, 'This is gridea test file. you can delete it.')
await client.put(localTestFilePath, remoteTestFilePath)
await client.delete(remoteTestFilePath)
} catch (e) {
console.error('SFTP Test Remote Error: ', e.message)
result.success = false
result.message = e.message
} finally {
if (fse.existsSync(localTestFilePath)) {
fse.unlinkSync(localTestFilePath)
}
}
} catch (e) {
console.error('SFTP Test Remote Error: ', e.message)
result.success = false
result.message = e.message
} finally {
await client.end()
}
return result
}
async publish() {
const result = {
success: true,
message: '',
}
const client = new NodeSsh()
const { setting } = this.db
const connectConfig: sftpConnectConfig = {
host: setting.server,
port: Number(setting.port),
type: 'sftp',
username: setting.username,
}
// node-ssh: privateKey is path string.
if (setting.privateKey) {
connectConfig.privateKey = setting.privateKey
} else {
connectConfig.password = setting.password
}
const localPath = normalizePath(path.join(this.buildDir))
const remotePath = normalizePath(path.join(setting.remotePath))
try {
await client.connect(connectConfig)
try {
await client.exec(`rm -rf ${remotePath}`)
await client.mkdir(remotePath)
const res = await client.putDirectory(localPath, remotePath, {
recursive: true,
concurrency: 1, // 解决同步丢失js、css、图片文件问题
validate: function (itemPath: string) {
const baseName = path.basename(itemPath)
return baseName.substr(0, 1) !== '.' // do not allow dot files
&& baseName !== 'node_modules' // do not allow node_modules
},
})
} catch (e) {
console.error('SFTP Publish Error: ', e.message)
result.success = false
result.message = e.message
}
} catch (e) {
console.error('SFTP Publish Error: ', e.message)
result.success = false
result.message = e.message
} finally {
await client.dispose()
}
return result
}
}
================================================
FILE: src/server/plugins/markdown.ts
================================================
import MarkdownIt from 'markdown-it'
import MarkdownItKatex from '@iktakahiro/markdown-it-katex'
import markdownItTocAndAnchor from 'markdown-it-toc-and-anchor'
import MarkdownItTaskLists from 'markdown-it-task-lists'
import MarkdownItMark from 'markdown-it-mark'
import MarkdownItSup from 'markdown-it-sup'
import MarkdownItSub from 'markdown-it-sub'
import MarkdownItAbbr from 'markdown-it-abbr'
import MarkdownItFootnote from 'markdown-it-footnote'
import MarkdownItImsize from 'markdown-it-imsize'
import MarkdownItEmoji from 'markdown-it-emoji'
import MarkdownItImplicitFigures from 'markdown-it-implicit-figures'
import MarkdownItImageLazyLoading from 'markdown-it-image-lazy-loading'
const markdownIt = new MarkdownIt({
html: true,
breaks: true,
})
const BAD_PROTO_RE = /^(vbscript|javascript|data):/
const GOOD_DATA_RE = /^data:image\/(gif|png|jpeg|webp);/
markdownIt.validateLink = function (url) {
url = url.trim().toLowerCase()
return BAD_PROTO_RE.test(url) ? (!!GOOD_DATA_RE.test(url)) : true
}
markdownIt.use(MarkdownItKatex)
markdownIt.use(markdownItTocAndAnchor, {
anchorLink: false,
})
markdownIt.use(MarkdownItTaskLists, {
label: true,
labelAfter: true,
})
markdownIt.use(MarkdownItMark)
markdownIt.use(MarkdownItSup)
markdownIt.use(MarkdownItSub)
markdownIt.use(MarkdownItAbbr)
markdownIt.use(MarkdownItFootnote)
markdownIt.use(MarkdownItImsize)
markdownIt.use(MarkdownItEmoji)
markdownIt.use(MarkdownItImplicitFigures, {
dataType: true, //
, default: false
figcaption: false, // alternative text , default: false
tabindex: true, // ..., default: false
link: false, // , default: false
})
markdownIt.use(MarkdownItImageLazyLoading)
export default markdownIt
================================================
FILE: src/server/posts.ts
================================================
import * as fs from 'fs'
import * as fse from 'fs-extra'
import * as path from 'path'
import matter from 'gray-matter'
import moment from 'moment'
import Bluebird from 'bluebird'
import junk from 'junk'
import Model from './model'
import { IPost, IPostDb } from './interfaces/post'
import ContentHelper from '../helpers/content-helper'
import { formatYamlString } from '../helpers/utils'
Bluebird.promisifyAll(fs)
export default class Posts extends Model {
postDir: string
postImageDir: string
constructor(appInstance: any) {
super(appInstance)
this.postDir = path.join(this.appDir, 'posts')
this.postImageDir = `${this.appDir}/post-images`
}
public async savePosts() {
const resultList: any = []
const requestList: any = []
let files = await fse.readdir(this.postDir)
files = files.filter(junk.not)
files.forEach((item) => {
requestList.push(fs.readFileSync(path.join(this.postDir, item), 'utf8'))
})
const results = await Bluebird.all(requestList)
const fixedResults = JSON.parse(JSON.stringify(results))
/**
* The format of the correction `tag` is changed from a string to an array, and the article source file is updated. from v0.7.6
*/
await Promise.all(results.map(async (result: any, index: any) => {
const postMatter = matter(result)
const data = (postMatter.data as any)
data.title = formatYamlString(data.title)
if (data && data.date) {
if (typeof data.date === 'string') {
data.date = moment(data.date).format('YYYY-MM-DD HH:mm:ss')
} else {
data.date = moment(data.date).subtract(8, 'hours').format('YYYY-MM-DD HH:mm:ss')
}
}
// If there is a `tag` and it is of string type, it is corrected to array type.
if (data && typeof data.tags === 'string') {
const tagReg = /tags: [^\s[]/i
const newTagString = data.tags.split(' ').toString()
if (tagReg.test(result)) {
const mdStr = `---
title: '${data.title}'
date: ${data.date}
tags: [${newTagString}]
published: ${data.published || false}
hideInList: ${data.hideInList || false}
feature: ${data.feature || ''}
isTop: ${data.isTop || false}
---
${postMatter.content}`
fixedResults[index] = mdStr
fse.writeFileSync(`${this.postDir}/${files[index]}`, mdStr)
}
}
}))
fixedResults.forEach((result: any, index: any) => {
const postMatter = matter(result)
const data = (postMatter.data as any)
// Remove useless `'` in formatYamlString generate
if (data && data.title) {
data.title = String(data.title).replace(/''/g, '\'')
}
// Fix matter's formatted `date` problem
if (data && data.date) {
if (typeof data.date === 'string') {
data.date = moment(data.date).format('YYYY-MM-DD HH:mm:ss')
} else {
data.date = moment(data.date).subtract(8, 'hours').format('YYYY-MM-DD HH:mm:ss')
}
}
delete postMatter.orig // Remove orig
const post = {
...postMatter,
abstract: '',
fileName: '',
}
const moreReg = /\n\s*\s*\n/i
const matchMore = moreReg.exec(post.content)
if (matchMore) {
post.abstract = (post.content).substring(0, matchMore.index) // Abstract
}
post.fileName = files[index].substring(0, files[index].length - 3) // To be optimized!
resultList.push(post)
})
const list: any = []
resultList.forEach((item: any) => {
// Articles migrated from hexo or other platforms do not have a `published` field
if (item.data.published === undefined) {
item.data.published = false
}
// Articles migrated from other platforms or old articles do not have `hideInList` fields
if (item.data.hideInList === undefined) {
item.data.hideInList = false
}
// Articles migrated from other platforms or old articles do not have `isTop` fields
if (item.data.isTop === undefined) {
item.data.isTop = false
}
list.push(item)
})
list.sort((a: any, b: any) => moment(b.data.date).unix() - moment(a.data.date).unix())
this.$posts.set('posts', list).write()
return true
}
async list() {
await this.savePosts()
const posts = await this.$posts.get('posts').value()
const helper = new ContentHelper()
const list = posts.map((post: IPostDb) => {
const item = JSON.parse(JSON.stringify(post))
item.content = helper.changeImageUrlDomainToLocal(item.content, this.appDir)
item.data.feature = item.data.feature && !item.data.feature.includes('http')
? helper.changeFeatureImageUrlDomainToLocal(item.data.feature, this.appDir)
: item.data.feature
return item
})
return list
}
/**
* Save Post to file
* @param post
*/
async savePostToFile(post: IPost): Promise {
const helper = new ContentHelper()
const content = helper.changeImageUrlLocalToDomain(post.content, this.db.setting.domain)
const extendName = (post.featureImage.name || 'jpg').split('.').pop()
post.title = formatYamlString(post.title)
const mdStr = `---
title: '${post.title}'
date: ${post.date}
tags: [${post.tags.join(',')}]
published: ${post.published}
hideInList: ${post.hideInList}
feature: ${post.featureImage.name ? `/post-images/${post.fileName}.${extendName}` : post.featureImagePath}
isTop: ${post.isTop}
---
${content}`
try {
// If exist feature image
if (post.featureImage.path) {
const filePath = `${this.postImageDir}/${post.fileName}.${extendName}`
if (post.featureImage.path !== filePath) {
fse.copySync(post.featureImage.path, filePath)
// Clean the old file
if (post.featureImage.path.includes(this.postImageDir)) {
fse.removeSync(post.featureImage.path)
}
}
}
// Write file must use fse, beause fs.writeFile need callback
await fse.writeFile(`${this.postDir}/${post.fileName}.md`, mdStr)
// Clean the old file
if (post.deleteFileName) {
fse.removeSync(`${this.postDir}/${post.deleteFileName}.md`)
}
} catch (e) {
console.error('ERROR: ', e)
}
return post
}
async deletePost(post: IPostDb) {
try {
const postUrl = `${this.postDir}/${post.fileName}.md`
fse.removeSync(postUrl)
// Clean feature image
if (post.data.feature) {
fse.removeSync(post.data.feature.replace('file://', ''))
}
// Clean post content image
const imageReg = /(!\[.*?\]\()(.+?)(\))/g
const imageList = post.content.match(imageReg)
if (imageList) {
const postImagePaths = imageList.map((item: string) => {
const index = item.indexOf('(')
return item.substring(index + 1, item.length - 1)
})
postImagePaths.forEach(async (filePath: string) => {
fse.removeSync(filePath.replace('file://', ''))
})
}
return true
} catch (e) {
console.error('Delete Error', e)
return false
}
}
async uploadImages(files: any[]) {
await fse.ensureDir(this.postImageDir)
const results = []
for (const file of files) {
const extendName = file.name.split('.').pop()
const newFileName = new Date().getTime()
const filePath = `${this.postImageDir}/${newFileName}.${extendName}`
fse.copySync(file.path, filePath)
results.push(filePath)
}
return results
}
}
================================================
FILE: src/server/renderer.ts
================================================
import * as fs from 'fs'
import urlJoin from 'url-join'
import Bluebird from 'bluebird'
import * as fse from 'fs-extra'
import ejs from 'ejs'
import moment from 'moment'
import less from 'less'
import { Feed } from 'feed'
import junk from 'junk'
import { wordCount, timeCalc } from '../helpers/words-count'
import Model from './model'
import ContentHelper from '../helpers/content-helper'
import { formatThemeCustomConfigToRender } from '../helpers/utils'
import {
IPostDb, IPostRenderData, ITagRenderData, ISiteTagsData,
} from './interfaces/post'
import { ITag } from './interfaces/tag'
import { DEFAULT_POST_PAGE_SIZE, DEFAULT_ARCHIVES_PAGE_SIZE } from '../helpers/constants'
import markdown from './plugins/markdown'
import { IMenu } from './interfaces/menu'
Bluebird.promisifyAll(fs)
const helper = new ContentHelper()
export default class Renderer extends Model {
outputDir: string = this.buildDir
themePath: string = ''
postsData: IPostRenderData[] = []
tagsData: ISiteTagsData[] = []
menuData: IMenu[] = []
siteData: any = {}
previewPort: number
utils: any = {}
constructor(appInstance: any) {
super(appInstance)
this.previewPort = appInstance.previewServer.get('port')
this.loadConfig()
this.utils.now = Date.now()
this.utils.moment = moment
}
async preview() {
this.db.themeConfig.domain = `http://localhost:${this.previewPort}`
await this.renderAll()
}
async renderAll() {
await this.clearOutputFolder()
await this.formatDataForRender()
await this.buildCss()
// Render post list page
await this.renderPostList('')
// Render archives page
await this.renderPostList(urlJoin('/', this.db.themeConfig.archivesPath))
// Render tag list page
await this.renderTags()
await this.renderPostDetail()
await this.renderTagDetail()
// Need before `renderCustomPage`, because maybe theme custom page include a `404 page`
await this.copyFiles()
// Render custom page
await this.renderCustomPage()
await this.buildCname()
await this.buildFeed()
}
/**
* Load Config
*/
async loadConfig() {
this.themePath = urlJoin(this.appDir, 'themes', this.db.themeConfig.themeName)
fse.ensureDirSync(urlJoin(this.outputDir))
}
/**
* Format data for rendering pages
*/
public formatDataForRender(): any {
const { themeConfig } = this.db
this.postsData = this.db.posts.filter((item: IPostDb) => item.data.published)
.map((item: IPostDb) => {
const currentTags = item.data.tags || []
let toc = ''
const content = markdown.render(helper.changeImageUrlLocalToDomain(item.content, themeConfig.domain), {
tocCallback(tocMarkdown: any, tocArray: any, tocHtml: any) {
toc = tocHtml
},
})
let words = 0
wordCount(content, (count: number) => {
words = count
})
const reading = timeCalc(content)
const stats = {
text: `${reading.minius} min read`,
time: reading.second * 1000, // ms
words,
minutes: reading.minius,
}
const result: IPostRenderData = {
content,
fileName: item.fileName,
abstract: markdown.render(helper.changeImageUrlLocalToDomain(item.abstract, themeConfig.domain)),
title: item.data.title,
tags: this.db.tags
.filter((tag: ITag) => currentTags.find(i => i === tag.name))
.map((tag: ITag) => ({ ...tag, link: urlJoin(themeConfig.domain, themeConfig.tagPath, `${tag.slug}`, '/') })),
date: item.data.date,
dateFormat: (themeConfig.dateFormat && moment(item.data.date).format(themeConfig.dateFormat)) || item.data.date,
feature: item.data.feature && !item.data.feature.includes('http')
? `${helper.changeFeatureImageUrlLocalToDomain(item.data.feature, themeConfig.domain)}`
: item.data.feature || '',
link: urlJoin(themeConfig.domain, themeConfig.postPath, item.fileName, '/'),
hideInList: !!item.data.hideInList,
isTop: !!item.data.isTop,
stats,
description: `${content.replace(/<[^>]*>/g, '').substring(0, 120)}${content[121] ? '...' : ''}`,
}
result.toc = toc
return result
})
.sort((a: IPostRenderData, b: IPostRenderData) => moment(b.date).unix() - moment(a.date).unix())
this.tagsData = []
this.postsData.forEach((item: IPostRenderData) => {
if (!item.hideInList) {
item.tags.forEach((tag: ITagRenderData) => {
const foundTag = this.tagsData.find((t: ITagRenderData) => t.link === tag.link)
if (!foundTag) {
this.tagsData.push({
...tag,
count: 1,
})
} else {
foundTag.count += 1
}
})
}
})
this.menuData = this.db.menus.map((menu: IMenu) => {
let link = menu.link.replace(this.db.setting.domain, this.db.themeConfig.domain)
const isSiteLink = menu.link.includes(this.db.setting.domain)
if (isSiteLink) {
link = `${link}`
}
return {
...menu,
link,
}
})
this.siteData = {
posts: this.postsData,
tags: this.tagsData,
menus: this.menuData,
themeConfig: this.db.themeConfig,
customConfig: formatThemeCustomConfigToRender(this.db.themeCustomConfig, this.db.currentThemeConfig),
utils: this.utils,
isHomepage: false,
}
}
/**
* Render the article list, excluding hidden articles.
* if extraPath exist, render archive template, if not render index template
*/
public async renderPostList(archivePath: string) {
const { postPageSize, archivesPageSize, domain } = this.db.themeConfig
// Compatible: < v0.7.0
const pageSize = archivePath
? archivesPageSize || DEFAULT_ARCHIVES_PAGE_SIZE
: postPageSize || DEFAULT_POST_PAGE_SIZE
let excludeHidePostsData = this.postsData.filter((item: IPostRenderData) => !item.hideInList)
const renderTemplatePath = urlJoin(this.themePath, 'templates', `${archivePath ? 'archives.ejs' : 'index.ejs'}`)
// If it is not archives, sort by `isTop` then to render
if (!archivePath) {
const isTopPosts = excludeHidePostsData.filter((item: IPostRenderData) => item.isTop)
const notTopPosts = excludeHidePostsData.filter((item: IPostRenderData) => !item.isTop)
excludeHidePostsData = isTopPosts.concat(notTopPosts)
}
const renderData: any = {
menus: this.menuData,
posts: [],
pagination: {
prev: '',
next: '',
},
themeConfig: this.db.themeConfig,
site: this.siteData,
}
let html = ''
const outputFolder = urlJoin(this.outputDir, archivePath)
let renderPath = urlJoin(outputFolder, 'index.html')
const renderFile = async (path: string, data: any) => {
await ejs.renderFile(path, data, {}, async (err: any, str) => {
if (err) {
console.error('❌ Render post list error')
this.mainWindow.webContents.send('log-error', {
type: 'Render post list error',
message: err.message,
})
}
if (str) {
html = str
}
})
}
// If there is no article to render
if (!excludeHidePostsData.length) {
renderData.site.isHomepage = !archivePath
fse.ensureDirSync(outputFolder)
renderFile(renderTemplatePath, renderData)
await fs.writeFileSync(renderPath, html)
return
}
for (let i = 0; i * pageSize < excludeHidePostsData.length; i += 1) {
renderData.posts = excludeHidePostsData.slice(i * pageSize, (i + 1) * pageSize)
renderData.site.isHomepage = !archivePath && !i
if (i === 0 && excludeHidePostsData.length > pageSize) {
fse.ensureDirSync(urlJoin(this.outputDir, archivePath, 'page'))
renderData.pagination.next = urlJoin(domain, archivePath, 'page', '2')
} else if (i > 0 && excludeHidePostsData.length > pageSize) {
fse.ensureDirSync(urlJoin(this.outputDir, archivePath, 'page', `${i + 1}`))
renderPath = urlJoin(this.outputDir, archivePath, 'page', `${i + 1}`, 'index.html')
renderData.pagination.prev = i === 1
? urlJoin(domain, archivePath, '/')
: urlJoin(domain, archivePath, 'page', `${i}/`)
renderData.pagination.next = (i + 1) * pageSize < excludeHidePostsData.length
? urlJoin(domain, archivePath, 'page', `${i + 2}/`)
: ''
} else {
fse.ensureDirSync(urlJoin(this.outputDir, archivePath))
}
renderFile(renderTemplatePath, renderData)
console.log('👏 PostList Page:', renderPath)
fs.writeFileSync(renderPath, html)
}
}
/**
* Render the article details page, including hidden articles.
*/
async renderPostDetail() {
for (let i = 0; i < this.postsData.length; i += 1) {
const post: IPostRenderData = { ...this.postsData[i] }
if (!post.hideInList) {
if (i < this.postsData.length - 1) {
const nexPost = this.postsData.slice(i + 1, this.postsData.length).find((item: IPostRenderData) => !item.hideInList)
if (nexPost) {
post.nextPost = nexPost
}
}
if (i > 0) {
const prevPost = this.postsData.slice(0, i).reverse().find((item: IPostRenderData) => !item.hideInList)
if (prevPost) {
post.prevPost = prevPost
}
}
}
const renderData = {
menus: this.menuData,
post,
themeConfig: this.db.themeConfig,
commentSetting: this.db.commentSetting,
site: this.siteData,
}
let html = ''
ejs.renderFile(urlJoin(this.themePath, 'templates', 'post.ejs'), renderData, {}, async (err: any, str) => {
if (err) {
console.error('❌ Render post detail error')
this.mainWindow.webContents.send('log-error', {
type: 'Render post detail error',
message: err.message,
})
}
if (str) {
html = str
}
})
const renderFolerPath = urlJoin(this.outputDir, `${this.db.themeConfig.postPath}`, post.fileName)
fse.ensureDirSync(renderFolerPath)
fs.writeFileSync(urlJoin(renderFolerPath, 'index.html'), html)
}
}
/**
* Render tags page
*/
async renderTags() {
const tagsFolder = urlJoin(this.outputDir, 'tags')
const renderPath = urlJoin(tagsFolder, 'index.html')
const renderData = {
tags: this.tagsData,
menus: this.menuData,
themeConfig: this.db.themeConfig,
site: this.siteData,
}
let html = ''
fse.ensureDirSync(tagsFolder)
await ejs.renderFile(urlJoin(this.themePath, 'templates', 'tags.ejs'), renderData, {}, async (err: any, str) => {
if (err) {
console.log('❌ Render tags page error', err)
this.mainWindow.webContents.send('log-error', {
type: 'Render tags page error',
message: err.message,
})
}
if (str) {
html = str
}
})
console.log('👏 Tags Page:', renderPath)
fs.writeFileSync(renderPath, html)
}
/**
* Render tag detail page
*/
async renderTagDetail() {
const usedTags = this.db.tags.filter((tag: ITag) => tag.used)
const { postPageSize, domain, tagPath } = this.db.themeConfig
// Compatible: < v0.7.0
const pageSize = postPageSize || DEFAULT_POST_PAGE_SIZE
for (const usedTag of usedTags) {
const posts = this.postsData.filter((post: IPostRenderData) => {
return post.tags.find((tag: ITagRenderData) => tag.slug === usedTag.slug)
})
const currentTag = usedTag
const tagFolderPath = urlJoin(this.outputDir, tagPath, `${currentTag.slug}`)
const tagDomainPath = urlJoin(domain, tagPath, `${currentTag.slug}`)
fse.ensureDirSync(tagFolderPath)
for (let i = 0; i * pageSize < posts.length; i += 1) {
const renderData = {
tag: currentTag,
menus: this.menuData,
posts: posts.slice(i * pageSize, (i + 1) * pageSize),
pagination: {
prev: '',
next: '',
},
themeConfig: this.db.themeConfig,
site: this.siteData,
}
// Paging
let renderPath = urlJoin(tagFolderPath, 'index.html')
if (i === 0 && posts.length > pageSize) {
fse.ensureDirSync(urlJoin(tagFolderPath, 'page'))
renderData.pagination.next = urlJoin(tagDomainPath, 'page', '2')
} else if (i > 0 && posts.length > pageSize) {
fse.ensureDirSync(urlJoin(tagFolderPath, 'page', `${i + 1}`))
renderPath = urlJoin(tagFolderPath, 'page', `${i + 1}`, 'index.html')
renderData.pagination.prev = i === 1
? tagDomainPath
: urlJoin(tagDomainPath, 'page', `${i}/`)
renderData.pagination.next = (i + 1) * pageSize < posts.length
? urlJoin(tagDomainPath, 'page', `${i + 2}/`)
: ''
}
let html = ''
ejs.renderFile(urlJoin(this.themePath, 'templates', 'tag.ejs'), renderData, {}, async (err: any, str) => {
if (err) {
console.log('❌ Render tag detail error', err)
this.mainWindow.webContents.send('log-error', {
type: 'Render tag detail error',
message: err.message,
})
}
if (str) {
html = str
}
})
console.log('👏 Tag Page:', renderPath)
fs.writeFileSync(renderPath, html)
}
}
}
/**
* Render custom page, eg. friends.ejs, about.ejs, home.ejs, projects.ejs...
*/
async renderCustomPage() {
const files = fse.readdirSync(urlJoin(this.themePath, 'templates'), { withFileTypes: true })
const customTemplates = files
.filter(item => !item.isDirectory())
.map(item => item.name)
.filter(junk.not)
.filter((name: string) => {
return ![
'index.ejs',
'post.ejs',
'tag.ejs',
'tags.ejs',
'archives.ejs',
// 👇 Gridea protected word, because these filename is gridea folder's name
'images.ejs',
'media.ejs',
'post-images.ejs',
'styles.ejs',
'tag.ejs',
'tags.ejs',
].includes(name)
})
const renderData = {
menus: this.menuData,
themeConfig: this.db.themeConfig,
commentSetting: this.db.commentSetting,
site: this.siteData,
}
customTemplates.forEach(async (name: string) => {
let renderFolder = urlJoin(this.outputDir, name.substring(0, name.length - 4))
let renderPath = urlJoin(renderFolder, 'index.html')
let html = ''
if (name === '404.ejs') {
renderFolder = this.outputDir
renderPath = urlJoin(renderFolder, '404.html')
}
fse.ensureDirSync(renderFolder)
await ejs.renderFile(urlJoin(this.themePath, 'templates', name), renderData, async (err: any, str) => {
if (err) {
console.error('❌ Render custom page error', err)
this.mainWindow.webContents.send('log-error', {
type: 'Render custom page error',
message: err.message,
})
}
if (str) {
html = str
}
})
fse.writeFileSync(renderPath, html)
console.log('✅ Render custom page success', renderPath)
})
}
/**
* Build CSS and write file
*/
async buildCss() {
const lessFilePath = urlJoin(this.themePath, 'assets', 'styles', 'main.less')
const cssFolderPath = urlJoin(this.outputDir, 'styles')
fse.ensureDirSync(cssFolderPath)
const lessString = fs.readFileSync(lessFilePath, 'utf8')
return new Promise((resolve, reject) => {
less.render(lessString, { filename: lessFilePath }, async (err: any, cssString: Less.RenderOutput) => {
if (err) {
console.log(err)
reject(err)
}
let { css } = cssString
// if have override
const customConfig = this.db.themeCustomConfig
const currentThemePath = urlJoin(this.appDir, 'themes', this.db.themeConfig.themeName)
const styleOverridePath = urlJoin(currentThemePath, 'style-override.js')
const existOverrideFile = await fse.pathExists(styleOverridePath)
if (existOverrideFile) {
// clean cache
delete __non_webpack_require__.cache[__non_webpack_require__.resolve(styleOverridePath)]
const generateOverride = __non_webpack_require__(styleOverridePath)
const customCss = generateOverride(customConfig)
css += customCss
}
fs.writeFileSync(urlJoin(cssFolderPath, 'main.css'), css)
resolve(true)
})
})
}
/**
* Create CNAME file
*/
async buildCname() {
const cnamePath = urlJoin(this.outputDir, 'CNAME')
if (this.db.setting.cname) {
fs.writeFileSync(cnamePath, this.db.setting.cname)
} else {
fse.removeSync(cnamePath)
}
}
/**
* Build Feed
*/
async buildFeed() {
const DEFAULT_FEED_COUNT = 10
const feedFilename = 'atom.xml'
const { themeConfig } = this.db
const feed = new Feed({
title: themeConfig.siteName,
description: themeConfig.siteDescription,
id: themeConfig.domain,
link: themeConfig.domain,
image: urlJoin(themeConfig.domain, 'images', 'avatar.png'),
favicon: urlJoin(themeConfig.domain, 'favicon.ico'),
copyright: `All rights reserved ${(new Date()).getFullYear()}, ${themeConfig.siteName}`,
feedLinks: {
atom: urlJoin(themeConfig.domain, feedFilename),
},
})
const postsData = this.postsData
.filter((item: IPostRenderData) => !item.hideInList)
.slice(0, themeConfig.feedCount || DEFAULT_FEED_COUNT)
const feedFullText = (typeof themeConfig.feedFullText) === 'undefined' ? true : themeConfig.feedFullText
postsData.forEach((post: IPostRenderData) => {
feed.addItem({
title: post.title,
id: post.link,
link: post.link,
description: post.abstract,
content: feedFullText ? post.content : post.abstract,
image: post.feature,
date: new Date(post.date),
})
})
fs.writeFileSync(urlJoin(this.outputDir, feedFilename), feed.atom1())
}
/**
* Copy file to output folder
*/
async copyFiles() {
const postImageInputPath = urlJoin(this.appDir, 'post-images')
const postImageOutputPath = urlJoin(this.outputDir, 'post-images')
fse.ensureDirSync(postImageOutputPath)
fse.copySync(postImageInputPath, postImageOutputPath)
const imagesInputPath = urlJoin(this.appDir, 'images')
const imagesOutputPath = urlJoin(this.outputDir, 'images')
fse.ensureDirSync(imagesOutputPath)
fse.copySync(imagesInputPath, imagesOutputPath)
const mediaInputPath = urlJoin(this.themePath, 'assets', 'media')
const mediaOutputPath = urlJoin(this.outputDir, 'media')
fse.ensureDirSync(mediaInputPath)
fse.copySync(mediaInputPath, mediaOutputPath)
// Copy /static
const staticFilesPath = urlJoin(this.appDir, 'static')
if (fse.existsSync(staticFilesPath)) {
fse.copySync(staticFilesPath, this.outputDir)
}
// Copy favicon.ico
const faviconInputPath = urlJoin(this.appDir, 'favicon.ico')
if (fse.existsSync(faviconInputPath)) {
fse.copyFileSync(faviconInputPath, urlJoin(this.outputDir, 'favicon.ico'))
}
}
async clearOutputFolder() {
try {
fse.emptyDirSync(this.outputDir)
} catch (e) {
console.log('Delete file error', e)
}
}
}
================================================
FILE: src/server/setting.ts
================================================
import * as fse from 'fs-extra'
import * as path from 'path'
import Model from './model'
import { ICommentSetting } from './interfaces/setting'
import { ISetting } from '../interfaces/setting'
export default class Setting extends Model {
getSetting() {
const setting = this.$setting.get('config').value()
return setting
}
getGitalkSetting() {
const setting = this.$setting.get('gitalk').value()
return setting
}
getCommentSetting() {
const setting = this.$setting.get('comment').value()
return setting
}
public async saveSetting(setting: ISetting) {
await this.$setting.set('config', setting).write()
return true
}
public async saveCommentSetting(setting: ICommentSetting) {
await this.$setting.set('comment', setting).write()
return true
}
async uploadFavicon(filePath: string) {
const faviconPath = path.join(this.appDir, 'favicon.ico')
fse.copySync(filePath, faviconPath)
}
async uploadAvatar(filePath: string) {
const avatarPath = path.join(this.appDir, 'images/avatar.png')
fse.copySync(filePath, avatarPath)
}
}
================================================
FILE: src/server/tags.ts
================================================
import shortid from 'shortid'
import Model from './model'
import { ITag } from './interfaces/tag'
import slug from '../helpers/slug'
import { UrlFormats } from '../helpers/enums'
export default class Tags extends Model {
public async saveTags() {
const posts = this.$posts.get('posts').value()
let list: any = []
posts.forEach((post: any) => {
if (Array.isArray(post.data.tags)) {
list = list.concat(post.data.tags)
}
})
list = Array.from(new Set([...list]))
const themeConfig = await this.$theme.get('config').value()
const tagUrlFormat = themeConfig.tagUrlFormat || UrlFormats.Slug
let existUsedTags = this.$posts.get('tags').filter({ used: true }).value()
// If you delete an article after using a tag, there may be a tag unused state.
existUsedTags = existUsedTags.map((tag: ITag) => {
return {
...tag,
used: list.includes(tag.name),
}
})
const unusedTags = this.$posts.get('tags').filter({ used: false }).value()
// The tag of the imported article is newly used
const newUsedTags = list
.filter((item: any) => !existUsedTags.find((tag: ITag) => tag.name === item))
.map((item: any) => {
const foundItem = unusedTags.find((tag: ITag) => tag.name === item)
if (foundItem) {
// remove from unusedTags
const foundItemIndex = unusedTags.indexOf(foundItem)
unusedTags.splice(foundItemIndex, 1)
return {
...foundItem,
used: true,
}
}
return {
name: item,
slug: tagUrlFormat === UrlFormats.Slug ? slug(item) : shortid.generate(),
used: true,
}
})
const tags = [...newUsedTags, ...existUsedTags, ...unusedTags]
this.$posts.set('tags', tags).write()
}
async list() {
await this.saveTags()
const tags = await this.$posts.get('tags').value()
return tags
}
public async saveTag(tag: ITag) {
const tags = await this.$posts.get('tags').value()
if (typeof tag.index === 'number' && tag.index >= 0) {
tags[tag.index] = tag
} else {
tags.push(tag)
}
await this.$posts.set('tags', tags).write()
return tags
}
public async deleteTag(tagValue: string) {
const tag = await this.$posts.get('tags').remove({ name: tagValue }).write()
return tag
}
}
================================================
FILE: src/server/theme.ts
================================================
import * as path from 'path'
import * as fse from 'fs-extra'
import junk from 'junk'
import Model from './model'
import { ITheme } from './interfaces/theme'
export default class Theme extends Model {
themeDir: string
themeList: string[]
themeConfig: any
currentThemePath = ''
constructor(appInstance: any) {
super(appInstance)
this.themeDir = path.join(this.appDir, 'themes')
this.themeConfig = {}
this.themeList = []
}
/**
* Get the theme list
*/
async getThemeList() {
let themes = await fse.readdir(this.themeDir)
themes = themes.filter(junk.not)
const result = await Promise.all(themes.map(async (item: string) => {
const data = {
folder: item,
name: item,
version: '',
author: '',
repository: '',
}
const themeConfigPath = path.join(this.themeDir, item, 'config.json')
if (fse.existsSync(themeConfigPath)) {
const config = fse.readJSONSync(themeConfigPath)
data.name = config.name
data.version = config.version
data.author = config.repository
data.repository = config.repository
}
return data
}))
return result
}
/**
* Get the theme configuration
*/
async getThemeConfig() {
this.themeConfig = await this.$theme.get('config').value()
this.currentThemePath = path.join(this.appDir, 'themes', this.themeConfig.themeName)
return this.themeConfig
}
/**
* Save the theme configuration
*/
public async saveThemeConfig(themeConfig: ITheme) {
await this.$theme.set('config', themeConfig).write()
// If there is a backup of the custom configuration, copy the backup to the custom configuration
const themeConfigBackupPath = path.join(this.appDir, 'config', `theme.${themeConfig.themeName}.config.json`)
const existThemeConfigBackupFile = await fse.pathExists(themeConfigBackupPath)
if (existThemeConfigBackupFile) {
const config = fse.readJSONSync(themeConfigBackupPath)
await this.$theme.set('customConfig', config).write()
} else {
await this.$theme.set('customConfig', {}).write()
}
return themeConfig
}
/**
* Save the theme custom configuration
*/
public async saveThemeCustomConfig(config: any) {
if (Object.keys(config).length > 0) {
// Save the picture type configuration
const toPath = path.join(this.appDir, 'themes', this.db.themeConfig.themeName, 'assets', 'media', 'images')
const includedArrayTypeImages: string[] = []
for (const configItem of this.db.currentThemeConfig) {
const configValue = config[configItem.name]
// Picture upload config type data need to upload image to folder
if (configItem.type === 'picture-upload') {
if (
typeof configValue === 'string'
&& configValue !== configItem.value
&& !configValue.startsWith('/media/')
) {
const extendName = configValue.split('.').pop()
const fileName = `custom-${configItem.name}.${extendName}`
fse.ensureDirSync(toPath)
fse.copySync(configValue, path.join(toPath, fileName))
// Change value to finally value
config[configItem.name] = path.join('/', 'media', 'images', fileName)
} else if (typeof configValue === 'undefined' || configValue === configItem.value) {
const currentConfigValue = this.db.themeCustomConfig[configItem.name]
if (currentConfigValue && currentConfigValue !== configItem.value) {
const extendName = this.db.themeCustomConfig[configItem.name].split('.').pop()
const fileName = `custom-${configItem.name}.${extendName}`
fse.removeSync(path.join(toPath, fileName))
}
}
}
// Array config type data need to find image config to upload folder
if (configItem.type === 'array') {
for (let arrItemIndex = 0; arrItemIndex < configValue.length; arrItemIndex += 1) {
const foundConfigItem = this.db.currentThemeConfig.find((i: any) => i.name === configItem.name)
const arrayItemKeys = Object.keys(configValue[arrItemIndex])
for (let keyIndex = 0; keyIndex < arrayItemKeys.length; keyIndex += 1) {
const key = arrayItemKeys[keyIndex]
const foundPictureTypeField = foundConfigItem.arrayItems.find((i: any) => i.name === key && i.type === 'picture-upload')
if (foundPictureTypeField) {
const fieldValue = configValue[arrItemIndex][key]
if (
typeof fieldValue === 'string'
&& fieldValue !== foundPictureTypeField.value
&& !fieldValue.startsWith('/media/')
) {
const extendName = fieldValue.split('.').pop()
const fileName = `custom-array-${configItem.name}-${new Date().getTime()}-${key}.${extendName}`
fse.ensureDirSync(toPath)
fse.copySync(fieldValue, path.join(toPath, fileName))
// Change value to finally value
configValue[arrItemIndex][key] = path.join('/', 'media', 'images', fileName)
includedArrayTypeImages.push(configValue[arrItemIndex][key])
} else if (typeof fieldValue === 'undefined' || fieldValue === foundPictureTypeField.value) {
console.log('run...')
} else {
includedArrayTypeImages.push(fieldValue)
}
}
}
}
}
}
// Remove unused array type config images
const assetsFolderPath = path.join(this.appDir, 'themes', this.db.themeConfig.themeName, 'assets')
const imagesFolderPath = path.join(assetsFolderPath, 'media', 'images')
if (fse.existsSync(imagesFolderPath)) {
const files = await fse.readdirSync(imagesFolderPath, { withFileTypes: true })
const arrayTypeImages = files
.filter(item => !item.isDirectory())
.map(item => path.join('/', 'media', 'images', item.name))
.filter(item => item.includes('custom-array'))
arrayTypeImages.forEach((name: string) => {
if (!includedArrayTypeImages.includes(name)) {
fse.removeSync(path.join(assetsFolderPath, name))
}
})
}
}
await this.$theme.set('customConfig', config).write()
// Backup theme custom config
const themeConfigBackupPath = path.join(this.appDir, 'config', `theme.${this.db.themeConfig.themeName}.config.json`)
fse.writeJSONSync(themeConfigBackupPath, config)
return config
}
/**
* Get the theme custom configuration
*/
public async getThemeCustomConfig() {
const config = await this.$theme.get('customConfig').value()
return config
}
/**
* Get current theme custom configuration
*/
public async getCurrentThemeCustomConfig() {
const themeConfigPath = path.join(this.currentThemePath, 'config.json')
const existThemeConfigFile = await fse.pathExists(themeConfigPath)
if (existThemeConfigFile) {
const themeConfig = fse.readJSONSync(themeConfigPath)
if (themeConfig && themeConfig.customConfig) {
return themeConfig.customConfig
}
}
return []
}
}
================================================
FILE: src/server.ts
================================================
import express from 'express'
export default function initServer() {
const app = express()
let server: any = null
function listen(port: number) {
server = app.listen(port, 'localhost').on('error', (err: NodeJS.ErrnoException) => {
if (err) {
if (err.message === 'getaddrinfo ENOTFOUND localhost') {
// Fixed: 修复渲染服务在找不到 localhost 时崩溃问题
console.log('\x1B[31m%s\x1B[0m', 'Localhost is not found so that the preview server is not working. Please check your hosts file and then restart the application.')
} else {
console.log(`Preview server port ${port} is busy, trying with port ${port + 1}`)
listen(port + 1)
}
}
}).on('listening', () => {
app.set('port', port)
console.log(`Preview server is running on port : ${port}`)
})
}
listen(4000)
return {
server,
app,
}
}
================================================
FILE: src/shims-tsx.d.ts
================================================
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}
================================================
FILE: src/shims-vue.d.ts
================================================
declare module '*.vue' {
import { ComponentOptions } from 'vue'
export default Vue
}
declare module '*.json' {
const data: any
export default data
}
declare module 'vue2-transitions'
declare module '@iktakahiro/markdown-it-katex'
declare module 'markdown-it-toc-and-anchor'
declare module 'markdown-it-task-lists'
declare module 'markdown-it-abbr'
declare module 'markdown-it-footnote'
declare module 'markdown-it-mark'
declare module 'markdown-it-sub'
declare module 'markdown-it-sup'
declare module 'markdown-it-imsize'
declare module 'markdown-it-emoji'
declare module 'markdown-it-implicit-figures'
declare module 'markdown-it-image-lazy-loading'
declare module 'electron-google-analytics'
declare module 'macaddress'
declare module 'v-emoji-picker'
declare module 'vue-shortkey'
declare module 'easy-ftp'
declare module 'node-ssh'
declare module 'vuedraggable' {
export interface DraggedContext {
index: number;
futureIndex: number;
element: T;
}
export interface DropContext {
index: number;
component: Vue;
element: T;
}
export interface Rectangle {
top: number;
right: number;
bottom: number;
left: number;
width: number;
height: number;
}
export interface MoveEvent {
originalEvent: DragEvent;
dragged: Element;
draggedContext: DraggedContext;
draggedRect: Rectangle;
related: Element;
relatedContext: DropContext;
relatedRect: Rectangle;
from: Element;
to: Element;
willInsertAfter: boolean;
isTrusted: boolean;
}
const draggableComponent: ComponentOptions
export default draggableComponent
}
================================================
FILE: src/shims.d.ts
================================================
import { AllElectron } from 'electron'
declare module 'vue/types/vue' {
interface Vue {
readonly $electron: AllElectron,
$bus: any,
}
}
================================================
FILE: src/store/index.ts
================================================
import Vue from 'vue'
import Vuex from 'vuex'
import site from './modules/site'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
site,
},
strict: process.env.NODE_ENV !== 'production',
})
================================================
FILE: src/store/modules/site.ts
================================================
import { ActionTree, Module, MutationTree } from 'vuex'
import { IPost } from '../../interfaces/post'
import { ITag } from '../../interfaces/tag'
import { ITheme } from '../../interfaces/theme'
import { IMenu } from '../../interfaces/menu'
import { ISetting, ICommentSetting } from '../../interfaces/setting'
import {
DEFAULT_POST_PAGE_SIZE, DEFAULT_ARCHIVES_PAGE_SIZE, DEFAULT_FEED_COUNT, DEFAULT_ARCHIVES_PATH, DEFAULT_POST_PATH, DEFAULT_TAG_PATH,
} from '../../helpers/constants'
export interface Site {
appDir: string
config: any
posts: IPost[]
tags: ITag[]
menus: IMenu[]
themeConfig: ITheme
themeCustomConfig: any
currentThemeConfig: any
themes: string[]
setting: ISetting
commentSetting: ICommentSetting
}
const siteState: Site = {
appDir: '',
config: {},
posts: [],
tags: [],
menus: [],
themeConfig: {
themeName: '',
postPageSize: DEFAULT_POST_PAGE_SIZE,
archivesPageSize: DEFAULT_ARCHIVES_PAGE_SIZE,
siteName: '',
siteDescription: '',
footerInfo: 'Powered by Gridea',
showFeatureImage: true,
postUrlFormat: 'SLUG',
tagUrlFormat: 'SLUG',
dateFormat: 'YYYY-MM-DD',
feedCount: DEFAULT_FEED_COUNT,
feedFullText: true,
archivesPath: DEFAULT_ARCHIVES_PATH,
postPath: DEFAULT_POST_PATH,
tagPath: DEFAULT_TAG_PATH,
},
themeCustomConfig: {},
currentThemeConfig: {},
themes: [],
setting: {
platform: 'github',
domain: '',
repository: '',
branch: '',
username: '',
email: '',
tokenUsername: '',
token: '',
cname: '',
port: '22',
server: '',
password: '',
privateKey: '',
remotePath: '',
proxyPath: '',
proxyPort: '',
enabledProxy: 'direct',
netlifySiteId: '',
netlifyAccessToken: '',
},
commentSetting: {
showComment: false,
commentPlatform: 'gitalk',
gitalkSetting: {
clientId: '',
clientSecret: '',
repository: '',
owner: '',
},
disqusSetting: {
api: '',
apikey: '',
shortname: '',
},
},
}
const mutations: MutationTree = {
updateSite(state, siteData: Site) {
console.log('data', siteData)
state.appDir = siteData.appDir
state.posts = siteData.posts
state.tags = siteData.tags
state.menus = siteData.menus
state.config = siteData.config
state.themeConfig = siteData.themeConfig
state.themeConfig.postUrlFormat = siteData.themeConfig.postUrlFormat || 'SLUG'
state.themeConfig.tagUrlFormat = siteData.themeConfig.tagUrlFormat || 'SLUG'
state.themeConfig.dateFormat = siteData.themeConfig.dateFormat || 'YYYY-MM-DD'
state.themeConfig.postPageSize = siteData.themeConfig.postPageSize || DEFAULT_POST_PAGE_SIZE
state.themeConfig.archivesPageSize = siteData.themeConfig.archivesPageSize || DEFAULT_ARCHIVES_PAGE_SIZE
state.themeConfig.feedCount = siteData.themeConfig.feedCount || DEFAULT_FEED_COUNT
state.themeConfig.feedFullText = (typeof siteData.themeConfig.feedFullText) === 'undefined' ? true : siteData.themeConfig.feedFullText // from > 0.8.0
state.themes = siteData.themes
state.setting = siteData.setting
state.commentSetting = siteData.commentSetting
state.themeCustomConfig = siteData.themeCustomConfig
state.currentThemeConfig = siteData.currentThemeConfig
},
updatePosts(state, posts: IPost[]) {
state.posts = posts
},
}
const actions: ActionTree = {
updatePosts({ commit }, posts: IPost[]) {
commit('updatePosts', posts)
},
updateSite({ commit }, siteData: Site) {
console.log('siteData:', siteData)
commit('updateSite', siteData)
},
}
const module: Module = {
namespaced: true,
state: siteState,
mutations,
actions,
}
export default module
================================================
FILE: src/views/article/ArticleUpdate.vue
================================================
{{ form.title }}
{{ form.date.format(site.themeConfig.dateFormat) }}
{{ tag }}
{{$t('default')}}
{{$t('external')}}
{{$t('pathContainHttps')}}
{{ postStatusTip }}
================================================
FILE: src/views/article/Articles.vue
================================================
{}" :checked="selectedPost.includes(post)" @change="onSelectChange(post)">
{{ post.data.title }}
{{ post.data.published ? $t('published') : $t('draft') }}
{{ $moment(post.data.date).format('YYYY-MM-DD') }}
================================================
FILE: src/views/loading/Index.vue
================================================
{{ $t('inConfig') }}...
================================================
FILE: src/views/menu/Index.vue
================================================
{{ item }}
{{ item.text }}
{{ $t('cancel') }}
{{ $t('save') }}
================================================
FILE: src/views/setting/Index.vue
================================================
================================================
FILE: src/views/setting/includes/BasicSetting.vue
================================================
Github Pages
Netlify
Coding Pages
Gitee Pages
SFTP
https://
http://
如何配置?
Password
SSH Key
Direct
Proxy
{{ $t('testConnection') }}
{{ $t('save') }}
================================================
FILE: src/views/setting/includes/CommentSetting.vue
================================================
================================================
FILE: src/views/setting/includes/DisqusSetting.vue
================================================
================================================
FILE: src/views/setting/includes/GitalkSetting.vue
================================================
================================================
FILE: src/views/tags/Index.vue
================================================
{{ $t('cancel') }}
{{ $t('save') }}
================================================
FILE: src/views/theme/Index.vue
================================================
================================================
FILE: src/views/theme/includes/AvatarSetting.vue
================================================
================================================
FILE: src/views/theme/includes/BasicSetting.vue
================================================
{{ item.name }}
{{ item.version }}
{{ $t('htmlSupport') }}
{{ $t('htmlSupport') }}
{{ item.text }}
{{ item.text }}
{{$t('default')}}
{{$t('concise')}}
{{$t('default')}}
{{$t('concise')}}
{{$t('showFullText')}}
{{$t('showAbstractOnly')}}
================================================
FILE: src/views/theme/includes/CustomSetting.vue
================================================
{{ getPostTitleByLink(form[item.name]) }}
{{ option.label }}
{{ option.label }}
{{ getPostTitleByLink(configItem[field.name]) }}
{{ option.label }}
{{ option.label }}
{{ $t('noCustomConfigTip') }}
================================================
FILE: src/views/theme/includes/FaviconSetting.vue
================================================
================================================
FILE: src/vue-bus.ts
================================================
import _Vue from 'vue'
declare module 'vue/types/vue' {
interface Vue {
$bus: any
}
}
class VueBus {
static install(Vue: any, options: any) {
const bus = new Vue()
Vue.bus = bus
Vue.prototype.$bus = bus
}
}
// eslint-disable-next-line
if ('Vue' in window) {
_Vue.use(VueBus)
}
export default VueBus
================================================
FILE: tailwind.config.js
================================================
module.exports = {
prefix: '',
important: false,
separator: ':',
theme: {
screens: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
},
colors: {
transparent: 'transparent',
black: '#000',
white: '#fff',
gray: {
100: '#FAFAFA',
200: '#EAEAEA',
300: '#999',
400: '#888',
500: '#666',
600: '#444',
700: '#333',
800: '#111',
900: '#000',
},
red: {
100: '#fff5f5',
200: '#fed7d7',
300: '#feb2b2',
400: '#fc8181',
500: '#f56565',
600: '#e53e3e',
700: '#c53030',
800: '#9b2c2c',
900: '#742a2a',
},
orange: {
100: '#fffaf0',
200: '#feebc8',
300: '#fbd38d',
400: '#f6ad55',
500: '#ed8936',
600: '#dd6b20',
700: '#c05621',
800: '#9c4221',
900: '#7b341e',
},
yellow: {
100: '#fffff0',
200: '#fefcbf',
300: '#faf089',
400: '#f6e05e',
500: '#ecc94b',
600: '#d69e2e',
700: '#b7791f',
800: '#975a16',
900: '#744210',
},
green: {
100: '#f0fff4',
200: '#c6f6d5',
300: '#9ae6b4',
400: '#68d391',
500: '#48bb78',
600: '#38a169',
700: '#2f855a',
800: '#276749',
900: '#22543d',
},
teal: {
100: '#e6fffa',
200: '#b2f5ea',
300: '#81e6d9',
400: '#4fd1c5',
500: '#38b2ac',
600: '#319795',
700: '#2c7a7b',
800: '#285e61',
900: '#234e52',
},
blue: {
100: '#ebf8ff',
200: '#bee3f8',
300: '#90cdf4',
400: '#63b3ed',
500: '#4299e1',
600: '#3182ce',
700: '#2b6cb0',
800: '#2c5282',
900: '#2a4365',
},
indigo: {
100: '#ebf4ff',
200: '#c3dafe',
300: '#a3bffa',
400: '#7f9cf5',
500: '#667eea',
600: '#5a67d8',
700: '#4c51bf',
800: '#434190',
900: '#3c366b',
},
purple: {
100: '#faf5ff',
200: '#e9d8fd',
300: '#d6bcfa',
400: '#b794f4',
500: '#9f7aea',
600: '#805ad5',
700: '#6b46c1',
800: '#553c9a',
900: '#44337a',
},
pink: {
100: '#fff5f7',
200: '#fed7e2',
300: '#fbb6ce',
400: '#f687b3',
500: '#ed64a6',
600: '#d53f8c',
700: '#b83280',
800: '#97266d',
900: '#702459',
},
},
spacing: {
px: '1px',
'0': '0',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'20': '5rem',
'24': '6rem',
'32': '8rem',
'40': '10rem',
'48': '12rem',
'56': '14rem',
'64': '16rem',
},
backgroundColor: theme => theme('colors'),
backgroundPosition: {
bottom: 'bottom',
center: 'center',
left: 'left',
'left-bottom': 'left bottom',
'left-top': 'left top',
right: 'right',
'right-bottom': 'right bottom',
'right-top': 'right top',
top: 'top',
},
backgroundSize: {
auto: 'auto',
cover: 'cover',
contain: 'contain',
},
borderColor: theme => ({
...theme('colors'),
default: theme('colors.gray.300', 'currentColor'),
}),
borderRadius: {
none: '0',
sm: '0.125rem',
default: '0.25rem',
lg: '0.5rem',
full: '9999px',
},
borderWidth: {
default: '1px',
'0': '0',
'2': '2px',
'4': '4px',
'8': '8px',
},
boxShadow: {
default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
outline: '0 0 0 3px rgba(66, 153, 225, 0.5)',
none: 'none',
},
container: {},
cursor: {
auto: 'auto',
default: 'default',
pointer: 'pointer',
wait: 'wait',
text: 'text',
move: 'move',
'not-allowed': 'not-allowed',
},
fill: {
current: 'currentColor',
},
flex: {
'1': '1 1 0%',
auto: '1 1 auto',
initial: '0 1 auto',
none: 'none',
},
flexGrow: {
'0': '0',
default: '1',
},
flexShrink: {
'0': '0',
default: '1',
},
fontFamily: {
sans: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'"Noto Sans"',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
'"Noto Color Emoji"',
],
serif: [
'Georgia',
'Cambria',
'"Times New Roman"',
'Times',
'serif',
],
mono: [
'Menlo',
'Monaco',
'Consolas',
'"Liberation Mono"',
'"Courier New"',
'monospace',
],
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
'6xl': '4rem',
},
fontWeight: {
hairline: '100',
thin: '200',
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
black: '900',
},
height: theme => ({
auto: 'auto',
...theme('spacing'),
full: '100%',
screen: '100vh',
}),
inset: {
'0': '0',
auto: 'auto',
},
letterSpacing: {
tighter: '-0.05em',
tight: '-0.025em',
normal: '0',
wide: '0.025em',
wider: '0.05em',
widest: '0.1em',
},
lineHeight: {
none: '1',
tight: '1.25',
snug: '1.375',
normal: '1.5',
relaxed: '1.625',
loose: '2',
},
listStyleType: {
none: 'none',
disc: 'disc',
decimal: 'decimal',
},
margin: (theme, { negative }) => ({
auto: 'auto',
...theme('spacing'),
...negative(theme('spacing')),
}),
maxHeight: {
full: '100%',
screen: '100vh',
},
maxWidth: {
xs: '20rem',
sm: '24rem',
md: '28rem',
lg: '32rem',
xl: '36rem',
'2xl': '42rem',
'3xl': '48rem',
'4xl': '56rem',
'5xl': '64rem',
'6xl': '72rem',
full: '100%',
},
minHeight: {
'0': '0',
full: '100%',
screen: '100vh',
},
minWidth: {
'0': '0',
full: '100%',
},
objectPosition: {
bottom: 'bottom',
center: 'center',
left: 'left',
'left-bottom': 'left bottom',
'left-top': 'left top',
right: 'right',
'right-bottom': 'right bottom',
'right-top': 'right top',
top: 'top',
},
opacity: {
'0': '0',
'25': '0.25',
'50': '0.5',
'75': '0.75',
'100': '1',
},
order: {
first: '-9999',
last: '9999',
none: '0',
'1': '1',
'2': '2',
'3': '3',
'4': '4',
'5': '5',
'6': '6',
'7': '7',
'8': '8',
'9': '9',
'10': '10',
'11': '11',
'12': '12',
},
padding: theme => theme('spacing'),
placeholderColor: theme => theme('colors'),
stroke: {
current: 'currentColor',
},
textColor: theme => theme('colors'),
width: theme => ({
auto: 'auto',
...theme('spacing'),
'1/2': '50%',
'1/3': '33.333333%',
'2/3': '66.666667%',
'1/4': '25%',
'2/4': '50%',
'3/4': '75%',
'1/5': '20%',
'2/5': '40%',
'3/5': '60%',
'4/5': '80%',
'1/6': '16.666667%',
'2/6': '33.333333%',
'3/6': '50%',
'4/6': '66.666667%',
'5/6': '83.333333%',
'1/12': '8.333333%',
'2/12': '16.666667%',
'3/12': '25%',
'4/12': '33.333333%',
'5/12': '41.666667%',
'6/12': '50%',
'7/12': '58.333333%',
'8/12': '66.666667%',
'9/12': '75%',
'10/12': '83.333333%',
'11/12': '91.666667%',
full: '100%',
screen: '100vw',
}),
zIndex: {
auto: 'auto',
'0': '0',
'10': '10',
'20': '20',
'30': '30',
'40': '40',
'50': '50',
},
},
variants: {
accessibility: ['responsive', 'focus'],
alignContent: ['responsive'],
alignItems: ['responsive'],
alignSelf: ['responsive'],
appearance: ['responsive'],
backgroundAttachment: ['responsive'],
backgroundColor: ['responsive', 'hover', 'focus'],
backgroundPosition: ['responsive'],
backgroundRepeat: ['responsive'],
backgroundSize: ['responsive'],
borderCollapse: ['responsive'],
borderColor: ['responsive', 'hover', 'focus'],
borderRadius: ['responsive'],
borderStyle: ['responsive'],
borderWidth: ['responsive'],
boxShadow: ['responsive', 'hover', 'focus'],
cursor: ['responsive'],
display: ['responsive'],
fill: ['responsive'],
flex: ['responsive'],
flexDirection: ['responsive'],
flexGrow: ['responsive'],
flexShrink: ['responsive'],
flexWrap: ['responsive'],
float: ['responsive'],
fontFamily: ['responsive'],
fontSize: ['responsive'],
fontSmoothing: ['responsive'],
fontStyle: ['responsive'],
fontWeight: ['responsive', 'hover', 'focus'],
height: ['responsive'],
inset: ['responsive'],
justifyContent: ['responsive'],
letterSpacing: ['responsive'],
lineHeight: ['responsive'],
listStylePosition: ['responsive'],
listStyleType: ['responsive'],
margin: ['responsive'],
maxHeight: ['responsive'],
maxWidth: ['responsive'],
minHeight: ['responsive'],
minWidth: ['responsive'],
objectFit: ['responsive'],
objectPosition: ['responsive'],
opacity: ['responsive', 'hover', 'focus'],
order: ['responsive'],
outline: ['responsive', 'focus'],
overflow: ['responsive'],
padding: ['responsive'],
placeholderColor: ['responsive', 'focus'],
pointerEvents: ['responsive'],
position: ['responsive'],
resize: ['responsive'],
stroke: ['responsive'],
tableLayout: ['responsive'],
textAlign: ['responsive'],
textColor: ['responsive', 'hover', 'focus'],
textDecoration: ['responsive', 'hover', 'focus'],
textTransform: ['responsive'],
userSelect: ['responsive'],
verticalAlign: ['responsive'],
visibility: ['responsive'],
whitespace: ['responsive'],
width: ['responsive'],
wordBreak: ['responsive'],
zIndex: ['responsive'],
},
corePlugins: {},
plugins: [],
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"skipLibCheck": true,
"allowJs": true,
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
],
"*": ["node_modules/*"]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules",
"node_modules/gray-matter/gray-matter.d.ts"
]
}
================================================
FILE: vue.config.js
================================================
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
css: {
loaderOptions: {
less: {
import: [
resolve('src/assets/styles/var.less'),
],
modifyVars: {
'btn-height-base': '30px',
'input-height-base': '30px',
},
javascriptEnabled: true,
},
},
},
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
builderOptions: {
productName: 'Gridea',
win: {
icon: './public/app-icons/gridea.ico',
// target: [
// {
// target: 'nsis',
// arch: [
// 'ia32',
// 'x64',
// ],
// },
// ],
},
mac: {
icon: './public/app-icons/gridea.icns',
},
linux: {
icon: './public/app-icons/gridea.png',
target: [
{
target: 'AppImage',
},
{
target: 'deb',
},
{
target: 'snap',
},
],
},
asar: false,
nsis: {
oneClick: false, // 是否一键安装
allowElevation: true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
allowToChangeInstallationDirectory: true, // 允许修改安装目录
createDesktopShortcut: true, // 创建桌面图标
createStartMenuShortcut: true, // 创建开始菜单图标
shortcutName: 'Gridea', // 图标名称
},
publish: ['github'],
},
// mainProcessWatch: [
// 'src/server/**/*',
// ],
},
},
}