Compare commits
27 Commits
26.1.18(26
...
26.2.22(28
| Author | SHA1 | Date | |
|---|---|---|---|
| b1f8d6f15b | |||
| c7af2eeb0f | |||
| 1fbac018f9 | |||
| cbd3979476 | |||
| 5bcec415b3 | |||
| 1ada9579ce | |||
| 1215d1ca22 | |||
| 56264d3504 | |||
| 251b22f1b5 | |||
| 2c5cbfad01 | |||
| 42fedf3c3d | |||
| ecd4fb2886 | |||
| fff5f3ba2b | |||
| 5faa7e4220 | |||
| 568c7a7427 | |||
| bef3d5ce77 | |||
| 00b034f02b | |||
| f27da216e0 | |||
| 051aadc565 | |||
| 26ad7fed76 | |||
| 73a1b4044d | |||
| 1a6a60d5cc | |||
| 8cdbd059bb | |||
| 9ec1a05160 | |||
| 07ad900625 | |||
| 3a986b01eb | |||
| f9203e5815 |
@@ -253,93 +253,95 @@ const clearTags = () => {
|
||||
</div>
|
||||
|
||||
<Transition name="expand" mode="out-in">
|
||||
<aside v-if="isSettingsOpen" ref="settingsPanelRef" class="panel">
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h6>排序</h6>
|
||||
</div>
|
||||
<div class="page-size-options">
|
||||
<MaterialButton
|
||||
:color="sortField === 'date' ? 'filled' : 'tonal'"
|
||||
size="s"
|
||||
class="group horizontal"
|
||||
icon="acute"
|
||||
@click="sortField = 'date'"
|
||||
>
|
||||
时间
|
||||
</MaterialButton>
|
||||
<MaterialButton
|
||||
:color="sortField === 'title' ? 'filled' : 'tonal'"
|
||||
size="s"
|
||||
class="group horizontal"
|
||||
icon="match_case"
|
||||
@click="sortField = 'title'"
|
||||
>
|
||||
标题
|
||||
</MaterialButton>
|
||||
<div>
|
||||
<aside v-if="isSettingsOpen" class="panel">
|
||||
<div ref="settingsPanelRef" class="container">
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h6>排序</h6>
|
||||
</div>
|
||||
<div class="page-size-options">
|
||||
<MaterialButton
|
||||
:icon="sortOrder === 'asc' ? 'arrow_upward' : 'arrow_downward'"
|
||||
color="tonal"
|
||||
:color="sortField === 'date' ? 'filled' : 'tonal'"
|
||||
size="s"
|
||||
@click="sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'"
|
||||
class="group horizontal"
|
||||
icon="acute"
|
||||
@click="sortField = 'date'"
|
||||
>
|
||||
{{ sortOrder === "asc" ? "正序" : "倒序" }}
|
||||
时间
|
||||
</MaterialButton>
|
||||
<MaterialButton
|
||||
:color="sortField === 'title' ? 'filled' : 'tonal'"
|
||||
size="s"
|
||||
class="group horizontal"
|
||||
icon="match_case"
|
||||
@click="sortField = 'title'"
|
||||
>
|
||||
标题
|
||||
</MaterialButton>
|
||||
<div>
|
||||
<MaterialButton
|
||||
:icon="sortOrder === 'asc' ? 'arrow_upward' : 'arrow_downward'"
|
||||
color="tonal"
|
||||
size="s"
|
||||
@click="sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'"
|
||||
>
|
||||
{{ sortOrder === "asc" ? "正序" : "倒序" }}
|
||||
</MaterialButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h6>每页显示</h6>
|
||||
</div>
|
||||
<div class="page-size-options">
|
||||
<MaterialButton
|
||||
v-for="opt in pageSizeOptions"
|
||||
:key="opt"
|
||||
:color="pageSize === opt ? 'filled' : 'tonal'"
|
||||
:icon="pageSize === opt ? 'check' : ''"
|
||||
class="group horizontal"
|
||||
size="s"
|
||||
@click="pageSize = opt"
|
||||
>
|
||||
{{ opt }}
|
||||
</MaterialButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h6>每页显示</h6>
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h6>分类 <span v-if="selectedCategory" @click="selectedCategory = ''">clear</span></h6>
|
||||
</div>
|
||||
<div class="chip-container">
|
||||
<MaterialChip
|
||||
v-for="cat in postsStore.allCategories"
|
||||
:key="cat"
|
||||
:color="selectedCategory === cat ? 'tonal' : 'outlined'"
|
||||
:icon="selectedCategory === cat ? 'check' : ''"
|
||||
@click="toggleCategory(cat)"
|
||||
>
|
||||
{{ cat }}
|
||||
</MaterialChip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-size-options">
|
||||
<MaterialButton
|
||||
v-for="opt in pageSizeOptions"
|
||||
:key="opt"
|
||||
:color="pageSize === opt ? 'filled' : 'tonal'"
|
||||
:icon="pageSize === opt ? 'check' : ''"
|
||||
class="group horizontal"
|
||||
size="s"
|
||||
@click="pageSize = opt"
|
||||
>
|
||||
{{ opt }}
|
||||
</MaterialButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h6>分类 <span v-if="selectedCategory" @click="selectedCategory = ''">clear</span></h6>
|
||||
</div>
|
||||
<div class="chip-container">
|
||||
<MaterialChip
|
||||
v-for="cat in postsStore.allCategories"
|
||||
:key="cat"
|
||||
:color="selectedCategory === cat ? 'tonal' : 'outlined'"
|
||||
:icon="selectedCategory === cat ? 'check' : ''"
|
||||
@click="toggleCategory(cat)"
|
||||
>
|
||||
{{ cat }}
|
||||
</MaterialChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h6>标签 <span v-if="selectedTags.length" @click="clearTags">clear</span></h6>
|
||||
</div>
|
||||
<div class="chip-container">
|
||||
<MaterialChip
|
||||
v-for="tag in postsStore.allTags"
|
||||
:key="tag"
|
||||
:color="selectedTags.includes(tag) ? 'tonal' : 'outlined'"
|
||||
:icon="selectedTags.includes(tag) ? 'check' : ''"
|
||||
@click="toggleTag(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</MaterialChip>
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h6>标签 <span v-if="selectedTags.length" @click="clearTags">clear</span></h6>
|
||||
</div>
|
||||
<div class="chip-container">
|
||||
<MaterialChip
|
||||
v-for="tag in postsStore.allTags"
|
||||
:key="tag"
|
||||
:color="selectedTags.includes(tag) ? 'tonal' : 'outlined'"
|
||||
:icon="selectedTags.includes(tag) ? 'check' : ''"
|
||||
@click="toggleTag(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</MaterialChip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -28,7 +28,7 @@ const siteVersion = theme.value.siteVersion;
|
||||
</div>
|
||||
<div class="beian-info">
|
||||
<div class="beian-gongan">
|
||||
<img src="/assets/images/beian.png" loading="eager" />
|
||||
<img src="/assets/images/beian.webp" loading="eager" />
|
||||
<a href="https://beian.mps.gov.cn/#/query/webSearch?code=23020002230215" target="_blank"
|
||||
>黑公网安备23020002230215</a
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ const config = reactive({
|
||||
animFast: 300,
|
||||
});
|
||||
|
||||
const { frontmatter, theme } = useGlobalData();
|
||||
const { frontmatter, theme, page } = useGlobalData();
|
||||
const headerRef = ref<HTMLElement | null>(null);
|
||||
const isHovering = useElementHover(headerRef);
|
||||
|
||||
@@ -56,6 +56,16 @@ const currentRealIndex = computed(() => {
|
||||
return ((virtualIndex.value % totalCount.value) + totalCount.value) % totalCount.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取带 _mesh-gradient 后缀的图片地址
|
||||
* @param url 原始图片地址
|
||||
*/
|
||||
const getGradientUrl = (url: string): string => {
|
||||
if (!url) return "";
|
||||
// 在扩展名前插入 _mesh-gradient
|
||||
return url.replace(/(\.[^.]+)$/, "_mesh-gradient$1");
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 CSS 变量中的时间值
|
||||
* @param cssVar CSS 变量名
|
||||
@@ -72,11 +82,16 @@ const parseTimeToken = (cssVar: string, defaultVal: number): number => {
|
||||
|
||||
/**
|
||||
* 并行加载图片并存入 Blob 缓存以消除闪烁
|
||||
* 同时缓存原图和梯度背景图
|
||||
* @param urls 图片地址列表
|
||||
*/
|
||||
const cacheImages = async (urls: string[]) => {
|
||||
if (!isClient()) return;
|
||||
const uncached = urls.filter((url) => !blobCache.has(url));
|
||||
|
||||
// 生成所有需要缓存的 URL(原图 + 梯度图)
|
||||
const allUrls = urls.flatMap((url) => [url, getGradientUrl(url)]);
|
||||
const uncached = allUrls.filter((url) => !blobCache.has(url));
|
||||
|
||||
await Promise.all(
|
||||
uncached.map(async (url) => {
|
||||
try {
|
||||
@@ -84,9 +99,9 @@ const cacheImages = async (urls: string[]) => {
|
||||
const blob = await res.blob();
|
||||
blobCache.set(url, URL.createObjectURL(blob));
|
||||
} catch {
|
||||
blobCache.set(url, url);
|
||||
blobCache.set(url, url); // 失败时回退到原始 URL
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -118,8 +133,17 @@ const slotStates = computed(() => {
|
||||
];
|
||||
const { cls, order, offset } = stateMap[relativePos];
|
||||
const imgIndex = (((currentRealIndex.value + offset) % totalCount.value) + totalCount.value) % totalCount.value;
|
||||
|
||||
const rawUrl = rawImgList.value[imgIndex];
|
||||
return { id: slotId, className: cls, imgUrl: blobCache.get(rawUrl) || rawUrl, order };
|
||||
const rawGradientUrl = getGradientUrl(rawUrl);
|
||||
|
||||
return {
|
||||
id: slotId,
|
||||
className: cls,
|
||||
imgUrl: blobCache.get(rawUrl) || rawUrl,
|
||||
gradientUrl: blobCache.get(rawGradientUrl) || rawGradientUrl,
|
||||
order,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,7 +156,7 @@ const { pause, resume } = useRafFn(
|
||||
step(1).then(() => (remainingTime.value = config.duration));
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -173,7 +197,7 @@ watch(
|
||||
await cacheImages(newList);
|
||||
if (hasMultiple.value && isClient()) resume();
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
@@ -193,56 +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="{ backgroundImage: `url('${slot.imgUrl}')`, order: slot.order }"
|
||||
></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>
|
||||
<div class="single" :style="{ backgroundImage: `url('${blobCache.get(rawImgList[0]) || rawImgList[0]}')` }"></div>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
<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 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>
|
||||
|
||||
@@ -6,14 +6,19 @@ import { useGlobalData } from "../composables/useGlobalData";
|
||||
import { useNavStateStore } from "../stores/navState";
|
||||
import { useScreenWidthStore } from "../stores/screenWidth";
|
||||
import { useSearchStateStore } from "../stores/searchState";
|
||||
import { useThemeStateStore } from "../stores/themeState";
|
||||
|
||||
const { page, theme } = useGlobalData();
|
||||
const screenWidthStore = useScreenWidthStore();
|
||||
const searchStateStore = useSearchStateStore();
|
||||
const navStateStore = useNavStateStore();
|
||||
const themeStateStore = useThemeStateStore();
|
||||
|
||||
/** 标签动画状态 */
|
||||
const isLabelAnimating = ref(false);
|
||||
/** 清理函数列表 */
|
||||
const cleanupFunctions: Array<() => void> = [];
|
||||
/** 已观察元素集合 */
|
||||
const observedElements = new WeakSet<HTMLElement>();
|
||||
|
||||
/**
|
||||
@@ -32,17 +37,18 @@ function observeWidth(el: HTMLElement, parentSelector: string) {
|
||||
parentSelector,
|
||||
ignoreParentLimit: true, // 允许撑开父级
|
||||
},
|
||||
[el]
|
||||
[el],
|
||||
);
|
||||
cleanupFunctions.push(cleanup);
|
||||
}
|
||||
|
||||
// 计算 Segments
|
||||
/** 计算导航分段数据 */
|
||||
const navSegment = computed(() => {
|
||||
const items = theme.value.navSegment;
|
||||
return Array.isArray(items) && items.length > 0 ? items : [];
|
||||
});
|
||||
|
||||
/** 计算导航栏容器类名 */
|
||||
const navClass = computed(() => {
|
||||
let baseClass = "";
|
||||
if (screenWidthStore.screenWidth > 840) {
|
||||
@@ -56,14 +62,15 @@ const navClass = computed(() => {
|
||||
return `${baseClass} ${expansionClass}`;
|
||||
});
|
||||
|
||||
/** 计算标签类名 */
|
||||
const labelClass = computed(() => [
|
||||
navStateStore.isNavExpanded ? "right" : "bottom",
|
||||
isLabelAnimating.value ? "animating" : "",
|
||||
]);
|
||||
|
||||
/**
|
||||
* 规范化路径,去除 /index.md、.md、.html 后缀及末尾斜杠
|
||||
* @param path
|
||||
* 规范化路径,去除后缀及末尾斜杠
|
||||
* @param path 原始路径
|
||||
*/
|
||||
function normalizePath(path: string): string {
|
||||
return path.replace(/(\/index)?\.(md|html)$/, "").replace(/\/$/, "");
|
||||
@@ -71,7 +78,7 @@ function normalizePath(path: string): string {
|
||||
|
||||
/**
|
||||
* 检查链接是否为当前活动页面的链接
|
||||
* @param link
|
||||
* @param link 目标链接
|
||||
*/
|
||||
function isActive(link: string): boolean {
|
||||
const currentPath = normalizePath(page.value.relativePath);
|
||||
@@ -81,7 +88,7 @@ function isActive(link: string): boolean {
|
||||
|
||||
/**
|
||||
* 检查链接是否为外部链接
|
||||
* @param link
|
||||
* @param link 目标链接
|
||||
*/
|
||||
function isExternalLink(link: string): boolean {
|
||||
return /^https?:\/\//.test(link);
|
||||
@@ -89,7 +96,7 @@ function isExternalLink(link: string): boolean {
|
||||
|
||||
/**
|
||||
* 切换搜索栏状态
|
||||
* @param event
|
||||
* @param event 鼠标事件
|
||||
*/
|
||||
function toggleSearch(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
@@ -97,21 +104,29 @@ function toggleSearch(event: MouseEvent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换导航栏状态
|
||||
* @param event
|
||||
* 切换导航栏展开状态
|
||||
* @param event 鼠标事件
|
||||
*/
|
||||
function toggleNav(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
// 暂时只有在屏幕宽度大于断点时才能切换导航栏状态
|
||||
if (screenWidthStore.isAboveBreakpoint) {
|
||||
navStateStore.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换颜色偏好 (自动/亮/暗)
|
||||
* @param event 鼠标事件
|
||||
*/
|
||||
function toggleTheme(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
themeStateStore.cycleTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 label 引用并初始化观察器
|
||||
* @param el
|
||||
* @param parentSelector
|
||||
* @param el DOM 元素
|
||||
* @param parentSelector 父级选择器
|
||||
*/
|
||||
function setLabelRef(el: any, parentSelector: string) {
|
||||
if (el instanceof HTMLElement) {
|
||||
@@ -120,8 +135,8 @@ function setLabelRef(el: any, parentSelector: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理动画结束
|
||||
* @param el
|
||||
* 处理动画结束事件
|
||||
* @param el 事件目标
|
||||
*/
|
||||
function onAnimationEnd(el: EventTarget | null) {
|
||||
if (el) {
|
||||
@@ -129,7 +144,7 @@ function onAnimationEnd(el: EventTarget | null) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听状态变化,触发宽度计算 */
|
||||
/** 监听导航栏展开状态,触发重绘和动画 */
|
||||
watch(
|
||||
() => [navStateStore.isNavExpanded],
|
||||
() => {
|
||||
@@ -137,13 +152,14 @@ watch(
|
||||
nextTick(() => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (isClient()) {
|
||||
onMounted(() => {
|
||||
screenWidthStore.init();
|
||||
navStateStore.init();
|
||||
themeStateStore.init();
|
||||
|
||||
nextTick(() => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
@@ -185,6 +201,18 @@ if (isClient()) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<MaterialButton
|
||||
class="theme-btn"
|
||||
size="m"
|
||||
color="text"
|
||||
:title="themeStateStore.currentLabel"
|
||||
:icon="themeStateStore.currentIcon"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
</MaterialButton>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const { start: lockTimer } = useTimeoutFn(
|
||||
isLocked.value = false;
|
||||
},
|
||||
1200,
|
||||
{ immediate: false }
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
/** 计算文章 ID 与短链 */
|
||||
@@ -126,11 +126,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 +153,7 @@ watch(
|
||||
} else {
|
||||
indicator.value.opacity = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(headingsActiveId, () => {
|
||||
@@ -174,7 +174,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 }}
|
||||
|
||||
@@ -12,10 +12,10 @@ const { copy: copyToClipboard } = useClipboard();
|
||||
const publishTime = computed(() => frontmatter.value?.date);
|
||||
const lastUpdatedTime = computed(() => page.value?.lastUpdated);
|
||||
const formattedPublishDate = computed(() =>
|
||||
publishTime.value ? useDateFormat(publishTime.value, "YYYY年M月D日").value : ""
|
||||
publishTime.value ? useDateFormat(publishTime.value, "YYYY年M月D日").value : "",
|
||||
);
|
||||
const lastUpdatedRawTime = computed(() =>
|
||||
lastUpdatedTime.value ? useDateFormat(lastUpdatedTime.value, "YYYY-MM-DD HH:mm:ss").value : ""
|
||||
lastUpdatedTime.value ? useDateFormat(lastUpdatedTime.value, "YYYY-MM-DD HH:mm:ss").value : "",
|
||||
);
|
||||
|
||||
/** 相对时间显示配置 */
|
||||
@@ -35,7 +35,7 @@ const timeAgo = useTimeAgo(
|
||||
minute: (n: number) => `${n}分钟`,
|
||||
second: (n: number) => `${n}秒`,
|
||||
} as any,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 计算最终显示的编辑时间文本 */
|
||||
@@ -142,7 +142,7 @@ if (isClient()) {
|
||||
enhanceDomStyles();
|
||||
bindImageEvents();
|
||||
},
|
||||
{ childList: true, subtree: true, characterData: true }
|
||||
{ childList: true, subtree: true, characterData: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
@@ -157,21 +157,15 @@ if (isClient()) {
|
||||
<template>
|
||||
<Header />
|
||||
<main id="article-content" ref="articleContentRef" @click="handleAnchorClick">
|
||||
<hgroup>
|
||||
<h1>{{ frontmatter.title || page.title }}</h1>
|
||||
<div v-if="frontmatter.description">
|
||||
<hr />
|
||||
<h6>{{ frontmatter.description }}</h6>
|
||||
</div>
|
||||
</hgroup>
|
||||
<Content />
|
||||
<PrevNext />
|
||||
</main>
|
||||
<aside id="article-aside">
|
||||
<div class="post-info">
|
||||
<p class="date-publish" v-if="formattedPublishDate">发布于 {{ formattedPublishDate }}</p>
|
||||
<p v-if="frontmatter.description" class="description">{{ frontmatter.description }}</p>
|
||||
<p v-if="formattedPublishDate" class="date-publish">发布于 {{ formattedPublishDate }}</p>
|
||||
<ClientOnly>
|
||||
<p class="date-update" :title="lastUpdatedRawTime" v-if="formattedLastUpdated">
|
||||
<p v-if="formattedLastUpdated" :title="lastUpdatedRawTime" class="date-update">
|
||||
{{ formattedLastUpdated }}
|
||||
</p>
|
||||
</ClientOnly>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useScreenWidthStore } from "./screenWidth";
|
||||
/** 导出 */
|
||||
export const useNavStateStore = defineStore("navState", () => {
|
||||
const isNavExpanded = ref<boolean>(false);
|
||||
const cookieName = "nav-expanded";
|
||||
const cookieName = "navbar_expanded";
|
||||
const screenWidthStore = useScreenWidthStore();
|
||||
|
||||
/** 初始从 Cookie 读取状态 */
|
||||
@@ -36,7 +36,7 @@ export const useNavStateStore = defineStore("navState", () => {
|
||||
isNavExpanded.value = true;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -91,7 +91,7 @@ export const useNavStateStore = defineStore("navState", () => {
|
||||
if (!isAbove) {
|
||||
isNavExpanded.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
90
.vitepress/theme/stores/themeState.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 颜色主题偏好管理
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
export type ThemePreference = "auto" | "light" | "dark";
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 监听用户偏好变化,持久化到 Cookie */
|
||||
watch(preference, (val) => {
|
||||
setCookie("theme_preference", val);
|
||||
});
|
||||
|
||||
/** 切换主题模式 */
|
||||
const cycleTheme = () => {
|
||||
const modes: ThemePreference[] = ["auto", "light", "dark"];
|
||||
const nextIndex = (modes.indexOf(preference.value) + 1) % modes.length;
|
||||
preference.value = modes[nextIndex];
|
||||
};
|
||||
|
||||
/** 当前状态对应的图标 */
|
||||
const currentIcon = computed(() => {
|
||||
switch (preference.value) {
|
||||
case "light":
|
||||
return "light_mode";
|
||||
case "dark":
|
||||
return "dark_mode";
|
||||
default:
|
||||
return "brightness_auto";
|
||||
}
|
||||
});
|
||||
|
||||
/** 当前状态对应的文本标签 */
|
||||
const currentLabel = computed(() => {
|
||||
switch (preference.value) {
|
||||
case "light":
|
||||
return "亮色模式";
|
||||
case "dark":
|
||||
return "深色模式";
|
||||
default:
|
||||
return "跟随系统";
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@@ -20,28 +20,32 @@
|
||||
position: relative;
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
top: -12px;
|
||||
|
||||
padding: 24px;
|
||||
|
||||
max-width: 520px;
|
||||
min-width: 340px;
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
|
||||
box-shadow: 0px 1px 6px -3px var(--md-sys-color-shadow);
|
||||
overflow-y: overlay;
|
||||
transform-origin: 0px 0px;
|
||||
transform-origin: top left;
|
||||
z-index: 100;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
padding: 24px;
|
||||
|
||||
max-width: 520px;
|
||||
min-width: 340px;
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-extra-large);
|
||||
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
|
||||
box-shadow: 0px 1px 6px -3px var(--md-sys-color-shadow);
|
||||
overflow-y: overlay;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.section {
|
||||
h6 {
|
||||
display: inline-flex;
|
||||
@@ -52,6 +56,9 @@
|
||||
|
||||
margin-block-end: 12px;
|
||||
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
||||
span {
|
||||
@include mixin.material-symbols($size: 16);
|
||||
|
||||
@@ -73,6 +80,36 @@
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px) {
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
backdrop-filter: brightness(0.5);
|
||||
transform-origin: center center;
|
||||
z-index: 999;
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
|
||||
max-width: 380px;
|
||||
min-width: 330px;
|
||||
|
||||
transform-origin: center center;
|
||||
translate: -50% -50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,20 +185,27 @@
|
||||
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
transition:
|
||||
transform var(--md-sys-motion-spring-default-spatial-duration) var(--md-sys-motion-spring-default-spatial),
|
||||
opacity var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-slow-effect);
|
||||
transition: opacity var(--md-sys-motion-spring-slow-effect-duration) var(--md-sys-motion-spring-slow-effect);
|
||||
|
||||
.container {
|
||||
transition: transform var(--md-sys-motion-spring-default-spatial-duration) var(--md-sys-motion-spring-default-spatial);
|
||||
}
|
||||
}
|
||||
|
||||
.expand-enter-from,
|
||||
.expand-leave-to {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: scale(0.8);
|
||||
|
||||
.container {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.expand-enter-to,
|
||||
.expand-leave-from {
|
||||
transform: scale(1);
|
||||
.container {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
z-index: 0;
|
||||
|
||||
.single {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 50% 50%;
|
||||
|
||||
position: relative;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -24,7 +30,116 @@
|
||||
|
||||
background-size: cover;
|
||||
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
h1 {
|
||||
@include mixin.typescale-style(
|
||||
"display-large",
|
||||
$font-size: 72rem,
|
||||
$line-height: 78rem,
|
||||
$font-variation-settings: "wght" 900
|
||||
);
|
||||
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin-inline-start: 48px;
|
||||
|
||||
color: transparent;
|
||||
|
||||
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: 72rem,
|
||||
$line-height: 78rem,
|
||||
$font-variation-settings: "wght" 800
|
||||
);
|
||||
|
||||
margin-inline: 36px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
@include mixin.typescale-style(
|
||||
"display-large",
|
||||
$font-size: 48rem,
|
||||
$line-height: 54rem,
|
||||
$font-variation-settings: "wght" 900
|
||||
);
|
||||
|
||||
grid-column: 1 / 3;
|
||||
|
||||
margin-inline: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
object-fit: cover;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
||||
&:nth-of-type(1) {
|
||||
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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stage {
|
||||
@@ -54,6 +169,13 @@
|
||||
overflow: hidden;
|
||||
transition: var(--carousel-duration) var(--md-sys-motion-spring-slow-effect);
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&.current {
|
||||
width: 90%;
|
||||
|
||||
@@ -89,6 +211,30 @@
|
||||
opacity: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
&.current {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
&.next {
|
||||
width: calc(15% - 12px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
&.current {
|
||||
width: 100%;
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
}
|
||||
|
||||
&.next {
|
||||
width: 0%;
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,56 +315,79 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px) {
|
||||
.Header {
|
||||
@media screen and (max-width: 1600px) {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.Header {
|
||||
@media screen and (max-width: 1200px) {
|
||||
grid-column: span 8;
|
||||
|
||||
height: 46vw;
|
||||
min-height: 270px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
.Header {
|
||||
@media screen and (max-width: 840px) {
|
||||
grid-column: span 6;
|
||||
|
||||
min-height: 270px;
|
||||
|
||||
.carousel-container .stage .item {
|
||||
&.current {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
&.next {
|
||||
width: calc(15% - 12px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.Header {
|
||||
@media screen and (max-width: 600px) {
|
||||
grid-column: span 4;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-container .stage .item {
|
||||
&.current {
|
||||
width: 100%;
|
||||
.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);
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
.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);
|
||||
}
|
||||
|
||||
&.next {
|
||||
width: 0%;
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-large);
|
||||
&: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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
|
||||
cursor: pointer;
|
||||
transition: grid var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial),
|
||||
transition:
|
||||
grid var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial),
|
||||
gap var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -57,7 +58,9 @@
|
||||
width: 56px;
|
||||
|
||||
text-align: center;
|
||||
font-variation-settings: "FILL" 1, "wght" 300;
|
||||
font-variation-settings:
|
||||
"FILL" 1,
|
||||
"wght" 300;
|
||||
|
||||
transition: font-variation-settings var(--md-sys-motion-spring-fast-spatial-duration)
|
||||
var(--md-sys-motion-spring-fast-spatial);
|
||||
@@ -77,11 +80,15 @@
|
||||
}
|
||||
|
||||
&:hover span {
|
||||
font-variation-settings: "FILL" 1, "wght" 600;
|
||||
font-variation-settings:
|
||||
"FILL" 1,
|
||||
"wght" 600;
|
||||
}
|
||||
|
||||
&:active span {
|
||||
font-variation-settings: "FILL" 1, "wght" 200;
|
||||
font-variation-settings:
|
||||
"FILL" 1,
|
||||
"wght" 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +111,8 @@
|
||||
|
||||
position: relative;
|
||||
|
||||
transition: grid var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial),
|
||||
transition:
|
||||
grid var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial),
|
||||
gap var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect);
|
||||
}
|
||||
|
||||
@@ -122,7 +130,8 @@
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
|
||||
overflow: hidden;
|
||||
transition: height var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect),
|
||||
transition:
|
||||
height var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect),
|
||||
width var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect);
|
||||
z-index: 1;
|
||||
|
||||
@@ -135,7 +144,9 @@
|
||||
span {
|
||||
@include mixin.material-symbols();
|
||||
|
||||
font-variation-settings: "FILL" 1, "wght" 300;
|
||||
font-variation-settings:
|
||||
"FILL" 1,
|
||||
"wght" 300;
|
||||
|
||||
transition: font-variation-settings var(--md-sys-motion-spring-fast-spatial-duration)
|
||||
var(--md-sys-motion-spring-fast-spatial);
|
||||
@@ -195,7 +206,8 @@
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
|
||||
pointer-events: none;
|
||||
transition: background-color var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect),
|
||||
transition:
|
||||
background-color var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect),
|
||||
height var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect),
|
||||
width var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial);
|
||||
z-index: 0;
|
||||
@@ -220,19 +232,40 @@
|
||||
}
|
||||
|
||||
&.inactive .accent .icon span {
|
||||
font-variation-settings: "FILL" 0, "wght" 200;
|
||||
font-variation-settings:
|
||||
"FILL" 0,
|
||||
"wght" 200;
|
||||
}
|
||||
|
||||
&:hover .accent .icon span {
|
||||
font-variation-settings: "FILL" 1, "wght" 400;
|
||||
font-variation-settings:
|
||||
"FILL" 1,
|
||||
"wght" 400;
|
||||
}
|
||||
|
||||
&:active .accent .icon span {
|
||||
font-variation-settings: "FILL" 1, "wght" 200;
|
||||
font-variation-settings:
|
||||
"FILL" 1,
|
||||
"wght" 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
margin-block: 24px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.bar {
|
||||
flex-direction: row;
|
||||
|
||||
@@ -310,6 +343,12 @@
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
.label {
|
||||
@include mixin.typescale-style("label-large");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -474,11 +513,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
.NavBar.rail {
|
||||
display: none;
|
||||
@media screen and (max-width: 840px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,59 +13,6 @@
|
||||
margin-block-start: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
> hgroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
|
||||
margin-block-end: 24px;
|
||||
|
||||
h1 {
|
||||
@include mixin.typescale-style("display-large");
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
hr {
|
||||
flex-grow: 1;
|
||||
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
display: grid;
|
||||
align-items: start;
|
||||
gap: 6px;
|
||||
grid-template-columns: max-content auto;
|
||||
|
||||
width: max-content;
|
||||
|
||||
line-height: 18px;
|
||||
text-align: justify;
|
||||
|
||||
&::before {
|
||||
@include mixin.material-symbols($name: "message", $size: 18);
|
||||
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*[class^="language-"] {
|
||||
@@ -96,7 +43,8 @@
|
||||
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: border-radius var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial),
|
||||
transition:
|
||||
border-radius var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial),
|
||||
opacity var(--md-sys-motion-spring-default-effect-duration) var(--md-sys-motion-spring-default-effect);
|
||||
visibility: hidden;
|
||||
z-index: 2;
|
||||
@@ -389,7 +337,7 @@
|
||||
}
|
||||
|
||||
&:first-child .title-with-achor {
|
||||
margin-block-start: 0px;
|
||||
margin-block-start: 24px;
|
||||
}
|
||||
|
||||
.title-with-achor {
|
||||
@@ -402,6 +350,7 @@
|
||||
display: inline-block;
|
||||
|
||||
line-height: 54px;
|
||||
font-variation-settings: "wght" 600;
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-medium);
|
||||
|
||||
@@ -503,7 +452,12 @@
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline solid;
|
||||
text-decoration: underline 2px dotted;
|
||||
text-underline-offset: 3px;
|
||||
|
||||
&:hover {
|
||||
text-decoration-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@@ -821,6 +775,12 @@
|
||||
|
||||
&::before {
|
||||
@include mixin.material-symbols($size: 14);
|
||||
|
||||
margin-block: 1px auto;
|
||||
}
|
||||
|
||||
&.description::before {
|
||||
content: "message";
|
||||
}
|
||||
|
||||
&.date-publish::before {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,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;
|
||||
|
||||
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM node:lts-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app
|
||||
|
||||
RUN npm install
|
||||
|
||||
EXPOSE 4173
|
||||
|
||||
CMD ["npm", "run", "docs:preview"]
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 sendevia
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
16
package.json
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "26.1.18(262)",
|
||||
"version": "26.2.22(289)",
|
||||
"scripts": {
|
||||
"update-version": "bash ./scripts/update-version.sh",
|
||||
"docs:dev": "vitepress dev",
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/core": "^14.2.0",
|
||||
"pinia": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdit/plugin-align": "^0.22.2",
|
||||
"@mdit/plugin-footnote": "^0.22.3",
|
||||
"@mdit/plugin-tasklist": "^0.22.2",
|
||||
"@mdit/plugin-align": "^0.23.1",
|
||||
"@mdit/plugin-footnote": "^0.22.4",
|
||||
"@mdit/plugin-tasklist": "^0.22.3",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"sass-embedded": "^1.93.0",
|
||||
"vitepress": "^2.0.0-alpha.15",
|
||||
"vue": "^3.5.0"
|
||||
"sass-embedded": "^1.97.3",
|
||||
"vitepress": "^2.0.0-alpha.16",
|
||||
"vue": "^3.5.28"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: "Markdown 扩展示例"
|
||||
description: "本页面展示了 VitePress 提供的一些内置 markdown 扩展功能。"
|
||||
color: ""
|
||||
impression: ""
|
||||
color: "#f53283"
|
||||
impression: "/assets/images/138124971_p0.webp"
|
||||
categories:
|
||||
tags:
|
||||
- markdown 语法
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: "关于这个主题的一些事"
|
||||
description: ""
|
||||
color: "#59f3b3"
|
||||
impression: "/assets/images/133925125_p0.webp"
|
||||
color: "#0084ff"
|
||||
impression: "/assets/images/131559307_p0.webp"
|
||||
categories:
|
||||
- 随笔
|
||||
tags:
|
||||
|
||||
@@ -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/121337686_p0_mesh-gradient.webp
Normal file
|
After Width: | Height: | Size: 12 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: 624 KiB After Width: | Height: | Size: 538 KiB |
BIN
public/assets/images/132307491_p0_mesh-gradient.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/assets/images/133925125_p0_mesh-gradient.webp
Normal file
|
After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 653 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 |
|
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 |
BIN
public/assets/images/beian.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# 远程仓库配置
|
||||
REMOTE="${1:-origin}"
|
||||
BRANCH="${2:-master}"
|
||||
|
||||
echo "🚀 开始版本更新流程..."
|
||||
echo "远程: ${REMOTE}/${BRANCH}"
|
||||
echo "::>------------------------"
|
||||
|
||||
# 获取当前提交数
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
|
||||
@@ -13,16 +23,36 @@ DAY=$(date +%-d)
|
||||
|
||||
NEW_VERSION="${YEAR}.${MONTH}.${DAY}(${NEXT_COMMIT_COUNT})"
|
||||
|
||||
echo "📝 更新版本号..."
|
||||
# 使用 sed 更新 package.json 中的版本号
|
||||
# 匹配 "version": "..." 模式
|
||||
sed -i "s/\"version\": \".*\"/\"version\": \"${NEW_VERSION}\"/" package.json
|
||||
|
||||
echo "Version updated to: ${NEW_VERSION}"
|
||||
echo "版本将更新到: ${NEW_VERSION}"
|
||||
echo "::>------------------------"
|
||||
|
||||
# Git 操作
|
||||
echo "📦 提交更改..."
|
||||
git add package.json
|
||||
git commit -m "chore(package): update to version ${NEW_VERSION}"
|
||||
echo "Committed: chore(package): update to version ${NEW_VERSION}"
|
||||
echo "已提交: chore(package): update to version ${NEW_VERSION}"
|
||||
echo "::>------------------------"
|
||||
|
||||
echo "🏷️ 创建标签..."
|
||||
git tag "${NEW_VERSION}"
|
||||
echo "Tagged: ${NEW_VERSION}"
|
||||
echo "已创建标签: ${NEW_VERSION}"
|
||||
echo "::>------------------------"
|
||||
|
||||
echo "🌐 推送更改..."
|
||||
git push "${REMOTE}" "${BRANCH}"
|
||||
echo "已推送提交到 ${REMOTE}/${BRANCH}"
|
||||
echo "::>------------------------"
|
||||
|
||||
echo "🌐 推送标签..."
|
||||
git push "${REMOTE}" "${NEW_VERSION}"
|
||||
echo "已推送标签: ${NEW_VERSION}"
|
||||
echo "::>------------------------"
|
||||
|
||||
echo "==========================="
|
||||
echo "✅ 版本更新完成!"
|
||||
echo "新版本: ${NEW_VERSION}"
|
||||
echo "==========================="
|
||||
|
||||