::: 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,打开命令行,以下的操作均在命令行中完成

创建Valaxy项目
# 安装pnpm
npm i -g pnpm
# 创建valaxy项目
pnpm create valaxy
把上面我给出的两条指令打进命令行中,以拉取最新的Valaxy项目(这里我把命令行的颜色改为紫色,以便更好地查看)
这里提示选择拉取的项目
因为我们是做个人博客使用,所以这里选择Blog:
这里提示我们输入博客文件夹的名称,我们默认即可,当然你也可以选择自定义的名称:
这里会提示我们选择是否需要安装,我们输入y:
按上下方向键选择安装使用的代理,这里选择 pnpm,回车:
酱酱!我们很简单的就完成了Valaxy的搭建
这里我们输入o可以快速打开网页预览
输入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官网开发文档进行配置,也可以参考我的配置文件:
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
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为例)
安装依赖
pnpm add valaxy-addon-twikoo
加载插件
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组件,希望可以帮到你
- 网站页脚插入更多的链接
<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>
- DomainCheck.vue帮助你更好的检测你的网站是否被盗用
原理就是检测用户进入的域名是否为你设置的域名,不是的话提示并且让用户跳转
<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>
- NoticeBar.vue公告栏组件
<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中引用你的自定义组件
<script lang="ts" setup>
//...
import DomainCheck from '../components/DomainCheck.vue' // 引入DomainCheck组件
import NoticeBar from '../components/NoticeBar.vue' // 引入公告栏组件
//...
</script>
//...
<template>
<DomainCheck /> <!-- 使用DomainCheck -->
<NoticeBar /> <!-- 使用NoticeBar -->
//...
<template>
像这样就可以引用你刚才的组件了
部署
自行部署
使用以下命令构建打包:
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
:::
:::


评论区加载中...