├── .github
└── dependabot.yml
├── .gitignore
├── .husky
└── _
│ ├── pre-commit
│ └── prepare-commit-msg
├── .prettierignore
├── .prettierrc.mjs
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── astro.config.mjs
├── bun.lock
├── eslint.config.cjs
├── lefthook.yml
├── package.json
├── public
├── avatar.png
├── fonts
│ └── hwmc.otf
├── images
│ └── page-meta
│ │ ├── en
│ │ ├── about.png
│ │ ├── archive.png
│ │ └── links.png
│ │ └── zh
│ │ ├── about.png
│ │ ├── archive.png
│ │ └── links.png
├── links
│ └── astro.svg
├── noise.png
└── preview.png
├── src
├── components
│ ├── astro
│ │ ├── comments.astro
│ │ ├── footer.astro
│ │ ├── header.astro
│ │ ├── intro.astro
│ │ ├── link-card.astro
│ │ ├── nav.astro
│ │ ├── post-list.astro
│ │ ├── recent-blogs.astro
│ │ ├── tag.astro
│ │ └── twikoo.astro
│ └── react
│ │ ├── language-toggle.tsx
│ │ ├── noise-background.tsx
│ │ └── theme-toggle.tsx
├── config
│ ├── en
│ │ ├── about.mdx
│ │ ├── intro.mdx
│ │ └── links.mdx
│ ├── index.ts
│ ├── links.ts
│ └── zh
│ │ ├── about.mdx
│ │ ├── intro.mdx
│ │ └── links.mdx
├── content.config.ts
├── content
│ └── posts
│ │ ├── en
│ │ ├── post-1.md
│ │ ├── post-2.md
│ │ ├── post-3.md
│ │ ├── post-4.md
│ │ ├── post-5.md
│ │ ├── post-6.md
│ │ ├── post-7.md
│ │ └── post-8.md
│ │ └── zh
│ │ ├── post-1.mdx
│ │ ├── post-2.md
│ │ ├── post-3.md
│ │ ├── post-4.md
│ │ ├── post-5.md
│ │ ├── post-6.md
│ │ ├── post-7.md
│ │ └── post-8.md
├── i18n
│ ├── ui.ts
│ └── utils.ts
├── layouts
│ ├── base.astro
│ └── main.astro
├── pages
│ ├── 404.astro
│ ├── [lang]
│ │ ├── about
│ │ │ └── index.astro
│ │ ├── archive
│ │ │ └── index.astro
│ │ ├── index.astro
│ │ ├── links
│ │ │ └── index.astro
│ │ ├── posts
│ │ │ └── [...slug].astro
│ │ ├── rss.xml.ts
│ │ └── tags
│ │ │ └── [tag].astro
│ ├── index.astro
│ ├── og
│ │ └── [...route].ts
│ └── rss.xml.ts
├── styles
│ ├── post.css
│ ├── post.scss
│ ├── tailwind.css
│ ├── twikoo.css
│ └── view-transition.css
├── types
│ └── twikoo.d.ts
└── utils
│ ├── index.ts
│ └── langs.ts
├── tailwind.config.mjs
└── tsconfig.json
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 |
4 | # generated types
5 | .astro/
6 |
7 | # dependencies
8 | node_modules/
9 |
10 | # logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
23 | # jetbrains setting folder
24 | .idea/
25 |
26 | # astro tmp pages
27 | astro_tmp_pages_*
28 |
--------------------------------------------------------------------------------
/.husky/_/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
4 | set -x
5 | fi
6 |
7 | if [ "$LEFTHOOK" = "0" ]; then
8 | exit 0
9 | fi
10 |
11 | call_lefthook()
12 | {
13 | if test -n "$LEFTHOOK_BIN"
14 | then
15 | "$LEFTHOOK_BIN" "$@"
16 | elif lefthook -h >/dev/null 2>&1
17 | then
18 | lefthook "$@"
19 | else
20 | dir="$(git rev-parse --show-toplevel)"
21 | osArch=$(uname | tr '[:upper:]' '[:lower:]')
22 | cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
23 | if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook"
24 | then
25 | "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@"
26 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook"
27 | then
28 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@"
29 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook"
30 | then
31 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@"
32 | elif test -f "$dir/node_modules/lefthook/bin/index.js"
33 | then
34 | "$dir/node_modules/lefthook/bin/index.js" "$@"
35 |
36 | elif bundle exec lefthook -h >/dev/null 2>&1
37 | then
38 | bundle exec lefthook "$@"
39 | elif yarn lefthook -h >/dev/null 2>&1
40 | then
41 | yarn lefthook "$@"
42 | elif pnpm lefthook -h >/dev/null 2>&1
43 | then
44 | pnpm lefthook "$@"
45 | elif swift package plugin lefthook >/dev/null 2>&1
46 | then
47 | swift package --disable-sandbox plugin lefthook "$@"
48 | elif command -v mint >/dev/null 2>&1
49 | then
50 | mint run csjones/lefthook-plugin "$@"
51 | else
52 | echo "Can't find lefthook in PATH"
53 | fi
54 | fi
55 | }
56 |
57 | call_lefthook run "pre-commit" "$@"
58 |
--------------------------------------------------------------------------------
/.husky/_/prepare-commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
4 | set -x
5 | fi
6 |
7 | if [ "$LEFTHOOK" = "0" ]; then
8 | exit 0
9 | fi
10 |
11 | call_lefthook()
12 | {
13 | if test -n "$LEFTHOOK_BIN"
14 | then
15 | "$LEFTHOOK_BIN" "$@"
16 | elif lefthook -h >/dev/null 2>&1
17 | then
18 | lefthook "$@"
19 | else
20 | dir="$(git rev-parse --show-toplevel)"
21 | osArch=$(uname | tr '[:upper:]' '[:lower:]')
22 | cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
23 | if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook"
24 | then
25 | "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@"
26 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook"
27 | then
28 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@"
29 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook"
30 | then
31 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@"
32 | elif test -f "$dir/node_modules/lefthook/bin/index.js"
33 | then
34 | "$dir/node_modules/lefthook/bin/index.js" "$@"
35 |
36 | elif bundle exec lefthook -h >/dev/null 2>&1
37 | then
38 | bundle exec lefthook "$@"
39 | elif yarn lefthook -h >/dev/null 2>&1
40 | then
41 | yarn lefthook "$@"
42 | elif pnpm lefthook -h >/dev/null 2>&1
43 | then
44 | pnpm lefthook "$@"
45 | elif swift package plugin lefthook >/dev/null 2>&1
46 | then
47 | swift package --disable-sandbox plugin lefthook "$@"
48 | elif command -v mint >/dev/null 2>&1
49 | then
50 | mint run csjones/lefthook-plugin "$@"
51 | else
52 | echo "Can't find lefthook in PATH"
53 | fi
54 | fi
55 | }
56 |
57 | call_lefthook run "prepare-commit-msg" "$@"
58 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # lockfiles
2 | package-lock.json
3 | pnpm-lock.yaml
4 |
5 | # dependencies
6 | node_modules
--------------------------------------------------------------------------------
/.prettierrc.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | export default {
3 | semi: false,
4 | singleQuote: false,
5 | trailingComma: "all",
6 | endOfLine: "lf",
7 | plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
8 | overrides: [
9 | {
10 | files: "*.astro",
11 | options: {
12 | parser: "astro",
13 | },
14 | },
15 | ],
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "astro-build.astro-vscode",
4 | "esbenp.prettier-vscode",
5 | "dbaeumer.vscode-eslint",
6 | "unifiedjs.vscode-mdx"
7 | ],
8 | "unwantedRecommendations": []
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "biome.enabled": false,
3 | "editor.formatOnSave": true,
4 | "editor.defaultFormatter": "esbenp.prettier-vscode",
5 | "[astro]": {
6 | "editor.defaultFormatter": "astro-build.astro-vscode"
7 | },
8 | "editor.codeActionsOnSave": {
9 | "source.fixAll.eslint": "explicit",
10 | "source.organizeImports": "always"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) sun0225SUN
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Astro Air
2 |
3 | A minimalism, personal blog theme for Astro.
4 |
5 | > If you find this project helpful, please consider giving it a star ⭐️
6 |
7 | [](https://astro.build) [](https://app.netlify.com/sites/astro-air/deploys)
8 |
9 |
10 |
11 | ## Showcase
12 |
13 | - [Astro Air](https://astro-air.guoqi.dev)
14 | - [Guoqi's blog](https://blog.sunguoqi.com)
15 | - ...
16 |
17 | > welcome to add your own blog to the list ❤️
18 |
19 | ## Features
20 |
21 | - [x] 🌓 Dark mode support
22 | - [x] 📱 Fully device responsive
23 | - [x] 🎨 Clean and minimalist design
24 | - [x] 📝 Markdown/MDX for content authoring
25 | - [x] 🏄♂️ SSG static rendering, SEO friendly
26 | - [x] 🌐 i18n support (EN/ZH)
27 | - [x] 🔗 Social media integration
28 | - [x] 📰 RSS feed & sitemap support
29 | - [x] 🛠️ Google analysis integration
30 | - [x] 💬 Commenting Integration (Twikoo)
31 | - [x] 🎨 Enhance Transition and Animation
32 | - [ ] 🔍 Local search functionality
33 | - [ ] ...and more
34 |
35 | ## Quick Start
36 |
37 | [](https://vercel.com/new/clone?repository-url=https://github.com/sun0225SUN/astro-air)
38 |
39 | [](https://app.netlify.com/start/deploy?repository=https://github.com/sun0225SUN/astro-air)
40 |
41 | ## Configuration
42 |
43 | - Open `src/config/index.ts` and customize your site settings
44 |
45 | - Open `src/config/links.ts` and customize your site links
46 |
47 | - Open `src/config/zh(en)/about.mdx(intro.mdx、links.mdx)` and customize your pages content
48 |
49 | ## Writing Content
50 |
51 | 1. Create new blog posts in the `src/content/posts/` directory
52 | 2. Use the following frontmatter template:
53 |
54 | ```markdown
55 | ---
56 | title: "Your Post Title"
57 | description: "A brief description of your post"
58 | pubDate: YYYY-MM-DD
59 | updatedDate(optional): YYYY-MM-DD
60 | tags(optional): ["tag1", "tag2"]
61 | ogImage(optional): "cover image URL"
62 | ---
63 |
64 | Your content here...
65 | ```
66 |
67 | ## Update Theme
68 |
69 | ```bash
70 | git remote add upstream https://github.com/sun0225SUN/astro-air
71 |
72 | git fetch upstream
73 |
74 | git merge upstream/main --allow-unrelated-histories
75 |
76 | ```
77 |
78 | ## Contributing
79 |
80 | Contributions are welcome! Feel free to:
81 |
82 | 1. Fork the repository
83 | 2. Create your feature branch
84 | 3. Submit a pull request
85 |
86 | ```bash
87 | git clone https://github.com/sun0225SUN/astro-air
88 |
89 | cd astro-air
90 |
91 | pnpm install
92 |
93 | pnpm dev
94 | ```
95 |
96 | ## License
97 |
98 | This project is licensed under the MIT License - see the LICENSE file for details.
99 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import mdx from "@astrojs/mdx"
2 | import react from "@astrojs/react"
3 | import sitemap from "@astrojs/sitemap"
4 | import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections"
5 | import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers"
6 | import tailwindcss from "@tailwindcss/vite"
7 | import expressiveCode from "astro-expressive-code"
8 | import { defineConfig } from "astro/config"
9 |
10 | import robotsTxt from "astro-robots-txt"
11 |
12 | // https://astro.build/config
13 | export default defineConfig({
14 | output: "static",
15 | prefetch: true,
16 | site: "https://astro-air.guoqi.dev",
17 | vite: {
18 | plugins: [tailwindcss()],
19 | },
20 | integrations: [
21 | react(),
22 | sitemap(),
23 | expressiveCode({
24 | plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
25 | themes: ["material-theme-lighter", "material-theme-darker"],
26 | defaultProps: {
27 | showLineNumbers: true,
28 | },
29 | }),
30 | mdx(),
31 | robotsTxt(),
32 | ],
33 | })
34 |
--------------------------------------------------------------------------------
/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | const eslintPluginAstro = require("eslint-plugin-astro")
2 | module.exports = [
3 | // add more generic rule sets here, such as:
4 | // js.configs.recommended,
5 | ...eslintPluginAstro.configs["flat/recommended"], // In CommonJS, the `flat/` prefix is required.
6 | {
7 | rules: {
8 | // override/add rules settings here, such as:
9 | // "astro/no-set-html-directive": "error"
10 | },
11 | },
12 | ]
13 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | commands:
3 | prettier-js:
4 | glob: "*.{js,jsx,ts,tsx,astro}"
5 | run: npx prettier --write {staged_files}
6 | eslint-fix:
7 | glob: "*.{js,jsx,ts,tsx,astro}"
8 | run: npx eslint --fix {staged_files}
9 | eslint:
10 | glob: "*.{js,jsx,ts,tsx,astro}"
11 | run: npx eslint {staged_files}
12 | prettier-other:
13 | glob: "*.{json,css,md}"
14 | run: npx prettier --write {staged_files}
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-air",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "build": "astro check && astro build",
8 | "preview": "astro preview",
9 | "astro": "astro",
10 | "format": "prettier --write ."
11 | },
12 | "dependencies": {
13 | "@astrojs/check": "^0.9.4",
14 | "@astrojs/mdx": "^4.0.7",
15 | "@astrojs/react": "^4.2.0",
16 | "@astrojs/rss": "^4.0.11",
17 | "@astrojs/sitemap": "^3.2.1",
18 | "@expressive-code/plugin-collapsible-sections": "^0.40.1",
19 | "@expressive-code/plugin-line-numbers": "^0.40.1",
20 | "@tailwindcss/vite": "^4.0.3",
21 | "@types/react": "^19.0.8",
22 | "@types/react-dom": "^19.0.3",
23 | "astro": "^5.2.3",
24 | "astro-expressive-code": "^0.40.1",
25 | "astro-google-analytics": "^1.0.3",
26 | "astro-og-canvas": "^0.5.6",
27 | "astro-robots-txt": "^1.0.0",
28 | "canvaskit-wasm": "^0.39.1",
29 | "lefthook": "^1.10.10",
30 | "lucide-react": "^0.479.0",
31 | "react": "^19.0.0",
32 | "react-dom": "^19.0.0",
33 | "tailwindcss": "^4.0.3",
34 | "twikoo": "^1.6.41",
35 | "typescript": "^5.7.3"
36 | },
37 | "devDependencies": {
38 | "@tailwindcss/typography": "^0.5.16",
39 | "@types/dom-view-transitions": "^1.0.5",
40 | "@types/node": "^22.13.1",
41 | "@types/sanitize-html": "^2.13.0",
42 | "@typescript-eslint/parser": "^8.23.0",
43 | "eslint": "^9.19.0",
44 | "eslint-plugin-astro": "^1.3.1",
45 | "eslint-plugin-jsx-a11y": "^6.10.2",
46 | "prettier": "^3.4.2",
47 | "prettier-plugin-astro": "^0.14.1",
48 | "prettier-plugin-tailwindcss": "^0.6.11",
49 | "sass": "^1.83.4"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/public/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/avatar.png
--------------------------------------------------------------------------------
/public/fonts/hwmc.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/fonts/hwmc.otf
--------------------------------------------------------------------------------
/public/images/page-meta/en/about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/images/page-meta/en/about.png
--------------------------------------------------------------------------------
/public/images/page-meta/en/archive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/images/page-meta/en/archive.png
--------------------------------------------------------------------------------
/public/images/page-meta/en/links.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/images/page-meta/en/links.png
--------------------------------------------------------------------------------
/public/images/page-meta/zh/about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/images/page-meta/zh/about.png
--------------------------------------------------------------------------------
/public/images/page-meta/zh/archive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/images/page-meta/zh/archive.png
--------------------------------------------------------------------------------
/public/images/page-meta/zh/links.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/images/page-meta/zh/links.png
--------------------------------------------------------------------------------
/public/links/astro.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/noise.png
--------------------------------------------------------------------------------
/public/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sun0225SUN/astro-air/1b4d159f996b155042ceb6dcd45030920f781538/public/preview.png
--------------------------------------------------------------------------------
/src/components/astro/comments.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Twikoo from "~/components/astro/twikoo.astro"
3 | import { common } from "~/config"
4 | ---
5 |
6 | {common.comments.enabled && common.comments.twikoo.enabled && }
7 |
--------------------------------------------------------------------------------
/src/components/astro/footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const currentYear = new Date().getFullYear()
3 | ---
4 |
5 |
34 |
--------------------------------------------------------------------------------
/src/components/astro/header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Rss } from "lucide-react"
3 | import { LanguageToggle } from "~/components/react/language-toggle"
4 | import { ThemeToggle } from "~/components/react/theme-toggle"
5 | import { en, zh } from "~/config"
6 | import { getLangFromUrl } from "~/i18n/utils"
7 |
8 | const lang = getLangFromUrl(Astro.url)
9 | const config = lang === "zh" ? zh : en
10 | ---
11 |
12 |
48 |
--------------------------------------------------------------------------------
/src/components/astro/intro.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import IntroContentEn from "~/config/en/intro.mdx"
3 | import IntroContentZh from "~/config/zh/intro.mdx"
4 | import { getLangFromUrl } from "~/i18n/utils"
5 |
6 | const lang = getLangFromUrl(Astro.url)
7 | const IntroContent = lang === "zh" ? IntroContentZh : IntroContentEn
8 | ---
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/astro/link-card.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | name: string
4 | link: string
5 | description: string
6 | avatar: string
7 | }
8 |
9 | const { name, link, description, avatar } = Astro.props
10 | ---
11 |
12 |
17 |
22 |
23 |
24 | {name}
25 |
26 |
29 | {description}
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/astro/nav.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { en, zh } from "~/config"
3 | import { getLangFromUrl, useTranslations } from "~/i18n/utils"
4 |
5 | const lang = getLangFromUrl(Astro.url)
6 | const t = useTranslations(lang)
7 |
8 | const { home, archive, custom, links, about } =
9 | lang === "zh" ? zh.navigation : en.navigation
10 | ---
11 |
12 |
82 |
--------------------------------------------------------------------------------
/src/components/astro/post-list.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | post: any
4 | lang: string
5 | dateFormat?: string
6 | dateWidth?: string
7 | }
8 |
9 | const { post, lang, dateFormat = "default", dateWidth = "w-36" } = Astro.props
10 | import { formatDate } from "~/utils"
11 | ---
12 |
13 |
17 |
20 |
21 |
22 | {post.data.title}
23 |
24 |
--------------------------------------------------------------------------------
/src/components/astro/recent-blogs.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { common } from "~/config"
3 | import { getLangFromUrl, useTranslations } from "~/i18n/utils"
4 | import { getPostsByLocale } from "~/utils"
5 | import PostList from "./post-list.astro"
6 |
7 | const lang = getLangFromUrl(Astro.url)
8 | const t = useTranslations(lang)
9 |
10 | const allPosts = await getPostsByLocale(lang)
11 | const posts = allPosts.slice(0, common.latestPosts)
12 | ---
13 |
14 | {t("blog.latest")}
15 | {
16 | posts.map((post: any) => (
17 |
23 | ))
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/astro/tag.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLangFromUrl, useTranslations } from "~/i18n/utils"
3 | import MainLayout from "~/layouts/main.astro"
4 | import PostList from "./post-list.astro"
5 |
6 | interface Props {
7 | posts: any[]
8 | tag: string
9 | }
10 |
11 | const { posts, tag } = Astro.props
12 |
13 | const lang = getLangFromUrl(Astro.url)
14 | const t = useTranslations(lang)
15 |
16 | const filteredPosts = posts.filter((post: any) => post.data.tags?.includes(tag))
17 | ---
18 |
19 |
20 | #{tag}
21 |
22 | {
23 | filteredPosts.length > 0 ? (
24 |
25 | {filteredPosts.map((post: any) => (
26 |
32 | ))}
33 |
34 | ) : (
35 | {t("tag.no_posts")}
36 | )
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/src/components/astro/twikoo.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/src/components/react/language-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { Languages } from "lucide-react"
2 |
3 | export function LanguageToggle() {
4 | const handleLanguageToggle = () => {
5 | const currentPath = window.location.pathname
6 |
7 | const newPath = currentPath.includes("/en")
8 | ? currentPath.replace("/en", "/zh")
9 | : currentPath.replace("/zh", "/en")
10 |
11 | window.location.href = newPath
12 | }
13 |
14 | return
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/react/noise-background.tsx:
--------------------------------------------------------------------------------
1 | export function NoiseBackground() {
2 | return (
3 |
4 | )
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/react/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { Moon, Sun } from "lucide-react"
2 |
3 | export function ThemeToggle() {
4 | const updateTheme = () => {
5 | const isDark = document.documentElement.classList.contains("dark")
6 | localStorage.setItem("theme", isDark ? "dark" : "light")
7 | document.documentElement.setAttribute(
8 | "data-theme",
9 | isDark ? "material-theme-darker" : "material-theme-lighter",
10 | )
11 | }
12 |
13 | const handleToggleClick = () => {
14 | const element = document.documentElement
15 |
16 | // if not supported, just toggle the theme
17 | if (!document.startViewTransition) {
18 | element.classList.toggle("dark")
19 | updateTheme()
20 | return
21 | }
22 |
23 | document.startViewTransition(() => {
24 | element.classList.toggle("dark")
25 | updateTheme()
26 | })
27 | }
28 |
29 | return (
30 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/config/en/about.mdx:
--------------------------------------------------------------------------------
1 | export const title = "Hello, I'm Guoqi Sun ~"
2 |
3 | {title}
4 |
5 |
9 |
13 |
--------------------------------------------------------------------------------
/src/config/en/intro.mdx:
--------------------------------------------------------------------------------
1 |
2 |
3 | 🖥️ Front-end developer (React)|📸 Photography enthusiast (Nikon)|🛸 Travel
4 | explorer (experiencer)|🚴 Cycling freestyler (Java SILURO6-TOP 6)|🍎
5 | Technology product enthusiast (Apple & Xiaomi)
6 |
7 |
💬 Try, fail, retry. That's the rhythm of growth.
8 |
9 |
--------------------------------------------------------------------------------
/src/config/en/links.mdx:
--------------------------------------------------------------------------------
1 | export const title = "My Link"
2 |
3 | {title}
4 |
5 | ```js
6 | name: "Guoqi Sun",
7 | description: "Try, fail, retry. That's the rhythm of growth.",
8 | link: "https://blog.sunguoqi.com",
9 | avatar: "https://assets.guoqi.dev/images/avatar.png",
10 | ```
11 |
12 |
13 | Want to add a friend link? Please leave a comment in the comment area below,
14 | and I will add it as soon as possible.
15 |
16 |
17 | ```js
18 | name: "your site name",
19 | description: "your site description",
20 | link: "your site link",
21 | avatar: "your site avatar",
22 | ```
23 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import { Github, Twitter } from "lucide-react"
2 |
3 | export const defaultLanguage: string = "en"
4 |
5 | export const common = {
6 | domain: "https://astro-air.guoqi.dev",
7 | meta: {
8 | favicon: "/avatar.png",
9 | url: "https://blog.sunguoqi.com",
10 | },
11 | googleAnalyticsId: "",
12 | social: [
13 | {
14 | icon: Twitter,
15 | label: "X",
16 | link: "https://x.com/sun0225SUN",
17 | },
18 | {
19 | icon: Github,
20 | label: "GitHub",
21 | link: "https://github.com/sun0225SUN",
22 | },
23 | ],
24 | rss: true,
25 | navigation: {
26 | home: true,
27 | archive: true,
28 | custom: [
29 | {
30 | label: "CamLife",
31 | link: "https://camlife.cn",
32 | },
33 | ],
34 | links: true,
35 | about: true,
36 | },
37 | latestPosts: 8,
38 | comments: {
39 | enabled: true,
40 | twikoo: {
41 | enabled: true,
42 | // replace with your own envId
43 | envId: import.meta.env.PUBLIC_TWIKOO_ENV_ID ?? "",
44 | },
45 | },
46 | }
47 |
48 | export const zh = {
49 | ...common,
50 | siteName: "小孙同学",
51 | meta: {
52 | ...common.meta,
53 | title: "小孙同学",
54 | slogan: "一个浪漫的理性主义者",
55 | description: "读书、摄影、编程、旅行",
56 | },
57 | navigation: {
58 | ...common.navigation,
59 | custom: [
60 | {
61 | label: "影集",
62 | link: "https://camlife.cn",
63 | },
64 | ],
65 | },
66 | pageMeta: {
67 | archive: {
68 | title: "归档",
69 | description: "小孙同学的所有文章",
70 | ogImage: "/images/page-meta/zh/archive.png",
71 | },
72 | links: {
73 | title: "朋友们",
74 | description: "小孙同学的和他朋友们",
75 | ogImage: "/images/page-meta/zh/links.png",
76 | },
77 | about: {
78 | title: "关于我",
79 | description: "小孙同学的自我介绍",
80 | ogImage: "/images/page-meta/zh/about.png",
81 | },
82 | },
83 | }
84 |
85 | export const en = {
86 | ...common,
87 | siteName: "Guoqi Sun",
88 | meta: {
89 | ...common.meta,
90 | title: "Guoqi Sun",
91 | slogan: "A Romantic Rationalist",
92 | description: "Reading, Photography, Programming, Traveling",
93 | },
94 | navigation: {
95 | ...common.navigation,
96 | custom: [
97 | {
98 | label: "CamLife",
99 | link: "https://camlife.cn",
100 | },
101 | ],
102 | },
103 | pageMeta: {
104 | archive: {
105 | title: "All Posts",
106 | description: "Here are Guoqi Sun's all posts",
107 | ogImage: "/images/page-meta/en/archive.png",
108 | },
109 | links: {
110 | title: "My Friends",
111 | description: "Here are Guoqi Sun's friends",
112 | ogImage: "/images/page-meta/en/links.png",
113 | },
114 | about: {
115 | title: "About Me",
116 | description: "Here is Guoqi Sun's self-introduction",
117 | ogImage: "/images/page-meta/en/about.png",
118 | },
119 | },
120 | }
121 |
--------------------------------------------------------------------------------
/src/config/links.ts:
--------------------------------------------------------------------------------
1 | export const links = [
2 | {
3 | name: "Astro",
4 | link: "https://astro.build",
5 | description: "The web framework for content-driven websites",
6 | avatar: "/links/astro.svg",
7 | },
8 | {
9 | name: "Guoqi Sun",
10 | link: "https://blog.sunguoqi.com",
11 | description: "Try, fail, retry. That's the rhythm of growth.",
12 | avatar: "https://assets.guoqi.dev/images/avatar.png",
13 | },
14 | ]
15 |
--------------------------------------------------------------------------------
/src/config/zh/about.mdx:
--------------------------------------------------------------------------------
1 | export const title = "你好,我是小孙同学~"
2 |
3 | {title}
4 |
5 |
9 |
13 |
--------------------------------------------------------------------------------
/src/config/zh/intro.mdx:
--------------------------------------------------------------------------------
1 |
2 |
3 | 🖥️ 前端小学生|📸 摄影爱好者|🛸 旅行探索家|🚴 骑行蹭风选手|🍎
4 | 科技产品发烧友
5 |
6 |
💬 路虽远行则将至,事虽难做则必成!
7 |
8 |
--------------------------------------------------------------------------------
/src/config/zh/links.mdx:
--------------------------------------------------------------------------------
1 | export const title = "我的链接"
2 |
3 | {title}
4 |
5 | ```json
6 | name: "Guoqi Sun"
7 | description: "Try, fail, retry. That's the rhythm of growth."
8 | link: "https://blog.sunguoqi.com"
9 | avatar: "https://assets.guoqi.dev/images/avatar.png"
10 | ```
11 |
12 |
13 | 想要添加友情链接?请按照如下格式在评论区留言,我会及时添加。
14 |
15 |
16 | ```json
17 | name: "your site name"
18 | description: "your site description"
19 | link: "your site link"
20 | avatar: "your site avatar"
21 | ```
22 |
--------------------------------------------------------------------------------
/src/content.config.ts:
--------------------------------------------------------------------------------
1 | import { glob } from "astro/loaders"
2 | import { defineCollection, z } from "astro:content"
3 |
4 | const postSchema = z.object({
5 | title: z.string(),
6 | description: z.string(),
7 | pubDate: z.coerce.date(),
8 | updatedDate: z.coerce.date().optional(),
9 | heroImage: z.string().optional(),
10 | ogImage: z.string().optional(),
11 | tags: z.array(z.string()).optional(),
12 | })
13 |
14 | const enPostsCollection = defineCollection({
15 | loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/posts/en" }),
16 | schema: postSchema,
17 | })
18 |
19 | const zhPostsCollection = defineCollection({
20 | loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/posts/zh" }),
21 | schema: postSchema,
22 | })
23 |
24 | export const collections = {
25 | enPosts: enPostsCollection,
26 | zhPosts: zhPostsCollection,
27 | }
28 |
--------------------------------------------------------------------------------
/src/content/posts/en/post-1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "My First Blog Post"
3 | pubDate: 2020-07-01
4 | description: "This is the first post of my new Astro blog."
5 | ogImage: "https://sunguoqi.com/me.png"
6 | author: "Astro Learner"
7 | image:
8 | url: "https://docs.astro.build/assets/rose.webp"
9 | alt: "The Astro logo on a dark background with a pink glow."
10 | tags: ["astro", "blogging", "learning-in-public"]
11 | ---
12 |
13 | Welcome to my _new blog_ about learning Astro! Here, I will share my learning journey as I build a new website.
14 |
15 | ## What I've accomplished
16 |
17 | 1. **Installing Astro**: First, I created a new Astro project and set up my online accounts.
18 |
19 | 2. **Making Pages**: I then learned how to make pages by creating new `.astro` files and placing them in the `src/pages/` folder.
20 |
21 | 3. **Making Blog Posts**: This is my first blog post! I now have Astro pages and Markdown posts!
22 |
23 | ## What's next
24 |
25 | I will finish the Astro tutorial, and then keep adding more posts. Watch this space for more to come.
26 |
--------------------------------------------------------------------------------
/src/content/posts/en/post-2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: My Second Blog Post
3 | author: Astro Learner
4 | description: "After learning some Astro, I couldn't stop!"
5 | image:
6 | url: "https://docs.astro.build/assets/arc.webp"
7 | alt: "The Astro logo on a dark background with a purple gradient arc."
8 | pubDate: 2020-07-08
9 | tags: ["astro", "blogging", "learning-in-public", "successes"]
10 | ---
11 |
12 | After a successful first week learning Astro, I decided to try some more. I wrote and imported a small component from memory!
13 |
--------------------------------------------------------------------------------
/src/content/posts/en/post-3.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: My Third Blog Post
3 | author: Astro Learner
4 | description: "I had some challenges, but asking in the community really helped!"
5 | image:
6 | url: "https://docs.astro.build/assets/rays.webp"
7 | alt: "The Astro logo on a dark background with rainbow rays."
8 | pubDate: 2021-07-15
9 | tags: ["astro", "learning-in-public", "setbacks", "community"]
10 | ---
11 |
12 | It wasn't always smooth sailing, but I'm enjoying building with Astro. And, the [Discord community](https://astro.build/chat) is really friendly and helpful!
13 |
--------------------------------------------------------------------------------
/src/content/posts/en/post-4.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Building a Personal Blog with Astro"
3 | author: Astro Learner
4 | description: "Sharing my experience and insights on building a personal blog with Astro"
5 | image:
6 | url: "https://docs.astro.build/assets/full-logo-light.png"
7 | alt: "Astro logo with full text"
8 | pubDate: 2022-07-22
9 | tags: ["astro", "blogging", "web-development", "tech-sharing"]
10 | ---
11 |
12 | Over the past few weeks, I've been building my personal blog using Astro. Astro is a modern static site generator that offers excellent performance and a great development experience.
13 |
14 | ## Why Astro?
15 |
16 | 1. **Lightning-fast page loads**: Astro uses innovative partial hydration techniques
17 | 2. **Great developer experience**: Supports components from React, Vue, Svelte, and more
18 | 3. **Powerful Markdown support**: Makes writing content a breeze
19 |
20 | ## Learnings from the Development Process
21 |
22 | Through this project, I've not only learned how to use Astro but also gained a deeper understanding of many modern frontend development concepts. I plan to continue improving this blog and adding more features.
23 |
--------------------------------------------------------------------------------
/src/content/posts/en/post-5.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Customizing Your Astro Blog Theme"
3 | author: Astro Learner
4 | description: "Learn how to customize your Astro blog theme with Tailwind CSS"
5 | image:
6 | url: "https://docs.astro.build/assets/rays.webp"
7 | alt: "Astro rays on a dark background"
8 | pubDate: 2022-07-29
9 | tags: ["astro", "css", "tailwind", "design"]
10 | ---
11 |
12 | Today I'll share how I customized my Astro blog theme using Tailwind CSS. The combination of Astro and Tailwind makes styling incredibly efficient.
13 |
14 | ## Key Customizations
15 |
16 | 1. **Color Scheme**: Created a custom dark/light mode theme
17 | 2. **Typography**: Set up a consistent type scale
18 | 3. **Layout**: Implemented responsive design patterns
19 | 4. **Components**: Styled reusable UI components
20 |
21 | ## Tips for Theme Development
22 |
23 | - Start with a clear design system
24 | - Use Tailwind's configuration file for custom values
25 | - Leverage CSS variables for dynamic theming
26 | - Test across different devices and screen sizes
27 |
--------------------------------------------------------------------------------
/src/content/posts/en/post-6.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Adding Dynamic Features to Static Astro Sites"
3 | author: Astro Learner
4 | description: "Exploring ways to add dynamic features to Astro's static pages"
5 | image:
6 | url: "https://docs.astro.build/assets/arc.webp"
7 | alt: "Astro arc on dark background"
8 | pubDate: 2023-08-05
9 | tags: ["astro", "javascript", "dynamic-content", "web-development"]
10 | ---
11 |
12 | While Astro excels at static site generation, sometimes we need dynamic features. Here's how to add interactivity without sacrificing performance.
13 |
14 | ## Dynamic Features Added
15 |
16 | 1. **Search Functionality**: Implemented client-side search
17 | 2. **Comment System**: Integrated with a third-party service
18 | 3. **Like Button**: Added interactive reactions
19 | 4. **View Counter**: Track page views
20 |
21 | ## Implementation Details
22 |
23 | The key is using Astro's partial hydration to only load JavaScript where needed. This keeps the site fast while adding necessary dynamic features.
24 |
--------------------------------------------------------------------------------
/src/content/posts/en/post-7.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Adding Internationalization to Astro"
3 | author: Astro Learner
4 | description: "A guide to implementing i18n in your Astro website"
5 | image:
6 | url: "https://docs.astro.build/assets/full-logo-light.png"
7 | alt: "Astro logo"
8 | pubDate: 2024-08-19
9 | tags: ["astro", "i18n", "localization", "web-development"]
10 | ---
11 |
12 | Making your website accessible to a global audience is important. Here's how to implement internationalization in Astro.
13 |
14 | ## Implementation Steps
15 |
16 | 1. **Setup**: Installing necessary i18n packages
17 | 2. **Content Structure**: Organizing content for multiple languages
18 | 3. **Language Switching**: Adding a language toggle
19 | 4. **URL Management**: Handling language-specific routes
20 |
21 | ## Best Practices
22 |
23 | - Keep translations in separate files
24 | - Use ISO language codes
25 | - Implement fallback languages
26 | - Consider right-to-left languages
27 |
--------------------------------------------------------------------------------
/src/content/posts/en/post-8.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Deploying Your Astro Site"
3 | author: Astro Learner
4 | description: "A comprehensive guide to deploying your Astro site to various platforms"
5 | image:
6 | url: "https://docs.astro.build/assets/arc.webp"
7 | alt: "Astro deployment illustration"
8 | pubDate: 2024-08-26
9 | tags: ["astro", "deployment", "hosting", "CI CD"]
10 | ---
11 |
12 | Ready to share your Astro site with the world? Let's explore different deployment options and best practices.
13 |
14 | ## Deployment Platforms
15 |
16 | 1. **Netlify**: Perfect for static sites
17 | 2. **Vercel**: Great for serverless functions
18 | 3. **GitHub Pages**: Ideal for project sites
19 | 4. **Self-hosted**: Maximum control
20 |
21 | ## Deployment Process
22 |
23 | We'll walk through the setup process for each platform, including configuration files, environment variables, and continuous deployment workflows.
24 |
--------------------------------------------------------------------------------
/src/content/posts/zh/post-1.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "我的第一篇博客文章"
3 | pubDate: 2020-07-01
4 | updatedDate: 2020-07-01
5 | description: "这是我 Astro 博客的第一篇文章。"
6 | author: "Astro 学习者"
7 | image:
8 | url: "https://docs.astro.build/assets/rose.webp"
9 | alt: "在黑色背景上带有粉色光晕的 Astro 标志。"
10 | tags: ["astro", "博客", "公开学习"]
11 | ---
12 |
13 | export const title = "mdx"
14 |
15 | ## {title}
16 |
17 | 欢迎来到我学习关于 Astro 的新博客!在这里,我将分享我建立新网站的学习历程。
18 |
19 | ## 我做了什么
20 |
21 | 1. **安装 Astro**:首先,我创建了一个新的 Astro 项目并设置好了我的在线账号。
22 |
23 | 2. **制作页面**:然后我学习了如何通过创建新的 `.astro` 文件并将它们保存在 `src/pages/` 文件夹里来制作页面。
24 |
25 | 3. **发表博客文章**:这是我的第一篇博客文章!我现在有用 Astro 编写的页面和用 Markdown 写的文章了!
26 |
27 | ## 下一步计划
28 |
29 | 我将完成 Astro 教程,然后继续编写更多内容。关注我以获取更多信息。
30 |
31 | ```python
32 | print("Hello, World!")
33 | ```
34 |
--------------------------------------------------------------------------------
/src/content/posts/zh/post-2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 我的第二篇博客文章
3 | author: Astro 学习者
4 | description: "在学习了一些 Astro 之后,我无法停止!"
5 | image:
6 | url: "https://docs.astro.build/assets/arc.webp"
7 | alt: "在深色背景上带有紫色渐变弧形的 Astro 标志。"
8 | pubDate: 2020-07-08
9 | tags: ["astro", "博客", "公开学习", "成功"]
10 | ---
11 |
12 | 在成功的第一周学习 Astro 之后,我决定再试试。我从记忆中编写并导入了一个小组件!
13 |
--------------------------------------------------------------------------------
/src/content/posts/zh/post-3.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 我的第三篇博客文章
3 | author: Astro 学习者
4 | description: "我遇到了一些挑战,但在社区中提问真的帮助了我!"
5 | image:
6 | url: "https://docs.astro.build/assets/rays.webp"
7 | alt: "在黑色背景上带有彩虹光线的 Astro 标志。"
8 | pubDate: 2021-07-15
9 | tags: ["astro", "公开学习", "挫折", "社区"]
10 | ---
11 |
12 | 事情并不总是一帆风顺,但我很享受使用 Astro 构建的过程。而且,[Discord 社区](https://astro.build/chat)真的很友好和乐于助人!
13 |
--------------------------------------------------------------------------------
/src/content/posts/zh/post-4.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "使用 Astro 构建个人博客"
3 | author: Astro 学习者
4 | description: "分享我使用 Astro 构建个人博客的经验和见解"
5 | image:
6 | url: "https://docs.astro.build/assets/full-logo-light.png"
7 | alt: "带有完整文本的 Astro 徽标"
8 | pubDate: 2022-07-22
9 | tags: ["astro", "博客", "网页开发", "技术分享"]
10 | ---
11 |
12 | 在过去的几周里,我一直在使用 Astro 构建我的个人博客。Astro 是一个现代静态网站生成器,提供出色的性能和良好的开发体验。
13 |
14 | ## 为什么选择 Astro?
15 |
16 | 1. **闪电般快速的页面加载**:Astro 使用创新的部分水合技术
17 | 2. **出色的开发者体验**:支持来自 React、Vue、Svelte 等的组件
18 | 3. **强大的 Markdown 支持**:让写作内容变得轻而易举
19 |
20 | ## 开发过程中的学习
21 |
22 | 通过这个项目,我不仅学会了如何使用 Astro,还对许多现代前端开发概念有了更深入的理解。我计划继续改进这个博客并添加更多功能。
23 |
--------------------------------------------------------------------------------
/src/content/posts/zh/post-5.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "自定义你的 Astro 博客主题"
3 | author: Astro 学习者
4 | description: "学习如何使用 Tailwind CSS 自定义你的 Astro 博客主题"
5 | image:
6 | url: "https://docs.astro.build/assets/rays.webp"
7 | alt: "黑色背景上的 Astro 光线"
8 | pubDate: 2022-07-29
9 | tags: ["astro", "css", "tailwind", "设计"]
10 | ---
11 |
12 | 今天我将分享如何使用 Tailwind CSS 自定义我的 Astro 博客主题。Astro 和 Tailwind 的结合使得样式设计变得非常高效。
13 |
14 | ## 关键自定义
15 |
16 | 1. **颜色方案**: 创建了一个自定义的暗/亮模式主题
17 | 2. **排版**: 设置了一致的字体比例
18 | 3. **布局**: 实现了响应式设计模式
19 | 4. **组件**: 样式化可重用的 UI 组件
20 |
21 | ## 主题开发提示
22 |
23 | - 从清晰的设计系统开始
24 | - 使用 Tailwind 的配置文件进行自定义值
25 | - 利用 CSS 变量进行动态主题
26 | - 在不同设备和屏幕尺寸上进行测试
27 |
--------------------------------------------------------------------------------
/src/content/posts/zh/post-6.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "为静态 Astro 网站添加动态功能"
3 | author: Astro Learner
4 | description: "探索为 Astro 的静态页面添加动态功能的方法"
5 | image:
6 | url: "https://docs.astro.build/assets/arc.webp"
7 | alt: "黑色背景上的 Astro 弧形图"
8 | pubDate: 2023-08-05
9 | tags: ["astro", "javascript", "动态内容", "网页开发"]
10 | ---
11 |
12 | 虽然 Astro 在静态网站生成方面表现出色,但有时我们需要动态功能。以下是如何在不牺牲性能的情况下添加交互性。
13 |
14 | ## 添加的动态功能
15 |
16 | 1. **搜索功能**:实现了客户端搜索
17 | 2. **评论系统**:与第三方服务集成
18 | 3. **点赞按钮**:添加了互动反应
19 | 4. **浏览计数器**:跟踪页面浏览量
20 |
21 | ## 实施细节
22 |
23 | 关键是使用 Astro 的部分水合,只在需要的地方加载 JavaScript。这使得网站保持快速,同时添加必要的动态功能。
24 |
--------------------------------------------------------------------------------
/src/content/posts/zh/post-7.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "为 Astro 添加国际化"
3 | author: Astro Learner
4 | description: "在您的 Astro 网站中实现 i18n 的指南"
5 | image:
6 | url: "https://docs.astro.build/assets/full-logo-light.png"
7 | alt: "Astro 标志"
8 | pubDate: 2024-08-19
9 | tags: ["astro", "i18n", "本地化", "网页开发"]
10 | ---
11 |
12 | 让您的网站能够接触到全球受众是很重要的。以下是如何在 Astro 中实现国际化的方法。
13 |
14 | ## 实施步骤
15 |
16 | 1. **设置**:安装必要的 i18n 包
17 | 2. **内容结构**:为多种语言组织内容
18 | 3. **语言切换**:添加语言切换功能
19 | 4. **URL 管理**:处理特定语言的路由
20 |
21 | ## 最佳实践
22 |
23 | - 将翻译保存在单独的文件中
24 | - 使用 ISO 语言代码
25 | - 实现后备语言
26 | - 考虑从右到左的语言
27 |
--------------------------------------------------------------------------------
/src/content/posts/zh/post-8.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "部署您的 Astro 网站"
3 | author: Astro 学习者
4 | description: "全面指南,帮助您将 Astro 网站部署到各种平台"
5 | image:
6 | url: "https://docs.astro.build/assets/arc.webp"
7 | alt: "Astro 部署插图"
8 | pubDate: 2024-08-26
9 | tags: ["astro", "i18n", "本地化", "网页开发"]
10 | ---
11 |
12 | 准备好与世界分享您的 Astro 网站了吗?让我们探索不同的部署选项和最佳实践。
13 |
14 | ## 部署平台
15 |
16 | 1. **Netlify**: 适合静态网站
17 | 2. **Vercel**: 适合无服务器函数
18 | 3. **GitHub Pages**: 理想的项目网站
19 | 4. **自托管**: 最大的控制权
20 |
21 | ## 部署过程
22 |
23 | 我们将逐步介绍每个平台的设置过程,包括配置文件、环境变量和持续部署工作流。
24 |
--------------------------------------------------------------------------------
/src/i18n/ui.ts:
--------------------------------------------------------------------------------
1 | export const languages = {
2 | en: "English",
3 | zh: "简体中文",
4 | }
5 |
6 | export const defaultLang = "en"
7 |
8 | export const langs = ["en", "zh"]
9 |
10 | export const ui = {
11 | en: {
12 | "nav.home": "Home",
13 | "nav.archive": "Archive",
14 | "nav.about": "About",
15 | "nav.links": "Links",
16 | "blog.latest": "Latest Posts",
17 | "archive.title": "All Posts",
18 | "links.title": "My Friends",
19 | "tag.title": "Tag:",
20 | "tag.no_posts": "No posts found for tag",
21 | },
22 | zh: {
23 | "nav.home": "首页",
24 | "nav.about": "关于",
25 | "nav.archive": "归档",
26 | "nav.links": "友链",
27 | "links.title": "朋友们",
28 | "blog.latest": "近期文章",
29 | "archive.title": "所有文章",
30 | "tag.title": "标签:",
31 | "tag.no_posts": "没有找到标签为的文章",
32 | },
33 | } as const
34 |
--------------------------------------------------------------------------------
/src/i18n/utils.ts:
--------------------------------------------------------------------------------
1 | import { defaultLang, ui } from "./ui"
2 |
3 | export function getLangFromUrl(url: URL) {
4 | const [, lang] = url.pathname.split("/")
5 | if (lang in ui) return lang as keyof typeof ui
6 | return defaultLang
7 | }
8 |
9 | export function useTranslations(lang: keyof typeof ui) {
10 | return function t(key: keyof (typeof ui)[typeof defaultLang]) {
11 | // @ts-ignore
12 | return ui[lang][key] || ui[defaultLang][key]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/layouts/base.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { NoiseBackground } from "~/components/react/noise-background"
3 | import { getLangFromUrl } from "~/i18n/utils"
4 | import { GoogleAnalytics } from "astro-google-analytics"
5 | import { en, zh } from "~/config"
6 | import { ClientRouter } from "astro:transitions"
7 |
8 | import "~/styles/tailwind.css"
9 | import "~/styles/view-transition.css"
10 |
11 | const lang = getLangFromUrl(Astro.url)
12 |
13 | const { title, description, ogImage } = Astro.props
14 |
15 | const ogImageURL = new URL(ogImage, Astro.site).href
16 | const permalink = new URL(Astro.url.pathname, Astro.site).href
17 |
18 | const config = lang === "zh" ? zh : en
19 | ---
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {
29 | title
30 | ? `${config.meta.title} - ${title}`
31 | : `${config.meta.title} - ${config.meta.slogan}`
32 | }
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
82 |
83 |
88 |
89 | {
90 | config.googleAnalyticsId && (
91 |
92 | )
93 | }
94 |
95 |
96 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/src/layouts/main.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Comments from "~/components/astro/comments.astro"
3 | import Header from "~/components/astro/header.astro"
4 | import Navigation from "~/components/astro/nav.astro"
5 | import BaseLayout from "~/layouts/base.astro"
6 |
7 | const { title, description, ogImage, needComment } = Astro.props
8 | const filename = Astro.url.pathname.split("/").filter(Boolean).pop() ?? ""
9 | const openGraphImage = !ogImage ? `/og/${filename}.png` : ogImage
10 | ---
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 | {needComment && }
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/pages/404.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import MainLayout from "~/layouts/main.astro"
3 | ---
4 |
5 |
6 |
7 |
8 |
404
9 |
10 |
11 |
Oops! Page not found
12 |
13 | The page you're looking for doesn't exist or has been moved.
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/pages/[lang]/about/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { en, zh } from "~/config"
3 | import AboutContentEn from "~/config/en/about.mdx"
4 | import AboutContentZh from "~/config/zh/about.mdx"
5 | import { getLangFromUrl } from "~/i18n/utils"
6 | import MainLayout from "~/layouts/main.astro"
7 | import { getLanguagePaths } from "~/utils/langs"
8 |
9 | export function getStaticPaths() {
10 | return getLanguagePaths()
11 | }
12 |
13 | const lang = getLangFromUrl(Astro.url)
14 | const pageMeta = lang === "zh" ? zh.pageMeta : en.pageMeta
15 | const AboutContent = lang === "zh" ? AboutContentZh : AboutContentEn
16 | ---
17 |
18 |
24 |
27 |
28 |
--------------------------------------------------------------------------------
/src/pages/[lang]/archive/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import PostList from "~/components/astro/post-list.astro"
3 | import { en, zh } from "~/config"
4 | import { getLangFromUrl } from "~/i18n/utils"
5 | import MainLayout from "~/layouts/main.astro"
6 | import { getPostsByLocale } from "~/utils"
7 | import { getLanguagePaths } from "~/utils/langs"
8 |
9 | const lang = getLangFromUrl(Astro.url)
10 | const pageMeta = lang === "zh" ? zh.pageMeta : en.pageMeta
11 |
12 | export function getStaticPaths() {
13 | return getLanguagePaths()
14 | }
15 |
16 | const posts = await getPostsByLocale(lang)
17 |
18 | const postsByYear = posts.reduce(
19 | (acc: Record, post: any) => {
20 | const year = new Date(post.data.pubDate).getFullYear().toString()
21 | if (!acc[year]) {
22 | acc[year] = []
23 | }
24 | acc[year].push(post)
25 | return acc
26 | },
27 | {} as Record,
28 | )
29 |
30 | const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a))
31 | ---
32 |
33 |
38 | {
39 | years.map((year) => (
40 |
41 |
{year}
42 | {postsByYear[year].map((post: any) => (
43 |
49 | ))}
50 |
51 | ))
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/src/pages/[lang]/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Footer from "~/components/astro/footer.astro"
3 | import Intro from "~/components/astro/intro.astro"
4 | import RecentBlogs from "~/components/astro/recent-blogs.astro"
5 | import MainLayout from "~/layouts/main.astro"
6 | import { getLanguagePaths } from "~/utils/langs"
7 |
8 | export function getStaticPaths() {
9 | return getLanguagePaths()
10 | }
11 | ---
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/pages/[lang]/links/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import LinkCard from "~/components/astro/link-card.astro"
3 | import { en, zh } from "~/config"
4 | import LinksContentEn from "~/config/en/links.mdx"
5 | import { links } from "~/config/links"
6 | import LinksContentZh from "~/config/zh/links.mdx"
7 | import { getLangFromUrl, useTranslations } from "~/i18n/utils"
8 | import MainLayout from "~/layouts/main.astro"
9 | import { getLanguagePaths } from "~/utils/langs"
10 |
11 | export function getStaticPaths() {
12 | return getLanguagePaths()
13 | }
14 |
15 | const lang = getLangFromUrl(Astro.url)
16 | const t = useTranslations(lang)
17 | const LinksContent = lang === "zh" ? LinksContentZh : LinksContentEn
18 | const pageMeta = lang === "zh" ? zh.pageMeta : en.pageMeta
19 | ---
20 |
21 |
27 | {t("links.title")}
28 |
29 |
30 | {links.map((link) => )}
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/pages/[lang]/posts/[...slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { render } from "astro:content"
3 | import Comments from "~/components/astro/comments.astro"
4 | import { langs } from "~/i18n/ui"
5 | import { getLangFromUrl } from "~/i18n/utils"
6 | import MainLayout from "~/layouts/main.astro"
7 | import "~/styles/post.css"
8 | import "~/styles/post.scss"
9 | import { formatDate, getPostsByLocale } from "~/utils"
10 |
11 | export async function getStaticPaths() {
12 | const allPaths = []
13 |
14 | for (const lang of langs) {
15 | const posts = await getPostsByLocale(lang)
16 | const paths = posts.map((post: any) => ({
17 | params: { lang, slug: post.id },
18 | props: { post },
19 | }))
20 | allPaths.push(...paths)
21 | }
22 |
23 | return allPaths
24 | }
25 |
26 | const lang = getLangFromUrl(Astro.url)
27 |
28 | const { post } = Astro.props
29 |
30 | const { Content } = await render(post)
31 | ---
32 |
33 |
34 |
35 |
36 |
{post.data.title}
37 |
38 | {formatDate(post.data.pubDate)}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {
48 | post.data.tags.map((tag: string) => (
49 |
50 | #{tag}
51 |
52 | ))
53 | }
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/pages/[lang]/rss.xml.ts:
--------------------------------------------------------------------------------
1 | import rss from "@astrojs/rss"
2 | import { en, zh } from "~/config"
3 | import { getPostsByLocale } from "~/utils"
4 | import { getLanguagePaths } from "~/utils/langs"
5 |
6 | export function getStaticPaths() {
7 | return getLanguagePaths()
8 | }
9 |
10 | export async function GET(request: { url: URL }) {
11 | const isEn = request.url.pathname.includes("en")
12 |
13 | const lang = isEn ? "en" : "zh"
14 | const config = isEn ? en : zh
15 |
16 | const posts = await getPostsByLocale(lang)
17 |
18 | return rss({
19 | title: config.meta.title,
20 | description: config.meta.description,
21 | site:
22 | process.env.NODE_ENV === "development"
23 | ? "http://localhost:4321"
24 | : config.meta.url,
25 | items: posts.map((post: any) => ({
26 | title: post.data.title,
27 | description: post.data.description,
28 | pubDate: post.data.pubDate,
29 | link: `/posts/${post.id}/`,
30 | content: post.rendered ? post.rendered.html : post.data.description,
31 | })),
32 | customData: "",
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/[lang]/tags/[tag].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import TagComponent from "~/components/astro/tag.astro"
3 | import { langs } from "~/i18n/ui"
4 | import { getPostsByLocale } from "~/utils"
5 |
6 | export interface Props {
7 | posts: any
8 | tag: string
9 | }
10 |
11 | export async function getStaticPaths() {
12 | const paths = await Promise.all(
13 | langs.map(async (lang) => {
14 | const posts = await getPostsByLocale(lang)
15 | const uniqueTags = [
16 | ...new Set(posts.flatMap((post: any) => post.data.tags || [])),
17 | ]
18 |
19 | return uniqueTags.map((tag) => ({
20 | params: { tag, lang },
21 | props: {
22 | posts,
23 | tag,
24 | },
25 | }))
26 | }),
27 | )
28 |
29 | return paths.flat()
30 | }
31 | ---
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { defaultLanguage } from "~/config"
3 | ---
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/pages/og/[...route].ts:
--------------------------------------------------------------------------------
1 | import { OGImageRoute } from "astro-og-canvas"
2 | import { defaultLanguage } from "~/config"
3 | import { getPostsByLocale } from "~/utils"
4 |
5 | const posts = await getPostsByLocale(defaultLanguage)
6 |
7 | // Transform the collection into an object
8 | // @ts-ignore
9 | const pages = Object.fromEntries(posts.map(({ id, data }) => [id, { data }]))
10 |
11 | export const { getStaticPaths, GET } = OGImageRoute({
12 | // The name of your dynamic route segment.
13 | // In this case it’s `route`, because the file is named `[...route].ts`.
14 | param: "route",
15 |
16 | // A collection of pages to generate images for.
17 | pages,
18 | // For each page, this callback will be used to customize the OG image.
19 | getImageOptions: async (_, { data }: (typeof pages)[string]) => {
20 | return {
21 | title: data.title,
22 | description: data.description,
23 | bgGradient: [
24 | [6, 38, 45],
25 | [8, 3, 2],
26 | ],
27 | logo: {
28 | path: "./public/avatar.png",
29 | size: [100],
30 | },
31 | fonts: ["./public/fonts/hwmc.otf"],
32 | }
33 | },
34 | })
35 |
--------------------------------------------------------------------------------
/src/pages/rss.xml.ts:
--------------------------------------------------------------------------------
1 | import rss from "@astrojs/rss"
2 | import { defaultLanguage, en, zh } from "~/config"
3 | import { getPostsByLocale } from "~/utils"
4 |
5 | export async function GET() {
6 | const posts = await getPostsByLocale(defaultLanguage)
7 | const config = defaultLanguage === "en" ? en : zh
8 |
9 | return rss({
10 | title: config.meta.title,
11 | description: config.meta.description,
12 | site:
13 | process.env.NODE_ENV === "development"
14 | ? "http://localhost:4321"
15 | : config.meta.url,
16 | items: posts.map((post: any) => ({
17 | title: post.data.title,
18 | description: post.data.description,
19 | pubDate: post.data.pubDate,
20 | link: `/posts/${post.id}/`,
21 | content: post.rendered ? post.rendered.html : post.data.description,
22 | })),
23 | customData: "",
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/styles/post.css:
--------------------------------------------------------------------------------
1 | article code {
2 | border-radius: 0.25rem;
3 | background-color: rgba(249, 115, 22, 0.5);
4 | padding-left: 0.25rem;
5 | padding-right: 0.25rem;
6 | }
7 |
8 | .dark article code {
9 | background-color: rgba(249, 115, 22, 0.8);
10 | }
11 |
12 | article code::before,
13 | article code::after {
14 | content: none !important;
15 | }
16 |
--------------------------------------------------------------------------------
/src/styles/post.scss:
--------------------------------------------------------------------------------
1 | article {
2 | h2 {
3 | margin: 2rem 0 !important;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @plugin "@tailwindcss/typography";
3 | @custom-variant dark (&:where(.dark, .dark *));
4 |
--------------------------------------------------------------------------------
/src/styles/twikoo.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Twikoo v1.6.40
3 | * (c) 2020-2024 iMaeGoo
4 | * Released under the MIT License.
5 | * Last Update: 11/17/2024, 2:31:23 AM
6 | */
7 |
8 | .tk-avatar {
9 | flex-shrink: 0;
10 | height: 2.5rem;
11 | width: 2.5rem;
12 | overflow: hidden;
13 | text-align: center;
14 | border-radius: 5px;
15 | margin-right: 1rem;
16 | }
17 | .tk-comment .tk-submit .tk-avatar,
18 | .tk-replies .tk-avatar {
19 | height: 1.6rem;
20 | width: 1.6rem;
21 | }
22 | .tk-avatar.tk-has-avatar {
23 | background-color: rgba(144, 147, 153, 0.13);
24 | }
25 | .tk-avatar.tk-clickable {
26 | cursor: pointer;
27 | }
28 | .tk-avatar .tk-avatar-img {
29 | height: 2.5rem;
30 | color: #c0c4cc;
31 | }
32 | .tk-comment .tk-submit .tk-avatar .tk-avatar-img,
33 | .tk-replies .tk-avatar .tk-avatar-img {
34 | height: 1.6rem;
35 | }
36 |
37 | .tk-meta-input {
38 | display: flex;
39 | }
40 | .tk-meta-input .el-input {
41 | width: auto;
42 | width: calc((100% - 1rem) / 3); /* Fix Safari */
43 | flex: 1;
44 | }
45 | .tk-meta-input .el-input + .el-input {
46 | margin-left: 0.5rem;
47 | }
48 | .tk-meta-input .el-input .el-input-group__prepend {
49 | padding: 0 1rem;
50 | }
51 | .tk-meta-input .el-input input:invalid {
52 | border: 1px solid #f56c6c;
53 | box-shadow: none;
54 | }
55 | @media screen and (max-width: 767px) {
56 | .tk-meta-input {
57 | flex-direction: column;
58 | }
59 | .tk-meta-input .el-input {
60 | width: auto;
61 | }
62 | .tk-meta-input .el-input + .el-input {
63 | margin-left: 0;
64 | margin-top: 0.5rem;
65 | }
66 | }
67 |
68 | .tk-submit {
69 | display: flex;
70 | flex-direction: column;
71 | }
72 | .tk-row {
73 | display: flex;
74 | flex-direction: row;
75 | }
76 | .tk-col {
77 | flex: 1;
78 | display: flex;
79 | flex-direction: column;
80 | }
81 | .tk-meta-input {
82 | margin-bottom: 0.5rem;
83 | }
84 | .tk-row.actions {
85 | position: relative;
86 | margin-top: 1rem;
87 | margin-bottom: 1rem;
88 | margin-left: 3.5rem;
89 | align-items: center;
90 | justify-content: flex-end;
91 | }
92 | .tk-row-actions-start {
93 | flex: 1;
94 | display: flex;
95 | align-items: center;
96 | }
97 | .tk-submit-action-icon {
98 | align-self: center;
99 | display: inline-block;
100 | width: 1.25em;
101 | line-height: 0;
102 | margin-right: 10px;
103 | cursor: pointer;
104 | flex-shrink: 0;
105 | }
106 | .tk-submit-action-icon svg:hover {
107 | opacity: 0.8;
108 | }
109 | .tk-submit-action-icon.__markdown {
110 | color: #909399;
111 | }
112 | .tk-error-message {
113 | word-break: break-all;
114 | color: #ff0000;
115 | font-size: 0.75em;
116 | flex-shrink: 1;
117 | }
118 | .tk-input-image {
119 | display: none;
120 | }
121 | .tk-input {
122 | flex: 1;
123 | }
124 | .tk-input .el-textarea__inner {
125 | background-position: right bottom;
126 | background-repeat: no-repeat;
127 | }
128 | .tk-turnstile-container {
129 | position: absolute;
130 | right: 0;
131 | bottom: -75px;
132 | z-index: 1;
133 | }
134 | .tk-turnstile {
135 | display: flex;
136 | flex-direction: column;
137 | }
138 | .tk-preview-container {
139 | margin-left: 3rem;
140 | margin-bottom: 1rem;
141 | padding: 5px 15px;
142 | border: 1px solid rgba(128, 128, 128, 0.31);
143 | border-radius: 4px;
144 | word-break: break-word;
145 | }
146 | .tk-fade-in {
147 | animation: tkFadeIn 0.3s;
148 | }
149 | @keyframes tkFadeIn {
150 | 0% {
151 | opacity: 0;
152 | }
153 | to {
154 | opacity: 1;
155 | }
156 | }
157 |
158 | .tk-action {
159 | display: flex;
160 | align-items: center;
161 | }
162 | .tk-action-link {
163 | margin-left: 0.5rem;
164 | color: #409eff;
165 | text-decoration: none;
166 | display: flex;
167 | align-items: center;
168 | }
169 | .tk-action-link .tk-action-icon-solid {
170 | display: none;
171 | }
172 | .tk-action-link.tk-liked .tk-action-icon,
173 | .tk-action-link:hover .tk-action-icon {
174 | display: none;
175 | }
176 | .tk-action-link.tk-liked .tk-action-icon-solid,
177 | .tk-action-link:hover .tk-action-icon-solid {
178 | display: block;
179 | }
180 | .tk-action-count {
181 | margin-left: 0.25rem;
182 | font-size: 0.75rem;
183 | height: 1.5rem;
184 | line-height: 1.5rem;
185 | }
186 | .tk-action-icon {
187 | display: inline-block;
188 | height: 1em;
189 | width: 1em;
190 | line-height: 0;
191 | color: #409eff;
192 | }
193 |
194 | .tk-main {
195 | flex: 1;
196 | width: 0;
197 | }
198 | .tk-row {
199 | flex: 1;
200 | display: flex;
201 | flex-direction: row;
202 | justify-content: space-between;
203 | }
204 | .tk-nick-link {
205 | color: inherit;
206 | text-decoration: none;
207 | }
208 | .tk-replies .tk-nick-link {
209 | font-size: 0.9em;
210 | }
211 | .tk-nick-link:hover {
212 | color: #409eff;
213 | }
214 | .tk-actions {
215 | display: none;
216 | margin-left: 1em;
217 | }
218 | .tk-comment:hover .tk-actions {
219 | display: inline;
220 | }
221 | .tk-extras {
222 | color: #999999;
223 | font-size: 0.875em;
224 | display: flex;
225 | flex-wrap: wrap;
226 | }
227 | .tk-extra {
228 | margin-top: 0.5rem;
229 | margin-right: 0.75rem;
230 | display: flex;
231 | align-items: center;
232 | }
233 | .tk-icon.__comment {
234 | height: 1em;
235 | width: 1em;
236 | line-height: 1;
237 | }
238 | .tk-extra-text {
239 | line-height: 1;
240 | }
241 | .tk-tag {
242 | display: inline-block;
243 | padding: 0 0.5em;
244 | font-size: 0.75em;
245 | background-color: #f2f6fc;
246 | }
247 | .tk-tag-green {
248 | background-color: rgba(103, 194, 58, 0.13);
249 | border: 1px solid rgba(103, 194, 58, 0.5);
250 | border-radius: 2px;
251 | color: #67c23a;
252 | }
253 | .tk-tag-yellow {
254 | background-color: rgba(230, 162, 60, 0.13);
255 | border: 1px solid rgba(230, 162, 60, 0.5);
256 | border-radius: 2px;
257 | color: #e6a23c;
258 | }
259 | .tk-tag-blue {
260 | background-color: rgba(64, 158, 255, 0.13);
261 | border: 1px solid rgba(64, 158, 255, 0.5);
262 | border-radius: 2px;
263 | color: #409eff;
264 | }
265 | .tk-tag-red {
266 | background-color: rgba(245, 108, 108, 0.13);
267 | border: 1px solid rgba(245, 108, 108, 0.5);
268 | border-radius: 2px;
269 | color: #f56c6c;
270 | }
271 | .tk-comment {
272 | margin-top: 1rem;
273 | display: flex;
274 | flex-direction: row;
275 | word-break: break-all;
276 | }
277 | .tk-content {
278 | margin-top: 0.5rem;
279 | overflow: hidden;
280 | max-height: 500px;
281 | position: relative;
282 | }
283 | .tk-content-expand {
284 | max-height: none;
285 | }
286 | .tk-replies .tk-content {
287 | font-size: 0.9em;
288 | }
289 | .tk-comment .vemoji {
290 | max-height: 2em;
291 | vertical-align: middle;
292 | }
293 | .tk-replies {
294 | max-height: 200px;
295 | overflow: hidden;
296 | position: relative;
297 | }
298 | .tk-replies-expand {
299 | max-height: none;
300 | overflow: unset;
301 | }
302 | .tk-submit {
303 | margin-top: 1rem;
304 | }
305 | .tk-expand {
306 | font-size: 0.75em;
307 | }
308 | .tk-lightbox {
309 | display: block;
310 | position: fixed;
311 | background-color: rgba(0, 0, 0, 0.3);
312 | top: 0;
313 | left: 0;
314 | right: 0;
315 | bottom: 0;
316 | z-index: 999;
317 | }
318 | .tk-lightbox-image {
319 | min-width: 100px;
320 | min-height: 30px;
321 | width: auto;
322 | height: auto;
323 | max-width: 95%;
324 | max-height: 95%;
325 | position: absolute;
326 | top: 50%;
327 | left: 50%;
328 | transform: translate(-50%, -50%);
329 | background: linear-gradient(90deg, #eeeeee 50%, #e3e3e3 0);
330 | background-size: 40px 100%;
331 | }
332 |
333 | .tk-comments-title {
334 | font-size: 1.25rem;
335 | font-weight: bold;
336 | margin-bottom: 1rem;
337 | display: flex;
338 | align-items: baseline;
339 | justify-content: space-between;
340 | }
341 | .tk-comments-count.__hidden {
342 | visibility: hidden;
343 | }
344 | .tk-comments-container {
345 | min-height: 10rem;
346 | display: flex;
347 | flex-direction: column;
348 | }
349 | .tk-comments-no {
350 | flex: 1;
351 | text-align: center;
352 | display: flex;
353 | align-items: center;
354 | justify-content: center;
355 | }
356 | .tk-comments-error {
357 | font-size: 0.75em;
358 | color: #ff0000;
359 | }
360 | .tk-icon.__comments {
361 | display: inline-flex;
362 | align-items: center;
363 | justify-content: center;
364 | vertical-align: sub;
365 | margin-left: 0.5em;
366 | height: 0.75em;
367 | width: 0.75em;
368 | line-height: 0;
369 | cursor: pointer;
370 | color: #409eff;
371 | }
372 | .twikoo div.code-toolbar {
373 | position: relative;
374 | border-radius: 0.3em;
375 | }
376 | .twikoo div.code-toolbar > .toolbar {
377 | position: absolute;
378 | right: 4px;
379 | top: 4px;
380 | font-size: 0.8125rem;
381 | font-weight: 500;
382 | display: flex;
383 | }
384 | .twikoo div.code-toolbar > .toolbar > .toolbar-item {
385 | margin-left: 0.3em;
386 | }
387 | .twikoo div.code-toolbar > .toolbar > .toolbar-item > a,
388 | .twikoo div.code-toolbar > .toolbar > .toolbar-item > button,
389 | .twikoo div.code-toolbar > .toolbar > .toolbar-item > span {
390 | padding: 2px 4px;
391 | border-radius: 0.3em;
392 | }
393 | .twikoo div.code-toolbar > .toolbar > .toolbar-item > button {
394 | border: 1px solid rgba(128, 128, 128, 0.31);
395 | }
396 | .twikoo div.code-toolbar > .toolbar > .toolbar-item > button:hover {
397 | cursor: pointer;
398 | }
399 |
400 | .tk-footer {
401 | width: 100%;
402 | text-align: end;
403 | font-size: 0.75em;
404 | color: #999999;
405 | margin-top: 1em;
406 | }
407 |
408 | .tk-pagination,
409 | .tk-pagination-pagers {
410 | display: flex;
411 | }
412 | .tk-pagination {
413 | width: 100%;
414 | align-items: center;
415 | justify-content: space-between;
416 | flex-wrap: wrap;
417 | }
418 | .tk-pagination-options {
419 | display: flex;
420 | align-items: center;
421 | }
422 | .tk-pagination-pager {
423 | width: 2em;
424 | height: 2em;
425 | display: flex;
426 | align-items: center;
427 | justify-content: center;
428 | cursor: pointer;
429 | }
430 | .tk-pagination-pager.__current {
431 | background-color: #409eff;
432 | pointer-events: none;
433 | }
434 | .tk-pagination .el-input {
435 | width: 50px;
436 | }
437 | .tk-pagination .el-input .el-input__inner {
438 | padding: 0;
439 | height: 28px;
440 | text-align: center;
441 | -moz-appearance: textfield;
442 | appearance: textfield;
443 | }
444 | .tk-pagination .el-input .el-input__inner::-webkit-inner-spin-button,
445 | .tk-pagination .el-input .el-input__inner::-webkit-outer-spin-button {
446 | -webkit-appearance: none;
447 | appearance: none;
448 | margin: 0;
449 | }
450 |
451 | .tk-admin-comment {
452 | display: flex;
453 | flex-direction: column;
454 | align-items: center;
455 | }
456 | .tk-admin-comment a {
457 | color: currentColor;
458 | text-decoration: underline;
459 | }
460 | .tk-admin-warn {
461 | margin-bottom: 1em;
462 | }
463 | .tk-admin-comment-filter {
464 | width: 100%;
465 | display: flex;
466 | align-items: center;
467 | justify-content: flex-start;
468 | }
469 | .tk-admin-comment-filter-keyword {
470 | flex: 1;
471 | }
472 | .tk-admin-comment-filter-type {
473 | height: 32px;
474 | margin: 0 0.5em;
475 | padding: 0 0.5em;
476 | color: #ffffff;
477 | background: none;
478 | border: 1px solid rgba(144, 147, 153, 0.31);
479 | border-radius: 4px;
480 | position: relative;
481 | -moz-appearance: none;
482 | -webkit-appearance: none;
483 | }
484 | .tk-admin-comment-filter-type:focus {
485 | border-color: #409eff;
486 | }
487 | .tk-admin-comment-filter-type option {
488 | color: initial;
489 | }
490 | .tk-admin-comment-list {
491 | margin-top: 1em;
492 | }
493 | .tk-admin-comment-list,
494 | .tk-admin-comment-item {
495 | width: 100%;
496 | display: flex;
497 | flex-direction: column;
498 | justify-content: stretch;
499 | }
500 | .tk-admin-comment-meta {
501 | display: flex;
502 | align-items: center;
503 | flex-wrap: wrap;
504 | margin-bottom: 0.5em;
505 | }
506 | .tk-admin-comment .tk-avatar {
507 | margin-right: 0.5em;
508 | }
509 | .tk-admin-comment .tk-content {
510 | max-height: none;
511 | }
512 | .tk-admin-actions {
513 | display: flex;
514 | margin-bottom: 1em;
515 | border-bottom: 1px solid rgba(255, 255, 255, 0.5);
516 | }
517 |
518 | .tk-admin-config-groups {
519 | overflow-y: auto;
520 | padding-right: 0.5em;
521 | }
522 | .tk-admin-config-groups .tk-admin-config-group,
523 | .tk-admin-config-groups .tk-admin-config-group-title {
524 | background: transparent;
525 | }
526 | .tk-admin-config-group-title {
527 | margin-top: 1em;
528 | font-size: 1.25rem;
529 | font-weight: bold;
530 | }
531 | .tk-admin-config-item {
532 | display: grid;
533 | align-items: center;
534 | grid-template-columns: 30% 70%;
535 | margin-top: 1em;
536 | }
537 | .tk-admin-config-title {
538 | text-align: right;
539 | margin-right: 1em;
540 | overflow: hidden;
541 | text-overflow: ellipsis;
542 | white-space: nowrap;
543 | }
544 | .tk-admin-config-desc {
545 | margin-top: 0.5em;
546 | font-size: 0.75em;
547 | overflow-wrap: break-word;
548 | }
549 | .tk-admin-config-actions {
550 | display: flex;
551 | align-items: center;
552 | justify-content: center;
553 | margin-top: 1em;
554 | }
555 | .tk-admin-config-message {
556 | margin-top: 0.5em;
557 | text-align: center;
558 | }
559 | .tk-admin-config-email-test-desc {
560 | margin: 1em 0;
561 | }
562 |
563 | .tk-admin-import {
564 | display: flex;
565 | flex-direction: column;
566 | }
567 | .tk-admin-import-label {
568 | margin-top: 1em;
569 | font-size: 1.25rem;
570 | font-weight: bold;
571 | }
572 | .tk-admin-import select,
573 | .tk-admin-import input,
574 | .tk-admin-import .el-button,
575 | .tk-admin-import .el-textarea {
576 | margin-top: 1em;
577 | }
578 |
579 | .tk-admin-container {
580 | position: absolute;
581 | top: 0;
582 | left: 0;
583 | width: 100%;
584 | height: 100%;
585 | overflow: hidden;
586 | pointer-events: none;
587 | }
588 | .tk-admin {
589 | position: absolute;
590 | top: 0;
591 | left: 100%;
592 | width: 100%;
593 | height: 100%;
594 | overflow-y: auto;
595 | pointer-events: all;
596 | color: #ffffff;
597 | background-color: rgba(0, 0, 0, 0.6);
598 | backdrop-filter: blur(5px);
599 | transition: all 0.5s ease;
600 | visibility: hidden;
601 | }
602 | .tk-admin::-webkit-scrollbar {
603 | width: 5px;
604 | background-color: transparent;
605 | }
606 | .tk-admin::-webkit-scrollbar-track {
607 | background-color: transparent;
608 | }
609 | .tk-admin::-webkit-scrollbar-thumb {
610 | background-color: rgba(255, 255, 255, 0.31);
611 | }
612 | .tk-admin.__show {
613 | left: 0;
614 | visibility: visible;
615 | }
616 | .tk-admin-close {
617 | position: sticky;
618 | float: right;
619 | display: block;
620 | top: 0;
621 | right: 0;
622 | width: 1rem;
623 | height: 1rem;
624 | padding: 1rem;
625 | box-sizing: content-box;
626 | color: #ffffff;
627 | }
628 | .tk-login,
629 | .tk-regist {
630 | display: flex;
631 | flex-direction: column;
632 | align-items: center;
633 | width: 100%;
634 | padding: 0 2rem;
635 | }
636 | .tk-login-title {
637 | color: #ffffff;
638 | font-size: 1.25rem;
639 | text-align: center;
640 | margin-top: 10rem;
641 | }
642 | .tk-password,
643 | .tk-login-msg {
644 | color: #ffffff;
645 | width: 80%;
646 | text-align: center;
647 | margin-top: 1rem;
648 | }
649 | .tk-password .el-input__inner {
650 | min-width: 100px;
651 | }
652 | .tk-login-msg a {
653 | color: #ffffff;
654 | margin-left: 1em;
655 | text-decoration: underline;
656 | }
657 | .tk-regist-button {
658 | margin-top: 1rem;
659 | }
660 | .tk-panel {
661 | color: #ffffff;
662 | padding: 2rem;
663 | }
664 | .tk-panel-title {
665 | font-size: 1.5rem;
666 | display: flex;
667 | align-items: flex-end;
668 | justify-content: space-between;
669 | }
670 | .tk-panel-logout {
671 | color: #ffffff;
672 | font-size: 1rem;
673 | text-decoration: underline;
674 | }
675 | .tk-panel .tk-tabs {
676 | display: flex;
677 | margin-bottom: 1em;
678 | border-bottom: 2px solid #c0c4cc;
679 | }
680 | .tk-panel .tk-tab {
681 | color: #c0c4cc;
682 | cursor: pointer;
683 | line-height: 2em;
684 | margin-right: 2em;
685 | margin-bottom: -2px;
686 | }
687 | .tk-panel .tk-tab.__active {
688 | color: #ffffff;
689 | border-bottom: 2px solid #ffffff;
690 | }
691 |
692 | .twikoo {
693 | position: relative;
694 | }
695 | .twikoo svg {
696 | width: 100%;
697 | height: 100%;
698 | fill: currentColor;
699 | }
700 |
701 | /* 全局 CSS */
702 | .tk-expand {
703 | width: 100%;
704 | cursor: pointer;
705 | padding: 0.75em;
706 | text-align: center;
707 | transition: all 0.5s;
708 | }
709 | .tk-expand:hover {
710 | background-color: rgba(0, 0, 0, 0.13);
711 | }
712 | .tk-expand:active {
713 | background-color: rgba(0, 0, 0, 0.19);
714 | }
715 | .tk-content img {
716 | max-width: 300px;
717 | max-height: 300px;
718 | vertical-align: middle;
719 | }
720 | .tk-owo-emotion,
721 | .twikoo .OwO-item img {
722 | width: 3em;
723 | height: auto;
724 | }
725 |
726 | /* element-ui overwrite */
727 | .twikoo .el-input__inner,
728 | .twikoo .el-textarea__inner {
729 | color: currentColor;
730 | background-color: transparent;
731 | border-color: rgba(144, 147, 153, 0.31);
732 | }
733 | .twikoo .el-input__inner:hover,
734 | .twikoo .el-textarea__inner:hover {
735 | border-color: rgba(144, 147, 153, 0.5);
736 | }
737 | .twikoo .el-input__inner:focus,
738 | .twikoo .el-textarea__inner:focus {
739 | border-color: #409eff;
740 | }
741 | .twikoo .el-input-group__append,
742 | .twikoo .el-input-group__prepend {
743 | color: currentColor;
744 | background-clip: padding-box;
745 | background-color: rgba(144, 147, 153, 0.13);
746 | border-color: rgba(144, 147, 153, 0.31);
747 | }
748 | .twikoo .el-button:not(.el-button--primary):not(.el-button--text) {
749 | color: currentColor;
750 | background-color: rgba(144, 147, 153, 0.063);
751 | border-color: rgba(144, 147, 153, 0.31);
752 | }
753 | .twikoo .el-button:not(.el-button--primary):not(.el-button--text):active,
754 | .twikoo .el-button:not(.el-button--primary):not(.el-button--text):focus,
755 | .twikoo .el-button:not(.el-button--primary):not(.el-button--text):hover {
756 | color: #409eff;
757 | background-color: rgba(64, 158, 255, 0.063);
758 | border-color: rgba(64, 158, 255, 0.5);
759 | }
760 | .twikoo .el-button--primary.is-disabled,
761 | .twikoo .el-button--primary.is-disabled:active,
762 | .twikoo .el-button--primary.is-disabled:focus,
763 | .twikoo .el-button--primary.is-disabled:hover {
764 | color: rgba(255, 255, 255, 0.63);
765 | background-color: rgba(64, 158, 255, 0.5);
766 | border-color: transparent;
767 | }
768 | .twikoo .el-loading-mask {
769 | background-color: transparent;
770 | backdrop-filter: opacity(20%);
771 | }
772 | .twikoo .el-textarea .el-input__count {
773 | color: currentColor;
774 | background: transparent;
775 | }
776 | .tk-admin-warn {
777 | padding: 1rem 1.5rem;
778 | background-color: #fff7d0;
779 | border-left: 0.5rem solid #e7c000;
780 | color: #6b5900;
781 | align-self: stretch;
782 | }
783 |
784 | .el-button {
785 | display: inline-block;
786 | line-height: 1;
787 | white-space: nowrap;
788 | cursor: pointer;
789 | background: #fff;
790 | border: 1px solid #dcdfe6;
791 | color: #606266;
792 | -webkit-appearance: none;
793 | text-align: center;
794 | -webkit-box-sizing: border-box;
795 | box-sizing: border-box;
796 | outline: 0;
797 | margin: 0;
798 | -webkit-transition: 0.1s;
799 | transition: 0.1s;
800 | font-weight: 500;
801 | -moz-user-select: none;
802 | -webkit-user-select: none;
803 | -ms-user-select: none;
804 | padding: 12px 20px;
805 | font-size: 14px;
806 | border-radius: 4px;
807 | }
808 | .el-button + .el-button {
809 | margin-left: 10px;
810 | }
811 | .el-button:focus,
812 | .el-button:hover {
813 | color: #409eff;
814 | border-color: #c6e2ff;
815 | background-color: #ecf5ff;
816 | }
817 | .el-button:active {
818 | color: #3a8ee6;
819 | border-color: #3a8ee6;
820 | outline: 0;
821 | }
822 | .el-button::-moz-focus-inner {
823 | border: 0;
824 | }
825 | .el-button [class*="el-icon-"] + span {
826 | margin-left: 5px;
827 | }
828 | .el-button.is-plain:focus,
829 | .el-button.is-plain:hover {
830 | background: #fff;
831 | border-color: #409eff;
832 | color: #409eff;
833 | }
834 | .el-button.is-active,
835 | .el-button.is-plain:active {
836 | color: #3a8ee6;
837 | border-color: #3a8ee6;
838 | }
839 | .el-button.is-plain:active {
840 | background: #fff;
841 | outline: 0;
842 | }
843 | .el-button.is-disabled,
844 | .el-button.is-disabled:focus,
845 | .el-button.is-disabled:hover {
846 | color: #c0c4cc;
847 | cursor: not-allowed;
848 | background-image: none;
849 | background-color: #fff;
850 | border-color: #ebeef5;
851 | }
852 | .el-button.is-disabled.el-button--text {
853 | background-color: transparent;
854 | }
855 | .el-button.is-disabled.is-plain,
856 | .el-button.is-disabled.is-plain:focus,
857 | .el-button.is-disabled.is-plain:hover {
858 | background-color: #fff;
859 | border-color: #ebeef5;
860 | color: #c0c4cc;
861 | }
862 | .el-button.is-loading {
863 | position: relative;
864 | pointer-events: none;
865 | }
866 | .el-button.is-loading:before {
867 | pointer-events: none;
868 | content: "";
869 | position: absolute;
870 | left: -1px;
871 | top: -1px;
872 | right: -1px;
873 | bottom: -1px;
874 | border-radius: inherit;
875 | background-color: rgba(255, 255, 255, 0.35);
876 | }
877 | .el-button.is-round {
878 | border-radius: 20px;
879 | padding: 12px 23px;
880 | }
881 | .el-button.is-circle {
882 | border-radius: 50%;
883 | padding: 12px;
884 | }
885 | .el-button--primary {
886 | color: #fff;
887 | background-color: #409eff;
888 | border-color: #409eff;
889 | }
890 | .el-button--primary:focus,
891 | .el-button--primary:hover {
892 | background: #66b1ff;
893 | border-color: #66b1ff;
894 | color: #fff;
895 | }
896 | .el-button--primary:active {
897 | background: #3a8ee6;
898 | border-color: #3a8ee6;
899 | color: #fff;
900 | outline: 0;
901 | }
902 | .el-button--primary.is-active {
903 | background: #3a8ee6;
904 | border-color: #3a8ee6;
905 | color: #fff;
906 | }
907 | .el-button--primary.is-disabled,
908 | .el-button--primary.is-disabled:active,
909 | .el-button--primary.is-disabled:focus,
910 | .el-button--primary.is-disabled:hover {
911 | color: #fff;
912 | background-color: #a0cfff;
913 | border-color: #a0cfff;
914 | }
915 | .el-button--primary.is-plain {
916 | color: #409eff;
917 | background: #ecf5ff;
918 | border-color: #b3d8ff;
919 | }
920 | .el-button--primary.is-plain:focus,
921 | .el-button--primary.is-plain:hover {
922 | background: #409eff;
923 | border-color: #409eff;
924 | color: #fff;
925 | }
926 | .el-button--primary.is-plain:active {
927 | background: #3a8ee6;
928 | border-color: #3a8ee6;
929 | color: #fff;
930 | outline: 0;
931 | }
932 | .el-button--primary.is-plain.is-disabled,
933 | .el-button--primary.is-plain.is-disabled:active,
934 | .el-button--primary.is-plain.is-disabled:focus,
935 | .el-button--primary.is-plain.is-disabled:hover {
936 | color: #8cc5ff;
937 | background-color: #ecf5ff;
938 | border-color: #d9ecff;
939 | }
940 | .el-button--success {
941 | color: #fff;
942 | background-color: #67c23a;
943 | border-color: #67c23a;
944 | }
945 | .el-button--success:focus,
946 | .el-button--success:hover {
947 | background: #85ce61;
948 | border-color: #85ce61;
949 | color: #fff;
950 | }
951 | .el-button--success.is-active,
952 | .el-button--success:active {
953 | background: #5daf34;
954 | border-color: #5daf34;
955 | color: #fff;
956 | }
957 | .el-button--success:active {
958 | outline: 0;
959 | }
960 | .el-button--success.is-disabled,
961 | .el-button--success.is-disabled:active,
962 | .el-button--success.is-disabled:focus,
963 | .el-button--success.is-disabled:hover {
964 | color: #fff;
965 | background-color: #b3e19d;
966 | border-color: #b3e19d;
967 | }
968 | .el-button--success.is-plain {
969 | color: #67c23a;
970 | background: #f0f9eb;
971 | border-color: #c2e7b0;
972 | }
973 | .el-button--success.is-plain:focus,
974 | .el-button--success.is-plain:hover {
975 | background: #67c23a;
976 | border-color: #67c23a;
977 | color: #fff;
978 | }
979 | .el-button--success.is-plain:active {
980 | background: #5daf34;
981 | border-color: #5daf34;
982 | color: #fff;
983 | outline: 0;
984 | }
985 | .el-button--success.is-plain.is-disabled,
986 | .el-button--success.is-plain.is-disabled:active,
987 | .el-button--success.is-plain.is-disabled:focus,
988 | .el-button--success.is-plain.is-disabled:hover {
989 | color: #a4da89;
990 | background-color: #f0f9eb;
991 | border-color: #e1f3d8;
992 | }
993 | .el-button--warning {
994 | color: #fff;
995 | background-color: #e6a23c;
996 | border-color: #e6a23c;
997 | }
998 | .el-button--warning:focus,
999 | .el-button--warning:hover {
1000 | background: #ebb563;
1001 | border-color: #ebb563;
1002 | color: #fff;
1003 | }
1004 | .el-button--warning.is-active,
1005 | .el-button--warning:active {
1006 | background: #cf9236;
1007 | border-color: #cf9236;
1008 | color: #fff;
1009 | }
1010 | .el-button--warning:active {
1011 | outline: 0;
1012 | }
1013 | .el-button--warning.is-disabled,
1014 | .el-button--warning.is-disabled:active,
1015 | .el-button--warning.is-disabled:focus,
1016 | .el-button--warning.is-disabled:hover {
1017 | color: #fff;
1018 | background-color: #f3d19e;
1019 | border-color: #f3d19e;
1020 | }
1021 | .el-button--warning.is-plain {
1022 | color: #e6a23c;
1023 | background: #fdf6ec;
1024 | border-color: #f5dab1;
1025 | }
1026 | .el-button--warning.is-plain:focus,
1027 | .el-button--warning.is-plain:hover {
1028 | background: #e6a23c;
1029 | border-color: #e6a23c;
1030 | color: #fff;
1031 | }
1032 | .el-button--warning.is-plain:active {
1033 | background: #cf9236;
1034 | border-color: #cf9236;
1035 | color: #fff;
1036 | outline: 0;
1037 | }
1038 | .el-button--warning.is-plain.is-disabled,
1039 | .el-button--warning.is-plain.is-disabled:active,
1040 | .el-button--warning.is-plain.is-disabled:focus,
1041 | .el-button--warning.is-plain.is-disabled:hover {
1042 | color: #f0c78a;
1043 | background-color: #fdf6ec;
1044 | border-color: #faecd8;
1045 | }
1046 | .el-button--danger {
1047 | color: #fff;
1048 | background-color: #f56c6c;
1049 | border-color: #f56c6c;
1050 | }
1051 | .el-button--danger:focus,
1052 | .el-button--danger:hover {
1053 | background: #f78989;
1054 | border-color: #f78989;
1055 | color: #fff;
1056 | }
1057 | .el-button--danger.is-active,
1058 | .el-button--danger:active {
1059 | background: #dd6161;
1060 | border-color: #dd6161;
1061 | color: #fff;
1062 | }
1063 | .el-button--danger:active {
1064 | outline: 0;
1065 | }
1066 | .el-button--danger.is-disabled,
1067 | .el-button--danger.is-disabled:active,
1068 | .el-button--danger.is-disabled:focus,
1069 | .el-button--danger.is-disabled:hover {
1070 | color: #fff;
1071 | background-color: #fab6b6;
1072 | border-color: #fab6b6;
1073 | }
1074 | .el-button--danger.is-plain {
1075 | color: #f56c6c;
1076 | background: #fef0f0;
1077 | border-color: #fbc4c4;
1078 | }
1079 | .el-button--danger.is-plain:focus,
1080 | .el-button--danger.is-plain:hover {
1081 | background: #f56c6c;
1082 | border-color: #f56c6c;
1083 | color: #fff;
1084 | }
1085 | .el-button--danger.is-plain:active {
1086 | background: #dd6161;
1087 | border-color: #dd6161;
1088 | color: #fff;
1089 | outline: 0;
1090 | }
1091 | .el-button--danger.is-plain.is-disabled,
1092 | .el-button--danger.is-plain.is-disabled:active,
1093 | .el-button--danger.is-plain.is-disabled:focus,
1094 | .el-button--danger.is-plain.is-disabled:hover {
1095 | color: #f9a7a7;
1096 | background-color: #fef0f0;
1097 | border-color: #fde2e2;
1098 | }
1099 | .el-button--info {
1100 | color: #fff;
1101 | background-color: #909399;
1102 | border-color: #909399;
1103 | }
1104 | .el-button--info:focus,
1105 | .el-button--info:hover {
1106 | background: #a6a9ad;
1107 | border-color: #a6a9ad;
1108 | color: #fff;
1109 | }
1110 | .el-button--info.is-active,
1111 | .el-button--info:active {
1112 | background: #82848a;
1113 | border-color: #82848a;
1114 | color: #fff;
1115 | }
1116 | .el-button--info:active {
1117 | outline: 0;
1118 | }
1119 | .el-button--info.is-disabled,
1120 | .el-button--info.is-disabled:active,
1121 | .el-button--info.is-disabled:focus,
1122 | .el-button--info.is-disabled:hover {
1123 | color: #fff;
1124 | background-color: #c8c9cc;
1125 | border-color: #c8c9cc;
1126 | }
1127 | .el-button--info.is-plain {
1128 | color: #909399;
1129 | background: #f4f4f5;
1130 | border-color: #d3d4d6;
1131 | }
1132 | .el-button--info.is-plain:focus,
1133 | .el-button--info.is-plain:hover {
1134 | background: #909399;
1135 | border-color: #909399;
1136 | color: #fff;
1137 | }
1138 | .el-button--info.is-plain:active {
1139 | background: #82848a;
1140 | border-color: #82848a;
1141 | color: #fff;
1142 | outline: 0;
1143 | }
1144 | .el-button--info.is-plain.is-disabled,
1145 | .el-button--info.is-plain.is-disabled:active,
1146 | .el-button--info.is-plain.is-disabled:focus,
1147 | .el-button--info.is-plain.is-disabled:hover {
1148 | color: #bcbec2;
1149 | background-color: #f4f4f5;
1150 | border-color: #e9e9eb;
1151 | }
1152 | .el-button--medium {
1153 | padding: 10px 20px;
1154 | font-size: 14px;
1155 | border-radius: 4px;
1156 | }
1157 | .el-button--mini,
1158 | .el-button--small {
1159 | font-size: 12px;
1160 | border-radius: 3px;
1161 | }
1162 | .el-button--medium.is-round {
1163 | padding: 10px 20px;
1164 | }
1165 | .el-button--medium.is-circle {
1166 | padding: 10px;
1167 | }
1168 | .el-button--small,
1169 | .el-button--small.is-round {
1170 | padding: 9px 15px;
1171 | }
1172 | .el-button--small.is-circle {
1173 | padding: 9px;
1174 | }
1175 | .el-button--mini,
1176 | .el-button--mini.is-round {
1177 | padding: 7px 15px;
1178 | }
1179 | .el-button--mini.is-circle {
1180 | padding: 7px;
1181 | }
1182 | .el-button--text {
1183 | border-color: transparent;
1184 | color: #409eff;
1185 | background: 0 0;
1186 | padding-left: 0;
1187 | padding-right: 0;
1188 | }
1189 | .el-button--text:focus,
1190 | .el-button--text:hover {
1191 | color: #66b1ff;
1192 | border-color: transparent;
1193 | background-color: transparent;
1194 | }
1195 | .el-button--text:active {
1196 | color: #3a8ee6;
1197 | border-color: transparent;
1198 | background-color: transparent;
1199 | }
1200 | .el-button--text.is-disabled,
1201 | .el-button--text.is-disabled:focus,
1202 | .el-button--text.is-disabled:hover {
1203 | border-color: transparent;
1204 | }
1205 | .el-button-group .el-button--danger:last-child,
1206 | .el-button-group .el-button--danger:not(:first-child):not(:last-child),
1207 | .el-button-group .el-button--info:last-child,
1208 | .el-button-group .el-button--info:not(:first-child):not(:last-child),
1209 | .el-button-group .el-button--primary:last-child,
1210 | .el-button-group .el-button--primary:not(:first-child):not(:last-child),
1211 | .el-button-group .el-button--success:last-child,
1212 | .el-button-group .el-button--success:not(:first-child):not(:last-child),
1213 | .el-button-group .el-button--warning:last-child,
1214 | .el-button-group .el-button--warning:not(:first-child):not(:last-child),
1215 | .el-button-group > .el-dropdown > .el-button {
1216 | border-left-color: rgba(255, 255, 255, 0.5);
1217 | }
1218 | .el-button-group .el-button--danger:first-child,
1219 | .el-button-group .el-button--danger:not(:first-child):not(:last-child),
1220 | .el-button-group .el-button--info:first-child,
1221 | .el-button-group .el-button--info:not(:first-child):not(:last-child),
1222 | .el-button-group .el-button--primary:first-child,
1223 | .el-button-group .el-button--primary:not(:first-child):not(:last-child),
1224 | .el-button-group .el-button--success:first-child,
1225 | .el-button-group .el-button--success:not(:first-child):not(:last-child),
1226 | .el-button-group .el-button--warning:first-child,
1227 | .el-button-group .el-button--warning:not(:first-child):not(:last-child) {
1228 | border-right-color: rgba(255, 255, 255, 0.5);
1229 | }
1230 | .el-button-group {
1231 | display: inline-block;
1232 | vertical-align: middle;
1233 | }
1234 | .el-button-group::after,
1235 | .el-button-group::before {
1236 | display: table;
1237 | content: "";
1238 | }
1239 | .el-button-group::after {
1240 | clear: both;
1241 | }
1242 | .el-button-group > .el-button {
1243 | float: left;
1244 | position: relative;
1245 | }
1246 | .el-button-group > .el-button + .el-button {
1247 | margin-left: 0;
1248 | }
1249 | .el-button-group > .el-button.is-disabled {
1250 | z-index: 1;
1251 | }
1252 | .el-button-group > .el-button:first-child {
1253 | border-top-right-radius: 0;
1254 | border-bottom-right-radius: 0;
1255 | }
1256 | .el-button-group > .el-button:last-child {
1257 | border-top-left-radius: 0;
1258 | border-bottom-left-radius: 0;
1259 | }
1260 | .el-button-group > .el-button:first-child:last-child {
1261 | border-radius: 4px;
1262 | }
1263 | .el-button-group > .el-button:first-child:last-child.is-round {
1264 | border-radius: 20px;
1265 | }
1266 | .el-button-group > .el-button:first-child:last-child.is-circle {
1267 | border-radius: 50%;
1268 | }
1269 | .el-button-group > .el-button:not(:first-child):not(:last-child) {
1270 | border-radius: 0;
1271 | }
1272 | .el-button-group > .el-button:not(:last-child) {
1273 | margin-right: -1px;
1274 | }
1275 | .el-button-group > .el-button.is-active,
1276 | .el-button-group > .el-button:not(.is-disabled):active,
1277 | .el-button-group > .el-button:not(.is-disabled):focus,
1278 | .el-button-group > .el-button:not(.is-disabled):hover {
1279 | z-index: 1;
1280 | }
1281 | .el-button-group > .el-dropdown > .el-button {
1282 | border-top-left-radius: 0;
1283 | border-bottom-left-radius: 0;
1284 | }
1285 | .el-input__inner,
1286 | .el-textarea__inner {
1287 | background-image: none;
1288 | -webkit-box-sizing: border-box;
1289 | -webkit-transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
1290 | }
1291 | .el-textarea {
1292 | position: relative;
1293 | display: inline-block;
1294 | width: 100%;
1295 | vertical-align: bottom;
1296 | font-size: 14px;
1297 | }
1298 | .el-textarea__inner {
1299 | display: block;
1300 | resize: vertical;
1301 | padding: 5px 15px;
1302 | line-height: 1.5;
1303 | box-sizing: border-box;
1304 | width: 100%;
1305 | font-size: inherit;
1306 | color: #606266;
1307 | background-color: #fff;
1308 | border: 1px solid #dcdfe6;
1309 | border-radius: 4px;
1310 | transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
1311 | }
1312 | .el-textarea__inner::-webkit-input-placeholder {
1313 | color: #c0c4cc;
1314 | }
1315 | .el-textarea__inner:-ms-input-placeholder {
1316 | color: #c0c4cc;
1317 | }
1318 | .el-textarea__inner::-ms-input-placeholder {
1319 | color: #c0c4cc;
1320 | }
1321 | .el-textarea__inner::placeholder {
1322 | color: #c0c4cc;
1323 | }
1324 | .el-textarea__inner:hover {
1325 | border-color: #c0c4cc;
1326 | }
1327 | .el-textarea__inner:focus {
1328 | outline: 0;
1329 | border-color: #409eff;
1330 | }
1331 | .el-textarea .el-input__count {
1332 | color: #909399;
1333 | background: #fff;
1334 | position: absolute;
1335 | font-size: 12px;
1336 | bottom: 5px;
1337 | right: 10px;
1338 | }
1339 | .el-textarea.is-disabled .el-textarea__inner {
1340 | background-color: #f5f7fa;
1341 | border-color: #e4e7ed;
1342 | color: #c0c4cc;
1343 | cursor: not-allowed;
1344 | }
1345 | .el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder {
1346 | color: #c0c4cc;
1347 | }
1348 | .el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder {
1349 | color: #c0c4cc;
1350 | }
1351 | .el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder {
1352 | color: #c0c4cc;
1353 | }
1354 | .el-textarea.is-disabled .el-textarea__inner::placeholder {
1355 | color: #c0c4cc;
1356 | }
1357 | .el-textarea.is-exceed .el-textarea__inner {
1358 | border-color: #f56c6c;
1359 | }
1360 | .el-textarea.is-exceed .el-input__count {
1361 | color: #f56c6c;
1362 | }
1363 | .el-input {
1364 | position: relative;
1365 | font-size: 14px;
1366 | display: inline-block;
1367 | width: 100%;
1368 | }
1369 | .el-input::-webkit-scrollbar {
1370 | z-index: 11;
1371 | width: 6px;
1372 | }
1373 | .el-input::-webkit-scrollbar:horizontal {
1374 | height: 6px;
1375 | }
1376 | .el-input::-webkit-scrollbar-thumb {
1377 | border-radius: 5px;
1378 | width: 6px;
1379 | background: #b4bccc;
1380 | }
1381 | .el-input::-webkit-scrollbar-corner {
1382 | background: #fff;
1383 | }
1384 | .el-input::-webkit-scrollbar-track {
1385 | background: #fff;
1386 | }
1387 | .el-input::-webkit-scrollbar-track-piece {
1388 | background: #fff;
1389 | width: 6px;
1390 | }
1391 | .el-input .el-input__clear {
1392 | color: #c0c4cc;
1393 | font-size: 14px;
1394 | cursor: pointer;
1395 | -webkit-transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
1396 | transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
1397 | }
1398 | .el-input .el-input__clear:hover {
1399 | color: #909399;
1400 | }
1401 | .el-input .el-input__count {
1402 | height: 100%;
1403 | display: -webkit-inline-box;
1404 | display: -ms-inline-flexbox;
1405 | display: inline-flex;
1406 | -webkit-box-align: center;
1407 | -ms-flex-align: center;
1408 | align-items: center;
1409 | color: #909399;
1410 | font-size: 12px;
1411 | }
1412 | .el-input-group__append .el-button,
1413 | .el-input-group__append .el-input,
1414 | .el-input-group__prepend .el-button,
1415 | .el-input-group__prepend .el-input,
1416 | .el-input__inner {
1417 | font-size: inherit;
1418 | }
1419 | .el-input .el-input__count .el-input__count-inner {
1420 | background: #fff;
1421 | line-height: initial;
1422 | display: inline-block;
1423 | padding: 0 5px;
1424 | }
1425 | .el-input__inner {
1426 | -webkit-appearance: none;
1427 | background-color: #fff;
1428 | border-radius: 4px;
1429 | border: 1px solid #dcdfe6;
1430 | box-sizing: border-box;
1431 | color: #606266;
1432 | display: inline-block;
1433 | height: 40px;
1434 | line-height: 40px;
1435 | outline: 0;
1436 | padding: 0 15px;
1437 | transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
1438 | width: 100%;
1439 | }
1440 | .el-input__prefix,
1441 | .el-input__suffix {
1442 | position: absolute;
1443 | top: 0;
1444 | -webkit-transition: all 0.3s;
1445 | text-align: center;
1446 | height: 100%;
1447 | color: #c0c4cc;
1448 | }
1449 | .el-input__inner::-ms-reveal {
1450 | display: none;
1451 | }
1452 | .el-input__inner::-webkit-input-placeholder {
1453 | color: #c0c4cc;
1454 | }
1455 | .el-input__inner:-ms-input-placeholder {
1456 | color: #c0c4cc;
1457 | }
1458 | .el-input__inner::-ms-input-placeholder {
1459 | color: #c0c4cc;
1460 | }
1461 | .el-input__inner::placeholder {
1462 | color: #c0c4cc;
1463 | }
1464 | .el-input__inner:hover {
1465 | border-color: #c0c4cc;
1466 | }
1467 | .el-input.is-active .el-input__inner,
1468 | .el-input__inner:focus {
1469 | border-color: #409eff;
1470 | outline: 0;
1471 | }
1472 | .el-input__suffix {
1473 | right: 5px;
1474 | transition: all 0.3s;
1475 | pointer-events: none;
1476 | }
1477 | .el-input__suffix-inner {
1478 | pointer-events: all;
1479 | }
1480 | .el-input__prefix {
1481 | left: 5px;
1482 | transition: all 0.3s;
1483 | }
1484 | .el-input__icon {
1485 | height: 100%;
1486 | width: 25px;
1487 | text-align: center;
1488 | -webkit-transition: all 0.3s;
1489 | transition: all 0.3s;
1490 | line-height: 40px;
1491 | }
1492 | .el-input__icon:after {
1493 | content: "";
1494 | height: 100%;
1495 | width: 0;
1496 | display: inline-block;
1497 | vertical-align: middle;
1498 | }
1499 | .el-input__validateIcon {
1500 | pointer-events: none;
1501 | }
1502 | .el-input.is-disabled .el-input__inner {
1503 | background-color: #f5f7fa;
1504 | border-color: #e4e7ed;
1505 | color: #c0c4cc;
1506 | cursor: not-allowed;
1507 | }
1508 | .el-input.is-disabled .el-input__inner::-webkit-input-placeholder {
1509 | color: #c0c4cc;
1510 | }
1511 | .el-input.is-disabled .el-input__inner:-ms-input-placeholder {
1512 | color: #c0c4cc;
1513 | }
1514 | .el-input.is-disabled .el-input__inner::-ms-input-placeholder {
1515 | color: #c0c4cc;
1516 | }
1517 | .el-input.is-disabled .el-input__inner::placeholder {
1518 | color: #c0c4cc;
1519 | }
1520 | .el-input.is-disabled .el-input__icon {
1521 | cursor: not-allowed;
1522 | }
1523 | .el-input.is-exceed .el-input__inner {
1524 | border-color: #f56c6c;
1525 | }
1526 | .el-input.is-exceed .el-input__suffix .el-input__count {
1527 | color: #f56c6c;
1528 | }
1529 | .el-input--suffix .el-input__inner {
1530 | padding-right: 30px;
1531 | }
1532 | .el-input--prefix .el-input__inner {
1533 | padding-left: 30px;
1534 | }
1535 | .el-input--medium {
1536 | font-size: 14px;
1537 | }
1538 | .el-input--medium .el-input__inner {
1539 | height: 36px;
1540 | line-height: 36px;
1541 | }
1542 | .el-input--medium .el-input__icon {
1543 | line-height: 36px;
1544 | }
1545 | .el-input--small {
1546 | font-size: 13px;
1547 | }
1548 | .el-input--small .el-input__inner {
1549 | height: 32px;
1550 | line-height: 32px;
1551 | }
1552 | .el-input--small .el-input__icon {
1553 | line-height: 32px;
1554 | }
1555 | .el-input--mini {
1556 | font-size: 12px;
1557 | }
1558 | .el-input--mini .el-input__inner {
1559 | height: 28px;
1560 | line-height: 28px;
1561 | }
1562 | .el-input--mini .el-input__icon {
1563 | line-height: 28px;
1564 | }
1565 | .el-input-group {
1566 | line-height: normal;
1567 | display: inline-table;
1568 | width: 100%;
1569 | border-collapse: separate;
1570 | border-spacing: 0;
1571 | }
1572 | .el-input-group > .el-input__inner {
1573 | vertical-align: middle;
1574 | display: table-cell;
1575 | }
1576 | .el-input-group__append,
1577 | .el-input-group__prepend {
1578 | background-color: #f5f7fa;
1579 | color: #909399;
1580 | vertical-align: middle;
1581 | display: table-cell;
1582 | position: relative;
1583 | border: 1px solid #dcdfe6;
1584 | border-radius: 4px;
1585 | padding: 0 20px;
1586 | width: 1px;
1587 | white-space: nowrap;
1588 | }
1589 | .el-input-group--prepend .el-input__inner,
1590 | .el-input-group__append {
1591 | border-top-left-radius: 0;
1592 | border-bottom-left-radius: 0;
1593 | }
1594 | .el-input-group--append .el-input__inner,
1595 | .el-input-group__prepend {
1596 | border-top-right-radius: 0;
1597 | border-bottom-right-radius: 0;
1598 | }
1599 | .el-input-group__append:focus,
1600 | .el-input-group__prepend:focus {
1601 | outline: 0;
1602 | }
1603 | .el-input-group__append .el-button,
1604 | .el-input-group__append .el-select,
1605 | .el-input-group__prepend .el-button,
1606 | .el-input-group__prepend .el-select {
1607 | display: inline-block;
1608 | margin: -10px -20px;
1609 | }
1610 | .el-input-group__append button.el-button,
1611 | .el-input-group__append div.el-select .el-input__inner,
1612 | .el-input-group__append div.el-select:hover .el-input__inner,
1613 | .el-input-group__prepend button.el-button,
1614 | .el-input-group__prepend div.el-select .el-input__inner,
1615 | .el-input-group__prepend div.el-select:hover .el-input__inner {
1616 | border-color: transparent;
1617 | background-color: transparent;
1618 | color: inherit;
1619 | border-top: 0;
1620 | border-bottom: 0;
1621 | }
1622 | .el-input-group__prepend {
1623 | border-right: 0;
1624 | }
1625 | .el-input-group__append {
1626 | border-left: 0;
1627 | }
1628 | .el-input-group--append .el-select .el-input.is-focus .el-input__inner,
1629 | .el-input-group--prepend .el-select .el-input.is-focus .el-input__inner {
1630 | border-color: transparent;
1631 | }
1632 | .el-input__inner::-ms-clear {
1633 | display: none;
1634 | width: 0;
1635 | height: 0;
1636 | }
1637 | .el-loading-parent--relative {
1638 | position: relative !important;
1639 | }
1640 | .el-loading-parent--hidden {
1641 | overflow: hidden !important;
1642 | }
1643 | .el-loading-mask {
1644 | position: absolute;
1645 | z-index: 2000;
1646 | background-color: rgba(255, 255, 255, 0.9);
1647 | margin: 0;
1648 | top: 0;
1649 | right: 0;
1650 | bottom: 0;
1651 | left: 0;
1652 | -webkit-transition: opacity 0.3s;
1653 | transition: opacity 0.3s;
1654 | }
1655 | .el-loading-mask.is-fullscreen {
1656 | position: fixed;
1657 | }
1658 | .el-loading-mask.is-fullscreen .el-loading-spinner {
1659 | margin-top: -25px;
1660 | }
1661 | .el-loading-mask.is-fullscreen .el-loading-spinner .circular {
1662 | height: 50px;
1663 | width: 50px;
1664 | }
1665 | .el-loading-spinner {
1666 | top: 50%;
1667 | margin-top: -21px;
1668 | width: 100%;
1669 | text-align: center;
1670 | position: absolute;
1671 | }
1672 | .el-loading-spinner .el-loading-text {
1673 | color: #409eff;
1674 | margin: 3px 0;
1675 | font-size: 14px;
1676 | }
1677 | .el-loading-spinner .circular {
1678 | height: 42px;
1679 | width: 42px;
1680 | -webkit-animation: loading-rotate 2s linear infinite;
1681 | animation: loading-rotate 2s linear infinite;
1682 | }
1683 | .el-loading-spinner .path {
1684 | -webkit-animation: loading-dash 1.5s ease-in-out infinite;
1685 | animation: loading-dash 1.5s ease-in-out infinite;
1686 | stroke-dasharray: 90, 150;
1687 | stroke-dashoffset: 0;
1688 | stroke-width: 2;
1689 | stroke: #409eff;
1690 | stroke-linecap: round;
1691 | }
1692 | .el-loading-spinner i {
1693 | color: #409eff;
1694 | }
1695 | .el-loading-fade-enter,
1696 | .el-loading-fade-leave-active {
1697 | opacity: 0;
1698 | }
1699 | @-webkit-keyframes loading-rotate {
1700 | 100% {
1701 | -webkit-transform: rotate(360deg);
1702 | transform: rotate(360deg);
1703 | }
1704 | }
1705 | @keyframes loading-rotate {
1706 | 100% {
1707 | -webkit-transform: rotate(360deg);
1708 | transform: rotate(360deg);
1709 | }
1710 | }
1711 | @-webkit-keyframes loading-dash {
1712 | 0% {
1713 | stroke-dasharray: 1, 200;
1714 | stroke-dashoffset: 0;
1715 | }
1716 | 50% {
1717 | stroke-dasharray: 90, 150;
1718 | stroke-dashoffset: -40px;
1719 | }
1720 | 100% {
1721 | stroke-dasharray: 90, 150;
1722 | stroke-dashoffset: -120px;
1723 | }
1724 | }
1725 | @keyframes loading-dash {
1726 | 0% {
1727 | stroke-dasharray: 1, 200;
1728 | stroke-dashoffset: 0;
1729 | }
1730 | 50% {
1731 | stroke-dasharray: 90, 150;
1732 | stroke-dashoffset: -40px;
1733 | }
1734 | 100% {
1735 | stroke-dasharray: 90, 150;
1736 | stroke-dashoffset: -120px;
1737 | }
1738 | }
1739 | /*!
1740 | * OwO v1.0.2
1741 | * Source: https://github.com/DIYgod/OwO/blob/master/dist/OwO.min.css
1742 | * Author: DIYgod
1743 | * Modified by: iMaeGoo
1744 | * Released under the MIT License.
1745 | */
1746 |
1747 | .OwO {
1748 | -webkit-user-select: none;
1749 | -moz-user-select: none;
1750 | -ms-user-select: none;
1751 | user-select: none;
1752 | }
1753 |
1754 | .OwO.OwO-open .OwO-body {
1755 | display: block;
1756 | }
1757 |
1758 | .OwO .OwO-logo {
1759 | width: 1.125em;
1760 | display: flex;
1761 | }
1762 |
1763 | .OwO .OwO-body {
1764 | display: none;
1765 | position: absolute;
1766 | left: 0;
1767 | right: 0;
1768 | max-width: 500px;
1769 | color: #4a4a4a;
1770 | background-color: #ffffff;
1771 | border: 1px solid rgba(144, 147, 153, 0.31);
1772 | top: 2em;
1773 | border-radius: 0 4px 4px;
1774 | z-index: 1000;
1775 | }
1776 |
1777 | .night .OwO .OwO-body,
1778 | .darkmode .OwO .OwO-body,
1779 | .DarkMode .OwO .OwO-body,
1780 | [data-theme="dark"] .OwO .OwO-body,
1781 | [data-user-color-scheme="dark"] .OwO .OwO-body {
1782 | color: #ffffff;
1783 | background-color: #4a4a4a;
1784 | }
1785 |
1786 | .OwO .OwO-body .OwO-items {
1787 | -webkit-user-select: none;
1788 | -moz-user-select: none;
1789 | -ms-user-select: none;
1790 | user-select: none;
1791 | display: none;
1792 | padding: 10px;
1793 | padding-right: 0;
1794 | margin: 0;
1795 | overflow: auto;
1796 | font-size: 0;
1797 | }
1798 |
1799 | .OwO .OwO-body .OwO-items .OwO-item {
1800 | list-style-type: none;
1801 | padding: 5px 10px;
1802 | border-radius: 5px;
1803 | display: inline-block;
1804 | font-size: 12px;
1805 | line-height: 14px;
1806 | cursor: pointer;
1807 | -webkit-transition: 0.3s;
1808 | transition: 0.3s;
1809 | text-align: center;
1810 | }
1811 |
1812 | .OwO .OwO-body .OwO-items .OwO-item:hover {
1813 | background-color: rgba(144, 147, 153, 0.13);
1814 | box-shadow:
1815 | 0 2px 2px 0 rgba(0, 0, 0, 0.14),
1816 | 0 3px 1px -2px rgba(0, 0, 0, 0.2),
1817 | 0 1px 5px 0 rgba(0, 0, 0, 0.12);
1818 | }
1819 |
1820 | .OwO .OwO-body .OwO-items-emoji .OwO-item {
1821 | font-size: 20px;
1822 | line-height: 19px;
1823 | }
1824 |
1825 | .OwO .OwO-body .OwO-items-image .OwO-item {
1826 | width: 14%;
1827 | box-sizing: border-box;
1828 | }
1829 |
1830 | @media screen and (max-width: 600px) {
1831 | #twikoo .OwO-items > .OwO-item {
1832 | width: 16%;
1833 | }
1834 | }
1835 |
1836 | @media screen and (max-width: 460px) {
1837 | #twikoo .OwO-items > .OwO-item {
1838 | width: 20%;
1839 | }
1840 | }
1841 |
1842 | @media screen and (max-width: 400px) {
1843 | #twikoo .OwO-items > .OwO-item {
1844 | width: 25%;
1845 | }
1846 | }
1847 |
1848 | @media screen and (max-width: 330px) {
1849 | #twikoo .OwO-items > .OwO-item {
1850 | width: 33%;
1851 | }
1852 | }
1853 |
1854 | .OwO .OwO-body .OwO-items-image .OwO-item img {
1855 | max-width: 100%;
1856 | }
1857 |
1858 | .OwO .OwO-body .OwO-items-show {
1859 | display: block;
1860 | }
1861 |
1862 | .OwO .OwO-body .OwO-bar {
1863 | width: 100%;
1864 | border-top: 1px solid rgba(144, 147, 153, 0.31);
1865 | border-radius: 0 0 4px 4px;
1866 | }
1867 |
1868 | .OwO .OwO-body .OwO-bar .OwO-packages {
1869 | margin: 0;
1870 | padding: 0;
1871 | font-size: 0;
1872 | }
1873 |
1874 | .OwO .OwO-body .OwO-bar .OwO-packages li {
1875 | list-style-type: none;
1876 | display: inline-block;
1877 | line-height: 30px;
1878 | font-size: 14px;
1879 | padding: 0 10px;
1880 | cursor: pointer;
1881 | margin-right: 3px;
1882 | }
1883 |
1884 | .OwO .OwO-body .OwO-bar .OwO-packages li:nth-child(1) {
1885 | border-radius: 0 0 0 3px;
1886 | }
1887 |
1888 | .OwO .OwO-body .OwO-bar .OwO-packages li:hover {
1889 | background-color: rgba(144, 147, 153, 0.13);
1890 | }
1891 |
1892 | .OwO .OwO-body .OwO-bar .OwO-packages .OwO-package-active {
1893 | background-color: rgba(144, 147, 153, 0.13);
1894 | -webkit-transition: 0.3s;
1895 | transition: 0.3s;
1896 | }
1897 |
1898 | /* --------- */
1899 | /* comments container */
1900 | .tk-comments-container {
1901 | margin-top: 50px !important;
1902 | }
1903 |
1904 | /* avatar*/
1905 | .tk-avatar {
1906 | background-color: transparent !important;
1907 | }
1908 |
1909 | .tk-avatar-img {
1910 | border-radius: 50% !important;
1911 | }
1912 |
1913 | /* comment margin */
1914 | .tk-comment {
1915 | margin-top: 36px !important;
1916 | }
1917 |
1918 | /* comment content */
1919 | .tk-main .tk-content {
1920 | border-radius: 10px !important;
1921 | padding: 10px !important;
1922 | background-color: rgba(144, 147, 153, 0.1) !important;
1923 | }
1924 |
1925 | /* PC 端宽度调整 */
1926 | @media screen and (min-width: 768px) {
1927 | .tk-main .tk-content {
1928 | max-width: 60% !important;
1929 | }
1930 | }
1931 |
1932 | /* comment content image */
1933 |
1934 | .tk-main .tk-content > span > p {
1935 | display: flex;
1936 | flex-direction: column;
1937 | justify-content: center;
1938 | gap: 10px;
1939 | }
1940 |
1941 | .tk-main .tk-content {
1942 | display: flex;
1943 | flex-direction: column;
1944 | justify-content: center;
1945 | gap: 10px;
1946 | }
1947 |
1948 | .tk-main .tk-content > span > p > img {
1949 | border-radius: 20px !important;
1950 | }
1951 |
1952 | .tk-extras {
1953 | margin-top: 5px !important;
1954 | }
1955 |
--------------------------------------------------------------------------------
/src/styles/view-transition.css:
--------------------------------------------------------------------------------
1 | /* Theme toggle effect */
2 | /* https://theme-toggle.rdsx.dev/ */
3 | /* https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API */
4 | ::view-transition-group(root) {
5 | animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
6 | }
7 |
8 | ::view-transition-new(root) {
9 | mask: url('data:image/svg+xml,')
10 | top left / 0 no-repeat;
11 | mask-origin: content-box;
12 | animation: scale 1s;
13 | transform-origin: top left;
14 | }
15 |
16 | ::view-transition-old(root),
17 | .dark::view-transition-old(root) {
18 | animation: scale 1s;
19 | transform-origin: top left;
20 | z-index: -1;
21 | }
22 |
23 | @keyframes scale {
24 | to {
25 | mask-size: 350vmax;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/types/twikoo.d.ts:
--------------------------------------------------------------------------------
1 | declare module "twikoo"
2 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { getCollection } from "astro:content"
2 |
3 | export const formatDate = (
4 | date: Date | string | undefined,
5 | format: string = "YYYY-MM-DD",
6 | ): string => {
7 | const validDate = date ? new Date(date) : new Date()
8 |
9 | const tokens: Record = {
10 | YYYY: validDate.getFullYear().toString(),
11 | MM: String(validDate.getMonth() + 1).padStart(2, "0"),
12 | DD: String(validDate.getDate()).padStart(2, "0"),
13 | HH: String(validDate.getHours()).padStart(2, "0"),
14 | mm: String(validDate.getMinutes()).padStart(2, "0"),
15 | ss: String(validDate.getSeconds()).padStart(2, "0"),
16 | }
17 |
18 | return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => tokens[match])
19 | }
20 |
21 | export const getPostsByLocale = async (locale: string) => {
22 | const posts =
23 | locale === "en"
24 | ? await getCollection("enPosts")
25 | : await getCollection("zhPosts")
26 | return posts.sort(
27 | (a: any, b: any) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/langs.ts:
--------------------------------------------------------------------------------
1 | import { langs } from "~/i18n/ui"
2 |
3 | export function getLanguagePaths() {
4 | return langs.map((lang) => ({
5 | params: { lang },
6 | }))
7 | }
8 |
--------------------------------------------------------------------------------
/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
4 | darkMode: ["class"],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [require("@tailwindcss/typography")],
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "include": [".astro/types.d.ts", "**/*", "eslint.config.cjs"],
4 | "exclude": ["dist"],
5 |
6 | "compilerOptions": {
7 | "jsx": "react-jsx",
8 | "jsxImportSource": "react",
9 | /* Path Aliases */
10 | "baseUrl": ".",
11 | "paths": {
12 | "~/*": ["./src/*"]
13 | },
14 | "strictNullChecks": true, // add if using `base` template
15 | "allowJs": true // required, and included with all Astro templates
16 | }
17 | }
18 |
--------------------------------------------------------------------------------