::: zh-CN

视频教程:

文字教程:


Valaxy官网

前情提要

为什么选择Valaxy
这边我引用Valaxy官网的一段话

为什么是 Valaxy?

构想新一代静态博客框架/生成器。
「告诉你两件好事吧」:
第一它与 Hexo 相比开发体验和速度上都更胜一筹
第二它与 VitePress/VuePress 相比拥有更多针对博客的集成功能,譬如文章列表钩子、自动路由与组件注册、可覆盖的布局与主题等。
我认为 Valaxy 最突出的优势在于它的热更新开发体验与可定制性,但你编写文章或博客配置时,你只需要保存,所有的变更将会即刻显示在页面上,几乎无需等待!

此外,Valaxy 的主题还较少,但以 valaxy-theme-yun 为例,你可以覆盖主题中的任何组件,来定制或编写你自己的主题。

由于我自己在搭建这个博客上踩过很多的坑,尝试搜索也没有很多的教程,只能靠着自己和开发文档一点一点摸索,我感觉我应该为博客圈做些什么……

创建Valaxy

环境配置

首先你需要有Node.js 16+的版本才可以进行Valaxy的搭建
你可以点我进入Node.js官网

安装Valaxy

由于大部分用户都为Windows系统,所以这里以Windows为例

创建项目文件夹

当然,我们不能直接进行部署,首先我们需要创建一个项目文件夹来存储我们项目开发时的文件
这里在你喜欢的地方创建一个文件夹,名称随意,但是切记,尽量不要出现中文和空格,不然后期会很烦人,打开它
我在这里以blog为例
创建好之后,双击文件夹的地址栏,输入cmd,打开命令行,以下的操作均在命令行中完成
教程1

创建Valaxy项目

1
2
3
4
# 安装pnpm
npm i -g pnpm
# 创建valaxy项目
pnpm create valaxy

把上面我给出的两条指令打进命令行中,以拉取最新的Valaxy项目(这里我把命令行的颜色改为紫色,以便更好地查看)
教程2
这里提示选择拉取的项目
因为我们是做个人博客使用,所以这里选择Blog:
教程3
这里提示我们输入博客文件夹的名称,我们默认即可,当然你也可以选择自定义的名称:
这里会提示我们选择是否需要安装,我们输入y:
教程4
按上下方向键选择安装使用的代理,这里选择 pnpm,回车:
教程5
酱酱!我们很简单的就完成了Valaxy的搭建
这里我们输入o可以快速打开网页预览
教程6
输入e可以自动打开VSCode进行编辑

小白看过来!
什么是VSCode?
Visual Studio Code,一款炒鸡方便的代码编辑器,建议大家人手一个

编辑配置

目录结构

pages: 所有页面
posts: 博客文章,文件格式为markdown
public: 静态资源文件夹,可以直接在文章中引用
styles: 覆盖主题样式,文件夹下的 index.xxx 文件将会被自动加载
components: 自定义你的组件
layouts: 自定义布局
locales: 自定义国际化关键词
valaxy.config.ts: 用户配置文件
site.config.ts: 站点配置文件

valaxy.config.ts

在我们初次打开VSCode之后,右下角会提示下载一些插件,直接选择安装
安装完成后,我们把鼠标悬浮在代码之上,就会看到一些简易的注释
您可以选择根据Valaxy官网开发文档进行配置,也可以参考我的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import type { UserThemeConfig } from 'valaxy-theme-yun'
import { defineValaxyConfig } from 'valaxy'

//这两行均需要安装对应的插件,详情请查看Valaxy开发文档
//import { addonTwikoo } from 'valaxy-addon-twikoo' //接入Twikoo评论系统
//import { addonMeting } from 'valaxy-addon-meting' //添加Meting音乐播放器
// add icons what you will need

const safelist = [
'i-ri-home-line',
]

