Compare commits
44 Commits
26.2.6(272
...
a7e0528c24
| Author | SHA1 | Date | |
|---|---|---|---|
| a7e0528c24 | |||
| 29885ae607 | |||
| dabb2f6164 | |||
| 1247943a5e | |||
| b822fe6227 | |||
| f01232241f | |||
| c8b917bda5 | |||
| aa3cdbc695 | |||
| 2bc889a388 | |||
| c093e395bd | |||
| 2d879372d2 | |||
| 0493426eaa | |||
| 042342f923 | |||
| 13548b222e | |||
| adb804f249 | |||
| d91f8b90aa | |||
| 69d117152a | |||
| b6e7649a4f | |||
| 1bb864cf24 | |||
| 68448d29fe | |||
| cad4130789 | |||
| b4df562522 | |||
| 0a4f340e88 | |||
| 6a0cc5f5cb | |||
| 008160b3c9 | |||
| 416426d769 | |||
| e07acc8e35 | |||
| b1f8d6f15b | |||
| c7af2eeb0f | |||
| 1fbac018f9 | |||
| cbd3979476 | |||
| 5bcec415b3 | |||
| 1ada9579ce | |||
| 1215d1ca22 | |||
| 56264d3504 | |||
| 251b22f1b5 | |||
| 2c5cbfad01 | |||
| 42fedf3c3d | |||
| ecd4fb2886 | |||
| fff5f3ba2b | |||
| 5faa7e4220 | |||
| 568c7a7427 | |||
| bef3d5ce77 | |||
| 00b034f02b |
65
.github/workflows/docker-build.yml
vendored
Normal 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 }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
2
.gitignore
vendored
@@ -5,3 +5,5 @@ node_modules
|
||||
package-lock.json
|
||||
cache/
|
||||
dist/
|
||||
.agents
|
||||
nginx-config.conf
|
||||
|
||||
@@ -2,15 +2,17 @@ import { defineConfig } from "vitepress";
|
||||
import packageJson from "../package.json";
|
||||
|
||||
// markdown-it plugins
|
||||
// https://mdit-plugins.github.io/zh/align.html
|
||||
// https://mdit-plugins.github.io/align.html
|
||||
import { align } from "@mdit/plugin-align";
|
||||
// https://github.com/valeriangalliat/markdown-it-anchor
|
||||
import anchor from "markdown-it-anchor";
|
||||
// https://mdit-plugins.github.io/footnote.html
|
||||
import { footnote } from "@mdit/plugin-footnote";
|
||||
// https://mdit-plugins.github.io/tasklist.html
|
||||
import { tasklist } from "@mdit/plugin-tasklist";
|
||||
import { wrapHeadingsAsSections } from "./theme/utils/sectionWrapper";
|
||||
// https://mdit-plugins.github.io/img-mark.html
|
||||
import { imgMark } from "@mdit/plugin-img-mark";
|
||||
import { sectionWrapper } from "./theme/utils/mdSectionWrapper";
|
||||
import { table } from "./theme/utils/mdTable";
|
||||
import { anchor } from "./theme/utils/mdCustomAnchor";
|
||||
|
||||
export default defineConfig({
|
||||
base: "/",
|
||||
@@ -20,18 +22,6 @@ export default defineConfig({
|
||||
titleTemplate: ":title",
|
||||
description: "随便写写的博客",
|
||||
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: {
|
||||
allowedAttributes: ["id", "class"],
|
||||
},
|
||||
@@ -39,8 +29,13 @@ export default defineConfig({
|
||||
codeCopyButtonTitle: "复制代码",
|
||||
config(md) {
|
||||
md.use(align);
|
||||
md.use(anchor, {
|
||||
levels: [1, 2, 3, 4],
|
||||
});
|
||||
md.use(footnote);
|
||||
md.use(wrapHeadingsAsSections);
|
||||
md.use(imgMark);
|
||||
md.use(sectionWrapper);
|
||||
md.use(table);
|
||||
md.use(tasklist, { label: true });
|
||||
},
|
||||
image: {
|
||||
@@ -94,6 +89,7 @@ export default defineConfig({
|
||||
navSegment: [
|
||||
{ text: "首页", icon: "home", link: "/" },
|
||||
{ text: "AincradMix", icon: "borg", link: "/posts/AincradMix" },
|
||||
{ text: "组件", icon: "code_blocks", link: "/posts/组件" },
|
||||
{
|
||||
text: "作品集",
|
||||
icon: "auto_awesome_mosaic",
|
||||
|
||||
43
.vitepress/theme/components/AnchorLink.vue
Normal 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>
|
||||
@@ -1,33 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
/** 外部链接类型定义 */
|
||||
interface ExternalLink {
|
||||
type: string;
|
||||
label: string;
|
||||
link: string;
|
||||
ariaLabel?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
link?: string;
|
||||
size?: "xs" | "s" | "m" | "l" | "xl";
|
||||
target?: string;
|
||||
type?: string;
|
||||
onClick?: (e?: Event) => void;
|
||||
}
|
||||
|
||||
/** 组件属性定义 */
|
||||
interface Props {
|
||||
ariaLabel?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
layout?: "horizontal" | "vertical";
|
||||
links?: ExternalLink[];
|
||||
size?: "xs" | "s" | "m" | "l" | "xl";
|
||||
layout?: "horizontal" | "vertical";
|
||||
target?: string;
|
||||
}
|
||||
|
||||
/** 组件属性默认值 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
layout: "horizontal",
|
||||
links: () => [],
|
||||
size: "s",
|
||||
layout: "horizontal",
|
||||
});
|
||||
|
||||
const getButtonColor = (type: string) => {
|
||||
/** 组件事件定义 */
|
||||
const emit = defineEmits<{
|
||||
(e: "click", event: Event, item: ExternalLink, index: number): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 根据按钮类型获取对应的颜色
|
||||
* @param type 按钮类型
|
||||
* @returns 对应的颜色
|
||||
*/
|
||||
const getButtonColor = (type?: string): string => {
|
||||
switch (type) {
|
||||
case "download":
|
||||
return "tonal";
|
||||
return "filled";
|
||||
case "normal":
|
||||
default:
|
||||
return "tonal";
|
||||
default:
|
||||
return "text";
|
||||
}
|
||||
};
|
||||
|
||||
const getButtonIcon = (type: string) => {
|
||||
/**
|
||||
* 根据按钮类型获取对应的图标
|
||||
* @param type 按钮类型
|
||||
* @returns 对应的图标名称
|
||||
*/
|
||||
const getButtonIcon = (type?: string): string => {
|
||||
switch (type) {
|
||||
case "download":
|
||||
return "download";
|
||||
@@ -37,22 +66,37 @@ const getButtonIcon = (type: string) => {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理按钮点击事件
|
||||
* @param e 事件对象
|
||||
* @param item 链接对象
|
||||
* @param index 索引
|
||||
*/
|
||||
const handleClick = (e: Event, item: ExternalLink, index: number) => {
|
||||
if (item.onClick) {
|
||||
item.onClick(e);
|
||||
}
|
||||
emit("click", e, item, index);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="links && links.length > 0" class="ButtonGroup" :class="[props.size, props.layout]">
|
||||
<div class="ButtonGroup" :class="[props.size, props.layout]" :aria-label="props.ariaLabel">
|
||||
<MaterialButton
|
||||
v-for="(item, index) in links"
|
||||
:key="index"
|
||||
class="group"
|
||||
:class="props.layout"
|
||||
:key="index"
|
||||
:href="item.link"
|
||||
:size="props.size"
|
||||
:color="getButtonColor(item.type)"
|
||||
:icon="getButtonIcon(item.type)"
|
||||
:target="'_blank'"
|
||||
:size="item.size || props.size"
|
||||
:color="item.color || props.color || getButtonColor(item.type)"
|
||||
:icon="item.icon || props.icon || getButtonIcon(item.type)"
|
||||
:target="item.target || props.target || (item.link ? '_blank' : undefined)"
|
||||
:aria-label="item.ariaLabel || props.ariaLabel || item.label"
|
||||
@click="handleClick($event, item, index)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<template v-if="item.label">{{ item.label }}</template>
|
||||
</MaterialButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,14 +27,8 @@ const siteVersion = theme.value.siteVersion;
|
||||
>
|
||||
</div>
|
||||
<div class="beian-info">
|
||||
<div class="beian-gongan">
|
||||
<img src="/assets/images/beian.png" loading="eager" />
|
||||
<a href="https://beian.mps.gov.cn/#/query/webSearch?code=23020002230215" target="_blank"
|
||||
>黑公网安备23020002230215</a
|
||||
>
|
||||
</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>
|
||||
|
||||
@@ -217,76 +217,87 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header ref="headerRef" class="Header">
|
||||
<div class="carousel-container" :impression-color="frontmatter.color">
|
||||
<template v-if="hasMultiple">
|
||||
<div class="stage" :style="{ '--carousel-duration': `${animDuration}ms` }">
|
||||
<div v-for="slot in slotStates" :key="slot.id" class="item" :class="slot.className" :style="{ order: slot.order }">
|
||||
<img :src="slot.imgUrl" />
|
||||
<Transition name="header" mode="in-out" :duration="10000" appear>
|
||||
<header ref="headerRef" class="Header">
|
||||
<div class="carousel-container" :impression-color="frontmatter.color">
|
||||
<template v-if="hasMultiple">
|
||||
<div class="stage" :style="{ '--carousel-duration': `${animDuration}ms` }">
|
||||
<div
|
||||
v-for="slot in slotStates"
|
||||
:key="slot.id"
|
||||
class="item"
|
||||
:class="slot.className"
|
||||
:style="{ order: slot.order }"
|
||||
>
|
||||
<img :src="slot.imgUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-ring">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="9" fill="none" stroke="var(--md-sys-color-tertiary-container)" stroke-width="5" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="9"
|
||||
fill="none"
|
||||
stroke="var(--md-sys-color-tertiary)"
|
||||
stroke-width="5"
|
||||
stroke-linecap="round"
|
||||
:style="{
|
||||
strokeDasharray: `${2 * Math.PI * 9}`,
|
||||
strokeDashoffset: `${2 * Math.PI * 9 * (1 - progress / 100)}`,
|
||||
transition: isFastForwarding ? 'none' : 'stroke-dashoffset 100ms linear',
|
||||
}"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="prev" title="上一张" @click="handleNav(-1)"></div>
|
||||
<div class="next" title="下一张" @click="handleNav(1)"></div>
|
||||
</div>
|
||||
<div class="indicators">
|
||||
<button
|
||||
v-for="(_, idx) in rawImgList"
|
||||
:key="idx"
|
||||
class="dot"
|
||||
:class="{ active: currentRealIndex === idx }"
|
||||
@click="jumpTo(idx)"
|
||||
></button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ClientOnly>
|
||||
<svg width="0" height="0">
|
||||
<defs>
|
||||
<filter id="noise-filter" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence
|
||||
:seed="frontmatter.date ? new Date(frontmatter.date).getTime() : 0"
|
||||
type="turbulence"
|
||||
baseFrequency="0.15"
|
||||
numOctaves="2"
|
||||
stitchTiles="stitch"
|
||||
></feTurbulence>
|
||||
<feColorMatrix type="saturate" values="1"></feColorMatrix>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="discrete" tableValues="0 0.1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
<feBlend mode="multiply" in2="SourceGraphic"></feBlend>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</ClientOnly>
|
||||
<div class="single">
|
||||
<h1>{{ frontmatter.title || page.title }}</h1>
|
||||
<img :src="getGradientUrl(rawImgList[0])" />
|
||||
<img :src="rawImgList[0]" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
<div class="progress-ring">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="9" fill="none" stroke="var(--md-sys-color-tertiary-container)" stroke-width="5" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="9"
|
||||
fill="none"
|
||||
stroke="var(--md-sys-color-tertiary)"
|
||||
stroke-width="5"
|
||||
stroke-linecap="round"
|
||||
:style="{
|
||||
strokeDasharray: `${2 * Math.PI * 9}`,
|
||||
strokeDashoffset: `${2 * Math.PI * 9 * (1 - progress / 100)}`,
|
||||
transition: isFastForwarding ? 'none' : 'stroke-dashoffset 100ms linear',
|
||||
}"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="prev" title="上一张" @click="handleNav(-1)"></div>
|
||||
<div class="next" title="下一张" @click="handleNav(1)"></div>
|
||||
</div>
|
||||
<div class="indicators">
|
||||
<button
|
||||
v-for="(_, idx) in rawImgList"
|
||||
:key="idx"
|
||||
class="dot"
|
||||
:class="{ active: currentRealIndex === idx }"
|
||||
@click="jumpTo(idx)"
|
||||
></button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ClientOnly>
|
||||
<svg width="0" height="0" style="display: none">
|
||||
<defs>
|
||||
<filter id="noise-filter" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence
|
||||
:seed="frontmatter.date ? new Date(frontmatter.date).getTime() : 0"
|
||||
type="turbulence"
|
||||
baseFrequency="0.15"
|
||||
numOctaves="2"
|
||||
stitchTiles="stitch"
|
||||
></feTurbulence>
|
||||
<feColorMatrix type="saturate" values="1"></feColorMatrix>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="discrete" tableValues="0 0.1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
<feBlend mode="multiply" in2="SourceGraphic"></feBlend>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</ClientOnly>
|
||||
<div class="single">
|
||||
<h1 class="overlay">{{ frontmatter.title || page.title }}</h1>
|
||||
<h1 :style="`background-image: url(${getGradientUrl(rawImgList[0])})`">
|
||||
{{ frontmatter.title || page.title }}
|
||||
</h1>
|
||||
<img :src="getGradientUrl(rawImgList[0])" />
|
||||
<img :src="rawImgList[0]" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -86,7 +86,7 @@ const show = () => {
|
||||
// 延迟一帧触发动画,确保 CSS 过渡生效
|
||||
isAnimating.value = true;
|
||||
// 将焦点转移到查看器内部的关闭按钮
|
||||
const closeBtn = document.querySelector<HTMLElement>(".btn-close");
|
||||
const closeBtn = document.querySelector<HTMLElement>(".ImageViewer .close");
|
||||
closeBtn?.focus();
|
||||
});
|
||||
};
|
||||
@@ -113,8 +113,32 @@ const hide = () => {
|
||||
const prevImage = () => hasPrevious.value && activeIndex.value--;
|
||||
const nextImage = () => hasNext.value && activeIndex.value++;
|
||||
|
||||
/** 键盘交互处理 */
|
||||
useEventListener("keydown", (e: KeyboardEvent) => {
|
||||
/** 导航 ButtonGroup 按钮配置 */
|
||||
const BUTTONS_NAV_CONFIG = computed(() => [
|
||||
{ id: "prev", icon: "chevron_left", ariaLabel: "上一张", type: "normal" },
|
||||
{
|
||||
id: "index",
|
||||
label: `${activeIndex.value + 1} / ${props.images.length}`,
|
||||
color: "tonal",
|
||||
ariaLabel: "当前页码",
|
||||
},
|
||||
{ id: "next", icon: "chevron_right", ariaLabel: "下一张", type: "normal" },
|
||||
]);
|
||||
|
||||
/** 处理按钮组点击事件 */
|
||||
const handleButtonGroupClick = (e: Event, item: any) => {
|
||||
switch (item.id) {
|
||||
case "prev":
|
||||
prevImage();
|
||||
break;
|
||||
case "next":
|
||||
nextImage();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理键盘快捷键 */
|
||||
const handleKeyboardShortcuts = (e: KeyboardEvent) => {
|
||||
if (!isVisible.value) return;
|
||||
|
||||
// 陷阱焦点逻辑
|
||||
@@ -146,7 +170,10 @@ useEventListener("keydown", (e: KeyboardEvent) => {
|
||||
imageScale.value = Math.max(ZOOM_CONFIG.MIN, imageScale.value - ZOOM_CONFIG.STEP);
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 键盘交互处理 */
|
||||
useEventListener("keydown", handleKeyboardShortcuts);
|
||||
|
||||
/** 滚轮缩放与翻页 */
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
@@ -202,7 +229,7 @@ const handleTouchMove = (e: TouchEvent) => {
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
imageScale.value = Math.min(
|
||||
Math.max((dist / initialTouchDist) * initialTouchScale, ZOOM_CONFIG.MIN),
|
||||
ZOOM_CONFIG.MAX_TOUCH
|
||||
ZOOM_CONFIG.MAX_TOUCH,
|
||||
);
|
||||
} else if (e.touches.length === 1) {
|
||||
const touch = e.touches[0];
|
||||
@@ -234,23 +261,15 @@ defineExpose({ show, hide });
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<MaterialButton @click="hide" class="btn-close" icon="close" color="text" size="m" aria-label="关闭" />
|
||||
<MaterialButton
|
||||
v-if="hasPrevious"
|
||||
@click="prevImage"
|
||||
class="btn-nav prev"
|
||||
icon="chevron_left"
|
||||
size="l"
|
||||
aria-label="上一张"
|
||||
/>
|
||||
<MaterialButton
|
||||
v-if="hasNext"
|
||||
@click="nextImage"
|
||||
class="btn-nav next"
|
||||
icon="chevron_right"
|
||||
size="l"
|
||||
aria-label="下一张"
|
||||
<ButtonGroup
|
||||
:links="BUTTONS_NAV_CONFIG"
|
||||
layout="horizontal"
|
||||
size="m"
|
||||
class="nav-group"
|
||||
@click="handleButtonGroupClick"
|
||||
/>
|
||||
<MaterialButton color="tonal" icon="close" aria-label="关闭" class="close" @click="hide"></MaterialButton>
|
||||
|
||||
<div
|
||||
class="content"
|
||||
@click.self="hide"
|
||||
@@ -280,18 +299,6 @@ defineExpose({ show, hide });
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<p class="index-text">{{ activeIndex + 1 }} / {{ images.length }}</p>
|
||||
<div class="thumbnails" v-if="images.length > 1">
|
||||
<button
|
||||
v-for="(img, idx) in images"
|
||||
:key="idx"
|
||||
class="thumbnail"
|
||||
:class="{ active: idx === activeIndex }"
|
||||
@click="activeIndex = idx"
|
||||
>
|
||||
<img :src="img" alt="thumbnail" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -159,7 +159,6 @@ if (isClient()) {
|
||||
onMounted(() => {
|
||||
screenWidthStore.init();
|
||||
navStateStore.init();
|
||||
themeStateStore.init();
|
||||
|
||||
nextTick(() => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
|
||||
@@ -25,7 +25,7 @@ const { start: lockTimer } = useTimeoutFn(
|
||||
isLocked.value = false;
|
||||
},
|
||||
1200,
|
||||
{ immediate: false }
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
/** 计算文章 ID 与短链 */
|
||||
@@ -48,11 +48,21 @@ const copyShortLink = async () => {
|
||||
const collectHeadings = () => {
|
||||
if (!isClient()) return;
|
||||
const nodes = Array.from(document.querySelectorAll("h1[id], h2[id]")) as HTMLElement[];
|
||||
headings.value = nodes.map((n) => ({
|
||||
id: n.id,
|
||||
text: n.textContent?.trim() || n.id,
|
||||
level: +n.tagName.replace("H", ""),
|
||||
}));
|
||||
|
||||
headings.value = nodes.map((n) => {
|
||||
// 克隆节点并移除行内锚点
|
||||
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", ""),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** 更新指示器(高亮边框)的位置和尺寸 */
|
||||
@@ -126,11 +136,11 @@ if (isClient()) {
|
||||
headingsActiveId.value = bestId;
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-20% 0px -60% 0px", threshold: [0, 0.1, 0.5, 1] }
|
||||
{ rootMargin: "-20% 0px -60% 0px", threshold: [0, 0.1, 0.5, 1] },
|
||||
);
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 监听容器及其子元素尺寸变化 */
|
||||
@@ -153,7 +163,7 @@ watch(
|
||||
} else {
|
||||
indicator.value.opacity = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(headingsActiveId, () => {
|
||||
@@ -174,7 +184,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<div ref="pageIndicator" class="PageIndicator">
|
||||
<div class="label">
|
||||
<p class="text">在此页上</p>
|
||||
<p class="text">文章短链</p>
|
||||
<p class="icon">link</p>
|
||||
<p class="article-id" :title="isCopied ? '已复制' : '复制短链'" v-if="articleId" @click="copyShortLink">
|
||||
{{ isCopied ? "已复制" : articleId }}
|
||||
|
||||
20
.vitepress/theme/composables/useAnchorLink.ts
Normal 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 };
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import Default from "./layouts/Default.vue";
|
||||
import NotFound from "./layouts/NotFound.vue";
|
||||
|
||||
// Components
|
||||
import AnchorLink from "./components/AnchorLink.vue";
|
||||
import AppBar from "./components/AppBar.vue";
|
||||
import ArticleMasonry from "./components/ArticleMasonry.vue";
|
||||
import Button from "./components/Button.vue";
|
||||
@@ -33,6 +34,7 @@ export default {
|
||||
|
||||
app.component("MainLayout", Default);
|
||||
|
||||
app.component("AnchorLink", AnchorLink);
|
||||
app.component("AppBar", AppBar);
|
||||
app.component("ArticleMasonry", ArticleMasonry);
|
||||
app.component("ButtonGroup", ButtonGroup);
|
||||
|
||||
@@ -85,25 +85,6 @@ const openImageViewer = (index: number, event: MouseEvent) => {
|
||||
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 旋转和有序列表对齐 */
|
||||
const enhanceDomStyles = () => {
|
||||
if (!articleContentRef.value) return;
|
||||
@@ -156,7 +137,7 @@ if (isClient()) {
|
||||
|
||||
<template>
|
||||
<Header />
|
||||
<main id="article-content" ref="articleContentRef" @click="handleAnchorClick">
|
||||
<main id="article-content" ref="articleContentRef">
|
||||
<Content />
|
||||
<PrevNext />
|
||||
</main>
|
||||
|
||||
@@ -158,7 +158,7 @@ onMounted(() => {
|
||||
<h3>
|
||||
{{ randomGreeting }}
|
||||
</h3>
|
||||
<img src="/assets/images/avatar_transparent.png" alt="" />
|
||||
<img src="/assets/images/avatar_transparent.webp" alt="" />
|
||||
<span></span>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
|
||||
@@ -3,88 +3,61 @@
|
||||
*/
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { usePreferredDark } from "@vueuse/core";
|
||||
import { setCookie, getCookie } from "../utils/cookie";
|
||||
import { isClient } from "../utils/env";
|
||||
import { computed } from "vue";
|
||||
import { useColorMode, useCycleList } from "@vueuse/core";
|
||||
import { setCookie, getCookie, deleteCookie } from "../utils/cookie";
|
||||
|
||||
export type ThemePreference = "auto" | "light" | "dark";
|
||||
|
||||
/** 偏好映射配置 */
|
||||
const THEME_MAP = {
|
||||
auto: { icon: "brightness_auto", label: "跟随系统" },
|
||||
light: { icon: "light_mode", label: "亮色模式" },
|
||||
dark: { icon: "dark_mode", label: "深色模式" },
|
||||
} as const;
|
||||
|
||||
export const useThemeStateStore = defineStore("themeState", () => {
|
||||
/** 系统是否偏好深色模式 */
|
||||
const systemDark = usePreferredDark();
|
||||
|
||||
/** 用户当前的偏好设置 */
|
||||
const preference = ref<ThemePreference>("auto");
|
||||
|
||||
/** 计算当前实际生效的颜色模式 */
|
||||
const isDarkActive = computed(() => {
|
||||
if (preference.value === "auto") {
|
||||
return systemDark.value;
|
||||
}
|
||||
return preference.value === "dark";
|
||||
});
|
||||
|
||||
/** 初始化颜色主题,从 Cookie 读取配置,默认为 auto */
|
||||
const init = () => {
|
||||
const stored = getCookie("theme_preference");
|
||||
if (stored === "light" || stored === "dark" || stored === "auto") {
|
||||
preference.value = stored;
|
||||
}
|
||||
};
|
||||
|
||||
/** 监听实际生效的深色状态变化,操作 DOM */
|
||||
watch(
|
||||
isDarkActive,
|
||||
(isDark) => {
|
||||
if (isClient()) {
|
||||
document.documentElement.classList.toggle("dark", isDark);
|
||||
const mode = useColorMode({
|
||||
emitAuto: true,
|
||||
storageKey: "theme_preference",
|
||||
storage: {
|
||||
getItem: (key) => getCookie(key) || "auto",
|
||||
setItem: (key, value) => setCookie(key, value),
|
||||
removeItem: (key) => deleteCookie(key),
|
||||
},
|
||||
onChanged: (val, defaultHandler) => {
|
||||
defaultHandler(val);
|
||||
// 确保同时切换 light/dark 类
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.classList.toggle("dark", val === "dark");
|
||||
document.documentElement.classList.toggle("light", val === "light");
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
});
|
||||
|
||||
/** 监听用户偏好变化,持久化到 Cookie */
|
||||
watch(preference, (val) => {
|
||||
setCookie("theme_preference", val);
|
||||
/** 循环切换列表 */
|
||||
const { next } = useCycleList(["auto", "light", "dark"] as ThemePreference[], {
|
||||
initialValue: mode.value as ThemePreference,
|
||||
});
|
||||
|
||||
/** 切换主题模式 */
|
||||
const cycleTheme = () => {
|
||||
const modes: ThemePreference[] = ["auto", "light", "dark"];
|
||||
const nextIndex = (modes.indexOf(preference.value) + 1) % modes.length;
|
||||
preference.value = modes[nextIndex];
|
||||
mode.value = next();
|
||||
};
|
||||
|
||||
/** 当前偏好设置 */
|
||||
const preference = computed(() => mode.value as ThemePreference);
|
||||
|
||||
/** 当前状态对应的图标 */
|
||||
const currentIcon = computed(() => {
|
||||
switch (preference.value) {
|
||||
case "light":
|
||||
return "light_mode";
|
||||
case "dark":
|
||||
return "dark_mode";
|
||||
default:
|
||||
return "brightness_auto";
|
||||
}
|
||||
});
|
||||
const currentIcon = computed(() => THEME_MAP[preference.value].icon);
|
||||
|
||||
/** 当前状态对应的文本标签 */
|
||||
const currentLabel = computed(() => {
|
||||
switch (preference.value) {
|
||||
case "light":
|
||||
return "亮色模式";
|
||||
case "dark":
|
||||
return "深色模式";
|
||||
default:
|
||||
return "跟随系统";
|
||||
}
|
||||
});
|
||||
const currentLabel = computed(() => THEME_MAP[preference.value].label);
|
||||
|
||||
return {
|
||||
preference,
|
||||
currentIcon,
|
||||
currentLabel,
|
||||
init,
|
||||
cycleTheme,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
.layout-enter-active {
|
||||
transition: opacity var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect),
|
||||
transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial)
|
||||
var(--md-sys-motion-duration-short1);
|
||||
}
|
||||
|
||||
.layout-leave-active {
|
||||
transition: opacity var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect),
|
||||
transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
}
|
||||
|
||||
.layout-enter-from,
|
||||
.layout-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
||||
102
.vitepress/theme/styles/components/AnchorLink.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,26 +37,59 @@
|
||||
@include mixin.typescale-style(
|
||||
"display-large",
|
||||
$font-size: 72rem,
|
||||
$line-height: 72rem,
|
||||
$font-variation-settings: "wght" 700
|
||||
$line-height: 78rem,
|
||||
$font-variation-settings: "wght" 900
|
||||
);
|
||||
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin-inline-start: 48px;
|
||||
|
||||
color: var(--md-ref-palette-secondary80);
|
||||
color: transparent;
|
||||
|
||||
mix-blend-mode: difference;
|
||||
background-clip: text;
|
||||
background-position: 0% 0%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
|
||||
animation: title-gradient 5s var(--md-sys-motion-spring-slow-effect) infinite alternate-reverse;
|
||||
z-index: 2;
|
||||
|
||||
&.overlay {
|
||||
color: var(--md-ref-palette-primary20);
|
||||
|
||||
background: none;
|
||||
|
||||
animation: none;
|
||||
mix-blend-mode: luminosity;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
@keyframes title-gradient {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
background-size: 200%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
@include mixin.typescale-style(
|
||||
"display-large",
|
||||
$font-size: 60rem,
|
||||
$line-height: 72rem,
|
||||
$font-variation-settings: "wght" 600
|
||||
$font-size: 72rem,
|
||||
$line-height: 78rem,
|
||||
$font-variation-settings: "wght" 800
|
||||
);
|
||||
|
||||
margin-inline: 36px;
|
||||
@@ -66,8 +99,8 @@
|
||||
@include mixin.typescale-style(
|
||||
"display-large",
|
||||
$font-size: 48rem,
|
||||
$line-height: 60rem,
|
||||
$font-variation-settings: "wght" 600
|
||||
$line-height: 54rem,
|
||||
$font-variation-settings: "wght" 900
|
||||
);
|
||||
|
||||
grid-column: 1 / 3;
|
||||
@@ -93,12 +126,19 @@
|
||||
backdrop-filter: blur(10px);
|
||||
filter: url(#noise-filter);
|
||||
mask-image: linear-gradient(to right, black 0%, transparent 60%);
|
||||
mix-blend-mode: screen;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
&:nth-of-type(1) {
|
||||
mask-image: linear-gradient(to right, black 0%, transparent 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,3 +337,58 @@
|
||||
grid-column: span 4;
|
||||
}
|
||||
}
|
||||
|
||||
.header-enter-active {
|
||||
transition:
|
||||
opacity var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect),
|
||||
transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial)
|
||||
var(--md-sys-motion-duration-short1);
|
||||
|
||||
.carousel-container .single {
|
||||
h1 {
|
||||
transition: var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-slow-effect);
|
||||
}
|
||||
|
||||
img {
|
||||
&:nth-of-type(1) {
|
||||
transition: 5s var(--md-sys-motion-spring-slow-effect);
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
transition: var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-default-effect)
|
||||
var(--md-sys-motion-spring-fast-effect-duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-leave-active {
|
||||
transition:
|
||||
opacity var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect),
|
||||
transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
}
|
||||
|
||||
.header-enter-from,
|
||||
.header-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.985);
|
||||
|
||||
.carousel-container .single {
|
||||
h1 {
|
||||
opacity: 0;
|
||||
transform: translateX(5%);
|
||||
}
|
||||
|
||||
img {
|
||||
&:nth-of-type(1) {
|
||||
opacity: 0;
|
||||
transform: translateX(-25%);
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
opacity: 0;
|
||||
transform: scale(1.005);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,47 +22,26 @@
|
||||
-moz-user-select: none;
|
||||
z-index: 9999;
|
||||
|
||||
.index-text {
|
||||
padding-block: 3px;
|
||||
padding-inline: 9px;
|
||||
.nav-group {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
|
||||
color: var(--md-sys-color-surface-variant);
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
|
||||
background-color: var(--md-sys-color-on-surface-variant);
|
||||
margin-block-end: 12px;
|
||||
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.btn-close,
|
||||
.btn-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
|
||||
position: absolute !important;
|
||||
margin-inline-end: 12px;
|
||||
margin-block-start: 12px;
|
||||
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.btn-nav {
|
||||
top: 50%;
|
||||
|
||||
&.prev {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
&.next {
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -74,70 +53,21 @@
|
||||
padding-block-start: 5vh;
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.content-image {
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
.content-image {
|
||||
background-color: var(--md-sys-color-surface-variant);
|
||||
|
||||
background-color: var(--md-sys-color-surface-variant);
|
||||
clip-path: circle(10%);
|
||||
object-fit: contain;
|
||||
transition: var(--md-sys-motion-spring-slow-spatial-duration) var(--md-sys-motion-spring-slow-spatial);
|
||||
|
||||
clip-path: circle(10%);
|
||||
object-fit: contain;
|
||||
transition: var(--md-sys-motion-spring-slow-spatial-duration) var(--md-sys-motion-spring-slow-spatial);
|
||||
&.transitioning {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.transitioning {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.notransition {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnails {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
max-width: calc(100% - 66px);
|
||||
|
||||
padding: 24px;
|
||||
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
|
||||
padding: 0px;
|
||||
|
||||
border: 0px;
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
|
||||
&.active {
|
||||
@include mixin.focus-ring($thickness: 1, $offset: 2);
|
||||
|
||||
outline-color: var(--md-sys-color-on-surface-variant) !important;
|
||||
transition: transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
object-fit: cover;
|
||||
&.notransition {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +76,6 @@
|
||||
opacity: 1;
|
||||
|
||||
.content-image {
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
|
||||
clip-path: circle(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ table {
|
||||
|
||||
tr {
|
||||
th {
|
||||
min-width: 10ch;
|
||||
|
||||
padding: 16px 24px;
|
||||
|
||||
text-transform: capitalize;
|
||||
|
||||
background-color: var(--md-sys-color-surface-variant);
|
||||
@@ -52,8 +48,6 @@ table {
|
||||
vertical-align: top;
|
||||
|
||||
td {
|
||||
padding: 16px 24px;
|
||||
|
||||
vertical-align: inherit;
|
||||
|
||||
code {
|
||||
@@ -63,9 +57,18 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
:is(td, th) {
|
||||
td,
|
||||
th {
|
||||
min-width: 16ch;
|
||||
|
||||
padding: 16px 24px;
|
||||
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code {
|
||||
padding-block: 0px !important;
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
}
|
||||
|
||||
&:first-child .title-with-achor {
|
||||
margin-block-start: 0px;
|
||||
margin-block-start: 24px;
|
||||
}
|
||||
|
||||
.title-with-achor {
|
||||
@@ -350,6 +350,7 @@
|
||||
display: inline-block;
|
||||
|
||||
line-height: 54px;
|
||||
font-variation-settings: "wght" 600;
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
|
||||
@@ -357,101 +358,24 @@
|
||||
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 {
|
||||
.title-anchor {
|
||||
&:hover .AnchorLink {
|
||||
opacity: 1;
|
||||
|
||||
.symbol {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline solid;
|
||||
text-decoration: underline 2px dotted;
|
||||
text-underline-offset: 3px;
|
||||
|
||||
&:hover {
|
||||
text-decoration-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@@ -619,24 +543,18 @@
|
||||
font-variation-settings: "wght" 700;
|
||||
}
|
||||
|
||||
&:has(img) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
img {
|
||||
display: inline-block;
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
width: 100%;
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
cursor: pointer;
|
||||
transition: border-radius var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
|
||||
cursor: pointer;
|
||||
transition: border-radius var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
|
||||
&:hover {
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
}
|
||||
&:hover {
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -836,10 +754,6 @@
|
||||
@media screen and (max-width: 600px) {
|
||||
#article-content {
|
||||
grid-column: 1 / 5;
|
||||
|
||||
p:has(img) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
#article-aside {
|
||||
|
||||
@@ -66,7 +66,8 @@
|
||||
animation: avatar-box-h3 var(--md-sys-motion-spring-slow-spatial-duration) var(--md-sys-motion-spring-slow-spatial)
|
||||
var(--md-sys-motion-spring-fast-spatial-duration) both;
|
||||
overflow: hidden;
|
||||
transition: color var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-slow-effect),
|
||||
transition:
|
||||
color var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-slow-effect),
|
||||
background-color var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-slow-effect);
|
||||
transform-origin: 0px 0px;
|
||||
z-index: 2;
|
||||
@@ -119,7 +120,9 @@
|
||||
width: 200px;
|
||||
|
||||
animation: avatar-box-span 60s linear infinite;
|
||||
mask: linear-gradient(45deg, black, transparent), var(--via-svg-mask) 0 / 100% no-repeat;
|
||||
mask:
|
||||
linear-gradient(45deg, black, transparent),
|
||||
var(--via-svg-mask) 0 / 100% no-repeat;
|
||||
mask-composite: intersect;
|
||||
mask-mode: alpha;
|
||||
opacity: 0.5;
|
||||
@@ -169,6 +172,7 @@
|
||||
h1 {
|
||||
@include mixin.typescale-style("display-large");
|
||||
|
||||
font-variation-settings: "wght" 600;
|
||||
text-align: center;
|
||||
word-break: keep-all;
|
||||
}
|
||||
@@ -254,3 +258,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-enter-active {
|
||||
transition:
|
||||
opacity var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect),
|
||||
transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial)
|
||||
var(--md-sys-motion-duration-short1);
|
||||
}
|
||||
|
||||
.layout-leave-active {
|
||||
transition:
|
||||
opacity var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect),
|
||||
transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
}
|
||||
|
||||
.layout-enter-from,
|
||||
.layout-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
h2,
|
||||
a {
|
||||
@include mixin.typescale-style("display-small");
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,18 @@ body {
|
||||
|
||||
background-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
&.light {
|
||||
img[data-mode="darkmode-only"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
img[data-mode="lightmode-only"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
@@ -122,7 +134,8 @@ a {
|
||||
|
||||
color: var(--md-sys-color-primary);
|
||||
letter-spacing: 0px;
|
||||
text-underline-offset: 5px;
|
||||
text-decoration: underline solid;
|
||||
text-underline-offset: 6px;
|
||||
|
||||
code {
|
||||
color: var(--md-sys-color-inverse-primary) !important;
|
||||
|
||||
@@ -195,73 +195,75 @@
|
||||
|
||||
// Colors scheme
|
||||
html {
|
||||
--md-sys-color-primary: var(--md-ref-palette-primary40);
|
||||
--md-sys-color-primary-container: var(--md-ref-palette-primary90);
|
||||
--md-sys-color-on-primary: var(--md-ref-palette-primary100);
|
||||
--md-sys-color-on-primary-container: var(--md-ref-palette-primary10);
|
||||
--md-sys-color-inverse-primary: var(--md-ref-palette-primary80);
|
||||
--md-sys-color-secondary: var(--md-ref-palette-secondary40);
|
||||
--md-sys-color-secondary-container: var(--md-ref-palette-secondary90);
|
||||
--md-sys-color-on-secondary: var(--md-ref-palette-secondary100);
|
||||
--md-sys-color-on-secondary-container: var(--md-ref-palette-secondary10);
|
||||
--md-sys-color-tertiary: var(--md-ref-palette-tertiary40);
|
||||
--md-sys-color-tertiary-container: var(--md-ref-palette-tertiary90);
|
||||
--md-sys-color-on-tertiary: var(--md-ref-palette-tertiary100);
|
||||
--md-sys-color-on-tertiary-container: var(--md-ref-palette-tertiary10);
|
||||
--md-sys-color-surface: var(--md-ref-palette-neutral98);
|
||||
--md-sys-color-surface-dim: var(--md-ref-palette-neutral87);
|
||||
--md-sys-color-surface-bright: var(--md-ref-palette-neutral98);
|
||||
--md-sys-color-surface-container-lowest: var(--md-ref-palette-neutral100);
|
||||
--md-sys-color-surface-container-low: var(--md-ref-palette-neutral96);
|
||||
--md-sys-color-surface-container: var(--md-ref-palette-neutral94);
|
||||
--md-sys-color-surface-container-high: var(--md-ref-palette-neutral92);
|
||||
--md-sys-color-surface-container-highest: var(--md-ref-palette-neutral90);
|
||||
--md-sys-color-surface-variant: var(--md-ref-palette-neutral-variant90);
|
||||
--md-sys-color-on-surface: var(--md-ref-palette-neutral10);
|
||||
--md-sys-color-on-surface-variant: var(--md-ref-palette-neutral-variant30);
|
||||
--md-sys-color-inverse-surface: var(--md-ref-palette-neutral20);
|
||||
--md-sys-color-inverse-on-surface: var(--md-ref-palette-neutral95);
|
||||
--md-sys-color-background: var(--md-ref-palette-neutral98);
|
||||
--md-sys-color-on-background: var(--md-ref-palette-neutral10);
|
||||
--md-sys-color-error: var(--md-ref-palette-error40);
|
||||
--md-sys-color-error-container: var(--md-ref-palette-error90);
|
||||
--md-sys-color-on-error: var(--md-ref-palette-error100);
|
||||
--md-sys-color-on-error-container: var(--md-ref-palette-error10);
|
||||
--md-sys-color-outline: var(--md-ref-palette-neutral-variant50);
|
||||
--md-sys-color-outline-variant: var(--md-ref-palette-neutral-variant80);
|
||||
--md-sys-color-shadow: var(--md-ref-palette-neutral0);
|
||||
--md-sys-color-surface-tint-color: var(--md-sys-color-primary);
|
||||
--md-sys-color-scrim: var(--md-ref-palette-neutral0);
|
||||
&.light {
|
||||
--md-sys-color-primary: var(--md-ref-palette-primary40);
|
||||
--md-sys-color-primary-container: var(--md-ref-palette-primary90);
|
||||
--md-sys-color-on-primary: var(--md-ref-palette-primary100);
|
||||
--md-sys-color-on-primary-container: var(--md-ref-palette-primary10);
|
||||
--md-sys-color-inverse-primary: var(--md-ref-palette-primary80);
|
||||
--md-sys-color-secondary: var(--md-ref-palette-secondary40);
|
||||
--md-sys-color-secondary-container: var(--md-ref-palette-secondary90);
|
||||
--md-sys-color-on-secondary: var(--md-ref-palette-secondary100);
|
||||
--md-sys-color-on-secondary-container: var(--md-ref-palette-secondary10);
|
||||
--md-sys-color-tertiary: var(--md-ref-palette-tertiary40);
|
||||
--md-sys-color-tertiary-container: var(--md-ref-palette-tertiary90);
|
||||
--md-sys-color-on-tertiary: var(--md-ref-palette-tertiary100);
|
||||
--md-sys-color-on-tertiary-container: var(--md-ref-palette-tertiary10);
|
||||
--md-sys-color-surface: var(--md-ref-palette-neutral98);
|
||||
--md-sys-color-surface-dim: var(--md-ref-palette-neutral87);
|
||||
--md-sys-color-surface-bright: var(--md-ref-palette-neutral98);
|
||||
--md-sys-color-surface-container-lowest: var(--md-ref-palette-neutral100);
|
||||
--md-sys-color-surface-container-low: var(--md-ref-palette-neutral96);
|
||||
--md-sys-color-surface-container: var(--md-ref-palette-neutral94);
|
||||
--md-sys-color-surface-container-high: var(--md-ref-palette-neutral92);
|
||||
--md-sys-color-surface-container-highest: var(--md-ref-palette-neutral90);
|
||||
--md-sys-color-surface-variant: var(--md-ref-palette-neutral-variant90);
|
||||
--md-sys-color-on-surface: var(--md-ref-palette-neutral10);
|
||||
--md-sys-color-on-surface-variant: var(--md-ref-palette-neutral-variant30);
|
||||
--md-sys-color-inverse-surface: var(--md-ref-palette-neutral20);
|
||||
--md-sys-color-inverse-on-surface: var(--md-ref-palette-neutral95);
|
||||
--md-sys-color-background: var(--md-ref-palette-neutral98);
|
||||
--md-sys-color-on-background: var(--md-ref-palette-neutral10);
|
||||
--md-sys-color-error: var(--md-ref-palette-error40);
|
||||
--md-sys-color-error-container: var(--md-ref-palette-error90);
|
||||
--md-sys-color-on-error: var(--md-ref-palette-error100);
|
||||
--md-sys-color-on-error-container: var(--md-ref-palette-error10);
|
||||
--md-sys-color-outline: var(--md-ref-palette-neutral-variant50);
|
||||
--md-sys-color-outline-variant: var(--md-ref-palette-neutral-variant80);
|
||||
--md-sys-color-shadow: var(--md-ref-palette-neutral0);
|
||||
--md-sys-color-surface-tint-color: var(--md-sys-color-primary);
|
||||
--md-sys-color-scrim: var(--md-ref-palette-neutral0);
|
||||
|
||||
--md-sys-color-red: var(--md-ref-palette-red40);
|
||||
--md-sys-color-red-container: var(--md-ref-palette-red90);
|
||||
--md-sys-color-on-red: var(--md-ref-palette-red95);
|
||||
--md-sys-color-on-red-container: var(--md-ref-palette-red10);
|
||||
--md-sys-color-inverse-red: var(--md-ref-palette-red80);
|
||||
--md-sys-color-red: var(--md-ref-palette-red40);
|
||||
--md-sys-color-red-container: var(--md-ref-palette-red90);
|
||||
--md-sys-color-on-red: var(--md-ref-palette-red95);
|
||||
--md-sys-color-on-red-container: var(--md-ref-palette-red10);
|
||||
--md-sys-color-inverse-red: var(--md-ref-palette-red80);
|
||||
|
||||
--md-sys-color-blue: var(--md-ref-palette-blue40);
|
||||
--md-sys-color-blue-container: var(--md-ref-palette-blue90);
|
||||
--md-sys-color-on-blue: var(--md-ref-palette-blue95);
|
||||
--md-sys-color-on-blue-container: var(--md-ref-palette-blue10);
|
||||
--md-sys-color-inverse-blue: var(--md-ref-palette-blue80);
|
||||
--md-sys-color-blue: var(--md-ref-palette-blue40);
|
||||
--md-sys-color-blue-container: var(--md-ref-palette-blue90);
|
||||
--md-sys-color-on-blue: var(--md-ref-palette-blue95);
|
||||
--md-sys-color-on-blue-container: var(--md-ref-palette-blue10);
|
||||
--md-sys-color-inverse-blue: var(--md-ref-palette-blue80);
|
||||
|
||||
--md-sys-color-green: var(--md-ref-palette-green40);
|
||||
--md-sys-color-green-container: var(--md-ref-palette-green90);
|
||||
--md-sys-color-on-green: var(--md-ref-palette-green95);
|
||||
--md-sys-color-on-green-container: var(--md-ref-palette-green10);
|
||||
--md-sys-color-inverse-green: var(--md-ref-palette-green80);
|
||||
--md-sys-color-green: var(--md-ref-palette-green40);
|
||||
--md-sys-color-green-container: var(--md-ref-palette-green90);
|
||||
--md-sys-color-on-green: var(--md-ref-palette-green95);
|
||||
--md-sys-color-on-green-container: var(--md-ref-palette-green10);
|
||||
--md-sys-color-inverse-green: var(--md-ref-palette-green80);
|
||||
|
||||
--md-sys-color-yellow: var(--md-ref-palette-yellow40);
|
||||
--md-sys-color-yellow-container: var(--md-ref-palette-yellow90);
|
||||
--md-sys-color-on-yellow: var(--md-ref-palette-yellow95);
|
||||
--md-sys-color-on-yellow-container: var(--md-ref-palette-yellow10);
|
||||
--md-sys-color-inverse-yellow: var(--md-ref-palette-yellow80);
|
||||
--md-sys-color-yellow: var(--md-ref-palette-yellow40);
|
||||
--md-sys-color-yellow-container: var(--md-ref-palette-yellow90);
|
||||
--md-sys-color-on-yellow: var(--md-ref-palette-yellow95);
|
||||
--md-sys-color-on-yellow-container: var(--md-ref-palette-yellow10);
|
||||
--md-sys-color-inverse-yellow: var(--md-ref-palette-yellow80);
|
||||
|
||||
--md-sys-color-purple: var(--md-ref-palette-purple40);
|
||||
--md-sys-color-purple-container: var(--md-ref-palette-purple90);
|
||||
--md-sys-color-on-purple: var(--md-ref-palette-purple95);
|
||||
--md-sys-color-on-purple-container: var(--md-ref-palette-purple10);
|
||||
--md-sys-color-inverse-purple: var(--md-ref-palette-purple80);
|
||||
--md-sys-color-purple: var(--md-ref-palette-purple40);
|
||||
--md-sys-color-purple-container: var(--md-ref-palette-purple90);
|
||||
--md-sys-color-on-purple: var(--md-ref-palette-purple95);
|
||||
--md-sys-color-on-purple-container: var(--md-ref-palette-purple10);
|
||||
--md-sys-color-inverse-purple: var(--md-ref-palette-purple80);
|
||||
}
|
||||
|
||||
&.dark {
|
||||
--md-sys-color-primary: var(--md-ref-palette-primary80);
|
||||
|
||||
82
.vitepress/theme/utils/mdCustomAnchor.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,12 +4,7 @@ import type MarkdownIt from "markdown-it";
|
||||
* 将连续的标题块包裹为独立的 section(headline-block),便于样式与交互处理
|
||||
* @param mdit - MarkdownIt 实例
|
||||
*/
|
||||
export function wrapHeadingsAsSections(mdit: MarkdownIt): void {
|
||||
if (!mdit || !mdit.core || !mdit.core.ruler) {
|
||||
console.warn("Invalid MarkdownIt instance provided");
|
||||
return;
|
||||
}
|
||||
|
||||
export function sectionWrapper(mdit: MarkdownIt): void {
|
||||
mdit.core.ruler.before("inline", "group_sections", (state) => {
|
||||
const tokens = state.tokens;
|
||||
const newTokens: any[] = [];
|
||||
131
.vitepress/theme/utils/mdTable.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type MarkdownIt from "markdown-it";
|
||||
type StateBlock = any;
|
||||
|
||||
/**
|
||||
* 一个简易的 markdown-it 表格插件
|
||||
* @param mdit MarkdownIt 实例
|
||||
*/
|
||||
export const table = (mdit: MarkdownIt) => {
|
||||
// 禁用原有的表格规则
|
||||
mdit.disable("table");
|
||||
|
||||
/** 表格块解析规则 */
|
||||
const tableBlock = (state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean => {
|
||||
let nextLine = startLine;
|
||||
let lineText = state.getLines(nextLine, nextLine + 1, 0, false).trim();
|
||||
|
||||
// 检查是否包含表格分隔符
|
||||
if (!lineText.includes("|")) return false;
|
||||
|
||||
const rows: string[] = [];
|
||||
while (nextLine < endLine) {
|
||||
lineText = state.getLines(nextLine, nextLine + 1, 0, false).trim();
|
||||
if (!lineText.includes("|") && lineText !== "") break;
|
||||
if (lineText !== "") rows.push(lineText);
|
||||
nextLine++;
|
||||
}
|
||||
|
||||
if (rows.length < 1) return false;
|
||||
|
||||
if (silent) return true;
|
||||
|
||||
const token = state.push("simple_table", "table", 0);
|
||||
token.map = [startLine, nextLine];
|
||||
token.content = rows.join("\n");
|
||||
|
||||
state.line = nextLine;
|
||||
return true;
|
||||
};
|
||||
|
||||
mdit.block.ruler.before("paragraph", "simple_table", tableBlock);
|
||||
|
||||
// 渲染逻辑
|
||||
mdit.renderer.rules.simple_table = (tokens, idx) => {
|
||||
const content = tokens[idx].content;
|
||||
const lines = content.split("\n").filter((l) => l.trim() !== "");
|
||||
if (lines.length === 0) return "";
|
||||
|
||||
let html = "<table>\n";
|
||||
|
||||
// 检查第二行是否是分隔行
|
||||
const hasSeparator = lines.length > 1 && /^[|:\-\s]+$/.test(lines[1]) && lines[1].includes("-");
|
||||
|
||||
let headerLines: string[] = [];
|
||||
let bodyLines: string[] = [];
|
||||
|
||||
if (hasSeparator) {
|
||||
headerLines = [lines[0]];
|
||||
bodyLines = lines.slice(2);
|
||||
} else {
|
||||
// 如果没有分隔行,用第一行作为表头
|
||||
headerLines = [lines[0]];
|
||||
bodyLines = lines.slice(1);
|
||||
}
|
||||
|
||||
// 分割单元格并清理首尾空的装饰管线
|
||||
const getCells = (line: string) => {
|
||||
const cells = line.split("|");
|
||||
if (cells.length > 0 && cells[0].trim() === "") cells.shift();
|
||||
if (cells.length > 0 && cells[cells.length - 1].trim() === "") cells.pop();
|
||||
return cells.map((c) => c.trim());
|
||||
};
|
||||
|
||||
// 解析单元格中的属性
|
||||
const parseAttributes = (cellContent: string) => {
|
||||
const attrRegex = /\{([^{}]+)\}\s*$/;
|
||||
const match = cellContent.match(attrRegex);
|
||||
const attrs: Record<string, string> = {};
|
||||
let cleanedContent = cellContent;
|
||||
|
||||
if (match) {
|
||||
cleanedContent = cellContent.replace(attrRegex, "").trim();
|
||||
const attrString = match[1];
|
||||
const parts = attrString.split(/\s+/);
|
||||
parts.forEach((part) => {
|
||||
const [key, value] = part.split("=");
|
||||
if (key && (key === "colspan" || key === "rowspan")) {
|
||||
attrs[key] = value || "1";
|
||||
}
|
||||
});
|
||||
}
|
||||
return { cleanedContent, attrs };
|
||||
};
|
||||
|
||||
// 渲染表头
|
||||
if (headerLines.length > 0) {
|
||||
html += "<thead>\n";
|
||||
headerLines.forEach((line) => {
|
||||
html += "<tr>\n";
|
||||
getCells(line).forEach((cell) => {
|
||||
const { cleanedContent, attrs } = parseAttributes(cell);
|
||||
const attrStr = Object.entries(attrs)
|
||||
.map(([k, v]) => ` ${k}="${v}"`)
|
||||
.join("");
|
||||
html += `<th${attrStr}>${mdit.renderInline(cleanedContent)}</th>\n`;
|
||||
});
|
||||
html += "</tr>\n";
|
||||
});
|
||||
html += "</thead>\n";
|
||||
}
|
||||
|
||||
// 渲染表体
|
||||
if (bodyLines.length > 0) {
|
||||
html += "<tbody>\n";
|
||||
bodyLines.forEach((line) => {
|
||||
html += "<tr>\n";
|
||||
getCells(line).forEach((cell) => {
|
||||
const { cleanedContent, attrs } = parseAttributes(cell);
|
||||
const attrStr = Object.entries(attrs)
|
||||
.map(([k, v]) => ` ${k}="${v}"`)
|
||||
.join("");
|
||||
html += `<td${attrStr}>${mdit.renderInline(cleanedContent)}</td>\n`;
|
||||
});
|
||||
html += "</tr>\n";
|
||||
});
|
||||
html += "</tbody>\n";
|
||||
}
|
||||
|
||||
html += "</table>";
|
||||
return html;
|
||||
};
|
||||
};
|
||||
27
Dockerfile
Normal 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 .
|
||||
|
||||
# 安装依赖并构建
|
||||
RUN npm i && npm run docs:build
|
||||
|
||||
# 最终阶段
|
||||
FROM nginx:stable-perl
|
||||
|
||||
# 从构建阶段复制 dist 产物到 workdir
|
||||
COPY --from=builder /app/.vitepress/dist /app/dist
|
||||
|
||||
# 暴露端口(80 HTTP 和 443 HTTPS)
|
||||
EXPOSE 80 443
|
||||
|
||||
# 启动 nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
19
package.json
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "26.2.6(272)",
|
||||
"version": "26.3.4(316)",
|
||||
"scripts": {
|
||||
"update-version": "bash ./scripts/update-version.sh",
|
||||
"docs:dev": "vitepress dev",
|
||||
@@ -9,16 +9,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"pinia": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdit/plugin-align": "^0.23.0",
|
||||
"@mdit/plugin-footnote": "^0.22.3",
|
||||
"@mdit/plugin-tasklist": "^0.22.2",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"sass-embedded": "^1.93.0",
|
||||
"vitepress": "^2.0.0-alpha.15",
|
||||
"vue": "^3.5.0"
|
||||
"@mdit/plugin-align": "^0.24.0",
|
||||
"@mdit/plugin-footnote": "^0.23.0",
|
||||
"@mdit/plugin-img-mark": "^0.23.0",
|
||||
"@mdit/plugin-tasklist": "^0.23.0",
|
||||
"@waline/client": "^3.13.0",
|
||||
"sass-embedded": "^1.97.3",
|
||||
"vitepress": "^2.0.0-alpha.16",
|
||||
"vue": "^3.5.29"
|
||||
}
|
||||
}
|
||||
|
||||
75
posts/Jekylmt.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: "关于 Jekylmt"
|
||||
description: "一个简洁美观的 Jekyll 主题"
|
||||
color: "#0084ff"
|
||||
impression: "/assets/images/131559307_p0.webp"
|
||||
categories:
|
||||
- 博客主题
|
||||
tags:
|
||||
- readme
|
||||
date: 2026-02-25T21:44:00Z
|
||||
external_links:
|
||||
- type: download
|
||||
icon: rocket_launch
|
||||
label: 使用主题模板
|
||||
link: https://github.com/new?template_name=jekylmt&template_owner=sendevia
|
||||
- type: normal
|
||||
icon: code
|
||||
label: Github 仓库
|
||||
link: https://github.com/sendevia/jekylmt
|
||||
- type: normal
|
||||
icon: arrow_outward
|
||||
label: 在线 Demo
|
||||
link: https://jekylmt.sendevia.top
|
||||
---
|
||||
|
||||
# 关于主题
|
||||
|
||||
不太擅长介绍,既然这个页面存在了,就简单写一下吧。 这是一个遵循 Material3 设计,并且使用了 Material Web 项目,轻量化的 Jekyll 主题。
|
||||
|
||||
由于 M3 的设计太好看了,一直想亲自动手设计一款使用这种设计的前端项目,作为练手,这个主题便诞生了。
|
||||
|
||||
我借助 Material Foundation 的 [material-color-utilities](https://github.com/material-foundation/material-color-utilities) 实现了 monet 取色。
|
||||
|
||||
你可以在每篇文章的头信息 `impression` 配置中指定头图,并通过 `color` 配置指定颜色来让主题生成调色板。
|
||||
|
||||
这个主题的参考来源主要是 [material.io](https://material.io) 官网,其次是 Google 提供的设计规范。
|
||||
|
||||
代码高亮支持的语言可以在 [rouge-ruby](https://rouge-ruby.github.io/docs/file.Languages.html) 的网站上找到。
|
||||
|
||||
# 主题截图
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 主要功能
|
||||
|
||||
1. Material 3 风格;
|
||||
2. 支持根据提供的 HEX 颜色动态生成调色板并应用颜色主题;
|
||||
3. 支持多种 Material 3 样式的组件;
|
||||
4. 响应式布局。
|
||||
|
||||
# 头信息
|
||||
|
||||
下面是所有头信息的详解:
|
||||
|
||||
| name { colspan=2 } | | description | type | default |
|
||||
| -------------------------- | ------------- | ---------------- | --------------------- | ----------------------------------------- |
|
||||
| 文章相关 { rowspan=9 } | title | 文章标题 | text { rowspan=5 } | 使用 `_config.yml` 中的配置 { rowspan=5 } |
|
||||
| | description | 文章简介 | | |
|
||||
| | author | 文章作者 | | |
|
||||
| | color | 文章主题颜色 | | |
|
||||
| | impression | 文章头图 | | |
|
||||
| | categories | 目录分类 | list { rowspan=2 } | 未定义 { rowspan=2 } |
|
||||
| | tags | 文章标签 | | |
|
||||
| | published | 是否发布文章 | boolean { rowspan=2 } | true { rowspan=2 } |
|
||||
| | toc | 是否生成文章目录 | | |
|
||||
| 页面导航相关 { rowspan=3 } | segment_icon | 导航栏中的图标 | text { rowspan=2 } | - { rowspan=3 } |
|
||||
| | segment_title | 导航栏中的标题 | | |
|
||||
| | navigation | 是否在导航中显示 | boolean | |
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: "Markdown 扩展示例"
|
||||
description: "本页面展示了 VitePress 提供的一些内置 markdown 扩展功能。"
|
||||
color: ""
|
||||
impression: ""
|
||||
color: "#f53283"
|
||||
impression: "/assets/images/138124971_p0.webp"
|
||||
categories:
|
||||
tags:
|
||||
- markdown 语法
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
title: "关于这个主题的一些事"
|
||||
description: ""
|
||||
color: "#59f3b3"
|
||||
impression: "/assets/images/133925125_p0.webp"
|
||||
categories:
|
||||
- 随笔
|
||||
tags:
|
||||
- readme
|
||||
date: 2023-05-28T04:00:00Z
|
||||
---
|
||||
|
||||
# 关于主题
|
||||
|
||||
由于 M3 的设计太好看了,一直想亲自动手设计一款使用这种设计的前端项目,作为练手,这个主题便诞生了。
|
||||
|
||||
我借助 Material Foundation 的 [material-color-utilities](https://github.com/material-foundation/material-color-utilities) 实现了 monet 取色。
|
||||
|
||||
你可以在每篇文章的头信息 `impression` 配置中指定头图,并通过 `color` 配置指定颜色来让主题生成调色板。
|
||||
|
||||
这个主题的参考来源主要是 [material.io](https://material.io) 官网,其次是 Google 提供的设计规范。
|
||||
|
||||
# 主要功能
|
||||
|
||||
1. Material 3 Expressive 设计风格;
|
||||
1. 支持根据提供的 HEX 颜色/输入的图片动态生成调色板并应用颜色主题;
|
||||
1. 响应式布局。
|
||||
|
||||
# 头信息
|
||||
|
||||
下面是所有头信息的详解:
|
||||
|
||||
| name || description | type | default |
|
||||
| ---------- | ------------- | ---------------- | ------- | --------------------------- |
|
||||
| 文章相关 | title | 文章标题 | text | 使用 `_config.yml` 中的配置 |
|
||||
| ^^ | description | 文章简介 | ^^ | ^^ |
|
||||
| ^^ | author | 文章作者 | ^^ | ^^ |
|
||||
| ^^ | color | 文章主题颜色 | ^^ | ^^ |
|
||||
| ^^ | impression | 文章头图 | ^^ | ^^ |
|
||||
| ^^ | categories | 目录分类 | list | 未定义 |
|
||||
| ^^ | tags | 文章标签 | ^^ | ^^ |
|
||||
144
posts/组件/ButtonGroup.md
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
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' }
|
||||
]"
|
||||
/>
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: "设置开机自启动的 Jekyll 服务"
|
||||
description: "通过 systemd 实现一个开机自启的 Jekyll 服务,通常来说,这对使用 Jekyll 作为服务后端的网站很有用。"
|
||||
color: ""
|
||||
impression: ""
|
||||
color: "#aa0c2b"
|
||||
impression: "/assets/images/120678678_p0.webp"
|
||||
categories:
|
||||
- 随笔
|
||||
tags:
|
||||
|
||||
BIN
public/assets/images/120678678_p0.webp
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
public/assets/images/120678678_p0_mesh-gradient.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 478 KiB After Width: | Height: | Size: 383 KiB |
BIN
public/assets/images/131559307_p0.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
public/assets/images/131559307_p0_mesh-gradient.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 593 KiB After Width: | Height: | Size: 538 KiB |
BIN
public/assets/images/138124971_p0.webp
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
public/assets/images/138124971_p0_mesh-gradient.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/assets/images/26/jekylmt_1.webp
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
public/assets/images/26/jekylmt_2.webp
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
public/assets/images/26/jekylmt_3.webp
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
public/assets/images/26/jekylmt_4.webp
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
public/assets/images/26/jekylmt_5.webp
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
public/assets/images/26/jekylmt_6.webp
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
public/assets/images/26/jekylmt_7.webp
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
public/assets/images/26/jekylmt_8.webp
Normal file
|
After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
BIN
public/assets/images/avatar_transparent.webp
Normal file
|
After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -7,8 +7,8 @@ REMOTE="${1:-origin}"
|
||||
BRANCH="${2:-master}"
|
||||
|
||||
echo "🚀 开始版本更新流程..."
|
||||
echo " 远程: ${REMOTE}/${BRANCH}"
|
||||
echo ""
|
||||
echo "远程: ${REMOTE}/${BRANCH}"
|
||||
echo "::>------------------------"
|
||||
|
||||
# 获取当前提交数
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
@@ -27,25 +27,32 @@ echo "📝 更新版本号..."
|
||||
# 使用 sed 更新 package.json 中的版本号
|
||||
# 匹配 "version": "..." 模式
|
||||
sed -i "s/\"version\": \".*\"/\"version\": \"${NEW_VERSION}\"/" package.json
|
||||
echo " ✓ 版本已更新为: ${NEW_VERSION}"
|
||||
echo "版本将更新到: ${NEW_VERSION}"
|
||||
echo "::>------------------------"
|
||||
|
||||
# Git 操作
|
||||
echo "📦 提交更改..."
|
||||
git add package.json
|
||||
git commit -m "chore(package): update to version ${NEW_VERSION}"
|
||||
echo "✓ 已提交: chore(package): update to version ${NEW_VERSION}"
|
||||
echo "已提交: chore(package): update to version ${NEW_VERSION}"
|
||||
echo "::>------------------------"
|
||||
|
||||
echo "🏷️ 创建标签..."
|
||||
git tag "${NEW_VERSION}"
|
||||
echo "✓ 已创建标签: ${NEW_VERSION}"
|
||||
echo "已创建标签: ${NEW_VERSION}"
|
||||
echo "::>------------------------"
|
||||
|
||||
echo "🌐 推送更改..."
|
||||
git push "${REMOTE}" "${BRANCH}"
|
||||
echo "✓ 已推送提交到 ${REMOTE}/${BRANCH}"
|
||||
echo "已推送提交到 ${REMOTE}/${BRANCH}"
|
||||
echo "::>------------------------"
|
||||
|
||||
echo "🌐 推送标签..."
|
||||
git push "${REMOTE}" "${NEW_VERSION}"
|
||||
echo "✓ 已推送标签: ${NEW_VERSION}"
|
||||
echo "已推送标签: ${NEW_VERSION}"
|
||||
echo "::>------------------------"
|
||||
|
||||
echo "==========================="
|
||||
echo "✅ 版本更新完成!"
|
||||
echo "新版本: ${NEW_VERSION}"
|
||||
echo "==========================="
|
||||
|
||||