1
0
mirror of https://github.com/sendevia/website.git synced 2026-03-07 16:22:34 +08:00

13 Commits

21 changed files with 284 additions and 139 deletions

View File

@@ -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(() => {
@@ -197,13 +221,9 @@ onUnmounted(() => {
<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 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">
@@ -239,7 +259,34 @@ onUnmounted(() => {
</div>
</template>
<template v-else>
<div class="single" :style="{ backgroundImage: `url('${blobCache.get(rawImgList[0]) || rawImgList[0]}')` }"></div>
<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>

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -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,113 @@
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);
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 +166,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 +208,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,57 +312,25 @@
}
}
}
}
@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%;
border-radius: var(--md-sys-shape-corner-large);
}
&.next {
width: 0%;
border-radius: var(--md-sys-shape-corner-large);
}
}
}
}

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -30,8 +30,6 @@
h2,
a {
@include mixin.typescale-style("display-small");
text-decoration: none;
}
}

View File

@@ -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;

View File

@@ -1,5 +1,5 @@
{
"version": "26.1.30(268)",
"version": "26.2.8(281)",
"scripts": {
"update-version": "bash ./scripts/update-version.sh",
"docs:dev": "vitepress dev",

View File

@@ -1,8 +1,8 @@
---
title: "Markdown 扩展示例"
description: "本页面展示了 VitePress 提供的一些内置 markdown 扩展功能。"
color: ""
impression: ""
color: "#f53283"
impression: "/assets/images/138124971_p0.webp"
categories:
tags:
- markdown 语法

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 KiB

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -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 "==========================="