/**
* User Config
*/
export default defineValaxyConfig<UserThemeConfig>({
// site config see site.config.ts

theme: 'yun',

themeConfig: {
banner: {
enable: true,
title: '無限进步',
},
pages: [
{
name: '网络世界的小伙伴们',
url: '/links/',
icon: 'i-ri-open-arm-line', //这里的icon是Valaxy自带的图标,你可以在https://icones.js.org/找到你需要的图标,然后复制到icon字段中
//这里的ico我踩过坑,所以多说两句,这里的ICO复制名字即可,但是你需要在前面添加i-ri-【ICO名字】
color: 'hotpink',
},
{
name: '感情故事', //这个girls文件夹默认是没有的,你需要自己在pages里创建,并在girls文件夹里创建index.md文件
url: '/girls/',
icon: 'i-ri-heart-3-line',
color: 'hotpink',
},
{
name: '分类',
url: '/categories/',
icon: 'i-ri-apps-line',
color: 'dodgerblue',
},
{
name: '标签',
url: '/tags/',
icon: 'i-ri-bookmark-3-line',
color: 'dodgerblue',
},
],
colors: {
primary: "#D69B54",
},
//页脚
footer: {
since: 2023,
powered: true, //这里是显示Valaxy驱动信息的,尊重作者劳动成果,我选择开启
beian: {
enable: true,
icp: '', //这里是备案号,如果你不需要备案号,可以将上面的enable改为false即可
},
},

//背景图,这里为我自己添加的字段
bg_image: {
enable: true, //这里是背景图的设置,你可以设置白日模式和夜间模式的背景图,如果你不需要背景图,可以将上面的enable改为false即可
url: "", // 白日模式背景
dark: "", // 夜间模式背景
},

//鼠标点击烟花特效
fireworks: {
enable: true,
colors: ['#FFE57D', '#FFCD88', '#E6F4AD']
},

},
unocss: { safelist },
siteConfig: {
// 启用评论
comment: {
enable: true //这里是评论的设置,如果你不需要评论,可以将enable改为false即可
},
},
// 设置 valaxy-addon-twikoo 配置项
addons: [
addonTwikoo({
envId: '', // 自建服务地址
}),
//音乐播放器,如需要配置,请查看https://github.com/metowolf/MetingJS
addonMeting({
global: true,
/** @see https://github.com/metowolf/MetingJS */
props: {
id: '',
server: '',
type: '',
mode: '',
},
})
],
})

site.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import { defineSiteConfig } from 'valaxy'

export default defineSiteConfig({
url: '', //你网站的URL
favicon: "", // 网页图标链接
lang: 'zh-CN', //默认语言
title: "", //网站标题
subtitle: '',//网站副标题
author: {
name: '',//博主名称
avatar: "", //头像链接
status: {
emoji: '💛' // 头像旁边的emoji
},
},

description: '', //简介
social: [
{
name: 'RSS',
link: '/atom.xml', //这个是博客自带的RSS订阅,尽量留着,方便其他博友为你订阅
icon: 'i-ri-rss-line',
color: 'orange',
},
{
name: 'GitHub',
link: '', //这里填写你的GitHub地址,不需要的话删除此字段即可
icon: 'i-ri-github-line',
color: '#6e5494',
},
{
name: '网易云音乐',
link: '', //这里填写你的网易云音乐地址,不需要的话删除此字段即可
icon: 'i-ri-netease-cloud-music-line',
color: '#C20C0C',
},
{
name: '哔哩哔哩',
link: '', //这里填写你的BiliBili地址,不需要的话删除此字段即可
icon: 'i-ri-bilibili-line',
color: '#FF8EB3',
},
{
name: 'Twitter',
link: '', //这里填写你的Twitter地址,不需要的话删除此字段即可
icon: 'i-ri-twitter-x-fill',
color: 'black',
},
{
name: 'E-Mail',
link: 'mailto:YourEmail', //这里在mailto后面填写你的Email地址,不需要的话删除此字段即可
icon: 'i-ri-mail-line',
color: '#8E71C1',
},
],

search: {
enable: true,
},
comment: {
enable: true
},
statistics: {
enable: true,
readTime: {
/**
* 阅读速度
*/
speed: {
cn: 300,
en: 200,
},
},
},

sponsor: {
enable: true,
title: '我很可爱,请给我钱!',
methods: [
{
name: '支付宝',
url: '', //这里填写你的支付宝收款码图片链接
color: '#00A3EE',
icon: 'i-ri-alipay-line',
},
{
name: '微信支付',
url: '',//这里填写你的微信收款码图片链接
color: '#2DC100',
icon: 'i-ri-wechat-pay-line',
},
],
},
})

