Skip to content

Commit e31ab9e

Browse files
committed
feat: 🚀 新增代码块新版样式,支持折叠和展开
1 parent 03cd8da commit e31ab9e

13 files changed

+255
-15
lines changed

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@
44

55
## TODO
66

7-
- 代码块支持折叠功能
87
- 归档页添加 commit 图标风格,如:`http://niubin.site/archive.html`
98
- 修改 themeConfig 动态生效,不需要重启服务
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default `<svg
2+
t="1739882271546"
3+
class="icon"
4+
viewBox="0 0 1024 1024"
5+
version="1.1"
6+
xmlns="http://www.w3.org/2000/svg"
7+
p-id="4346"
8+
width="200"
9+
height="200"
10+
>
11+
<path
12+
d="M959.429379 343.214852 890.590548 274.378068 511.268336 653.699256 131.944078 274.378068 63.105247 343.214852 501.1857 781.294282 521.348925 781.294282Z"
13+
p-id="4347"
14+
></path>
15+
</svg>`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<script setup lang="ts" name="CodeBlockToggle">
2+
import { onMounted } from "vue";
3+
import arrowSvg from "../assets/svg/arrow";
4+
import { useRouter } from "vitepress";
5+
6+
const foldClass = "fold";
7+
const circleClass = "circle";
8+
const arrowClass = "arrow";
9+
10+
/**
11+
* 初始化代码块
12+
*/
13+
const initCodeBlock = () => {
14+
const modes = document.querySelectorAll(".vp-doc div[class*='language-']");
15+
16+
Array.from(modes).forEach(item => {
17+
// 如果当前代码块已经初始化了,则不需要继续执行代码
18+
if (item.querySelector(`.${circleClass}`) && item.querySelector(`.${arrowClass}`)) return;
19+
20+
const arrowElement = createArrowElement(item);
21+
const circleElement = createCircleElement();
22+
23+
item.append(arrowElement);
24+
item.append(circleElement);
25+
});
26+
};
27+
28+
/**
29+
* 创建箭头元素,添加点击事件(折叠/展开)
30+
*/
31+
const createArrowElement = (item: Element) => {
32+
// 获取代码块原来的高度,进行备份
33+
const modeHeight = item.offsetHeight;
34+
// 初始化代码块高度,确保第一次折叠时就有动画
35+
item.style.height = `${modeHeight}px`;
36+
// 获取代码块的元素
37+
const pre = item.querySelector("pre");
38+
const wrapper = item.querySelector(".line-numbers-wrapper");
39+
40+
const div = document.createElement("div");
41+
div.classList.add(arrowClass);
42+
div.innerHTML = arrowSvg;
43+
44+
const codeBlockState = {
45+
expand: { height: `${modeHeight}px`, display: "block", speed: 80 },
46+
fold: { height: "var(--tk-code-block-fold-height)", display: "none", speed: 400 },
47+
};
48+
49+
let timeoutId: NodeJS.Timeout | null = null;
50+
51+
// 箭头点击事件
52+
div.onclick = () => {
53+
const isFold = div.classList.contains(foldClass);
54+
// 如果是折叠状态,则需要展开
55+
const state = codeBlockState[isFold ? "expand" : "fold"];
56+
57+
item.style.height = state.height;
58+
59+
clearTimeout(timeoutId);
60+
61+
timeoutId = setTimeout(() => {
62+
pre.style.display = state.display;
63+
wrapper.style.display = state.display;
64+
clearTimeout(timeoutId);
65+
}, state.speed);
66+
67+
div.classList.toggle(foldClass);
68+
};
69+
70+
return div;
71+
};
72+
/**
73+
* 创建三个圆圈元素
74+
*/
75+
const createCircleElement = () => {
76+
const div = document.createElement("div");
77+
div.classList.add(circleClass);
78+
return div;
79+
};
80+
81+
const router = useRouter();
82+
83+
const initRoute = () => {
84+
const selfOnAfterRouteChange = router.onAfterRouteChange;
85+
// 路由切换后的回调
86+
router.onAfterRouteChange = (href: string) => {
87+
selfOnAfterRouteChange?.(href);
88+
// 路由切换后初始化代码块
89+
initCodeBlock();
90+
};
91+
};
92+
93+
onMounted(() => {
94+
initCodeBlock();
95+
initRoute();
96+
});
97+
</script>
98+
99+
<template></template>
100+
101+
<style lang="scss">
102+
.vp-doc div[class*="language-"] {
103+
transition: height 0.3s;
104+
overflow: hidden;
105+
106+
.vp-code {
107+
padding-top: var(--tk-code-block-fold-height);
108+
}
109+
110+
.line-numbers-wrapper {
111+
margin-top: var(--tk-code-block-fold-height);
112+
padding-top: 0;
113+
}
114+
115+
/* 代码块三个圆圈 */
116+
.circle {
117+
position: absolute;
118+
top: calc(var(--tk-code-block-fold-height) / 2);
119+
left: 14px;
120+
transform: translateY(-50%);
121+
width: 12px;
122+
height: 12px;
123+
border-radius: 50%;
124+
background: #fc625d;
125+
-webkit-box-shadow:
126+
20px 0 #fdbc40,
127+
40px 0 #35cd4b;
128+
box-shadow:
129+
20px 0 #fdbc40,
130+
40px 0 #35cd4b;
131+
}
132+
133+
/* 代码块语言 */
134+
span.lang {
135+
position: absolute;
136+
z-index: 3;
137+
top: calc(var(--tk-code-block-fold-height) / 2);
138+
left: 75px;
139+
transform: translateY(-50%);
140+
font-size: 18px;
141+
color: var(--vp-c-text-1);
142+
text-transform: uppercase;
143+
font-weight: bold;
144+
width: fit-content;
145+
}
146+
147+
/* 一键复制图标 */
148+
button.copy {
149+
width: 18px;
150+
height: 18px;
151+
position: absolute;
152+
top: calc(var(--tk-code-block-fold-height) / 2);
153+
right: 36px;
154+
transform: translateY(-50%);
155+
opacity: 1;
156+
background-size: 14px;
157+
background-color: transparent;
158+
border: none;
159+
color: var(--vp-c-text-1);
160+
fill: var(--vp-c-text-1);
161+
&:hover,
162+
.copied {
163+
background-color: transparent;
164+
border: none;
165+
}
166+
}
167+
168+
/* 语言和一键复制图标不会消失 */
169+
&:hover button.copy + span.lang,
170+
button.copy:focus + span.lang,
171+
&:hover > button.copy,
172+
button.copy:focus {
173+
opacity: 1;
174+
}
175+
176+
/* 箭头 */
177+
.arrow {
178+
cursor: pointer;
179+
position: absolute;
180+
z-index: 3;
181+
top: calc(var(--tk-code-block-fold-height) / 2);
182+
right: 14px;
183+
transform: translateY(-50%);
184+
transition: all 0.3s;
185+
186+
svg {
187+
width: 16px;
188+
height: 16px;
189+
fill: var(--vp-c-text-1);
190+
}
191+
192+
/* 代码块折叠后后旋转 -90 度 */
193+
&.fold {
194+
transform: rotate(90deg) translateX(-50%);
195+
}
196+
}
197+
}
198+
</style>

