::: 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项目

# 安装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官网开发文档进行配置,也可以参考我的配置文件:

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组件,希望可以帮到你

  1. 网站页脚插入更多的链接
  <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帮助你更好的检测你的网站是否被盗用
    原理就是检测用户进入的域名是否为你设置的域名,不是的话提示并且让用户跳转
<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公告栏组件
<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

:::

:::