插件

评论区(以Twikoo为例)

安装依赖

1
pnpm add valaxy-addon-twikoo

加载插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import type { UserThemeConfig } from 'valaxy-theme-yun'
import { defineValaxyConfig } from 'valaxy'
import { addonTwikoo } from 'valaxy-addon-twikoo'
import { addonMeting } from 'valaxy-addon-meting'

export default defineValaxyConfig<UserThemeConfig>({
themeConfig: {
// ...
}
addons: [
addonTwikoo({
envId: '', // 自建服务地址
})
],
})

Twikoo的配置请查看Twikoo官方文档

Vue配置

如果你需要自定义Vue的组件,可以把组件放到/components文件夹下
如果你不知道Vue是什么,请查看Vue官方文档
但是我不喜欢把文件分散放,所以我直接在原本的Valaxy主题文件中放置了我的Vue
路径:\node_modules\valaxy-theme-yun\components
这里推荐几个我自己写的Vue组件,希望可以帮到你

  1. 网站页脚插入更多的链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue'

// 自定义链接
const customLinks = [
{
name: '开往',
link: 'https://www.travellings.cn/go-by-clouds.html',
icon: 'https://www.travellings.cn/assets/logo.gif',
},
]

// 旅行者一号距离地球的信息
const voyagerDistance = ref<string>('正在加载...')

// 模拟获取旅行者一号距离
const getVoyagerDistance = (): void => {
const now = new Date()
const start = new Date('01/17/2024 00:00:00') // 旅行者1号开始计算的时间
const timeDifferenceInSeconds = (now.getTime() - start.getTime()) / 1000 // 转换为秒
const distanceInKilometers = Math.trunc(23400000000 + timeDifferenceInSeconds * 17) // 距离=秒数*速度
const astronomicalUnits = (distanceInKilometers / 149600000).toFixed(6) // 天文单位

voyagerDistance.value = `旅行者 1 号当前距离地球 ${distanceInKilometers.toLocaleString()} 千米,约为 ${astronomicalUnits} 个天文单位 🚀`
}

// 定时器
let intervalId: NodeJS.Timeout | null = null

onMounted(() => {
getVoyagerDistance() // 初始化数据
intervalId = setInterval(getVoyagerDistance, 1000) // 每秒更新一次
})

onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId) // 组件卸载时清除定时器
}
})
</script>

<template>
<!-- 自定义链接 -->
<div class="custom-links flex justify-center items-center gap-2" p="1">
<template v-for="(link, index) in customLinks" :key="index">
<template v-if="link.icon">
<a :href="link.link" target="_blank" rel="noopener">
<img :src="link.icon" :alt="link.name" />
</a>
</template>
<template v-else>
<a :href="link.link" target="_blank" rel="noopener">{{ link.name }}</a>
</template>
<span v-if="index < customLinks.length - 1"> |</span>
</template>
</div>

<!-- 萌ICP备案信息 -->
<div class="beian" m="y-2">
<a href="https://icp.gov.moe/?keyword=[萌号]" target="_blank" rel="noopener">萌ICP备号</a>
</div>

<!-- 旅行者一号距离地球的信息 -->
<div class="voyager-distance" m="y-2">
{{ voyagerDistance }}
</div>
</template>

<style lang="scss">
.custom-links img {
height: 20px; /* 根据需要调整图标大小 */
vertical-align: middle;
}

.voyager-distance {
font-size: 14px;
color: #666;
text-align: center;
}
</style>
  1. DomainCheck.vue帮助你更好的检测你的网站是否被盗用
    原理就是检测用户进入的域名是否为你设置的域名,不是的话提示并且让用户跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<template>
<!-- 这个组件不需要模板内容,因为它主要是通过脚本逻辑来操作DOM -->
</template>

<script lang="ts" setup>
import { onMounted } from 'vue'