vitepress-theme-tk/src/components/CommentArtalk.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const reloadArtalk = () => {
4646
const initRoute = () => {
4747
const selfOnAfterRouteChange = router.onAfterRouteChange;
4848
// 路由切换后的回调
49-
router.onAfterRouteChange = async (href: string) => {
49+
router.onAfterRouteChange = (href: string) => {
5050
selfOnAfterRouteChange?.(href);
5151
// 路由切换后更新评论内容
5252
reloadArtalk();

vitepress-theme-tk/src/components/CommentGiscus.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const reloadGiscus = () => {
4646
const initRoute = () => {
4747
const selfOnAfterRouteChange = router.onAfterRouteChange;
4848
// 路由切换后的回调
49-
router.onAfterRouteChange = async (href: string) => {
49+
router.onAfterRouteChange = (href: string) => {
5050
selfOnAfterRouteChange?.(href);
5151
// 路由切换后更新评论内容
5252
reloadGiscus();

vitepress-theme-tk/src/components/CommentTwikoo.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const initJs = () => {
3131
3232
const selfOnAfterRouteChange = router.onAfterRouteChange;
3333
// 路由切换后的回调
34-
router.onAfterRouteChange = async (href: string) => {
34+
router.onAfterRouteChange = (href: string) => {
3535
selfOnAfterRouteChange?.(href);
3636
// 路由切换后更新评论内容
3737
reloadTwikoo(href);

vitepress-theme-tk/src/components/CommentWaline.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const initWaline = async () => {
3232
const initRoute = () => {
3333
const selfOnAfterRouteChange = router.onAfterRouteChange;
3434
// 路由切换后的回调
35-
router.onAfterRouteChange = async (href: string) => {
35+
router.onAfterRouteChange = (href: string) => {
3636
selfOnAfterRouteChange?.(href);
3737
// 路由切换后更新评论内容
3838
waline?.update();

vitepress-theme-tk/src/components/HomeFullscreenWallpaper.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ const handleFullscreenChange = () => {
8686
};
8787
8888
const addOrRemoveClass = (
89-
isFullscreen: boolean,
89+
add: boolean,
9090
options: { el: Element | null; executeClass?: string; notExecuteClass?: string; execute?: boolean }[]
9191
) => {
9292
// 进入全屏
93-
if (isFullscreen) {
93+
if (add) {
9494
options.forEach(item => {
9595
if (item.execute !== false) item.executeClass && item.el?.classList.add(item.executeClass);
9696
else item.notExecuteClass && item.el?.classList.add(item.notExecuteClass);

vitepress-theme-tk/src/config/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export interface TkThemeConfig {
4040
* @default '["#e74c3c", "#409EFF", "#DAA96E", "#0C819F", "#27ae60", "#ff5c93", "#fd726d", "#f39c12", "#9b59b6"]'
4141
*/
4242
bgColor?: string[];
43+
/**
44+
* 是否使用新版代码块样式,如果为 false 则使用官方默认样式
45+
*
46+
* @default true
47+
*/
48+
codeBlock?: boolean;
4349
/**
4450
* 在首页最顶部进入全屏后,使用壁纸模式,仅当 (banner.bgStyle = 'bigImg' & banner.imgSrc 存在) 或 bodyBgImg.imgSrc 存在才生效
4551
*/

vitepress-theme-tk/src/configProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const createConfigProvider = (Layout: Component) => {
1616
setup(_, { slots }) {
1717
const { theme } = useUnrefData();
1818
// 往主题注入数据
19-
provide(postsSymbol, theme.posts);
19+
provide(postsSymbol, theme.posts || emptyPost);
2020

2121
// 开启监听器
2222
usePermalinks().startWatch();

vitepress-theme-tk/src/layout/index.vue

+13-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import CommentArtalk from "../components/CommentArtalk.vue";
1818
import CommentGiscus from "../components/CommentGiscus.vue";
1919
import CommentWaline from "../components/CommentWaline.vue";
2020
import HomeFullscreenWallpaper from "../components/HomeFullscreenWallpaper.vue";
21+
import CodeBlockToggle from "../components/CodeBlockToggle.vue";
2122
2223
defineOptions({ name: "TkLayout" });
2324
@@ -29,7 +30,7 @@ const prefixClass = getPrefixClass("layout");
2930
const { theme, frontmatter } = useUnrefData();
3031
const { frontmatter: frontmatterRef } = useData();
3132
32-
const { tkTheme = true, tkHome = true, wallpaper = {} } = theme;
33+
const { tkTheme = true, tkHome = true, wallpaper = {}, codeBlock = true } = theme;
3334
3435
const { enabled = true, bgStyle, imgSrc } = { ...theme.banner, ...frontmatter.tk?.banner };
3536
const { provider, render } = { ...theme.comment };
@@ -43,8 +44,10 @@ const commentComponent = {
4344
</script>
4445

4546
<template>
46-
<RightBottomButton />
47-
<BodyBgImage v-if="theme.bodyBgImg?.imgSrc" />
47+
<template v-if="tkTheme">
48+
<RightBottomButton />
49+
<BodyBgImage v-if="theme.bodyBgImg?.imgSrc" />
50+
</template>
4851

4952
<Layout :class="prefixClass">
5053
<template #home-hero-before>
@@ -72,10 +75,13 @@ const commentComponent = {
7275

7376
<template #doc-before>
7477
<slot name="doc-before" />
75-
<ClientOnly>
76-
<ArticleAnalyze />
77-
<ArticleImagePreview />
78-
</ClientOnly>
78+
<template v-if="tkTheme">
79+
<ClientOnly>
80+
<ArticleAnalyze />
81+
<ArticleImagePreview />
82+
<CodeBlockToggle v-if="codeBlock" />
83+
</ClientOnly>
84+
</template>
7985
</template>
8086

8187
<template #doc-after>

vitepress-theme-tk/src/styles/index.scss

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
--tk-namespace: #{$theme-namespace};
4545
--tk-gap2: 10px;
4646
--tk-gap1: 20px;
47+
--tk-code-block-fold-height: 40px;
4748
}
4849

4950
// 使用自定义自定义全局样式变量

vitepress-theme-tk/src/styles/vp.scss

+15
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,18 @@
3232
color: var(--tk-theme-color);
3333
}
3434
}
35+
36+
// 因为 stroke 无法直接写 var(--vp-c-text-1),所以只能写死 var(--vp-c-text-1) 的具体颜色,如果改动了 var(--vp-c-text-1) 的值,则需要修改 stroke 为对于的值
37+
:root {
38+
/* clipboard */
39+
--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(60,60,67,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");
40+
/* clipboard-copy */
41+
--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(60,60,67,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E");
42+
}
43+
44+
:root.dark {
45+
/* clipboard */
46+
--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(223,223,214,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");
47+
/* clipboard-copy */
48+
--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(223,223,214,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E");
49+
}

0 commit comments

Comments
 (0)