1
0
mirror of https://github.com/sendevia/website.git synced 2026-03-08 08:44:15 +08:00

27 Commits

Author SHA1 Message Date
c5628d1661 chore(package): update to version 26.3.8.332 2026-03-08 00:45:23 +08:00
8795c219ac feat: add rewrite rule for posts index and move main index to posts 2026-03-08 00:45:09 +08:00
c856ce009d article: fix date timezone formatting 2026-03-08 00:41:14 +08:00
b87a0d2918 feat(ArticleMasonry): add preset category prop and dynamic tag filtering 2026-03-08 00:31:08 +08:00
b4c6347538 fix(AppBar): search bar displacement when scrolling on desktop 2026-03-07 22:42:35 +08:00
3b17ade7f7 chore: update version format 2026-03-07 22:31:45 +08:00
b81d7fd415 feat(Header): adjust header gradient mask 2026-03-06 16:56:40 +08:00
206cf373a3 fix(ci): move platforms to docker build and push step 2026-03-06 00:09:25 +08:00
9677ac1367 feat(ci): disable build cache 2026-03-05 23:30:50 +08:00
b41cea0bed chore(package): update to version 26.3.5(323) 2026-03-05 23:19:08 +08:00
8345515977 fix(ci): remove buildkitd-flags 2026-03-05 23:14:32 +08:00
dd069771a8 feat(ci): add multi-platform support and buildkit flags to docker workflow 2026-03-05 23:12:00 +08:00
e4217512aa feat(NavBar): wrap components in ClientOnly 2026-03-05 23:11:36 +08:00
93a42ec037 feat: switch from nginx to caddy for static site hosting 2026-03-05 22:53:31 +08:00
d489bf24c4 feat: add Caddyfile 2026-03-05 22:53:08 +08:00
67a26def4f feat(Article): wrap post info in ClientOnly 2026-03-05 22:28:21 +08:00
a7e0528c24 chore(package): update to version 26.3.4(316) 2026-03-04 22:50:40 +08:00
29885ae607 feat(Footer): 更新备案信息 2026-03-04 22:50:30 +08:00
dabb2f6164 feat(Dockerfile): simplify Dockerfile by removing self-signed SSL and nginx config 2026-03-04 22:08:52 +08:00
1247943a5e feat(.gitignore): update 2026-03-04 22:08:17 +08:00
b822fe6227 feat(ci): simplify docker image tagging and extract version from package.json 2026-03-04 21:15:43 +08:00
f01232241f feat(workflow): add GitHub Actions workflow for building and pushing Docker image 2026-03-04 21:07:12 +08:00
c8b917bda5 feat(Dockerfile): add Dockerfile for building and serving the application with Nginx 2026-03-04 20:44:01 +08:00
aa3cdbc695 fix(PageIndicator): remove inline anchors from headings collection 2026-03-04 15:36:53 +08:00
2bc889a388 chore(package): update to version 26.3.4(308) 2026-03-04 00:23:36 +08:00
c093e395bd feat(anchor): implement custom anchor links 2026-03-04 00:22:09 +08:00
2d879372d2 fix(Table.scss): adjust padding and min-width for table header and cells 2026-02-28 23:32:51 +08:00
29 changed files with 675 additions and 391 deletions