onMounted(() => {
const validDomain = ''; // 官方域名
const redirectUrl = 'https://'; // 重定向链接
const hostname = document.location.hostname;

function createWatermark(text) {
const watermarkDiv = document.createElement('div');
watermarkDiv.style.pointerEvents = 'none';
watermarkDiv.style.position = 'fixed';
watermarkDiv.style.top = '0';
watermarkDiv.style.left = '0';
watermarkDiv.style.width = '100%';
watermarkDiv.style.height = '100%';
watermarkDiv.style.zIndex = '9999';
watermarkDiv.style.opacity = '0.1';
watermarkDiv.style.background = 'transparent';
watermarkDiv.style.overflow = 'hidden';
watermarkDiv.style.display = 'flex';
watermarkDiv.style.justifyContent = 'center';
watermarkDiv.style.alignItems = 'center';
watermarkDiv.style.flexWrap = 'wrap';

const watermarkText = document.createElement('div');
watermarkText.innerText = text;
watermarkText.style.color = 'black';
watermarkText.style.fontSize = '30px';
watermarkText.style.transform = 'rotate(-30deg)';
watermarkText.style.whiteSpace = 'nowrap';
watermarkText.style.margin = '20px';

for (let i = 0; i < 100; i++) {
watermarkDiv.appendChild(watermarkText.cloneNode(true));
}

document.body.appendChild(watermarkDiv);
}

// 如果是 localhost,直接进入,并在控制台提示用户
if (hostname === 'localhost') {
console.log('Completely: 当前处于本地开发环境 (localhost)');
return;
}
// 如果访问的是 domain.com ,直接跳转到 www.domain.com
else if (hostname === 'domain.com') { //这里需要更改为你自己的域名
console.log('Completely: 已经跳转到官方域名 (www.domain.co)'); //这里需要改为你的官方域名
window.location.replace(redirectUrl);
}
// 如果访问的是其他域名,提示用户并添加水印
else if (hostname !== validDomain) {
createWatermark(validDomain); // 给页面加上前边设置的域名为水印
const userResponse = confirm("提示:您当前浏览的页面不是正版(或者站点已经迁移域名),建议您跳转至官方(www.domain.com)进行浏览!如果不是博主的域名及时留言反馈,博主域名为:www.domain.com");
if (userResponse) {
window.location.replace(redirectUrl);
}
}
});
</script>
  1. NoticeBar.vue公告栏组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
<script lang="ts" setup>
import { ref, onMounted } from 'vue';

const userIp = ref('');
const ipLocation = ref('');
const distance = ref(''); // 与站长的距离
const serverDistance = ref(''); // 与博客服务器的距离
const greeting = ref('');
const isLoading = ref(true);
const error = ref('');
const isVisible = ref(true); // 控制公告栏是否显示

