mirror of
https://github.com/RhenCloud/Cloud-Home.git
synced 2026-01-22 17:39:07 +08:00
Compare commits
7 Commits
d7b419f401
...
c86bd7708b
| Author | SHA1 | Date | |
|---|---|---|---|
| c86bd7708b | |||
| 12c327fa92 | |||
| 2cdd81e74b | |||
| 66d7c9cb3c | |||
| 175ceec184 | |||
| 5a2754b19e | |||
| 361999a8b1 |
@@ -1,5 +1,9 @@
|
||||
# Github Token
|
||||
VITE_GITHUB_TOKEN=your-github-token
|
||||
|
||||
# UMAMI API KEY
|
||||
VITE_UMAMI_API_KEY=your-umami-api-key
|
||||
|
||||
# SMTP 服务器地址
|
||||
SMTP_HOST=smtp.example.com
|
||||
|
||||
|
||||
58
README.md
58
README.md
@@ -15,6 +15,15 @@
|
||||
- 构建:Vite
|
||||
- 部署:Vercel(静态构建 + Serverless Functions)
|
||||
|
||||
## 致谢
|
||||
|
||||
排名不分先后
|
||||
|
||||
- [Skill Icons](https://github.com/tandpfun/skill-icons):技能图标库,本项目的技能图标来源。
|
||||
- [Netease Mini Player](https://github.com/numakkiyu/NeteaseMiniPlayer):迷你网易云播放器组件,为本项目的音乐播放功能提供支持。
|
||||
|
||||
感谢以上开源项目原作者与维护者的贡献。
|
||||
|
||||
## 配置指南
|
||||
|
||||
### 站点配置文件 (`src/config/siteConfig.ts`)
|
||||
@@ -28,6 +37,10 @@ const siteConfig: SiteConfig = {
|
||||
title: "I'm a software developer.", // 你的简介,可为空
|
||||
avatar: "avatar.webp", // 你的头像,可为public目录下的文件或外部链接
|
||||
bio: "Hello World", // 你的喜欢的一句话,可为空
|
||||
birthday: "xxxx-xx-xx", // 你的生日,可为空
|
||||
gender: "", // 你的性别,可为空
|
||||
pronouns: "", // 你希望别人如何称呼你,可为空
|
||||
location: "", // 你的居住地,可为空
|
||||
},
|
||||
|
||||
// 社交链接,预定义的社交链接可在 `src/components/SocialLink.vue` 中查阅
|
||||
@@ -52,8 +65,38 @@ const siteConfig: SiteConfig = {
|
||||
],
|
||||
|
||||
siteMeta: {
|
||||
title: "RhenCloud", // 网站标题
|
||||
title: "Example Title", // 网站标题
|
||||
icon: "favicon.ico", // 网站图标,可为public目录下的文件或外部链接
|
||||
startDate:"xxxx-xx-xx", // 网站创建日期
|
||||
},
|
||||
|
||||
music: {
|
||||
// 是否启用音乐播放器
|
||||
enable: true,
|
||||
// floating - 浮动模式播放器(推荐)- 用于播放网易云歌单
|
||||
// embed - 嵌入模式播放器 - 用于播放网易云单曲
|
||||
mode: "floating", // "floating" 或 "embed"
|
||||
// 歌单ID:从网易云音乐链接获取,如 https://music.163.com/#/playlist?id=14273792576
|
||||
playlistId: undefined, // 例如: "14273792576"
|
||||
// 歌曲ID:仅在嵌入模式下使用
|
||||
songId: undefined, // 例如: "554242291"
|
||||
// 播放器位置(浮动模式): "bottom-left" | "bottom-right" | "top-left" | "top-right"
|
||||
position: "bottom-left",
|
||||
// 是否显示歌词
|
||||
lyric: true,
|
||||
// 主题: "light" | "dark" | "auto"
|
||||
theme: "dark",
|
||||
// 是否自动播放
|
||||
autoplay: false,
|
||||
// 是否默认以黑胶唱片状态启动(仅浮动模式)
|
||||
defaultMinimized: true,
|
||||
},
|
||||
|
||||
umami: {
|
||||
enable: true, // 是否启用 Umami 分析
|
||||
url: "https://cloud.umami.is/script.js", // Umami 分析脚本 URL,一般无需修改
|
||||
websiteId: "YOUR_WEBSITE_ID", // Umami 网站 ID
|
||||
apiBase: "https://api.umami.is", // Umami API 地址,一般无需修改
|
||||
},
|
||||
|
||||
// 技能图标展示,详见https://github.com/tandpfun/skill-icons#icons-list
|
||||
@@ -100,9 +143,11 @@ const siteConfig: SiteConfig = {
|
||||
footer: {
|
||||
beian: "备案号", // 备案号,留空则不显示
|
||||
beianLink: "https://beian.miit.gov.cn/", // 备案号链接,一般无需修改
|
||||
showHitokoto: true, // 是否显示一言
|
||||
hitokotoType: "a&b&c&d&j", // 一言类型,详见 https://developer.hitokoto.cn/sentence/#%E5%8F%A5%E5%AD%90%E7%B1%BB%E5%9E%8B-%E5%8F%82%E6%95%B0
|
||||
customHtml: '', // 自定义 HTML 代码,如统计代码等
|
||||
hitokoto: {
|
||||
enable: true, // 是否启用一言
|
||||
type: "a&b&c&d&j", // 一言类型,详见 https://developer.hitokoto.cn/sentence/#%E5%8F%A5%E5%AD%90%E7%B1%BB%E5%9E%8B-%E5%8F%82%E6%95%B0
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -112,11 +157,12 @@ const siteConfig: SiteConfig = {
|
||||
- **404 页面**:修改 `public/404.html` 来自定义 404 错误页面的样式与内容。
|
||||
- **友链展示逻辑**:`FriendsSection.vue` 默认使用随机顺序渲染 `siteConfig.friends`,如需固定顺序请修改该组件。
|
||||
|
||||
## 环境变量(邮件发送)
|
||||
## 环境变量
|
||||
|
||||
在 Vercel 控制台或本地 `.env` 配置:
|
||||
|
||||
- `VITE_GITHUB_TOKEN`: 具有仓库读取权限的 GitHub Token,用于绕过 GitHub API 速率限制。
|
||||
- `VITE_GITHUB_TOKEN`: 具有仓库读取权限的 GitHub Token,用于绕过 GitHub API 速率限制。(可选)
|
||||
- `UMAMI_API_KEY`: Umami 分析的 API Key。
|
||||
- `SMTP_HOST`: 邮件服务器主机名
|
||||
- `SMTP_PORT`: 端口(如 465 或 587)
|
||||
- `SMTP_USER`: 发件人邮箱账号
|
||||
@@ -140,7 +186,7 @@ pnpm dev
|
||||
pnpm build
|
||||
```
|
||||
|
||||
产物输出到 `dist/`。
|
||||
产物输出到
|
||||
|
||||
## 部署到 Vercel
|
||||
|
||||
|
||||
@@ -6,11 +6,15 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Cloud Home</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<!-- NeteaseMiniPlayer v2 CSS -->
|
||||
<link rel="stylesheet" href="https://api.hypcvgm.top/NeteaseMiniPlayer/netease-mini-player-v2.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<!-- NeteaseMiniPlayer v2 JS -->
|
||||
<script src="/js/netease-mini-player-v2.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -8,7 +8,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jaseeey/vue-umami-plugin": "^1.4.0",
|
||||
"@vercel/node": "^5.5.15",
|
||||
"cros": "^1.1.0",
|
||||
"express": "^5.2.1",
|
||||
"he": "^1.2.0",
|
||||
"nodemailer": "^7.0.11",
|
||||
|
||||
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
BIN
public/background.png
Normal file
BIN
public/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
30
public/favicon.svg
Normal file
30
public/favicon.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 610.04">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f7e9df;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #f8b8b1;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="cls-2" d="M640,283.5l-53.17-23.12,38.18-43.63s-83.55-129.63-218.48-4.58C498.99,53.12,354.21,0,354.21,0l-34.21,46.81L285.79,0s-144.79,53.12-52.33,212.17c-134.93-125.06-218.48,4.58-218.48,4.58l38.18,43.63L0,283.5s15.97,121.34,143.47,108.7c-100.04,80.05-26.68,178.01-26.68,178.01l54.19-20.61,3.17,57.89s106.76,24.41,145.84-77.8c39.08,102.22,145.84,77.8,145.84,77.8l3.17-57.89,54.19,20.61s73.36-97.96-26.68-178.01c127.5,12.64,143.47-108.7,143.47-108.7ZM320,320.29h0,0Z"/>
|
||||
<path class="cls-1" d="M382.12,300.84l-10.32-4.49,7.41-8.47s-16.22-25.16-42.41-.89c17.95-30.88-10.16-41.19-10.16-41.19l-6.64,9.09-6.64-9.09s-28.11,10.31-10.16,41.19c-26.2-24.28-42.41.89-42.41.89l7.41,8.47-10.32,4.49s3.1,23.55,27.85,21.1c-19.42,15.54-5.18,34.56-5.18,34.56l10.52-4,.62,11.24s20.73,4.74,28.31-15.1c7.59,19.84,28.31,15.1,28.31,15.1l.62-11.24,10.52,4s14.24-19.02-5.18-34.56c24.75,2.45,27.85-21.1,27.85-21.1ZM320,307.98h0Z"/>
|
||||
<path class="cls-1" d="M53.17,260.38c86.15,12.93,173.16,28.52,258.54,45.85,0,0,8.29,1.75,8.29,1.75l-8.39-1.22c-86.11-13.26-173.14-28.72-258.45-46.38h0Z"/>
|
||||
<path class="cls-1" d="M320,307.98c-2.4-83.9-2.34-169.09-.27-253.01,0,0,.27-8.16.27-8.16l.27,8.16c2.07,83.92,2.13,169.1-.27,253.01h0Z"/>
|
||||
<path class="cls-1" d="M320,307.98c85.3-17.65,172.34-33.12,258.45-46.38,0,0,8.39-1.22,8.39-1.22,0,0-8.29,1.75-8.29,1.75-85.38,17.33-172.39,32.92-258.54,45.85h0Z"/>
|
||||
<path class="cls-1" d="M320,307.98c49.91,76.37,98.46,155.2,144.58,233.92,0,0,4.43,7.69,4.43,7.69l-4.88-7.41c-49.64-76.56-98.29-155.32-144.13-234.21h0Z"/>
|
||||
<path class="cls-1" d="M170.99,549.6c45.86-74.98,94.52-149.73,144.13-222.29,0,0,4.88-7.02,4.88-7.02,0,0-4.43,7.31-4.43,7.31-46.15,74.81-94.7,149.63-144.58,222h0Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-1" d="M233.47,212.17c15.24,14.76,30.27,31,44.58,46.72,14.2,15.84,28.81,32.41,41.95,49.09-15.25-14.75-30.27-30.99-44.58-46.72-14.19-15.85-28.8-32.42-41.95-49.09h0Z"/>
|
||||
<path class="cls-1" d="M143.47,392.2c57.53-30.13,116.9-58.48,176.53-84.22-57.52,30.15-116.91,58.46-176.53,84.22h0Z"/>
|
||||
<path class="cls-1" d="M320,529.69c-2.43-73.57-2.44-148.13,0-221.71,2.45,73.57,2.43,148.13,0,221.71h0Z"/>
|
||||
<path class="cls-1" d="M406.53,212.17c-13.14,16.66-27.76,33.25-41.95,49.09-14.32,15.73-29.32,31.95-44.58,46.72,13.13-16.67,27.76-33.26,41.95-49.09,14.32-15.73,29.33-31.94,44.58-46.72h0Z"/>
|
||||
<path class="cls-1" d="M496.53,392.2c-59.62-25.76-119-54.07-176.53-84.22,59.63,25.74,119,54.08,176.53,84.22h0Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1585
public/js/netease-mini-player-v2.js
Normal file
1585
public/js/netease-mini-player-v2.js
Normal file
File diff suppressed because it is too large
Load Diff
197
src/App.vue
197
src/App.vue
@@ -1,19 +1,82 @@
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<main class="app-body">
|
||||
<router-view />
|
||||
</main>
|
||||
<PageSwitcher />
|
||||
<FooterSection :contact="contact" />
|
||||
<div class="app-shell" :style="backgroundStyle">
|
||||
<div class="background-overlay" :style="overlayStyle"></div>
|
||||
<button
|
||||
class="background-toggle"
|
||||
@click="hideComponents = !hideComponents"
|
||||
:title="hideComponents ? '显示内容' : '隐藏内容'"
|
||||
:class="{ active: hideComponents }"
|
||||
>
|
||||
<span class="toggle-icon">{{ hideComponents ? "👁️" : "🙈" }}</span>
|
||||
<span class="toggle-label">{{ hideComponents ? "显示" : "隐藏" }}</span>
|
||||
</button>
|
||||
<Transition name="fade-down">
|
||||
<main class="app-body" v-if="!hideComponents" key="content">
|
||||
<router-view />
|
||||
</main>
|
||||
</Transition>
|
||||
<Transition name="fade-up">
|
||||
<PageSwitcher v-if="!hideComponents" key="switcher" />
|
||||
</Transition>
|
||||
<Transition name="fade-down">
|
||||
<FooterSection v-if="!hideComponents" :contact="contact" key="footer" />
|
||||
</Transition>
|
||||
<!-- 音乐播放器 -->
|
||||
<MusicPlayer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, computed, ref } from "vue";
|
||||
import PageSwitcher from "./components/PageSwitcher.vue";
|
||||
import FooterSection from "./components/FooterSection.vue";
|
||||
import MusicPlayer from "./components/MusicPlayer.vue";
|
||||
import siteConfig from "./config/siteConfig";
|
||||
|
||||
const contact = siteConfig.footer;
|
||||
const bg = siteConfig.appearance.background;
|
||||
const isMobile = ref(false);
|
||||
const hideComponents = ref(false);
|
||||
|
||||
// 检测是否为移动设备
|
||||
const checkIfMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkIfMobile();
|
||||
window.addEventListener("resize", checkIfMobile);
|
||||
});
|
||||
|
||||
const getBackgroundImage = () => {
|
||||
if (!bg.enable) return undefined;
|
||||
|
||||
// 根据屏幕尺寸选择图片
|
||||
const image = isMobile.value && bg.mobileImage ? bg.mobileImage : bg.image;
|
||||
|
||||
if (!image) return undefined;
|
||||
|
||||
return image.startsWith("http") ? image : `/${image}`;
|
||||
};
|
||||
|
||||
const backgroundStyle = computed(() => {
|
||||
const imageUrl = getBackgroundImage();
|
||||
|
||||
if (!imageUrl) return {};
|
||||
|
||||
return {
|
||||
backgroundImage: `url('${imageUrl}')`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundAttachment: "fixed",
|
||||
filter: `blur(${bg.blur}px)`,
|
||||
};
|
||||
});
|
||||
|
||||
const overlayStyle = computed(() => {
|
||||
if (!bg.enable || !getBackgroundImage()) return {};
|
||||
return { backgroundColor: bg.overlay };
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -21,10 +84,132 @@ const contact = siteConfig.footer;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.background-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.background-toggle {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: auto;
|
||||
padding: 8px 12px;
|
||||
border-radius: 50px;
|
||||
background: linear-gradient(135deg, rgba(124, 193, 255, 0.15), rgba(124, 193, 255, 0.05));
|
||||
border: 1.5px solid rgba(124, 193, 255, 0.3);
|
||||
color: #7cc1ff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
backdrop-filter: blur(12px);
|
||||
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
box-shadow: 0 8px 32px rgba(124, 193, 255, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.background-toggle:hover {
|
||||
background: linear-gradient(135deg, rgba(124, 193, 255, 0.25), rgba(124, 193, 255, 0.1));
|
||||
border-color: rgba(124, 193, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(124, 193, 255, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.background-toggle:active {
|
||||
transform: translateY(0px);
|
||||
box-shadow: 0 4px 16px rgba(124, 193, 255, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.background-toggle.active {
|
||||
background: linear-gradient(135deg, rgba(255, 107, 107, 0.2), rgba(255, 107, 107, 0.05));
|
||||
border-color: rgba(255, 107, 107, 0.4);
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.background-toggle.active:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 107, 107, 0.3), rgba(255, 107, 107, 0.1));
|
||||
border-color: rgba(255, 107, 107, 0.6);
|
||||
box-shadow: 0 12px 40px rgba(255, 107, 107, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.background-toggle {
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
/* 上段组件:向上淡出,向下淡入 */
|
||||
.fade-up-enter-active,
|
||||
.fade-up-leave-active {
|
||||
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.fade-up-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
.fade-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
/* 下段组件:向下淡出,向上淡入 */
|
||||
.fade-down-enter-active,
|
||||
.fade-down-leave-active {
|
||||
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.fade-down-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.fade-down-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
<p class="muted" v-if="showHitokoto && quote">
|
||||
「{{ quote }}」<span v-if="from" class="from">—— {{ from }}</span>
|
||||
</p>
|
||||
<p class="muted stats" v-if="showStats && !statsError">
|
||||
👁️ {{ visitors }} visitors · 📊 {{ pageviews }} pageviews
|
||||
</p>
|
||||
<!-- <p class="muted stats" v-if="showStats && statsError">🔒 由于启用了隐私保护拓展,禁用状态统计</p> -->
|
||||
<p class="muted beian" v-if="contact.beian">
|
||||
<a :href="contact.beianLink || 'https://beian.miit.gov.cn/'" target="_blank" rel="noreferrer">
|
||||
{{ contact.beian }}
|
||||
@@ -14,14 +18,19 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import siteConfig from "../config/siteConfig";
|
||||
const props = defineProps({ contact: Object });
|
||||
const quote = ref("");
|
||||
const from = ref("");
|
||||
const showHitokoto = props.contact?.showHitokoto !== false;
|
||||
const pageviews = ref(0);
|
||||
const visitors = ref(0);
|
||||
const statsError = ref(true);
|
||||
const showHitokoto = siteConfig.footer?.hitokoto?.enable;
|
||||
const showStats = ref(siteConfig.umami?.enable);
|
||||
|
||||
const buildHitokotoUrl = () => {
|
||||
const type = siteConfig.footer?.hitokoto?.type;
|
||||
const url = new URL("https://v1.hitokoto.cn/");
|
||||
const type = props.contact?.hitokotoType;
|
||||
if (Array.isArray(type)) {
|
||||
type.filter(Boolean).forEach((t) => url.searchParams.append("c", t));
|
||||
} else if (typeof type === "string") {
|
||||
@@ -44,8 +53,52 @@ const fetchHitokoto = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
if (!siteConfig.umami?.apiBase || !siteConfig.umami?.websiteId) {
|
||||
return;
|
||||
}
|
||||
const apiBase = siteConfig.umami.apiBase;
|
||||
const websiteId = siteConfig.umami.websiteId;
|
||||
const apiKey = import.meta.env.VITE_UMAMI_API_KEY;
|
||||
|
||||
if (!apiKey) return;
|
||||
|
||||
// 获取统计数据
|
||||
const endAt = Date.now();
|
||||
const startAt = new Date(siteConfig.siteMeta.startDate).getTime();
|
||||
|
||||
const resp = await fetch(`${apiBase}/v1/websites/${websiteId}/stats?startAt=${startAt}&endAt=${endAt}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
console.warn(`Stats API returned ${resp.status}`);
|
||||
statsError.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
if (data) {
|
||||
statsError.value = false;
|
||||
pageviews.value = data.pageviews;
|
||||
visitors.value = data.visitors;
|
||||
}
|
||||
|
||||
if (pageviews.value === 0 && visitors.value === 0) {
|
||||
showStats.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
statsError.value = true;
|
||||
console.debug("Stats fetch failed (this is normal if blocked by ad blocker):", e.message);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (showHitokoto) fetchHitokoto();
|
||||
if (showStats.value) fetchStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -54,14 +107,17 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.from {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.beian {
|
||||
font-size: 12px;
|
||||
margin: 6px 0;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.beian a {
|
||||
color: inherit;
|
||||
opacity: 0.85;
|
||||
@@ -69,12 +125,20 @@ onMounted(() => {
|
||||
border-radius: 8px;
|
||||
transition: color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.beian a:hover {
|
||||
color: var(--accent, #7cc1ff);
|
||||
background: rgba(124, 193, 255, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.custom-html {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
font-size: 12px;
|
||||
margin: 6px 0;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -148,6 +148,7 @@ h2 {
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
@@ -205,7 +206,7 @@ h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
.form-actions .primary {
|
||||
min-width: 120px;
|
||||
|
||||
30
src/components/MusicPlayer.vue
Normal file
30
src/components/MusicPlayer.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="music.enable && (music.playlistId || music.songId)"
|
||||
class="netease-mini-player"
|
||||
:data-playlist-id="music.mode === 'floating' ? music.playlistId : undefined"
|
||||
:data-song-id="music.mode === 'embed' ? music.songId : undefined"
|
||||
:data-embed="music.mode === 'embed'"
|
||||
:data-position="music.position"
|
||||
:data-lyric="music.lyric"
|
||||
:data-theme="music.theme"
|
||||
:data-autoplay="music.autoplay"
|
||||
:data-default-minimized="music.defaultMinimized"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import siteConfig from "../config/siteConfig";
|
||||
|
||||
const music = siteConfig.music;
|
||||
</script>
|
||||
|
||||
<!-- <style scoped>
|
||||
/* 音乐播放器样式由 NeteaseMiniPlayer 提供 */
|
||||
/* 使用 display: contents 使外层容器不占用空间 */
|
||||
/* 确保播放器浮动定位,不影响页面布局 */
|
||||
:deep(.netease-mini-player) {
|
||||
position: fixed !important;
|
||||
z-index: 999 !important;
|
||||
}
|
||||
</style> -->
|
||||
@@ -82,7 +82,7 @@ button:disabled {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.dots {
|
||||
justify-content: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,60 +1,4 @@
|
||||
interface SiteConfig {
|
||||
profile: {
|
||||
name: string;
|
||||
title: string;
|
||||
avatar: string;
|
||||
bio: string;
|
||||
birthday?: string;
|
||||
gender?: string;
|
||||
pronouns?: string;
|
||||
location?: string;
|
||||
};
|
||||
socialLinks: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
}>;
|
||||
github: {
|
||||
username: string;
|
||||
};
|
||||
about: Array<{
|
||||
title: string;
|
||||
desc: string;
|
||||
icon: string;
|
||||
}>;
|
||||
siteMeta: {
|
||||
title: string;
|
||||
icon: string;
|
||||
};
|
||||
skills: Array<{
|
||||
title: string;
|
||||
items: string[];
|
||||
}>;
|
||||
sites: Array<{
|
||||
name: string;
|
||||
desc: string;
|
||||
url: string;
|
||||
}>;
|
||||
projects: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
desc: string;
|
||||
}>;
|
||||
friends: Array<{
|
||||
name: string;
|
||||
desc: string;
|
||||
url: string;
|
||||
avatar: string;
|
||||
}>;
|
||||
footer: {
|
||||
beian: string;
|
||||
beianLink: string;
|
||||
showHitokoto: boolean;
|
||||
hitokotoType: string;
|
||||
customHtml: string;
|
||||
};
|
||||
}
|
||||
|
||||
const siteConfig: SiteConfig = {
|
||||
const siteConfig = {
|
||||
profile: {
|
||||
name: "RhenCloud",
|
||||
title: "I'm RhenCloud.",
|
||||
@@ -87,7 +31,47 @@ const siteConfig: SiteConfig = {
|
||||
|
||||
siteMeta: {
|
||||
title: "RhenCloud",
|
||||
icon: "favicon.ico", // public/favicon.ico
|
||||
icon: "favicon.svg", // public/favicon.svg
|
||||
startDate: "2025-12-06",
|
||||
},
|
||||
|
||||
appearance: {
|
||||
background: {
|
||||
enable: true,
|
||||
// URL 支持:可使用外部 URL 或本地路径
|
||||
// 例如: "https://example.com/bg.jpg" 或 "background.webp"
|
||||
image: "background.png", // 背景图片 URL 或本地路径(桌面端)
|
||||
mobileImage: "https://www.loliapi.com/acg/pe/", // 移动端背景图片(可选,不设置则使用 image)
|
||||
blur: 0, // 背景模糊程度 (0-100)
|
||||
overlay: "rgba(70, 59, 82, 0.4)", // 背景遮罩颜色与透明度
|
||||
},
|
||||
},
|
||||
|
||||
music: {
|
||||
enable: true,
|
||||
// 浮动模式播放器(推荐)- 用于播放网易云歌单
|
||||
mode: "floating", // "floating" 或 "embed"
|
||||
// 歌单ID:从网易云音乐链接获取,如 https://music.163.com/#/playlist?id=14273792576
|
||||
playlistId: "14366453940", // 例如: "14273792576"
|
||||
// 歌曲ID:仅在嵌入模式下使用
|
||||
songId: undefined, // 例如: "554242291"
|
||||
// 播放器位置(浮动模式): "bottom-left" | "bottom-right" | "top-left" | "top-right"
|
||||
position: "bottom-left",
|
||||
// 是否显示歌词
|
||||
lyric: true,
|
||||
// 主题: "light" | "dark" | "auto"
|
||||
theme: "dark",
|
||||
// 是否自动播放
|
||||
autoplay: false,
|
||||
// 是否默认以黑胶唱片状态启动(仅浮动模式)
|
||||
defaultMinimized: true,
|
||||
},
|
||||
|
||||
umami: {
|
||||
enable: true,
|
||||
url: "https://cloud.umami.is/script.js",
|
||||
websiteId: "ddcd51c3-ccc7-45e4-81e6-11567027f69b",
|
||||
apiBase: "https://api.umami.is",
|
||||
},
|
||||
|
||||
skills: [
|
||||
@@ -143,9 +127,11 @@ const siteConfig: SiteConfig = {
|
||||
footer: {
|
||||
beian: "津ICP备2025039003号-1",
|
||||
beianLink: "https://beian.miit.gov.cn/",
|
||||
showHitokoto: true,
|
||||
hitokotoType: "a&b&c&d&j",
|
||||
customHtml: '<span style="opacity:.8">© 2025 <a href="https://rhen.cloud">RhenCloud</a></span>',
|
||||
hitokoto: {
|
||||
enable: true,
|
||||
type: "a&b&c&d&j",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
12
src/main.ts
12
src/main.ts
@@ -1,6 +1,16 @@
|
||||
import { createApp } from "vue";
|
||||
import { VueUmamiPlugin } from "@jaseeey/vue-umami-plugin";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import "./styles.css";
|
||||
import siteConfig from "./config/siteConfig";
|
||||
|
||||
createApp(App).use(router).mount("#app");
|
||||
const app = createApp(App);
|
||||
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
if (siteConfig.umami?.enable) {
|
||||
app.use(VueUmamiPlugin({ websiteID: siteConfig.umami.websiteId, scriptSrc: siteConfig.umami.url, router }));
|
||||
}
|
||||
}
|
||||
|
||||
app.use(router).mount("#app");
|
||||
|
||||
101
src/styles.css
101
src/styles.css
@@ -1,3 +1,7 @@
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
@@ -7,7 +11,7 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
min-height: 100%;
|
||||
background: radial-gradient(circle at 20% 20%, #1b2b4b, #0f1629);
|
||||
}
|
||||
|
||||
@@ -90,3 +94,98 @@ p {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Ensure the Netease mini player doesn't occupy page layout space
|
||||
and stays fixed above content. Use !important to override
|
||||
styles injected by third-party scripts. */
|
||||
.netease-mini-player {
|
||||
position: fixed !important;
|
||||
bottom: 20px !important;
|
||||
left: 20px !important;
|
||||
right: auto !important;
|
||||
/* do not force width here — let the player's own minimized/expanded
|
||||
styles control sizing. Only constrain max width as a safety net. */
|
||||
max-width: calc(100% - 40px) !important;
|
||||
z-index: 9999 !important;
|
||||
margin: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* If the player injects a full-width bar, make sure it won't
|
||||
push page content by forcing it out of the normal flow. */
|
||||
.netease-mini-player > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Defensive: hide any accidental full-width injected container from the player
|
||||
that might occupy vertical space. This targets common helper classes used
|
||||
by the player script. */
|
||||
.netease-mini-player-embed,
|
||||
.nmpv2-player,
|
||||
.nmpv2-root {
|
||||
position: fixed !important;
|
||||
bottom: 20px !important;
|
||||
left: 20px !important;
|
||||
right: auto !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* Fix: prevent playlist dropdown from increasing document height
|
||||
by forcing the playlist container to be fixed and clipped to viewport. */
|
||||
.netease-mini-player[data-position="bottom-left"] .playlist-container,
|
||||
.netease-mini-player[data-position="bottom-right"] .playlist-container {
|
||||
position: fixed !important;
|
||||
bottom: calc(20px + 80px) !important;
|
||||
left: 20px !important;
|
||||
right: auto !important;
|
||||
width: 290px !important;
|
||||
max-height: 50vh !important;
|
||||
overflow: auto !important;
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
|
||||
.netease-mini-player[data-position="top-left"] .playlist-container,
|
||||
.netease-mini-player[data-position="top-right"] .playlist-container {
|
||||
position: fixed !important;
|
||||
top: calc(20px + 80px) !important;
|
||||
left: 20px !important;
|
||||
right: auto !important;
|
||||
width: 290px !important;
|
||||
max-height: 50vh !important;
|
||||
overflow: auto !important;
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
|
||||
/* If player is docked to the right, align playlist to right edge */
|
||||
.netease-mini-player[data-position="bottom-right"] .playlist-container,
|
||||
.netease-mini-player[data-position="top-right"] .playlist-container {
|
||||
left: auto !important;
|
||||
right: 20px !important;
|
||||
}
|
||||
|
||||
/* Ensure minimized player displays as a circle and its album cover fits */
|
||||
.netease-mini-player.minimized {
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
border-radius: 50% !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized .album-cover-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized .album-cover {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ const github = reactive({
|
||||
// 修改此处:使用 VITE_ 前缀
|
||||
const githubToken = import.meta.env.VITE_GITHUB_TOKEN ?? "";
|
||||
|
||||
console.log(githubToken);
|
||||
|
||||
onMounted(() => {
|
||||
document.title = siteMeta.title;
|
||||
const link = document.querySelector("link[rel~='icon']") || document.createElement("link");
|
||||
|
||||
Reference in New Issue
Block a user