65
.github/workflows/docker-build.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Build and Push Docker Image
on:
push:
branches:
- master
paths:
- "package.json"
- "Dockerfile"
- ".github/workflows/docker-build.yml"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Extract version from package.json
id: version
run: |
VERSION=$(grep '"version"' package.json | sed 's/.*"\([^"]*\)".*/\1/' | sed 's/(.*)//g')
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/website
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest
type=raw,value=${{ steps.version.outputs.version }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
no-cache: true
platforms: linux/amd64,linux/arm64

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ node_modules
package-lock.json package-lock.json
cache/ cache/
dist/ dist/
.agents
nginx-config.conf

View File

@@ -1,8 +1,6 @@
import { defineConfig } from "vitepress"; import { defineConfig } from "vitepress";
import packageJson from "../package.json"; import packageJson from "../package.json";
// https://github.com/valeriangalliat/markdown-it-anchor
import anchor from "markdown-it-anchor";
// markdown-it plugins // markdown-it plugins
// https://mdit-plugins.github.io/align.html // https://mdit-plugins.github.io/align.html
import { align } from "@mdit/plugin-align"; import { align } from "@mdit/plugin-align";
@@ -14,27 +12,19 @@ import { tasklist } from "@mdit/plugin-tasklist";
import { imgMark } from "@mdit/plugin-img-mark"; import { imgMark } from "@mdit/plugin-img-mark";
import { sectionWrapper } from "./theme/utils/mdSectionWrapper"; import { sectionWrapper } from "./theme/utils/mdSectionWrapper";
import { table } from "./theme/utils/mdTable"; import { table } from "./theme/utils/mdTable";
import { anchor } from "./theme/utils/mdCustomAnchor";
export default defineConfig({ export default defineConfig({
base: "/", base: "/",
cleanUrls: true, cleanUrls: true,
rewrites: {
"posts/index.md": "index.md",
},
lang: "zh_CN", lang: "zh_CN",
title: "sendevia 的小站", title: "sendevia 的小站",
titleTemplate: ":title", titleTemplate: ":title",
description: "随便写写的博客", description: "随便写写的博客",
markdown: { markdown: {
anchor: {
permalink: anchor.permalink.linkAfterHeader({
style: "visually-hidden",
symbol: "link",
class: "title-anchor",
assistiveText: () => "复制链接",
visuallyHiddenClass: "visually-hidden",
wrapper: ['<div class="title-with-achor">', "</div>"],
placement: "before",
space: false,
}),
},
attrs: { attrs: {
allowedAttributes: ["id", "class"], allowedAttributes: ["id", "class"],
}, },
@@ -42,11 +32,14 @@ export default defineConfig({
codeCopyButtonTitle: "复制代码", codeCopyButtonTitle: "复制代码",
config(md) { config(md) {
md.use(align); md.use(align);
md.use(anchor, {
levels: [1, 2, 3, 4],
});
md.use(footnote); md.use(footnote);
md.use(sectionWrapper);
md.use(tasklist, { label: true });
md.use(imgMark); md.use(imgMark);
md.use(sectionWrapper);
md.use(table); md.use(table);
md.use(tasklist, { label: true });
}, },
image: { image: {
lazyLoading: true, lazyLoading: true,

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref } from "vue";
import { useAchorLink } from "../composables/useAnchorLink";
interface Props {
href: string;
/** 锚点显示的字符串 */
symbol?: string;
/** 锚点的 class 名称 */
className?: string;
}
const props = withDefaults(defineProps<Props>(), {
symbol: "",
className: "",
});
const isCopied = ref(false);
const { handleCopy } = useAchorLink(props.href, () => {
isCopied.value = true;
setTimeout(() => {
isCopied.value = false;
}, 1000);
});
/** 处理锚点点击事件 */
const handleClick = (event: MouseEvent) => {
event.preventDefault();
handleCopy();
};
</script>
<template>
<span :class="className">
<a :href="href" class="symbol" @click="handleClick">{{ symbol }}</a>
<span v-if="isCopied" class="feedback">已复制</span>
</span>
</template>
<style lang="scss">
@use "sass:meta";
@include meta.load-css("../styles/components/AnchorLink");
</style>

View File

@@ -7,6 +7,15 @@ import { useGlobalData } from "../composables/useGlobalData";
const postsStore = usePostStore(); const postsStore = usePostStore();
const { theme } = useGlobalData(); const { theme } = useGlobalData();
interface Props {
/** 预设分类筛选 */
presetCategory?: string;
}
const props = withDefaults(defineProps<Props>(), {
presetCategory: "",
});
/** 设置面板是否打开 */ /** 设置面板是否打开 */
const isSettingsOpen = ref(false); const isSettingsOpen = ref(false);
/** 设置面板的 DOM 引用 */ /** 设置面板的 DOM 引用 */
@@ -14,7 +23,7 @@ const settingsPanelRef = ref<HTMLElement | null>(null);
/** 触发按钮的 DOM 引用 */ /** 触发按钮的 DOM 引用 */
const settingsTriggerRef = ref<HTMLElement | null>(null); const settingsTriggerRef = ref<HTMLElement | null>(null);
/** 选中的分类(单选,空字符串代表全部) */ /** 选中的分类(单选,空字符串代表全部) */
const selectedCategory = ref(""); const selectedCategory = ref(props.presetCategory);
/** 选中的标签(多选,空数组代表全部) */ /** 选中的标签(多选,空数组代表全部) */
const selectedTags = ref<string[]>([]); const selectedTags = ref<string[]>([]);
/** 排序字段: `date` | `title` */ /** 排序字段: `date` | `title` */
@@ -81,7 +90,7 @@ const columnCount = computed(() => {
/** /**
* 获取仅经过筛选(未排序)的文章列表 * 获取仅经过筛选(未排序)的文章列表
* 逻辑:筛选分类和标签 * 筛选分类和标签
*/ */
const filteredRawList = computed(() => { const filteredRawList = computed(() => {
let posts = [...(postsStore.posts || [])]; let posts = [...(postsStore.posts || [])];
@@ -101,9 +110,36 @@ const filteredRawList = computed(() => {
return posts; return posts;
}); });
/**
* 获取当前分类下的文章标签
* 如果当前有选中的分类,则只返回该分类下的文章所出现的标签
*/
const currentCategoryTags = computed(() => {
if (!selectedCategory.value) {
return postsStore.allTags;
}
const tagSet = new Set<string>();
postsStore.posts.forEach((post) => {
if (post.categories.includes(selectedCategory.value)) {
post.tags.forEach((tag) => tagSet.add(tag));
}
});
return Array.from(tagSet);
});
/**
* 判断是否有预设分类
* 如果有预设分类则分类section应该隐藏
*/
const hasPresetCategory = computed(() => {
return props.presetCategory !== "";
});
/** /**
* 对筛选后的列表进行排序 * 对筛选后的列表进行排序
* 逻辑:根据 sortField 和 sortOrder 排序 * 根据 sortField 和 sortOrder 排序
*/ */
const sortedArticlesList = useSorted(filteredRawList, (a, b) => { const sortedArticlesList = useSorted(filteredRawList, (a, b) => {
let comparison = 0; let comparison = 0;
@@ -310,7 +346,7 @@ const clearTags = () => {
</div> </div>
</div> </div>
<div class="section"> <div v-if="!hasPresetCategory" class="section">
<div class="section-header"> <div class="section-header">
<h6>分类 <span v-if="selectedCategory" @click="selectedCategory = ''">clear</span></h6> <h6>分类 <span v-if="selectedCategory" @click="selectedCategory = ''">clear</span></h6>
</div> </div>
@@ -333,7 +369,7 @@ const clearTags = () => {
</div> </div>
<div class="chip-container"> <div class="chip-container">
<MaterialChip <MaterialChip
v-for="tag in postsStore.allTags" v-for="tag in currentCategoryTags"
:key="tag" :key="tag"
:color="selectedTags.includes(tag) ? 'tonal' : 'outlined'" :color="selectedTags.includes(tag) ? 'tonal' : 'outlined'"
:icon="selectedTags.includes(tag) ? 'check' : ''" :icon="selectedTags.includes(tag) ? 'check' : ''"

View File

@@ -28,7 +28,7 @@ const siteVersion = theme.value.siteVersion;
</div> </div>
<div class="beian-info"> <div class="beian-info">
<div> <div>
<a href="https://beian.miit.gov.cn/" target="_blank">黑ICP备2024016516号-1</a> <a href="https://beian.miit.gov.cn/" target="_blank">黑ICP备2024016516号</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -174,7 +174,9 @@ if (isClient()) {
<template> <template>
<nav class="NavBar" :class="navClass"> <nav class="NavBar" :class="navClass">
<div class="fab-container"> <div class="fab-container">
<MaterialButton color="text" :icon="navStateStore.isNavExpanded ? 'menu_open' : 'menu'" @click="toggleNav" /> <ClientOnly>
<MaterialButton color="text" :icon="navStateStore.isNavExpanded ? 'menu_open' : 'menu'" @click="toggleNav" />
</ClientOnly>
<button class="fab" @mousedown.prevent @click.stop="toggleSearch"> <button class="fab" @mousedown.prevent @click.stop="toggleSearch">
<span>{{ searchStateStore.isSearchActive ? "close" : "search" }}</span> <span>{{ searchStateStore.isSearchActive ? "close" : "search" }}</span>
<p :ref="(el) => setLabelRef(el, '.fab')">搜索</p> <p :ref="(el) => setLabelRef(el, '.fab')">搜索</p>
@@ -202,15 +204,17 @@ if (isClient()) {
</div> </div>
<div class="actions"> <div class="actions">
<MaterialButton <ClientOnly>
class="theme-btn" <MaterialButton
size="m" class="theme-btn"
color="text" size="m"
:title="themeStateStore.currentLabel" color="text"
:icon="themeStateStore.currentIcon" :title="themeStateStore.currentLabel"
@click="toggleTheme" :icon="themeStateStore.currentIcon"
> @click="toggleTheme"
</MaterialButton> >
</MaterialButton>
</ClientOnly>
</div> </div>
</nav> </nav>
</template> </template>

View File

@@ -48,11 +48,21 @@ const copyShortLink = async () => {
const collectHeadings = () => { const collectHeadings = () => {
if (!isClient()) return; if (!isClient()) return;
const nodes = Array.from(document.querySelectorAll("h1[id], h2[id]")) as HTMLElement[]; const nodes = Array.from(document.querySelectorAll("h1[id], h2[id]")) as HTMLElement[];
headings.value = nodes.map((n) => ({
id: n.id, headings.value = nodes.map((n) => {
text: n.textContent?.trim() || n.id, // 克隆节点并移除行内锚点
level: +n.tagName.replace("H", ""), const clone = n.cloneNode(true) as HTMLElement;
})); const inlineAnchors = Array.from(clone.querySelectorAll(".AnchorLink.inline")) as HTMLElement[];
inlineAnchors.forEach((el) => el.remove());
const text = clone.textContent?.trim() || n.id;
return {
id: n.id,
text,
level: +n.tagName.replace("H", ""),
};
});
}; };
/** 更新指示器(高亮边框)的位置和尺寸 */ /** 更新指示器(高亮边框)的位置和尺寸 */

View File

@@ -0,0 +1,20 @@
import { useClipboard } from "@vueuse/core";
/**
* 复制锚点链接到剪贴板
* @param href - 锚点 URL
* @param onCopied - 复制成功后的回调函数
*/
export const useAchorLink = (href: string, onCopied?: () => void) => {
const { copy: copyToClipboard } = useClipboard();
const handleCopy = async () => {
const anchorId = href.startsWith("#") ? href : `#${href}`;
const fullUrl = `${window.location.origin}${window.location.pathname}${anchorId}`;
await copyToClipboard(fullUrl);
onCopied?.();
};
return { handleCopy };
};

View File

@@ -6,6 +6,7 @@ import Default from "./layouts/Default.vue";
import NotFound from "./layouts/NotFound.vue"; import NotFound from "./layouts/NotFound.vue";
// Components // Components
import AnchorLink from "./components/AnchorLink.vue";
import AppBar from "./components/AppBar.vue"; import AppBar from "./components/AppBar.vue";
import ArticleMasonry from "./components/ArticleMasonry.vue"; import ArticleMasonry from "./components/ArticleMasonry.vue";
import Button from "./components/Button.vue"; import Button from "./components/Button.vue";
@@ -33,6 +34,7 @@ export default {
app.component("MainLayout", Default); app.component("MainLayout", Default);
app.component("AnchorLink", AnchorLink);
app.component("AppBar", AppBar); app.component("AppBar", AppBar);
app.component("ArticleMasonry", ArticleMasonry); app.component("ArticleMasonry", ArticleMasonry);
app.component("ButtonGroup", ButtonGroup); app.component("ButtonGroup", ButtonGroup);

View File

@@ -85,25 +85,6 @@ const openImageViewer = (index: number, event: MouseEvent) => {
showImageViewer.value = true; showImageViewer.value = true;
}; };
/** 复制锚点链接到剪贴板 */
const handleAnchorClick = (event: MouseEvent) => {
const anchor = (event.target as HTMLElement).closest("a.title-anchor") as HTMLAnchorElement;
if (!anchor) return;
const href = anchor.getAttribute("href");
const fullUrl = `${window.location.origin}${window.location.pathname}${href}`;
copyToClipboard(fullUrl);
const label = anchor.querySelector("span.visually-hidden");
if (label) {
const originalText = label.textContent;
label.textContent = "已复制";
setTimeout(() => {
label.textContent = originalText;
}, 1000);
}
};
/** 处理列表 Bullet 旋转和有序列表对齐 */ /** 处理列表 Bullet 旋转和有序列表对齐 */
const enhanceDomStyles = () => { const enhanceDomStyles = () => {
if (!articleContentRef.value) return; if (!articleContentRef.value) return;
@@ -156,20 +137,20 @@ if (isClient()) {
<template> <template>
<Header /> <Header />
<main id="article-content" ref="articleContentRef" @click="handleAnchorClick"> <main id="article-content" ref="articleContentRef">
<Content /> <Content />
<PrevNext /> <PrevNext />
</main> </main>
<aside id="article-aside"> <aside id="article-aside">
<div class="post-info"> <ClientOnly>
<p v-if="frontmatter.description" class="description">{{ frontmatter.description }}</p> <div class="post-info">
<p v-if="formattedPublishDate" class="date-publish">发布于 {{ formattedPublishDate }}</p> <p v-if="frontmatter.description" class="description">{{ frontmatter.description }}</p>
<ClientOnly> <p v-if="formattedPublishDate" class="date-publish">发布于 {{ formattedPublishDate }}</p>
<p v-if="formattedLastUpdated" :title="lastUpdatedRawTime" class="date-update"> <p v-if="formattedLastUpdated" :title="lastUpdatedRawTime" class="date-update">
{{ formattedLastUpdated }} {{ formattedLastUpdated }}
</p> </p>
</ClientOnly> </div>
</div> </ClientOnly>
<ButtonGroup v-if="frontmatter?.external_links" :links="frontmatter.external_links" size="m" layout="vertical" /> <ButtonGroup v-if="frontmatter?.external_links" :links="frontmatter.external_links" size="m" layout="vertical" />
<PageIndicator /> <PageIndicator />
</aside> </aside>

View File

@@ -0,0 +1,102 @@
@use "../mixin";
.AnchorLink {
@include mixin.typescale-style("body-large");
transition: var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect);
user-select: none;
-moz-user-select: none;
&.normal {
display: flex;
align-items: start;
flex-direction: column;
gap: 3px;
position: absolute;
left: -60px;
top: -6px;
margin-block-start: 0px !important;
height: calc(100% + 6px);
width: 60px;
border-radius: var(--md-sys-shape-corner-full);
opacity: 0;
z-index: 999;
.symbol {
@include mixin.material-symbols($size: 24);
display: block;
position: sticky;
top: 0px;
height: 54px;
width: 54px;
color: var(--md-sys-color-on-surface);
line-height: 54px;
text-align: center;
text-decoration: none !important;
border-radius: var(--md-sys-shape-corner-full);
background-color: transparent;
opacity: 0;
&:hover {
background-color: var(--md-sys-color-surface-container-low);
}
&:focus-visible {
opacity: 1;
}
}
.feedback {
@include mixin.typescale-style("label-medium");
position: sticky;
top: 57px;
padding-block: 3px;
padding-inline: 9px;
word-break: keep-all;
border-radius: var(--md-sys-shape-corner-small);
background-color: var(--md-sys-color-surface-container-low);
}
&:focus-within {
opacity: 1;
}
}
&.inline {
@include mixin.material-symbols($size: 24);
display: none;
color: var(--md-sys-color-on-surface);
text-decoration: none !important;
opacity: 0.2;
}
@media screen and (max-width: 1600px) {
&.normal {
display: none;
}
&.inline {
display: inline-block;
}
}
}

View File

@@ -233,6 +233,10 @@
transition: opacity var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect); transition: opacity var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect);
} }
} }
&.hidden {
top: 0px;
}
} }
&.scroll { &.scroll {
@@ -246,10 +250,8 @@
&.hidden { &.hidden {
top: -64px; top: -64px;
} }
}
@media screen and (max-width: 840px) { @media screen and (max-width: 840px) {
.AppBar {
top: 0; top: 0;
width: 100%; width: 100%;

View File

@@ -125,7 +125,7 @@
&:nth-of-type(1) { &:nth-of-type(1) {
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
filter: url(#noise-filter); filter: url(#noise-filter);
mask-image: linear-gradient(to right, black 0%, transparent 60%); mask-image: linear-gradient(to right, black 0%, transparent 100%);
mix-blend-mode: screen; mix-blend-mode: screen;
z-index: 1; z-index: 1;
} }
@@ -133,12 +133,6 @@
&:nth-of-type(2) { &:nth-of-type(2) {
z-index: 0; z-index: 0;
} }
@media screen and (max-width: 840px) {
&:nth-of-type(1) {
mask-image: linear-gradient(to right, black 0%, transparent 100%);
}
}
} }
} }
@@ -350,10 +344,6 @@
} }
img { img {
&:nth-of-type(1) {
transition: 5s var(--md-sys-motion-spring-slow-effect);
}
&:nth-of-type(2) { &:nth-of-type(2) {
transition: var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-default-effect) transition: var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-default-effect)
var(--md-sys-motion-spring-fast-effect-duration); var(--md-sys-motion-spring-fast-effect-duration);
@@ -380,11 +370,6 @@
} }
img { img {
&:nth-of-type(1) {
opacity: 0;
transform: translateX(-25%);
}
&:nth-of-type(2) { &:nth-of-type(2) {
opacity: 0; opacity: 0;
transform: scale(1.005); transform: scale(1.005);

View File

@@ -30,10 +30,6 @@ table {
tr { tr {
th { th {
min-width: 10ch;
padding: 16px 24px;
text-transform: capitalize; text-transform: capitalize;
background-color: var(--md-sys-color-surface-variant); background-color: var(--md-sys-color-surface-variant);
@@ -52,8 +48,6 @@ table {
vertical-align: top; vertical-align: top;
td { td {
padding: 16px 24px;
vertical-align: inherit; vertical-align: inherit;
code { code {
@@ -63,7 +57,12 @@ table {
} }
} }
:is(td, th) { td,
th {
min-width: 16ch;
padding: 16px 24px;
border: 1px solid var(--md-sys-color-outline); border: 1px solid var(--md-sys-color-outline);
&:empty { &:empty {

View File

@@ -358,95 +358,13 @@
text-decoration: none; text-decoration: none;
} }
} }
.title-anchor {
@include mixin.typescale-style("body-large");
display: inline-flex;
align-items: center;
flex-direction: column;
gap: 3px;
position: absolute;
left: -60px;
top: 0px;
height: 54px;
width: 54px;
color: var(--md-sys-color-on-surface);
text-decoration: none;
border-radius: var(--md-sys-shape-corner-full);
opacity: 0;
transition: var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect);
user-select: none;
-moz-user-select: none;
&:focus-visible {
opacity: 1;
z-index: 2;
span:nth-of-type(1) {
opacity: 1;
}
}
span:nth-of-type(1) {
@include mixin.material-symbols($size: 24);
display: block;
height: 54px;
width: 54px;
line-height: 54px;
text-align: center;
border-radius: var(--md-sys-shape-corner-full);
background-color: transparent;
opacity: 0;
}
span.visually-hidden {
@include mixin.typescale-style("label-medium");
padding-block: 3px;
padding-inline: 9px;
word-break: keep-all;
border-radius: var(--md-sys-shape-corner-small);
background-color: var(--md-sys-color-surface-container-low);
opacity: 0;
visibility: hidden;
}
}
} }
&:hover { &:hover .AnchorLink {
.title-anchor { opacity: 1;
.symbol {
opacity: 1; opacity: 1;
span:nth-of-type(1) {
opacity: 1;
}
&:hover {
span:nth-of-type(1):hover {
background-color: var(--md-sys-color-surface-container-low);
+ span.visually-hidden {
opacity: 1;
visibility: visible;
}
}
}
} }
} }
} }

View File

@@ -0,0 +1,82 @@
import type MarkdownIt from "markdown-it";
/** 锚点配置项 */
interface AnchorOptions {
/** 锚点层级,例如 [1, 2, 3] 代表 h1, h2, h3 */
levels?: number[];
/** 锚点显示的字符串 */
symbol?: string;
/** 锚点的类名称 */
class?: string;
}
/**
* 为标题添加可复制的锚点链接,将同时生成两种锚点:
* 1. 标题行内锚点
* 2. 组件锚点
* @param mdit - MarkdownIt 实例
* @param options - 锚点配置项
*/
export function anchor(mdit: MarkdownIt, options: AnchorOptions = {}): void {
const { levels = [1, 2, 3, 4, 5, 6], symbol = "link", class: className = "AnchorLink" } = options;
// 在 core 规则中拦截 tokens在每个 heading 后添加 AnchorLink 组件以及行内锚点
mdit.core.ruler.push("add_anchor_links", (state) => {
const tokens = state.tokens;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
// 在 heading_close 处进行处理,此时 heading_open 和 inline 都在前面
if (token.type === "heading_close") {
const level = parseInt(token.tag.slice(1));
if (!levels.includes(level)) continue;
const headingOpenToken = tokens[i - 2];
const inlineToken = tokens[i - 1];
const idAttr = headingOpenToken?.attrGet("id");
if (idAttr) {
// 移除默认渲染的锚点VitePress 默认会添加 header-anchor
if (inlineToken && inlineToken.children) {
let anchorStartIndex = -1;
let anchorEndIndex = -1;
for (let j = 0; j < inlineToken.children.length; j++) {
const child = inlineToken.children[j];
// 查找带有 header-anchor 类名的 link_open
if (child.type === "link_open" && child.attrGet("class")?.includes("header-anchor")) {
anchorStartIndex = j;
}
// 找到对应的 link_close
if (anchorStartIndex !== -1 && child.type === "link_close") {
anchorEndIndex = j;
break;
}
}
// 如果找到了默认锚点,将其从 children 中移除
if (anchorStartIndex !== -1 && anchorEndIndex !== -1) {
inlineToken.children.splice(anchorStartIndex, anchorEndIndex - anchorStartIndex + 1);
}
}
// 添加标题行内锚点
if (inlineToken && inlineToken.children) {
const inlineAnchor = new state.Token("html_inline", "", 0);
inlineAnchor.content = `<a href="#${idAttr}" class="${className} inline">${symbol}</a>`;
// 将行内锚点添加到标题文本的末端
inlineToken.children.push(inlineAnchor);
}
// 添加 AnchorLink 组件
const anchorToken = new state.Token("html_block", "", 0);
anchorToken.content = `<AnchorLink href="#${idAttr}" symbol="${symbol}" className="${className} normal" />`;
// 将组件插入到 heading_close 之后
tokens.splice(i + 1, 0, anchorToken);
i++; // 跳过新插入的 token
}
}
}
});
}

37
Caddyfile Normal file
View File

@@ -0,0 +1,37 @@
:80 {
# 根目录指向构建产物
root * /app
# 关闭访问日志
log {
output discard
}
# 启用压缩
encode gzip zstd
# 文件服务器配置
try_files {path} {path}.html {path}/
file_server
# 错误页面处理
handle_errors {
@404 {
expression {http.error.status_code} == 404
}
@403 {
expression {http.error.status_code} == 403
}
rewrite @404 /404.html
rewrite @403 /404.html
file_server
}
# 静态资源缓存配置
@assets {
path /assets/*
}
header @assets {
Cache-Control "public, immutable, max-age=31536000"
}
}

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# 构建阶段
FROM node:trixie-slim AS builder
# 安装 git
RUN apt-get update && apt-get install -y git \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 拉取项目代码
RUN git clone https://github.com/sendevia/website . --depth=1
# 安装依赖并构建
RUN npm i && npm run docs:build
# 最终阶段
FROM caddy:alpine AS final
# 从构建阶段复制 dist 产物到工作目录
COPY --from=builder /app/.vitepress/dist /app
# 复制Caddyfile配置文件
COPY Caddyfile /etc/caddy/Caddyfile
# 启动Caddy服务器
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]

View File

@@ -1,5 +1,5 @@
{ {
"version": "26.2.28(305)", "version": "26.3.8.332",
"scripts": { "scripts": {
"update-version": "bash ./scripts/update-version.sh", "update-version": "bash ./scripts/update-version.sh",
"docs:dev": "vitepress dev", "docs:dev": "vitepress dev",
@@ -18,7 +18,6 @@
"@mdit/plugin-img-mark": "^0.23.0", "@mdit/plugin-img-mark": "^0.23.0",
"@mdit/plugin-tasklist": "^0.23.0", "@mdit/plugin-tasklist": "^0.23.0",
"@waline/client": "^3.13.0", "@waline/client": "^3.13.0",
"markdown-it-anchor": "^9.2.0",
"sass-embedded": "^1.97.3", "sass-embedded": "^1.97.3",
"vitepress": "^2.0.0-alpha.16", "vitepress": "^2.0.0-alpha.16",
"vue": "^3.5.29" "vue": "^3.5.29"

View File

@@ -2,7 +2,7 @@
title: "AincradMix" title: "AincradMix"
description: "一个 osu! 皮肤" description: "一个 osu! 皮肤"
color: "#8400db" color: "#8400db"
impression: impression:
- /assets/images/22/s0_amix_vision.webp - /assets/images/22/s0_amix_vision.webp
- /assets/images/22/screenshot01.webp - /assets/images/22/screenshot01.webp
categories: categories:
@@ -12,7 +12,7 @@ tags:
- osu! - osu!
- 示例文章 - 示例文章
- 作品介绍 - 作品介绍
date: 2022-07-04T04:00:00Z date: 2022-07-04T04:00:00+08
external_links: external_links:
- type: download - type: download
label: 直连下载 label: 直连下载
@@ -29,7 +29,7 @@ external_links:
1. 本皮肤通过游戏补丁的方式,做到了覆盖几乎全部的 osu! 界面元素。 1. 本皮肤通过游戏补丁的方式,做到了覆盖几乎全部的 osu! 界面元素。
2. 结合更现代的设计,扩展了《刀剑神域》中出现的界面设计。 2. 结合更现代的设计,扩展了《刀剑神域》中出现的界面设计。
::: :::
# 优点 # 优点
@@ -46,6 +46,7 @@ external_links:
- 本皮肤所提供的 dll 文件仅替换了图像资源,未做其他修改。如果不放心,你可以使用 dnSpy 自行替换文件 - 本皮肤所提供的 dll 文件仅替换了图像资源,未做其他修改。如果不放心,你可以使用 dnSpy 自行替换文件
[^1]: 通过修改 osu! 的资源 dll。 [^1]: 通过修改 osu! 的资源 dll。
[^2]: 此为估计得出。 [^2]: 此为估计得出。
# 皮肤预览 # 皮肤预览

View File

@@ -7,7 +7,7 @@ categories:
- 博客主题 - 博客主题
tags: tags:
- readme - readme
date: 2026-02-25T21:44:00Z date: 2026-02-25T21:44:00+08
external_links: external_links:
- type: download - type: download
icon: rocket_launch icon: rocket_launch

View File

@@ -0,0 +1,144 @@
---
title: "Button group"
description: "按钮组组件"
color: "#42b883"
categories:
- 组件
tags:
- Vue 3
- 开发文档
- UI
date: 2026-02-25T21:20:00+08
---
# ButtonGroup 按钮组组件
`ButtonGroup` 是一个用于聚合多个 `MaterialButton` 的容器组件。它支持灵活的布局切换、统一的属性默认值配置以及全局事件委托监听。
## 核心特性
- **布局自适应**支持水平Horizontal和垂直Vertical两种排列方式。
- **属性继承与覆盖**:可以在组级别设置默认的尺寸、颜色、图标等,也可以在具体的按钮项中进行个性化覆盖。
- **智能类型识别**:内置了对 `download``normal` 类型的样式及图标自动匹配逻辑。
- **事件委托**:支持统一监听 `@click` 事件,便捷获取点击的项信息及索引。
## 组件属性 (Props)
| 属性名 | 类型 | 默认值 | 可选值 | 说明 |
| :---------- | :--------------- | :------------- | :---------------------------------- | :--------------- |
| `links` | `ExternalLink[]` | `[]` | - | 按钮配置数组 |
| `layout` | `string` | `"horizontal"` | `"horizontal"`, `"vertical"` | 布局方向 |
| `size` | `string` | `"s"` | `"xs"`, `"s"`, `"m"`, `"l"`, `"xl"` | 默认按钮尺寸 |
| `color` | `string` | - | - | 默认按钮颜色样式 |
| `icon` | `string` | - | - | 默认按钮图标 |
| `target` | `string` | - | - | 默认链接打开方式 |
| `ariaLabel` | `string` | - | - | 组的无障碍标签 |
## 组件事件 (Emits)
| 事件名 | 回调参数 | 说明 |
| :------- | :-------------------------------------------------- | :------------------------- |
| `@click` | `(event: Event, item: ExternalLink, index: number)` | 当组内任意按钮被点击时触发 |
## 按钮配置项 (ExternalLink)
每一项 `links` 中的对象支持以下配置:
| 属性名 | 类型 | 说明 |
| :---------- | :--------- | :------------------------------------------------------ |
| `label` | `string` | 按钮显示的文本内容 |
| `id` | `string` | (可选) 自定义标识符,方便在事件处理中识别 |
| `link` | `string` | (可选) 点击跳转的链接地址 |
| `type` | `string` | (可选) 预设类型:`download` (充满色), `normal` (色调色) |
| `icon` | `string` | (可选) 覆盖组设置的图标 |
| `color` | `string` | (可选) 覆盖组设置的颜色 |
| `size` | `string` | (可选) 覆盖组设置的尺寸 |
| `target` | `string` | (可选) 覆盖组设置的打开方式 |
| `ariaLabel` | `string` | (可选) 按钮的无障碍标签 |
| `onClick` | `Function` | (可选) 单体独立的点击回调函数 |
## 使用示例
### 1. 基础用法 (水平排列)
```vue
<ButtonGroup
:links="[
{ label: '查看详情', id: 'detail' },
{ label: '下载资源', id: 'download', type: 'download' },
]"
@click="(e, item) => console.log('点击了:', item.id)"
/>
```
<ButtonGroup
:links="[
{ label: '查看详情', id: 'detail' },
{ label: '下载资源', id: 'download', type: 'download' }
]"
@click="(e, item) => console.log('点击了:', item.id)"
/>
### 2. 垂直排列与尺寸控制
```vue
<ButtonGroup
layout="vertical"
size="l"
:links="[
{ label: '选项一', icon: 'settings' },
{ label: '选项二', icon: 'person' },
]"
/>
```
<ButtonGroup
layout="vertical"
size="l"
:links="[
{ label: '选项一', icon: 'settings' },
{ label: '选项二', icon: 'person' }
]"
/>
### 3. 混合图标与文本
```vue
<ButtonGroup
size="m"
:links="[
{ id: 'prev', icon: 'chevron_left', ariaLabel: '上一页' },
{ id: 'index', label: '1 / 5', color: 'tonal' },
{ id: 'next', icon: 'chevron_right', ariaLabel: '下一页' },
]"
/>
```
<ButtonGroup
size="m"
:links="[
{ id: 'prev', icon: 'chevron_left', ariaLabel: '上一页' },
{ id: 'index', label: '1 / 5', color: 'tonal' },
{ id: 'next', icon: 'chevron_right', ariaLabel: '下一页' }
]"
/>
### 4. 链接跳转
```vue
<ButtonGroup
target="_blank"
:links="[
{ label: 'GitHub', link: 'https://github.com', icon: 'code' },
{ label: '首页', link: '/', icon: 'home' },
]"
/>
```
<ButtonGroup
target="_blank"
:links="[
{ label: 'GitHub', link: 'https://github.com', icon: 'code' },
{ label: '首页', link: '/', icon: 'home' }
]"
/>

View File

@@ -8,7 +8,7 @@ tags:
- markdown 语法 - markdown 语法
- 示例文章 - 示例文章
- 组件示例 - 组件示例
date: 2007-08-31T00:00:00Z date: 2007-08-31T00:00:00+08
--- ---
# 语法高亮 # 语法高亮
@@ -99,14 +99,14 @@ MaterialButton 组件是一个通用的按钮组件,支持多种样式和功
#### 属性列表 #### 属性列表
| 属性 | 类型 | 默认值 | 可选值 | 描述 | | 属性 | 类型 | 默认值 | 可选值 | 描述 |
| ---- | ---- | ---- | ---- | ---- | | -------- | -------- | ---------- | ------------------------------------------------------------------------- | -------------------------- |
| `shape` | `string` | `"round"` | `"round"`, `"square"` | 按钮形状 | | `shape` | `string` | `"round"` | `"round"`, `"square"` | 按钮形状 |
| `size` | `string` | `"s"` | `"xs"`, `"s"`, `"m"`, `"l"`, `"xl"` | 按钮尺寸 | | `size` | `string` | `"s"` | `"xs"`, `"s"`, `"m"`, `"l"`, `"xl"` | 按钮尺寸 |
| `color` | `string` | `"filled"` | `"elevated"`, `"filled"`, `"tonal"`, `"outlined"`, `"standard"`, `"text"` | 按钮颜色样式 | | `color` | `string` | `"filled"` | `"elevated"`, `"filled"`, `"tonal"`, `"outlined"`, `"standard"`, `"text"` | 按钮颜色样式 |
| `icon` | `string` | - | Material Icons 名称 | 按钮图标 | | `icon` | `string` | - | Material Icons 名称 | 按钮图标 |
| `href` | `string` | - | 任意URL | 如果提供,按钮将渲染为链接 | | `href` | `string` | - | 任意URL | 如果提供,按钮将渲染为链接 |
| `target` | `string` | `"_blank"` | `"_blank"`, `"_self"`, `"_parent"`, `"_top"` | 链接打开方式 | | `target` | `string` | `"_blank"` | `"_blank"`, `"_self"`, `"_parent"`, `"_top"` | 链接打开方式 |
#### 使用示例 #### 使用示例
@@ -140,34 +140,30 @@ Card 组件用于展示内容卡片,支持多种变体和样式。
#### 属性列表 #### 属性列表
| 属性 | 类型 | 默认值 | 可选值 | 描述 | | 属性 | 类型 | 默认值 | 可选值 | 描述 |
| ---- | ---- | ---- | ---- | ---- | | -------------- | ---------- | ---------- | -------------------------------------- | ------------ |
| `variant` | `string` | `"feed"` | `"feed"` | 卡片变体 | | `variant` | `string` | `"feed"` | `"feed"` | 卡片变体 |
| `size` | `string` | `"m"` | `"s"`, `"m"`, `"l"` | 卡片尺寸 | | `size` | `string` | `"m"` | `"s"`, `"m"`, `"l"` | 卡片尺寸 |
| `color` | `string` | `"filled"` | `"elevated"`, `"filled"`, `"outlined"` | 卡片颜色样式 | | `color` | `string` | `"filled"` | `"elevated"`, `"filled"`, `"outlined"` | 卡片颜色样式 |
| `title` | `string` | - | 任意字符串 | 卡片标题 | | `title` | `string` | - | 任意字符串 | 卡片标题 |
| `description` | `string` | - | 任意字符串 | 卡片描述 | | `description` | `string` | - | 任意字符串 | 卡片描述 |
| `date` | `string` | - | 日期字符串 | 发布日期 | | `date` | `string` | - | 日期字符串 | 发布日期 |
| `tags` | `string[]` | - | 字符串数组 | 标签列表 | | `tags` | `string[]` | - | 字符串数组 | 标签列表 |
| `category` | `string[]` | - | 字符串数组 | 分类列表 | | `category` | `string[]` | - | 字符串数组 | 分类列表 |
| `impression` | `string[]` | `[]` | 图片URL数组 | 印象图片 | | `impression` | `string[]` | `[]` | 图片URL数组 | 印象图片 |
| `href` | `string` | - | 任意URL | 卡片链接 | | `href` | `string` | - | 任意URL | 卡片链接 |
| `downloadable` | `boolean` | `false` | `true`, `false` | 是否可下载 | | `downloadable` | `boolean` | `false` | `true`, `false` | 是否可下载 |
#### 使用示例 #### 使用示例
```vue ```vue
<!-- 基础卡片 --> <!-- 基础卡片 -->
<Card <Card title="文章标题" description="这是一段文章描述,简要介绍文章内容。" date="2023-10-01" />
title="文章标题"
description="这是一段文章描述,简要介绍文章内容。"
date="2023-10-01"
/>
``` ```
```vue ```vue
<!-- 带图片卡片 --> <!-- 带图片卡片 -->
<Card <Card
title="项目展示" title="项目展示"
description="展示一个有趣的项目" description="展示一个有趣的项目"
impression="['/assets/images/project1.jpg', '/assets/images/project2.jpg']" impression="['/assets/images/project1.jpg', '/assets/images/project2.jpg']"
@@ -177,22 +173,12 @@ Card 组件用于展示内容卡片,支持多种变体和样式。
```vue ```vue
<!-- 可下载资源卡片 --> <!-- 可下载资源卡片 -->
<Card <Card title="资源下载" description="包含可下载的文件资源" downloadable tags="['资源', '下载']" category="['教程']" />
title="资源下载"
description="包含可下载的文件资源"
downloadable
tags="['资源', '下载']"
category="['教程']"
/>
``` ```
```vue ```vue
<!-- 不同样式卡片 --> <!-- 不同样式卡片 -->
<Card <Card title="轮廓样式" description="使用轮廓样式的卡片" color="outlined" />
title="轮廓样式"
description="使用轮廓样式的卡片"
color="outlined"
/>
``` ```
### ButtonGroup 组件 (ButtonGroup.vue) ### ButtonGroup 组件 (ButtonGroup.vue)
@@ -201,19 +187,19 @@ ButtonGroup 组件用于将多个按钮组合在一起,支持水平和垂直
#### 属性列表 #### 属性列表
| 属性 | 类型 | 默认值 | 可选值 | 描述 | | 属性 | 类型 | 默认值 | 可选值 | 描述 |
| ---- | ---- | ---- | ---- | ---- | | -------- | ---------------- | -------------- | ----------------------------------- | ------------ |
| `links` | `ExternalLink[]` | `[]` | 链接对象数组 | 按钮链接列表 | | `links` | `ExternalLink[]` | `[]` | 链接对象数组 | 按钮链接列表 |
| `size` | `string` | `"s"` | `"xs"`, `"s"`, `"m"`, `"l"`, `"xl"` | 按钮尺寸 | | `size` | `string` | `"s"` | `"xs"`, `"s"`, `"m"`, `"l"`, `"xl"` | 按钮尺寸 |
| `layout` | `string` | `"horizontal"` | `"horizontal"`, `"vertical"` | 布局方向 | | `layout` | `string` | `"horizontal"` | `"horizontal"`, `"vertical"` | 布局方向 |
#### ExternalLink 类型 #### ExternalLink 类型
```typescript ```typescript
interface ExternalLink { interface ExternalLink {
type: string; // 链接类型:'download' 或 'normal' type: string; // 链接类型:'download' 或 'normal'
label: string; // 按钮标签文本 label: string; // 按钮标签文本
link: string; // 链接地址 link: string; // 链接地址
} }
``` ```
@@ -225,7 +211,7 @@ interface ExternalLink {
:links="[ :links="[
{ type: 'normal', label: '查看文档', link: '/docs' }, { type: 'normal', label: '查看文档', link: '/docs' },
{ type: 'download', label: '下载资源', link: '/downloads/file.zip' }, { type: 'download', label: '下载资源', link: '/downloads/file.zip' },
{ type: 'normal', label: 'GitHub', link: 'https://github.com/example' } { type: 'normal', label: 'GitHub', link: 'https://github.com/example' },
]" ]"
size="m" size="m"
layout="horizontal" layout="horizontal"
@@ -236,26 +222,16 @@ interface ExternalLink {
:links="[ :links="[
{ type: 'normal', label: '选项一', link: '/option1' }, { type: 'normal', label: '选项一', link: '/option1' },
{ type: 'normal', label: '选项二', link: '/option2' }, { type: 'normal', label: '选项二', link: '/option2' },
{ type: 'normal', label: '选项三', link: '/option3' } { type: 'normal', label: '选项三', link: '/option3' },
]" ]"
size="s" size="s"
layout="vertical" layout="vertical"
/> />
<!-- 不同尺寸按钮组 --> <!-- 不同尺寸按钮组 -->
<ButtonGroup <ButtonGroup :links="[{ type: 'download', label: '小尺寸下载', link: '/download' }]" size="xs" />
:links="[
{ type: 'download', label: '小尺寸下载', link: '/download' }
]"
size="xs"
/>
<ButtonGroup <ButtonGroup :links="[{ type: 'normal', label: '大尺寸按钮', link: '/large' }]" size="xl" />
:links="[
{ type: 'normal', label: '大尺寸按钮', link: '/large' }
]"
size="xl"
/>
``` ```
# 更多信息 # 更多信息

View File

@@ -5,7 +5,7 @@ color: "#39c5bb"
impression: "/assets/images/132307491_p0.webp" impression: "/assets/images/132307491_p0.webp"
categories: categories:
tags: tags:
date: 2007-08-31T00:00:00Z date: 2007-08-31T00:00:00+08
--- ---
这只是一篇示例文章。 这只是一篇示例文章。

View File

@@ -1,144 +0,0 @@
---
title: "Button group"
description: "按钮组组件"
color: "#42b883"
categories:
- 开发文档
tags:
- Vue 3
- 组件
- UI
date: 2026-02-25T21:20:00Z
---
# ButtonGroup 按钮组组件
`ButtonGroup` 是一个用于聚合多个 `MaterialButton` 的容器组件。它支持灵活的布局切换、统一的属性默认值配置以及全局事件委托监听。
## 核心特性
- **布局自适应**支持水平Horizontal和垂直Vertical两种排列方式。
- **属性继承与覆盖**:可以在组级别设置默认的尺寸、颜色、图标等,也可以在具体的按钮项中进行个性化覆盖。
- **智能类型识别**:内置了对 `download``normal` 类型的样式及图标自动匹配逻辑。
- **事件委托**:支持统一监听 `@click` 事件,便捷获取点击的项信息及索引。
## 组件属性 (Props)
| 属性名 | 类型 | 默认值 | 可选值 | 说明 |
| :--- | :--- | :--- | :--- | :--- |
| `links` | `ExternalLink[]` | `[]` | - | 按钮配置数组 |
| `layout` | `string` | `"horizontal"` | `"horizontal"`, `"vertical"` | 布局方向 |
| `size` | `string` | `"s"` | `"xs"`, `"s"`, `"m"`, `"l"`, `"xl"` | 默认按钮尺寸 |
| `color` | `string` | - | - | 默认按钮颜色样式 |
| `icon` | `string` | - | - | 默认按钮图标 |
| `target` | `string` | - | - | 默认链接打开方式 |
| `ariaLabel` | `string` | - | - | 组的无障碍标签 |
## 组件事件 (Emits)
| 事件名 | 回调参数 | 说明 |
| :--- | :--- | :--- |
| `@click` | `(event: Event, item: ExternalLink, index: number)` | 当组内任意按钮被点击时触发 |
## 按钮配置项 (ExternalLink)
每一项 `links` 中的对象支持以下配置:
| 属性名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `label` | `string` | 按钮显示的文本内容 |
| `id` | `string` | (可选) 自定义标识符,方便在事件处理中识别 |
| `link` | `string` | (可选) 点击跳转的链接地址 |
| `type` | `string` | (可选) 预设类型:`download` (充满色), `normal` (色调色) |
| `icon` | `string` | (可选) 覆盖组设置的图标 |
| `color` | `string` | (可选) 覆盖组设置的颜色 |
| `size` | `string` | (可选) 覆盖组设置的尺寸 |
| `target` | `string` | (可选) 覆盖组设置的打开方式 |
| `ariaLabel` | `string` | (可选) 按钮的无障碍标签 |
| `onClick` | `Function` | (可选) 单体独立的点击回调函数 |
## 使用示例
### 1. 基础用法 (水平排列)
```vue
<ButtonGroup
:links="[
{ label: '查看详情', id: 'detail' },
{ label: '下载资源', id: 'download', type: 'download' }
]"
@click="(e, item) => console.log('点击了:', item.id)"
/>
```
<ButtonGroup
:links="[
{ label: '查看详情', id: 'detail' },
{ label: '下载资源', id: 'download', type: 'download' }
]"
@click="(e, item) => console.log('点击了:', item.id)"
/>
### 2. 垂直排列与尺寸控制
```vue
<ButtonGroup
layout="vertical"
size="l"
:links="[
{ label: '选项一', icon: 'settings' },
{ label: '选项二', icon: 'person' }
]"
/>
```
<ButtonGroup
layout="vertical"
size="l"
:links="[
{ label: '选项一', icon: 'settings' },
{ label: '选项二', icon: 'person' }
]"
/>
### 3. 混合图标与文本
```vue
<ButtonGroup
size="m"
:links="[
{ id: 'prev', icon: 'chevron_left', ariaLabel: '上一页' },
{ id: 'index', label: '1 / 5', color: 'tonal' },
{ id: 'next', icon: 'chevron_right', ariaLabel: '下一页' }
]"
/>
```
<ButtonGroup
size="m"
:links="[
{ id: 'prev', icon: 'chevron_left', ariaLabel: '上一页' },
{ id: 'index', label: '1 / 5', color: 'tonal' },
{ id: 'next', icon: 'chevron_right', ariaLabel: '下一页' }
]"
/>
### 4. 链接跳转
```vue
<ButtonGroup
target="_blank"
:links="[
{ label: 'GitHub', link: 'https://github.com', icon: 'code' },
{ label: '首页', link: '/', icon: 'home' }
]"
/>
```
<ButtonGroup
target="_blank"
:links="[
{ label: 'GitHub', link: 'https://github.com', icon: 'code' },
{ label: '首页', link: '/', icon: 'home' }
]"
/>

View File

@@ -3,11 +3,11 @@ title: "设置开机自启动的 Jekyll 服务"
description: "通过 systemd 实现一个开机自启的 Jekyll 服务,通常来说,这对使用 Jekyll 作为服务后端的网站很有用。" description: "通过 systemd 实现一个开机自启的 Jekyll 服务,通常来说,这对使用 Jekyll 作为服务后端的网站很有用。"
color: "#aa0c2b" color: "#aa0c2b"
impression: "/assets/images/120678678_p0.webp" impression: "/assets/images/120678678_p0.webp"
categories: categories:
- 随笔 - 随笔
tags: tags:
- jekyll - jekyll
date: 2024-07-11T04:00:00Z date: 2024-07-11T04:00:00+08
--- ---
::: info ::: info

View File

@@ -21,7 +21,7 @@ YEAR=$(date +%y)
MONTH=$(date +%-m) MONTH=$(date +%-m)
DAY=$(date +%-d) DAY=$(date +%-d)
NEW_VERSION="${YEAR}.${MONTH}.${DAY}(${NEXT_COMMIT_COUNT})" NEW_VERSION="${YEAR}.${MONTH}.${DAY}.${NEXT_COMMIT_COUNT}"
echo "📝 更新版本号..." echo "📝 更新版本号..."
# 使用 sed 更新 package.json 中的版本号 # 使用 sed 更新 package.json 中的版本号