const getCityCoordinates = async (ip: string) => {
const apiKey = 'API_KEY'; // 替换为实际的 API 密钥,在https://wcode.net/get-apikey
const response = await fetch(`https://wcode.net/api/ip/v3/info/free?ip=${ip}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
}
});
const data = await response.json();
//ipLocation.value = `${data.data.region.iso_code} ${data.data.region.name} ${data.data.city.name}` || '未知地区';

if (data.status === 'success' && data.data.extra_info.latitude && data.data.extra_info.longitude) {
return { lat: parseFloat(data.data.extra_info.latitude), lng: parseFloat(data.data.extra_info.longitude) };
} else {
console.error('Failed to get city coordinates:', data);
return { lat: 0, lng: 0 }; // 默认值
}
};

onMounted(async () => {
try {
// 使用 ip.useragentinfo.com 获取 IP 详细信息
const ipResponse = await fetch('https://ip.useragentinfo.com/json?ip='); //这里使用的公共API不保证可用,请更换为可用的API
const locationData = await ipResponse.json();

// 解析返回的数据
userIp.value = locationData.ip;
ipLocation.value = `${locationData.short_name} ${locationData.country} ${locationData.province} ${locationData.city} ${locationData.area}` || '未知地区'; //这里请根据你的API灵活更改显示信息

// 获取用户所在城市的经纬度
const userLocation = await getCityCoordinates(locationData.ip);

// 检查经纬度是否有效
if (userLocation.lat === 0 && userLocation.lng === 0) {
throw new Error('无法获取有效的经纬度信息');
}

// 计算与站长的距离(设站长位置为) //请自行设置站长经纬度
const stationLocation = { lat: , lng: };
distance.value = calculateDistance(stationLocation, userLocation).toFixed(2);

// 博客服务器经纬度(设服务器位置为) //请自行设置服务器经纬度
const serverLocation = { lat: , lng: };
// 计算与博客服务器的距离
serverDistance.value = calculateDistance(serverLocation, userLocation).toFixed(2);

// 根据时间生成问候语
const hour = new Date().getHours();
greeting.value = hour < 12 ? '早上好,美好的一天又开始了😎' : hour < 18 ? '下午好,累了就好好休息一下吧🎶' : '晚上好,在属于自己的时间里好好放松吧😶‍🌫️';
} catch (err) {
error.value = '无法加载公告信息,请稍后重试。刷新后如果问题仍然存在,请通过【替换为你的邮箱】联系我;
console.error('Error:', err);
} finally {
isLoading.value = false;
}
});

function calculateDistance(loc1: { lat: number, lng: number }, loc2: { lat: number, lng: number }) {
const R = 6371;
const dLat = (loc2.lat - loc1.lat) * Math.PI / 180;
const dLon = (loc2.lng - loc1.lng) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(loc1.lat * Math.PI / 180) * Math.cos(loc2.lat * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}

// 关闭公告栏
function closeNotice() {
isVisible.value = false;
}
</script>

<template>
<Transition name="slide">
<div v-if="isVisible" class="notice-bar">
<div class="notice-content">
<button class="close-button" @click="closeNotice">×</button>
<h3>公告</h3>
<p v-if="isLoading">加载中...</p>
<p v-else-if="error">{{ error }}</p>
<template v-else>
<p>欢迎来到Mete0r的博客!有任何问题请联系邮箱: <a :href="`mailto:【替换为你的邮箱】`">【替换为你的邮箱】</a></p>
<p>🎉 欢迎信息 🎉</p>
<p>  欢迎来自 <span class="city">{{ ipLocation }}</span> 的小伙伴,<span class="greeting">{{ greeting }}</span>!您现在距离站长约 <span class="distance">{{ distance }}</span> 公里,距离博客服务器约 <span class="distance">{{ serverDistance }}</span> 公里。当前的IP地址为: <span class="ip">{{ userIp }}</span>,祝您在我的博客里玩的开心!</p>
</template>
</div>
</div>
</Transition>
</template>

<style scoped>
.notice-bar {
position: fixed;
top: 20px;
right: 20px;
background-color: rgba(255, 255, 255, 0.75); /* 不透明度降低到 75% */
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
}

.notice-content {
position: relative;
}

.close-button {
position: absolute;
top: 0;
right: 0;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #666;
}

.close-button:hover {
color: #000;
}

.notice-content h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: bold;
}

.notice-content p {
margin: 5px 0;
font-size: 14px;
}

/* 颜色样式 */
.city {
color: #5bbad5;
}

.greeting {
color: #FFC0CB;
}

.distance {
color: #5bbad5;
}

.ip {
color: #5bd576;
}

/* 滑入滑出动画 */
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease-out;
}

.slide-enter-from,
.slide-leave-to {
transform: translateX(100%);
}
</style>

当然我就是一瓶不满半瓶晃荡,我不能保证我写的代码是最好的,但是起码是能用的

另外,千万不要忘了在YunFooter.vue和home.vue中引用你的自定义组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script lang="ts" setup>
//...
import DomainCheck from '../components/DomainCheck.vue' // 引入DomainCheck组件
import NoticeBar from '../components/NoticeBar.vue' // 引入公告栏组件
//...
</script>

//...

<template>
<DomainCheck /> <!-- 使用DomainCheck -->
<NoticeBar /> <!-- 使用NoticeBar -->
//...
<template>

像这样就可以引用你刚才的组件了

部署

自行部署

使用以下命令构建打包:

1
npm run build

将 dist 文件夹下的内容部署到自己的服务器上。

GitHub Pages部署

可自行搜索或者观看我的教程视频

最后的最后,感谢您的浏览,如有什么其他的问题,欢迎在评论区向我提问,我看到后会在第一时间回复您

:::

::: en

Technical teaching articles cannot be translated into English. Please understand

::: warning

This content is translated from Chinese by a machine and may contain unrealistic information

:::

:::