mirror of
https://github.com/RhenCloud/Cloud-Home.git
synced 2026-01-23 02:09:06 +08:00
Compare commits
14 Commits
6cc98acd88
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d5c9b4d3c8 | |||
| ab58d2e251 | |||
| cb1406661a | |||
| 8a2bbacaa3 | |||
| 20eebcca4f | |||
| f9e624d48e | |||
| 6edb6af6ee | |||
| 53685b1531 | |||
| 67708a34bc | |||
| 1c05fd7b1e | |||
| 4d8644629b | |||
| 618723a689 | |||
| 6b05f7c74e | |||
| bda4281fde |
80
.github/workflows/lint-format.yml
vendored
Normal file
80
.github/workflows/lint-format.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Lint and Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Enable corepack and install npm
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare npm@latest --activate
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --frozen-lockfile
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm lint
|
||||
|
||||
- name: Run Prettier check
|
||||
run: npm format:check
|
||||
|
||||
format:
|
||||
name: Auto-format (eslint --fix)
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Enable corepack and install npm
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare npm@latest --activate
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --frozen-lockfile
|
||||
|
||||
- name: Run Prettier --write
|
||||
run: npm format
|
||||
|
||||
- name: Run ESLint --fix
|
||||
run: npm lint:fix
|
||||
|
||||
- name: Commit & push fixes
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
if ! git diff --quiet; then
|
||||
git add -A
|
||||
git commit -m "chore: auto format by GitHub Actions" || echo "No changes to commit"
|
||||
git push
|
||||
else
|
||||
echo "No formatting changes"
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
# pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
|
||||
18
.prettierignore
Normal file
18
.prettierignore
Normal file
@@ -0,0 +1,18 @@
|
||||
node_modules/
|
||||
.output/
|
||||
.nuxt/
|
||||
dist/
|
||||
build/
|
||||
public/build/
|
||||
.vscode/
|
||||
.pnp.*
|
||||
coverage/
|
||||
*.log
|
||||
*.lock
|
||||
pnpm-lock.yaml
|
||||
.env
|
||||
.env.*
|
||||
.DS_Store
|
||||
|
||||
# ignore generated images
|
||||
public/images/generated/
|
||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css"
|
||||
}
|
||||
21
README.md
21
README.md
@@ -73,7 +73,7 @@ const siteConfig: SiteConfig = {
|
||||
siteMeta: {
|
||||
title: "Example Title", // 网站标题
|
||||
icon: "/favicon.ico", // 网站图标,可为public目录下的文件或外部链接
|
||||
startDate:"xxxx-xx-xx", // 网站创建日期
|
||||
startDate: "xxxx-xx-xx", // 网站创建日期
|
||||
},
|
||||
|
||||
music: {
|
||||
@@ -117,7 +117,10 @@ const siteConfig: SiteConfig = {
|
||||
// 技能图标展示,详见https://github.com/tandpfun/skill-icons#icons-list
|
||||
skills: [
|
||||
{ title: "前端", items: ["css", "html", "javascript", "typescript", "vue"] },
|
||||
{ title: "后端 / 云", items: ["cpp", "cloudflare", "docker", "java", "mysql", "nodejs", "python", "vercel"] },
|
||||
{
|
||||
title: "后端 / 云",
|
||||
items: ["cpp", "cloudflare", "docker", "java", "mysql", "nodejs", "python", "vercel"],
|
||||
},
|
||||
{ title: "工具", items: ["ae", "au", "git", "github", "md", "ps", "pr", "vscode"] },
|
||||
{ title: "操作系统", items: ["arch", "linux", "windows"] },
|
||||
],
|
||||
@@ -136,8 +139,16 @@ const siteConfig: SiteConfig = {
|
||||
],
|
||||
|
||||
projects: [
|
||||
{ name: "Example Project 1", url: "https://github.com/ExampleUser/example-project-1", desc: "Example Project 1" },
|
||||
{ name: "Example Project 2", url: "https://github.com/ExampleUser/example-project-2", desc: "Example Project 2" },
|
||||
{
|
||||
name: "Example Project 1",
|
||||
url: "https://github.com/ExampleUser/example-project-1",
|
||||
desc: "Example Project 1",
|
||||
},
|
||||
{
|
||||
name: "Example Project 2",
|
||||
url: "https://github.com/ExampleUser/example-project-2",
|
||||
desc: "Example Project 2",
|
||||
},
|
||||
],
|
||||
|
||||
friends: [
|
||||
@@ -158,7 +169,7 @@ const siteConfig: SiteConfig = {
|
||||
footer: {
|
||||
beian: "备案号", // 备案号,留空则不显示
|
||||
beianLink: "https://beian.miit.gov.cn/", // 备案号链接,一般无需修改
|
||||
customHtml: '', // 自定义 HTML 代码,如统计代码等
|
||||
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
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="app-shell" :style="backgroundStyle">
|
||||
<div class="background-overlay" :style="overlayStyle"></div>
|
||||
<div class="background-overlay" :style="overlayStyle" />
|
||||
<button
|
||||
class="background-toggle"
|
||||
@click="hideComponents = !hideComponents"
|
||||
:title="hideComponents ? '显示内容' : '隐藏内容'"
|
||||
:class="{ active: hideComponents }"
|
||||
@click="hideComponents = !hideComponents"
|
||||
>
|
||||
<span class="toggle-icon">{{ hideComponents ? "👁️" : "🙈" }}</span>
|
||||
<span class="toggle-label">{{ hideComponents ? "显示" : "隐藏" }}</span>
|
||||
</button>
|
||||
<div class="content-stack">
|
||||
<Transition name="fade-down">
|
||||
<main class="app-body" v-if="!hideComponents" key="content">
|
||||
<main v-if="!hideComponents" key="content" class="app-body">
|
||||
<NuxtPage />
|
||||
</main>
|
||||
</Transition>
|
||||
@@ -20,7 +20,7 @@
|
||||
<PageSwitcher v-if="!hideComponents" key="switcher" />
|
||||
</Transition>
|
||||
<Transition name="fade-down">
|
||||
<FooterSection v-if="!hideComponents" :contact="contact" key="footer" />
|
||||
<FooterSection v-if="!hideComponents" key="footer" :contact="contact" />
|
||||
</Transition>
|
||||
</div>
|
||||
<MusicPlayer />
|
||||
|
||||
@@ -4,54 +4,73 @@
|
||||
<p class="text-text-muted text-sm m-0 mb-3 block">关于我 · About Me</p>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-3.5">
|
||||
<article v-if="age"
|
||||
class="flex-1 min-w-[140px] flex items-center justify-between gap-2 bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-300 hover:-translate-y-1 hover:border-primary/40 hover:shadow-lg-dark hover:bg-gradient-to-br hover:from-primary/6">
|
||||
<article
|
||||
v-if="age"
|
||||
class="flex-1 min-w-[140px] flex items-center justify-between gap-2 bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-300 hover:-translate-y-1 hover:border-primary/40 hover:shadow-lg-dark hover:bg-gradient-to-br hover:from-primary/6"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xl leading-none">🎂</span>
|
||||
<h3 class="m-0 text-sm font-semibold text-white/90">年龄</h3>
|
||||
</div>
|
||||
<p class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60">
|
||||
<p
|
||||
class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60"
|
||||
>
|
||||
{{ age }} 岁
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article v-if="profile?.gender"
|
||||
class="flex-1 min-w-[140px] flex items-center justify-between gap-2 bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark">
|
||||
<article
|
||||
v-if="profile?.gender"
|
||||
class="flex-1 min-w-[140px] flex items-center justify-between gap-2 bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xl leading-none">⚧️</span>
|
||||
<h3 class="m-0 text-sm font-semibold text-white/90">性别</h3>
|
||||
</div>
|
||||
<p class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60">
|
||||
<p
|
||||
class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60"
|
||||
>
|
||||
{{ profile.gender }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article v-if="profile?.pronouns"
|
||||
class="flex-1 min-w-[140px] flex items-center justify-between gap-2 bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark">
|
||||
<article
|
||||
v-if="profile?.pronouns"
|
||||
class="flex-1 min-w-[140px] flex items-center justify-between gap-2 bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xl leading-none">🗣️</span>
|
||||
<h3 class="m-0 text-sm font-semibold text-white/90">代词</h3>
|
||||
</div>
|
||||
<p class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60">
|
||||
<p
|
||||
class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60"
|
||||
>
|
||||
{{ profile.pronouns }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article v-if="profile?.location"
|
||||
class="flex-1 min-w-[140px] flex items-center justify-between gap-2 bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark">
|
||||
<article
|
||||
v-if="profile?.location"
|
||||
class="flex-1 min-w-[140px] flex items-center justify-between gap-2 bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xl leading-none">📍</span>
|
||||
<h3 class="m-0 text-sm font-semibold text-white/90">地区</h3>
|
||||
</div>
|
||||
<p class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60">
|
||||
<p
|
||||
class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60"
|
||||
>
|
||||
{{ profile.location }}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5 mt-2.5">
|
||||
<article v-for="item in items" :key="item.title"
|
||||
class="bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-3 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark">
|
||||
<article
|
||||
v-for="item in items"
|
||||
:key="item.title"
|
||||
class="bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-3 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="text-2xl leading-none">{{ item.icon }}</span>
|
||||
<h3 class="m-0 text-base font-semibold">{{ item.title }}</h3>
|
||||
@@ -66,8 +85,14 @@
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
items: Array,
|
||||
profile: Object,
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
profile: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const age = computed(() => {
|
||||
|
||||
@@ -1,38 +1,55 @@
|
||||
<template>
|
||||
<footer class="card text-center mt-auto w-full flex flex-col gap-1">
|
||||
<!-- 一言 -->
|
||||
<p class="text-text-muted text-sm m-0 italic" v-if="showHitokoto && quote">
|
||||
<p v-if="showHitokoto && quote" class="text-text-muted text-sm m-0 italic">
|
||||
「{{ quote }}」<span v-if="from" class="ml-1.5">—— {{ from }}</span>
|
||||
</p>
|
||||
|
||||
<!-- 访问统计 -->
|
||||
<p class="text-text-muted text-xs m-0" v-if="showStats && !statsError">
|
||||
<p v-if="showStats && !statsError" class="text-text-muted text-xs m-0">
|
||||
👁️ {{ visitors }} · 📊 {{ pageviews }}
|
||||
</p>
|
||||
|
||||
<!-- 备案信息 -->
|
||||
<p class="text-text-muted text-xs m-0" v-if="contact.beian">
|
||||
<a :href="contact.beianLink || 'https://beian.miit.gov.cn/'" target="_blank" rel="noreferrer"
|
||||
class="opacity-85 transition-all duration-200 hover:text-primary hover:opacity-100">
|
||||
<p v-if="contact?.beian" class="text-text-muted text-xs m-0">
|
||||
<NuxtLink
|
||||
:to="contact.beianLink || '/'"
|
||||
class="opacity-85 transition-all duration-200 hover:text-primary hover:opacity-100"
|
||||
>
|
||||
{{ contact.beian }}
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
|
||||
<!-- 框架与技术栈信息 -->
|
||||
<p class="text-text-muted text-xs m-0">
|
||||
Powered by
|
||||
<a href="https://nuxt.com" target="_blank" rel="noreferrer"
|
||||
class="text-primary hover:text-accent transition-colors">Nuxt 4</a>
|
||||
<a
|
||||
href="https://nuxt.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-primary hover:text-accent transition-colors"
|
||||
>Nuxt 4</a
|
||||
>
|
||||
·
|
||||
<a href="https://tailwindcss.com" target="_blank" rel="noreferrer"
|
||||
class="text-primary hover:text-accent transition-colors">Tailwind CSS</a>
|
||||
<a
|
||||
href="https://tailwindcss.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-primary hover:text-accent transition-colors"
|
||||
>Tailwind CSS</a
|
||||
>
|
||||
·
|
||||
<a href="https://vuejs.org" target="_blank" rel="noreferrer"
|
||||
class="text-primary hover:text-accent transition-colors">Vue 3</a>
|
||||
<a
|
||||
href="https://vuejs.org"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-primary hover:text-accent transition-colors"
|
||||
>Vue 3</a
|
||||
>
|
||||
</p>
|
||||
|
||||
<!-- 自定义 HTML -->
|
||||
<div v-if="contact.customHtml" v-html="contact.customHtml"></div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="contact?.customHtml" v-html="contact.customHtml" />
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -40,7 +57,7 @@
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRuntimeConfig } from "#imports";
|
||||
import siteConfig from "~/config/siteConfig";
|
||||
const props = defineProps({ contact: Object });
|
||||
const contact = siteConfig.footer || {};
|
||||
const config = useRuntimeConfig();
|
||||
const quote = ref("");
|
||||
const from = ref("");
|
||||
@@ -56,7 +73,8 @@ const buildHitokotoUrl = () => {
|
||||
if (Array.isArray(type)) {
|
||||
type.filter(Boolean).forEach((t) => url.searchParams.append("c", t));
|
||||
} else if (typeof type === "string") {
|
||||
type.split("&")
|
||||
type
|
||||
.split("&")
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((t) => url.searchParams.append("c", t));
|
||||
@@ -90,11 +108,14 @@ const fetchStats = async () => {
|
||||
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}`, {
|
||||
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}`);
|
||||
|
||||
@@ -1,50 +1,70 @@
|
||||
<template>
|
||||
<div class="card panel flex flex-col gap-2.5">
|
||||
<h2 class="m-0 mb-1 gradient-text">友情链接</h2>
|
||||
<h2 class="m-0 mb-1 text-lg font-semibold">友情链接</h2>
|
||||
<p class="text-text-muted text-sm m-0 mb-3 block">欢迎互换友链 · Friends</p>
|
||||
<div class="grid grid-cols-1 gap-4 w-full max-w-[1100px] mx-auto sm:grid-cols-2">
|
||||
<article v-for="f in displayedFriends" :key="f.url"
|
||||
class="rounded-[14px] border border-white/10 bg-gradient-to-br from-white/5 to-white/0 px-4 py-3.5 transition-all duration-200 hover:-translate-y-[3px] hover:border-pink-400/50 w-[290px] h-[145px] flex flex-col">
|
||||
<article
|
||||
v-for="f in displayedFriends"
|
||||
:key="f.url"
|
||||
class="rounded-[14px] border border-white/10 bg-gradient-to-br from-white/5 to-white/0 px-4 py-3.5 transition-all duration-200 hover:-translate-y-[3px] hover:border-pink-400/50 w-[290px] h-[145px] flex flex-col"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<img v-if="f.avatar" :src="f.avatar" :alt="f.name" loading="lazy"
|
||||
class="w-12 h-12 rounded-full object-cover border border-white/15" />
|
||||
<NuxtImg
|
||||
v-if="f.avatar"
|
||||
:src="f.avatar"
|
||||
:alt="f.name"
|
||||
loading="lazy"
|
||||
class="w-12 h-12 rounded-full object-cover border border-white/15"
|
||||
/>
|
||||
<h3 class="m-0 font-semibold text-base whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{{ f.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<span class="rounded-full px-2.5 py-1 text-xs bg-purple-400/15 text-purple-300">友链</span>
|
||||
<span class="rounded-full px-2.5 py-1 text-xs bg-purple-400/15 text-purple-300"
|
||||
>友链</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-white/60 flex-1 overflow-hidden truncate-lines-2 mb-2">
|
||||
{{ f.desc || "一个有趣的站点" }}
|
||||
</p>
|
||||
|
||||
<a :href="f.url" target="_blank" rel="noreferrer"
|
||||
class="inline-flex items-center gap-1.5 mt-auto shrink-0 font-semibold text-pink-300 hover:text-pink-400 transition-all duration-200 hover:gap-2">
|
||||
<NuxtLink
|
||||
:to="f.url"
|
||||
class="inline-flex items-center gap-1.5 mt-auto shrink-0 font-semibold text-pink-300 hover:text-pink-400 transition-all duration-200 hover:gap-2"
|
||||
>
|
||||
访问 →
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<section class="card flex flex-col gap-2.5">
|
||||
<div class="flex justify-center items-center align-center flex-wrap">
|
||||
<button @click="openForm"
|
||||
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/80 hover:shadow-lg hover:shadow-primary/25">
|
||||
<button
|
||||
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/80 hover:shadow-lg hover:shadow-primary/25"
|
||||
@click="openForm"
|
||||
>
|
||||
申请友链
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<Teleport to="body">
|
||||
<div v-if="showDialog" class="fixed inset-0 bg-black/45 backdrop-blur-sm flex items-center justify-center z-50"
|
||||
@click.self="closeDialog">
|
||||
<div
|
||||
class="min-w-[280px] max-w-[420px] bg-gradient-to-br from-pink-500/12 to-white/8 border border-white/15 rounded-2xl p-4 shadow-xl">
|
||||
v-if="showDialog"
|
||||
class="fixed inset-0 bg-black/45 backdrop-blur-sm flex items-center justify-center z-50"
|
||||
@click.self="closeDialog"
|
||||
>
|
||||
<div
|
||||
class="min-w-[280px] max-w-[420px] bg-gradient-to-br from-pink-500/12 to-white/8 border border-white/15 rounded-2xl p-4 shadow-xl"
|
||||
>
|
||||
<h3 class="m-0 mb-2">{{ dialogTitle }}</h3>
|
||||
<p class="text-text-muted text-sm mb-4">{{ dialogText }}</p>
|
||||
<div class="flex justify-end">
|
||||
<button @click="closeDialog"
|
||||
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary cursor-pointer hover:bg-primary/20 transition-all">
|
||||
<button
|
||||
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary cursor-pointer hover:bg-primary/20 transition-all"
|
||||
@click="closeDialog"
|
||||
>
|
||||
好的
|
||||
</button>
|
||||
</div>
|
||||
@@ -54,65 +74,100 @@
|
||||
|
||||
<!-- 申请友链模态弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showFormModal"
|
||||
class="fixed inset-0 bg-black/45 backdrop-blur-sm flex items-center justify-center z-50"
|
||||
@click.self="showFormModal = false">
|
||||
<div
|
||||
class="w-[92%] max-w-[540px] bg-gradient-to-br from-white/8 to-primary/6 border border-white/15 rounded-2xl p-6 shadow-xl">
|
||||
v-if="showFormModal"
|
||||
class="fixed inset-0 bg-black/45 backdrop-blur-sm flex items-center justify-center z-50"
|
||||
@click.self="showFormModal = false"
|
||||
>
|
||||
<div
|
||||
class="w-[92%] max-w-[540px] bg-gradient-to-br from-white/8 to-primary/6 border border-white/15 rounded-2xl p-6 shadow-xl"
|
||||
>
|
||||
<h3 class="m-0 mb-4 text-center">申请友链</h3>
|
||||
|
||||
<div class="mb-4 text-sm text-text-primary">
|
||||
<div class="mb-2 font-semibold">请在申请前在你站点添加以下信息(示例 JSON):</div>
|
||||
<pre
|
||||
class="bg-white/6 border border-white/10 rounded-lg p-3 text-xs overflow-auto"><code>{{ exampleJson }}</code></pre>
|
||||
<pre class="bg-white/6 border border-white/10 rounded-lg p-3 text-xs overflow-auto">
|
||||
<code>{{ exampleJson }}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitForm" class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<form class="grid grid-cols-1 sm:grid-cols-2 gap-3" @submit.prevent="submitForm">
|
||||
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold sm:col-span-2">
|
||||
网站名称 *
|
||||
<input v-model="form.name" required placeholder="网站名称"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" />
|
||||
<input
|
||||
v-model="form.name"
|
||||
required
|
||||
placeholder="网站名称"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- URL 与 Email 同行 -->
|
||||
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold">
|
||||
<label class="flex fl ex-col gap-1 text-sm text-text-primary font-semibold">
|
||||
网站链接 *
|
||||
<input v-model="form.url" type="url" required placeholder="https://example.com"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" />
|
||||
<input
|
||||
v-model="form.url"
|
||||
type="url"
|
||||
required
|
||||
placeholder="https://example.com"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold">
|
||||
联系邮箱 *
|
||||
<input v-model="form.email" type="email" required placeholder="example@example.com"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" />
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="example@example.com"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- 描述 与 头像 同行 -->
|
||||
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold">
|
||||
网站描述
|
||||
<input v-model="form.desc" placeholder="可选"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" />
|
||||
<input
|
||||
v-model="form.desc"
|
||||
placeholder="可选"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold">
|
||||
头像链接
|
||||
<input v-model="form.avatar" type="url" placeholder="可选,展示头像"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" />
|
||||
<input
|
||||
v-model="form.avatar"
|
||||
type="url"
|
||||
placeholder="可选,展示头像"
|
||||
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold sm:col-span-2">
|
||||
想说的话
|
||||
<div class="flex items-center gap-2">
|
||||
<textarea v-model="form.message" placeholder="可选,最多50字" maxlength="50"
|
||||
class="flex-1 px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary h-24 resize-none"></textarea>
|
||||
<textarea
|
||||
v-model="form.message"
|
||||
placeholder="可选,最多50字"
|
||||
maxlength="50"
|
||||
class="flex-1 px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary h-24 resize-none"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="sm:col-span-2 flex items-center justify-center gap-3 mt-2">
|
||||
<button type="button" @click="showFormModal = false"
|
||||
class="px-3 py-2 rounded-2xl border border-white/10 bg-white/6">
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 rounded-2xl border border-white/10 bg-white/6"
|
||||
@click="showFormModal = false"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" :disabled="loading"
|
||||
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary disabled:opacity-50">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary disabled:opacity-50"
|
||||
>
|
||||
{{ loading ? "提交中..." : "提交" }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -181,7 +236,10 @@ const resolveUrl = (p) => {
|
||||
if (!p) return "";
|
||||
const s = String(p).trim();
|
||||
if (/^https?:\/\//i.test(s) || /^\/\//.test(s)) return s;
|
||||
const base = (siteConfig.siteMeta && siteConfig.siteMeta.url) ? String(siteConfig.siteMeta.url).replace(/\/$/, "") : "";
|
||||
const base =
|
||||
siteConfig.siteMeta && siteConfig.siteMeta.url
|
||||
? String(siteConfig.siteMeta.url).replace(/\/$/, "")
|
||||
: "";
|
||||
if (!base) return s;
|
||||
if (s.startsWith("/")) return base + s;
|
||||
return base + "/" + s;
|
||||
|
||||
@@ -4,25 +4,35 @@
|
||||
<div class="mt-3">
|
||||
<h3 class="m-0 mb-1">提交热力图</h3>
|
||||
<p class="text-text-muted text-sm m-0 mb-3 block">我的提交热力图 · Acitivity Heatmap</p>
|
||||
<img :src="github.heatmapUrl" alt="GitHub Heatmap" loading="lazy"
|
||||
<NuxtImg
|
||||
:src="github.heatmapUrl"
|
||||
alt="GitHub Heatmap"
|
||||
loading="lazy"
|
||||
class="rounded-xl border border-white/10 hover:border-primary/30 transition-all duration-200"
|
||||
class="w-full rounded-2xl border border-white/10" />
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<h3 class="m-0 mb-1">常用语言</h3>
|
||||
<p class="text-text-muted text-sm m-0 mb-3 block">我常用的语言 · Languages</p>
|
||||
<ul class="list-none p-0 m-0 flex flex-col gap-2.5">
|
||||
<li v-for="lang in topLanguages" :key="lang.name"
|
||||
class="bg-white/5 border border-white/10 rounded-xl p-2.5">
|
||||
<li
|
||||
v-for="lang in topLanguages"
|
||||
:key="lang.name"
|
||||
class="bg-white/5 border border-white/10 rounded-xl p-2.5"
|
||||
>
|
||||
<div class="flex items-center gap-2 font-semibold mb-1.5">
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block"
|
||||
:style="{ background: colorFor(lang.name) }"></span>
|
||||
<span
|
||||
class="w-2.5 h-2.5 rounded-full inline-block"
|
||||
:style="{ background: colorFor(lang.name) }"
|
||||
/>
|
||||
<span class="text-text-primary">{{ lang.name }}</span>
|
||||
<span class="text-text-muted text-sm">{{ lang.percent }}%</span>
|
||||
</div>
|
||||
<div class="h-2 rounded-full bg-white/5 overflow-hidden">
|
||||
<span class="block h-full rounded-full transition-all duration-300"
|
||||
:style="barStyle(lang)"></span>
|
||||
<span
|
||||
class="block h-full rounded-full transition-all duration-300"
|
||||
:style="barStyle(lang)"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -32,12 +42,21 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
const props = defineProps({ github: Object });
|
||||
const props = defineProps({
|
||||
github: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ languages: [] }),
|
||||
},
|
||||
});
|
||||
|
||||
const github = props.github;
|
||||
|
||||
const palette = ["#7cc1ff", "#6bdba6", "#ffd166", "#f497da", "#9b8cfc", "#5ce1e6", "#ffa3a3"];
|
||||
|
||||
const topLanguages = computed(() => (Array.isArray(github.languages) ? github.languages.slice(0, 5) : []));
|
||||
const topLanguages = computed(() =>
|
||||
Array.isArray(github.languages) ? github.languages.slice(0, 5) : []
|
||||
);
|
||||
|
||||
const colorFor = (name) => {
|
||||
const idx = github.languages.findIndex((l) => l.name === name);
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
<section class="card grid grid-cols-[120px_1fr] gap-4 items-center hover:shadow-lg-dark group">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute inset-0 rounded-full bg-gradient-to-br from-primary/30 to-accent/20 blur-xl group-hover:blur-2xl transition-all duration-300 opacity-0 group-hover:opacity-100">
|
||||
</div>
|
||||
<img class="relative w-30 h-30 rounded-full object-cover border-2 border-primary/40 shadow-md-dark bg-white transition-transform duration-300 group-hover:scale-105"
|
||||
:src="profile.avatar" alt="avatar" loading="lazy" />
|
||||
class="absolute inset-0 rounded-full bg-gradient-to-br from-primary/30 to-accent/20 blur-xl group-hover:blur-2xl transition-all duration-300 opacity-0 group-hover:opacity-100"
|
||||
/>
|
||||
<NuxtImg
|
||||
class="relative w-30 h-30 rounded-full object-cover border-2 border-primary/40 shadow-md-dark bg-white transition-transform duration-300 group-hover:scale-105"
|
||||
:src="profile.avatar"
|
||||
alt="avatar"
|
||||
loading="eager"
|
||||
/>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<h1 class="text-2xl font-bold">{{ profile.name }}</h1>
|
||||
@@ -16,5 +20,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ profile: Object });
|
||||
import siteConfig from "../config/siteConfig";
|
||||
|
||||
const { profile } = defineProps({
|
||||
profile: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => siteConfig.profile || {},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<div v-if="music.enable && (music.playlistId || music.songId)" class="netease-mini-player"
|
||||
<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"
|
||||
:data-auto-pause="music.autoPause" :data-api-urls="JSON.stringify(music.apiUrls)"></div>
|
||||
: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"
|
||||
:data-auto-pause="music.autoPause"
|
||||
:data-api-urls="JSON.stringify(music.apiUrls)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
<template>
|
||||
<div class="my-4 mx-auto max-w-3xl w-full px-4 py-3 grid grid-cols-[auto_1fr_auto] gap-3 items-center">
|
||||
<button :disabled="currentIndex <= 0" @click="goPrev"
|
||||
class="bg-white/10 text-text-primary border border-white/15 rounded-2xl px-3 py-2 cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<div
|
||||
class="my-4 mx-auto max-w-3xl w-full px-4 py-3 grid grid-cols-[auto_1fr_auto] gap-3 items-center"
|
||||
>
|
||||
<button
|
||||
:disabled="currentIndex <= 0"
|
||||
class="bg-white/10 text-text-primary border border-white/15 rounded-2xl px-3 py-2 cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="goPrev"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<div class="flex gap-2 flex-wrap justify-center">
|
||||
<button v-for="item in pages" :key="item.name" :class="{
|
||||
<button
|
||||
v-for="item in pages"
|
||||
:key="item.name"
|
||||
:class="{
|
||||
'bg-primary/30 border-primary/60 text-primary shadow-lg shadow-primary/25':
|
||||
item.name === route.name,
|
||||
}" @click="router.push({ name: item.name })"
|
||||
class="px-2.5 py-2 bg-white/10 text-text-primary border border-white/15 rounded-2xl cursor-pointer transition-all duration-200 hover:bg-white/15 hover:border-primary/40">
|
||||
}"
|
||||
class="px-2.5 py-2 bg-white/10 text-text-primary border border-white/15 rounded-2xl cursor-pointer transition-all duration-200 hover:bg-white/15 hover:border-primary/40"
|
||||
@click="router.push({ name: item.name })"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button :disabled="currentIndex >= pages.length - 1" @click="goNext"
|
||||
class="bg-white/10 text-text-primary border border-white/15 rounded-2xl px-3 py-2 cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<button
|
||||
:disabled="currentIndex >= pages.length - 1"
|
||||
class="bg-white/10 text-text-primary border border-white/15 rounded-2xl px-3 py-2 cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="goNext"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
@@ -33,6 +46,7 @@ const pages = [
|
||||
{ name: "sites", label: "网站" },
|
||||
{ name: "projects", label: "项目" },
|
||||
{ name: "friends", label: "友链" },
|
||||
{ name: "comments", label: "留言" },
|
||||
];
|
||||
|
||||
const currentIndex = computed(() => pages.findIndex((item) => item.name === route.name));
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<section class="card panel flex flex-col gap-2.5">
|
||||
<h2 class="m-0 mb-1 text-lg font-semibold">项目作品</h2>
|
||||
|
||||
<p class="text-sm text-white/60 mb-3">一些正在维护或已发布的项目 · Projects</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 w-full max-w-[1100px] mx-auto sm:grid-cols-2">
|
||||
<article v-for="p in projects" :key="p.url"
|
||||
class="rounded-[14px] border border-white/10 bg-gradient-to-br from-white/5 to-white/0 px-4 py-3.5 transition-all duration-200 hover:-translate-y-[3px] hover:border-yellow-400/50 w-[290px] h-[145px] flex flex-col">
|
||||
<article
|
||||
v-for="p in projects"
|
||||
:key="p.url"
|
||||
class="rounded-[14px] border border-white/10 bg-gradient-to-br from-white/5 to-white/0 px-4 py-3.5 transition-all duration-200 hover:-translate-y-[3px] hover:border-yellow-400/50 w-[290px] h-[145px] flex flex-col"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<h3 class="font-medium truncate">
|
||||
{{ p.name }}
|
||||
@@ -19,10 +20,12 @@
|
||||
{{ p.desc }}
|
||||
</p>
|
||||
|
||||
<a :href="p.url" target="_blank" rel="noreferrer"
|
||||
class="inline-flex items-center gap-1.5 mt-auto shrink-0 font-semibold text-yellow-300 hover:text-yellow-400 transition-all duration-200 hover:gap-2">
|
||||
<NuxtLink
|
||||
:to="p.url"
|
||||
class="inline-flex items-center gap-1.5 mt-auto shrink-0 font-semibold text-yellow-300 hover:text-yellow-400 transition-all duration-200 hover:gap-2"
|
||||
>
|
||||
查看仓库 →
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@@ -30,6 +33,9 @@
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
projects: Array,
|
||||
projects: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<section class="card panel flex flex-col gap-2.5">
|
||||
<h2 class="m-0 mb-1 text-lg font-semibold">项目作品</h2>
|
||||
<h2 class="m-0 mb-1 text-lg font-semibold">我的网站</h2>
|
||||
|
||||
<p class="text-sm text-white/60 mb-3">一些正在维护或已发布的项目 · Projects</p>
|
||||
<p class="text-sm text-white/60 mb-3">正在运行的站点 · Websites</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 w-full max-w-[1100px] mx-auto sm:grid-cols-2">
|
||||
<article v-for="site in sites" :key="site.url"
|
||||
class="rounded-[14px] border border-white/10 bg-gradient-to-br from-white/5 to-white/0 px-4 py-3.5 transition-all duration-200 hover:-translate-y-[3px] hover:border-blue-400/50 w-[290px] h-[145px] flex flex-col">
|
||||
<article
|
||||
v-for="site in sites"
|
||||
:key="site.url"
|
||||
class="rounded-[14px] border border-white/10 bg-gradient-to-br from-white/5 to-white/0 px-4 py-3.5 transition-all duration-200 hover:-translate-y-[3px] hover:border-blue-400/50 w-[290px] h-[145px] flex flex-col"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<h3 class="font-medium truncate">
|
||||
{{ site.name }}
|
||||
@@ -19,10 +22,12 @@
|
||||
{{ site.desc }}
|
||||
</p>
|
||||
|
||||
<a :href="site.url" target="_blank" rel="noreferrer"
|
||||
class="inline-flex items-center gap-1.5 mt-auto shrink-0 font-semibold text-blue-300 hover:text-blue-400 transition-all duration-200 hover:gap-2">
|
||||
<NuxtLink
|
||||
:to="site.url"
|
||||
class="inline-flex items-center gap-1.5 mt-auto shrink-0 font-semibold text-blue-300 hover:text-blue-400 transition-all duration-200 hover:gap-2"
|
||||
>
|
||||
查看 →
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@@ -30,6 +35,9 @@
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
sites: Array,
|
||||
sites: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<section class="card flex flex-col gap-2.5">
|
||||
<h2 class="m-0 mb-1 gradient-text">我的网站</h2>
|
||||
<p class="text-text-muted text-sm m-0 mb-3 block">正在运行的站点 · Websites</p>
|
||||
<div class="w-full -mx-[1.125rem] -mb-[1.125rem]">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-[1.125rem] pb-[1.125rem]">
|
||||
<article
|
||||
v-for="site in sites"
|
||||
:key="site.url"
|
||||
class="bg-gradient-to-br from-white/5 to-white/1 border border-white/10 rounded-2xl p-3.5 shadow-md-dark transition-all duration-300 hover:-translate-y-1 hover:border-blue-400/60 hover:shadow-lg-dark hover:bg-gradient-to-br hover:from-yellow-500/6"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<h3 class="m-0 font-semibold text-base">{{ site.name }}</h3>
|
||||
<span class="px-2.5 py-1 rounded-full bg-green-500/14 text-green-300 text-xs font-medium"
|
||||
>在线</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-text-muted text-sm m-0 mb-2.5">{{ site.desc }}</p>
|
||||
<a
|
||||
:href="site.url"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="inline-flex items-center gap-1.5 mt-2.5 text-blue-400 font-semibold text-sm hover:text-blue-300 transition-all duration-200 hover:gap-2"
|
||||
>查看 →</a
|
||||
>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ sites: Array });
|
||||
</script>
|
||||
@@ -1,20 +1,31 @@
|
||||
<template>
|
||||
<section class="card flex flex-col gap-2.5">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1">技能专长</h2>
|
||||
<h2 class="m-0 mb-1 font-semibold">技能专长</h2>
|
||||
<p class="text-text-muted text-sm m-0 mb-3">我常用的工具与技术 · Skills & Technologies</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<article v-for="group in skills" :key="group.title"
|
||||
class="bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-3.5 shadow-md-dark transition-all duration-300 hover:-translate-y-1 hover:border-primary/50 hover:shadow-lg-dark hover:bg-gradient-to-br hover:from-primary/8">
|
||||
<article
|
||||
v-for="group in skills"
|
||||
:key="group.title"
|
||||
class="bg-gradient-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-3.5 shadow-md-dark transition-all duration-300 hover:-translate-y-1 hover:border-primary/50 hover:shadow-lg-dark hover:bg-gradient-to-br hover:from-primary/8"
|
||||
>
|
||||
<header class="mb-3">
|
||||
<h3 class="text-base font-semibold m-0">{{ group.title }}</h3>
|
||||
</header>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span v-for="item in group.items" :key="item"
|
||||
class="inline-flex items-center p-1.5 rounded-2xl bg-primary/14 border border-primary/18 transition-all duration-200 hover:bg-primary/24 hover:border-primary/40 hover:scale-110">
|
||||
<img :src="iconSrc(item)" :alt="item" :title="item" loading="lazy"
|
||||
class="w-7 h-7 rounded-2xl" />
|
||||
<span
|
||||
v-for="item in group.items"
|
||||
:key="item"
|
||||
class="inline-flex items-center p-1.5 rounded-2xl bg-primary/14 border border-primary/18 transition-all duration-200 hover:bg-primary/24 hover:border-primary/40 hover:scale-110"
|
||||
>
|
||||
<NuxtImg
|
||||
:src="iconSrc(item)"
|
||||
:alt="item"
|
||||
:title="item"
|
||||
loading="lazy"
|
||||
class="w-7 h-7 rounded-2xl"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -1,62 +1,57 @@
|
||||
<template>
|
||||
<section class="card flex flex-col gap-2.5">
|
||||
<h2 class="m-0 mb-1">社交链接</h2>
|
||||
<h2 class="m-0 mb-1 text-lg font-semibold">社交链接</h2>
|
||||
<p class="text-text-muted text-sm m-0 mb-3 block">社交账号 · Links</p>
|
||||
<div class="flex flex-wrap gap-2.5">
|
||||
<a v-for="link in links" :key="link.url" :href="link.url" target="_blank" rel="noreferrer"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-text-primary text-sm font-medium transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary hover:-translate-y-1">
|
||||
<template v-for="link in links" :key="link.url">
|
||||
<NuxtLink
|
||||
:to="link.url"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-text-primary text-sm font-medium transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary hover:-translate-y-1"
|
||||
>
|
||||
<span v-if="iconFor(link)" class="inline-flex items-center justify-center w-5 h-5">
|
||||
<i v-if="iconFor(link).fa" :class="iconFor(link).fa"></i>
|
||||
<img v-else :src="iconFor(link).src" :alt="link.name" loading="lazy" class="w-full h-full" />
|
||||
<Icon v-if="iconFor(link).name" :name="iconFor(link).name" width="20" height="20" />
|
||||
</span>
|
||||
<span>{{ link.name }}</span>
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
const props = defineProps({ links: Array });
|
||||
defineProps({
|
||||
links: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const iconMap = {
|
||||
bilibili: "fa-brands fa-bilibili",
|
||||
github: "fa-brands fa-github",
|
||||
blog: "fa-solid fa-rss",
|
||||
email: "fa-solid fa-envelope",
|
||||
mail: "fa-solid fa-envelope",
|
||||
telegram: "fa-brands fa-telegram",
|
||||
twitter: "fa-brands fa-x-twitter",
|
||||
x: "fa-brands fa-x-twitter",
|
||||
linkedin: "fa-brands fa-linkedin",
|
||||
youtube: "fa-brands fa-youtube",
|
||||
facebook: "fa-brands fa-facebook",
|
||||
instagram: "fa-brands fa-instagram",
|
||||
reddit: "fa-brands fa-reddit",
|
||||
discord: "fa-brands fa-discord",
|
||||
weibo: "fa-brands fa-weibo",
|
||||
zhihu: "fa-brands fa-zhihu",
|
||||
wechat: "fa-brands fa-weixin",
|
||||
weixin: "fa-brands fa-weixin",
|
||||
qq: "fa-brands fa-qq",
|
||||
bilibili: "simple-icons:bilibili",
|
||||
github: "simple-icons:github",
|
||||
blog: "fa6-solid:book",
|
||||
email: "fa6-solid:envelope",
|
||||
mail: "fa6-solid:envelope",
|
||||
telegram: "simple-icons:telegram",
|
||||
twitter: "simple-icons:twitter",
|
||||
x: "simple-icons:x",
|
||||
linkedin: "simple-icons:linkedin",
|
||||
youtube: "simple-icons:youtube",
|
||||
facebook: "simple-icons:facebook",
|
||||
instagram: "simple-icons:instagram",
|
||||
reddit: "simple-icons:reddit",
|
||||
discord: "simple-icons:discord",
|
||||
weibo: "simple-icons:sinaweibo",
|
||||
zhihu: "simple-icons:zhihu",
|
||||
wechat: "simple-icons:wechat",
|
||||
weixin: "simple-icons:wechat",
|
||||
qq: "simple-icons:qq",
|
||||
};
|
||||
|
||||
const iconFor = (link) => {
|
||||
const key = (link.name || "").toLowerCase();
|
||||
if (iconMap[key]) return { fa: iconMap[key] };
|
||||
if (iconMap[key]) return { name: iconMap[key] };
|
||||
if (link.icon) return { src: link.icon };
|
||||
return null;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const id = "fa-cdn";
|
||||
if (document.getElementById(id)) return;
|
||||
const link = document.createElement("link");
|
||||
link.id = id;
|
||||
link.rel = "stylesheet";
|
||||
link.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?font-display=swap";
|
||||
link.crossOrigin = "anonymous";
|
||||
link.referrerPolicy = "no-referrer";
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<template>
|
||||
<section class="card">
|
||||
<div class="header">
|
||||
<h2>开发统计</h2>
|
||||
<h2 class="m-0 mb-1 font-semibold">开发统计</h2>
|
||||
<div class="tabs">
|
||||
<button class="tab-button" :class="{ active: activeTab === 'github' }" @click="activeTab = 'github'">
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'github' }"
|
||||
@click="activeTab = 'github'"
|
||||
>
|
||||
GitHub
|
||||
</button>
|
||||
<button class="tab-button" :class="{ active: activeTab === 'wakatime' }"
|
||||
@click="activeTab = 'wakatime'">
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'wakatime' }"
|
||||
@click="activeTab = 'wakatime'"
|
||||
>
|
||||
Wakatime
|
||||
</button>
|
||||
</div>
|
||||
@@ -18,7 +25,7 @@
|
||||
<div class="heatmap">
|
||||
<h3>提交热力图</h3>
|
||||
<p class="muted">我的提交热力图 · Activity Heatmap</p>
|
||||
<img :src="github.heatmapUrl" alt="GitHub Heatmap" loading="lazy" />
|
||||
<NuxtImg :src="github.heatmapUrl" alt="GitHub Heatmap" loading="lazy" />
|
||||
</div>
|
||||
<div class="lang-wrap">
|
||||
<h3>常用语言</h3>
|
||||
@@ -27,12 +34,12 @@
|
||||
<ul class="list lang-list">
|
||||
<li v-for="lang in githubLanguages" :key="lang.name" class="lang-row">
|
||||
<div class="lang-label">
|
||||
<span class="dot" :style="{ background: colorFor(lang.name, 'github') }"></span>
|
||||
<span class="dot" :style="{ background: colorFor(lang.name, 'github') }" />
|
||||
<span class="lang-name">{{ lang.name }}</span>
|
||||
<span class="lang-percent">{{ lang.percent }}%</span>
|
||||
</div>
|
||||
<div class="lang-bar">
|
||||
<span class="lang-bar-fill" :style="barStyle(lang, 'github')"></span>
|
||||
<span class="lang-bar-fill" :style="barStyle(lang, 'github')" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -50,62 +57,77 @@
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{
|
||||
currentWakatimeData?.total_seconds ? formatTime(currentWakatimeData.total_seconds) : "N/A"
|
||||
currentWakatimeData?.total_seconds
|
||||
? formatTime(currentWakatimeData.total_seconds)
|
||||
: "N/A"
|
||||
}}</span>
|
||||
<span class="stat-label">总时间</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{
|
||||
currentWakatimeData?.daily_average ? formatTime(currentWakatimeData.daily_average) : "N/A"
|
||||
currentWakatimeData?.daily_average
|
||||
? formatTime(currentWakatimeData.daily_average)
|
||||
: "N/A"
|
||||
}}</span>
|
||||
<span class="stat-label">日均</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ currentWakatimeData?.days_including_holidays ?? "N/A" }}</span>
|
||||
<span class="stat-value">{{
|
||||
currentWakatimeData?.days_including_holidays ?? "N/A"
|
||||
}}</span>
|
||||
<span class="stat-label">活跃天数</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lang-wrap" v-if="currentWakatimeData?.languages && currentWakatimeData.languages.length">
|
||||
<div
|
||||
v-if="currentWakatimeData?.languages && currentWakatimeData.languages.length"
|
||||
class="lang-wrap"
|
||||
>
|
||||
<h3>编程语言</h3>
|
||||
<p class="muted">语言使用统计 · Languages</p>
|
||||
<div class="lang-chart">
|
||||
<ul class="list lang-list">
|
||||
<li v-for="lang in wakatimeLanguages" :key="lang.name" class="lang-row">
|
||||
<div class="lang-label">
|
||||
<span class="dot" :style="{ background: colorFor(lang.name, 'wakatime') }"></span>
|
||||
<span class="dot" :style="{ background: colorFor(lang.name, 'wakatime') }" />
|
||||
<span class="lang-name">{{ lang.name }}</span>
|
||||
<span class="lang-percent">{{ lang.percent }}%</span>
|
||||
</div>
|
||||
<div class="lang-bar">
|
||||
<span class="lang-bar-fill" :style="barStyle(lang, 'wakatime')"></span>
|
||||
<span class="lang-bar-fill" :style="barStyle(lang, 'wakatime')" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wakatime-tabs" v-if="allTimeData">
|
||||
<div v-if="allTimeData" class="wakatime-tabs">
|
||||
<div class="wakatime-mini-tabs">
|
||||
<button class="wakatime-tab-button" :class="{ active: wakatimeActiveTab === 'weekly' }"
|
||||
@click="wakatimeActiveTab = 'weekly'">
|
||||
<button
|
||||
class="wakatime-tab-button"
|
||||
:class="{ active: wakatimeActiveTab === 'weekly' }"
|
||||
@click="wakatimeActiveTab = 'weekly'"
|
||||
>
|
||||
最近7天
|
||||
</button>
|
||||
<button class="wakatime-tab-button" :class="{ active: wakatimeActiveTab === 'allTime' }"
|
||||
@click="wakatimeActiveTab = 'allTime'">
|
||||
<button
|
||||
class="wakatime-tab-button"
|
||||
:class="{ active: wakatimeActiveTab === 'allTime' }"
|
||||
@click="wakatimeActiveTab = 'allTime'"
|
||||
>
|
||||
所有时间
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-wrap" v-if="statusData">
|
||||
<div v-if="statusData" class="status-wrap">
|
||||
<h3>当前状态</h3>
|
||||
<p class="muted">实时状态 · Current Status</p>
|
||||
<div class="status-item">
|
||||
<span class="status-indicator" :class="{ active: statusData.is_coding }"></span>
|
||||
<span class="status-indicator" :class="{ active: statusData.is_coding }" />
|
||||
<span class="status-text">{{ statusData.is_coding ? "正在编码" : "未在编码" }}</span>
|
||||
<span class="status-project" v-if="statusData.project">{{ statusData.project }}</span>
|
||||
<span v-if="statusData.project" class="status-project">{{ statusData.project }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +137,16 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
|
||||
const props = defineProps({ github: Object, wakatime: Object });
|
||||
const props = defineProps({
|
||||
github: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
wakatime: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const github = props.github;
|
||||
const wakatime = props.wakatime;
|
||||
|
||||
@@ -128,9 +159,19 @@ const statusData = ref(null);
|
||||
const showComponent = ref(true);
|
||||
|
||||
const githubPalette = ["#7cc1ff", "#6bdba6", "#ffd166", "#f497da", "#9b8cfc", "#5ce1e6", "#ffa3a3"];
|
||||
const wakatimePalette = ["#7cc1ff", "#6bdba6", "#ffd166", "#f497da", "#9b8cfc", "#5ce1e6", "#ffa3a3"];
|
||||
const wakatimePalette = [
|
||||
"#7cc1ff",
|
||||
"#6bdba6",
|
||||
"#ffd166",
|
||||
"#f497da",
|
||||
"#9b8cfc",
|
||||
"#5ce1e6",
|
||||
"#ffa3a3",
|
||||
];
|
||||
|
||||
const githubLanguages = computed(() => (Array.isArray(github.languages) ? github.languages.slice(0, 5) : []));
|
||||
const githubLanguages = computed(() =>
|
||||
Array.isArray(github.languages) ? github.languages.slice(0, 5) : []
|
||||
);
|
||||
|
||||
const currentWakatimeData = computed(() => {
|
||||
return wakatimeActiveTab.value === "weekly" ? weeklyData.value : allTimeData.value;
|
||||
@@ -143,7 +184,8 @@ const wakatimeLanguages = computed(() => {
|
||||
|
||||
const colorFor = (name, type) => {
|
||||
const palette = type === "github" ? githubPalette : wakatimePalette;
|
||||
const languages = type === "github" ? github.languages : currentWakatimeData.value?.languages || [];
|
||||
const languages =
|
||||
type === "github" ? github.languages : currentWakatimeData.value?.languages || [];
|
||||
const idx = languages.findIndex((l) => l.name === name);
|
||||
return palette[(idx >= 0 ? idx : 0) % palette.length];
|
||||
};
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<template>
|
||||
<section class="card" v-if="showComponent && (weeklyData || allTimeData)">
|
||||
<section v-if="showComponent && (weeklyData || allTimeData)" class="card">
|
||||
<div class="header">
|
||||
<h2>Wakatime</h2>
|
||||
<div class="tabs">
|
||||
<button class="tab-button" :class="{ active: activeTab === 'weekly' }" @click="activeTab = 'weekly'">
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'weekly' }"
|
||||
@click="activeTab = 'weekly'"
|
||||
>
|
||||
最近7天
|
||||
</button>
|
||||
<button class="tab-button" :class="{ active: activeTab === 'allTime' }" @click="activeTab = 'allTime'"
|
||||
v-if="allTimeData">
|
||||
<button
|
||||
v-if="allTimeData"
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'allTime' }"
|
||||
@click="activeTab = 'allTime'"
|
||||
>
|
||||
所有时间
|
||||
</button>
|
||||
</div>
|
||||
@@ -15,7 +23,9 @@
|
||||
|
||||
<div class="stats-wrap">
|
||||
<h3>编码统计</h3>
|
||||
<p class="muted">{{ activeTab === "weekly" ? "最近7天 · Last 7 Days" : "所有时间 · All Time" }}</p>
|
||||
<p class="muted">
|
||||
{{ activeTab === "weekly" ? "最近7天 · Last 7 Days" : "所有时间 · All Time" }}
|
||||
</p>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{
|
||||
@@ -36,32 +46,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lang-wrap" v-if="currentData.languages && currentData.languages.length">
|
||||
<div v-if="currentData.languages && currentData.languages.length" class="lang-wrap">
|
||||
<h3>编程语言</h3>
|
||||
<p class="muted">语言使用统计 · Languages</p>
|
||||
<div class="lang-chart">
|
||||
<ul class="list lang-list">
|
||||
<li v-for="lang in topLanguages" :key="lang.name" class="lang-row">
|
||||
<div class="lang-label">
|
||||
<span class="dot" :style="{ background: colorFor(lang.name) }"></span>
|
||||
<span class="dot" :style="{ background: colorFor(lang.name) }" />
|
||||
<span class="lang-name">{{ lang.name }}</span>
|
||||
<span class="lang-percent">{{ lang.percent }}%</span>
|
||||
</div>
|
||||
<div class="lang-bar">
|
||||
<span class="lang-bar-fill" :style="barStyle(lang)"></span>
|
||||
<span class="lang-bar-fill" :style="barStyle(lang)" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-wrap" v-if="statusData">
|
||||
<div v-if="statusData" class="status-wrap">
|
||||
<h3>当前状态</h3>
|
||||
<p class="muted">实时状态 · Current Status</p>
|
||||
<div class="status-item">
|
||||
<span class="status-indicator" :class="{ active: statusData.is_coding }"></span>
|
||||
<span class="status-indicator" :class="{ active: statusData.is_coding }" />
|
||||
<span class="status-text">{{ statusData.is_coding ? "正在编码" : "未在编码" }}</span>
|
||||
<span class="status-project" v-if="statusData.project">{{ statusData.project }}</span>
|
||||
<span v-if="statusData.project" class="status-project">{{ statusData.project }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -69,9 +79,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import siteConfig from "~/config/siteConfig";
|
||||
|
||||
const props = defineProps({ wakatime: Object });
|
||||
const wakatime = props.wakatime;
|
||||
const wakapi = siteConfig.wakapi;
|
||||
|
||||
const weeklyData = ref(null);
|
||||
const allTimeData = ref(null);
|
||||
@@ -79,7 +89,7 @@ const statusData = ref(null);
|
||||
const showComponent = ref(true);
|
||||
const activeTab = ref("weekly");
|
||||
|
||||
const palette = ["#7cc1ff", "#6bdba6", "#ffd166", "#f497da", "#9b8cfc", "#5ce1e6", "#ffa3a3"];
|
||||
const palette = ["#7cc1ff", "#6bdba6", "#ffd166", "#201a1fff", "#9b8cfc", "#5ce1e6", "#ffa3a3"];
|
||||
|
||||
const currentData = computed(() => {
|
||||
return activeTab.value === "weekly" ? weeklyData.value : allTimeData.value;
|
||||
@@ -107,65 +117,45 @@ const formatTime = (seconds) => {
|
||||
};
|
||||
|
||||
const fetchWakatimeData = async () => {
|
||||
if (!wakatime.enable) return;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (wakatime.apiUrl && wakatime.apiUrl !== "https://wakatime.com/api/v1") {
|
||||
params.append("apiUrl", wakatime.apiUrl);
|
||||
}
|
||||
const url = `/api/wakatime${params.toString() ? `?${params.toString()}` : ""}`;
|
||||
console.log("Fetching Wakatime data from:", url);
|
||||
const response = await fetch(url);
|
||||
console.log("Response status:", response.status);
|
||||
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log("Wakatime data:", data);
|
||||
weeklyData.value = data.weekly;
|
||||
allTimeData.value = data.allTime;
|
||||
statusData.value = data.status;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error("API Error:", response.status, errorText);
|
||||
if (response.status === 500 && errorText.includes("Wakatime API Key not configured")) {
|
||||
console.warn("Wakatime API Key not configured - hiding component");
|
||||
showComponent.value = false;
|
||||
if (!wakapi.enable) {
|
||||
console.warn("Wakatime is disabled in siteConfig.");
|
||||
return;
|
||||
}
|
||||
throw new Error(`API returned ${response.status}: ${errorText}`);
|
||||
|
||||
const apiUrl = wakapi.apiUrl || "https://wakatime.com/api/v1";
|
||||
const username = wakapi.username;
|
||||
|
||||
if (!username) {
|
||||
console.error("Wakatime username is not configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [weeklyStatsResponse, allTimeStatsResponse, statusResponse] = await Promise.all([
|
||||
fetch(`${apiUrl}/users/${username}/stats/last_7_days`),
|
||||
fetch(`${apiUrl}/users/${username}/stats`),
|
||||
fetch(`${apiUrl}/users/${username}/status`),
|
||||
]);
|
||||
|
||||
if (weeklyStatsResponse.ok) {
|
||||
weeklyData.value = await weeklyStatsResponse.json();
|
||||
} else {
|
||||
console.error("Failed to fetch weekly stats:", weeklyStatsResponse.status);
|
||||
}
|
||||
|
||||
if (allTimeStatsResponse.ok) {
|
||||
allTimeData.value = await allTimeStatsResponse.json();
|
||||
} else {
|
||||
console.warn("All-time stats not available:", allTimeStatsResponse.status);
|
||||
}
|
||||
|
||||
if (statusResponse.ok) {
|
||||
statusData.value = await statusResponse.json();
|
||||
} else {
|
||||
console.warn("Status data not available:", statusResponse.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Wakatime data:", error);
|
||||
// 在开发环境中,如果 API 不可用,设置一些示例数据
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("Using mock data for development");
|
||||
weeklyData.value = {
|
||||
total_seconds: 36000,
|
||||
daily_average: 5142,
|
||||
days_including_holidays: 7,
|
||||
languages: [
|
||||
{ name: "TypeScript", percent: 45.2, total_seconds: 16272 },
|
||||
{ name: "Vue", percent: 30.1, total_seconds: 10836 },
|
||||
{ name: "JavaScript", percent: 15.3, total_seconds: 5508 },
|
||||
{ name: "Python", percent: 9.4, total_seconds: 3384 },
|
||||
],
|
||||
};
|
||||
allTimeData.value = {
|
||||
total_seconds: 864000,
|
||||
daily_average: 2800,
|
||||
days_including_holidays: 308,
|
||||
languages: [
|
||||
{ name: "JavaScript", percent: 35.2, total_seconds: 304128 },
|
||||
{ name: "TypeScript", percent: 28.1, total_seconds: 242688 },
|
||||
{ name: "Python", percent: 20.3, total_seconds: 175392 },
|
||||
{ name: "Vue", percent: 10.1, total_seconds: 87296 },
|
||||
{ name: "CSS", percent: 6.3, total_seconds: 54432 },
|
||||
],
|
||||
};
|
||||
statusData.value = { is_coding: false };
|
||||
}
|
||||
console.error("Error fetching Wakatime data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ const siteConfig = {
|
||||
profile: {
|
||||
name: "RhenCloud",
|
||||
title: "I'm RhenCloud.",
|
||||
avatar: "/avatar.webp", // public/avatar.webp
|
||||
avatar: "/avatar-1.webp", // public/avatar.webp
|
||||
bio: "趁世界还未重启之前 约一次爱恋",
|
||||
email: "i@rhen.cloud",
|
||||
birthday: "2010-03-28",
|
||||
@@ -17,6 +17,7 @@ const siteConfig = {
|
||||
{ name: "Telegram", url: "https://t.me/RhenCloud" },
|
||||
{ name: "Bilibili", url: "https://space.bilibili.com/1502883335" },
|
||||
{ name: "Blog", url: "https://blog.rhen.cloud" },
|
||||
{ name: "Twitter", url: "https://x.com/RhenCloud75" },
|
||||
],
|
||||
|
||||
github: {
|
||||
@@ -32,9 +33,13 @@ const siteConfig = {
|
||||
|
||||
siteMeta: {
|
||||
title: "RhenCloud",
|
||||
description: "RhenCloud的个人主页,分享技术、生活、兴趣。",
|
||||
keywords: ["Technology", "Blog", "Development", "Programming"],
|
||||
author: "RhenCloud",
|
||||
url: "https://rhen.cloud",
|
||||
icon: "/favicon.svg", // public/favicon.svg
|
||||
favicon: "/favicon.svg", // public/favicon.svg
|
||||
startDate: "2025-12-06",
|
||||
lang: "zh-CN",
|
||||
},
|
||||
|
||||
appearance: {
|
||||
@@ -80,14 +85,18 @@ const siteConfig = {
|
||||
apiBase: "https://api.umami.is",
|
||||
},
|
||||
|
||||
wakatime: {
|
||||
enable: true,
|
||||
wakapi: {
|
||||
enable: false,
|
||||
apiUrl: "https://wakapi.rhen.cloud/api/v1",
|
||||
username: "RhenCloud",
|
||||
},
|
||||
|
||||
skills: [
|
||||
{ title: "前端", items: ["css", "html", "javascript", "typescript", "vue"] },
|
||||
{ title: "后端 / 云", items: ["cpp", "cloudflare", "docker", "java", "mysql", "nodejs", "python", "vercel"] },
|
||||
{
|
||||
title: "后端 / 云",
|
||||
items: ["cpp", "cloudflare", "docker", "java", "mysql", "nodejs", "python", "vercel"],
|
||||
},
|
||||
{ title: "工具", items: ["ae", "au", "git", "github", "md", "ps", "pr", "vscode"] },
|
||||
{ title: "操作系统", items: ["arch", "linux", "windows"] },
|
||||
],
|
||||
@@ -117,14 +126,18 @@ const siteConfig = {
|
||||
|
||||
projects: [
|
||||
{ name: "Cloud Home", url: "https://github.com/RhenCloud/cloud-home", desc: "个人主页模板" },
|
||||
{ name: "ILP", url: "https://github.com/RhenCloud/ILP", desc: "跨平台、多网站、模块化的小说下载器" },
|
||||
{ name: "ILP-C++", url: "https://github.com/RhenCloud/ILP-Cpp", desc: "跨平台、多网站、模块化的小说下载器" },
|
||||
{
|
||||
name: "Test",
|
||||
name: "ILP",
|
||||
url: "https://github.com/RhenCloud/ILP",
|
||||
desc: "跨平台、多网站、模块化的小说下载器",
|
||||
},
|
||||
{
|
||||
name: "ILP-C++",
|
||||
url: "https://github.com/RhenCloud/ILP-Cpp",
|
||||
desc: "",
|
||||
desc: "跨平台、多网站、模块化的小说下载器",
|
||||
},
|
||||
],
|
||||
|
||||
friends: [
|
||||
{
|
||||
name: "wuxian",
|
||||
@@ -138,14 +151,26 @@ const siteConfig = {
|
||||
url: "https://blog.sakura.ink",
|
||||
avatar: "https://q2.qlogo.cn/headimg_dl?dst_uin=2731443459&spec=5",
|
||||
},
|
||||
{
|
||||
name: "鈴奈咲桜のBlog",
|
||||
desc: "一个普普通通的Blog",
|
||||
url: "https://blog.sakura.ink",
|
||||
avatar: "https://q2.qlogo.cn/headimg_dl?dst_uin=2731443459&spec=5",
|
||||
},
|
||||
],
|
||||
|
||||
comments: {
|
||||
enable: true,
|
||||
// twikoo: {
|
||||
// url: "https://twikoo.rhen.cloud",
|
||||
// },
|
||||
giscus: {
|
||||
repo: "RhenCloud/Cloud-Home",
|
||||
repoId: "R_kgDOQjx8rQ",
|
||||
category: "Announcements",
|
||||
categoryId: "DIC_kwDOQjx8rc4Cz4Qb",
|
||||
mapping: "pathname",
|
||||
reactionsEnabled: "1",
|
||||
emitMetadata: "0",
|
||||
inputPosition: "bottom",
|
||||
theme: "preferred_color_scheme",
|
||||
},
|
||||
},
|
||||
|
||||
footer: {
|
||||
beian: "津ICP备2025039003号-1",
|
||||
beianLink: "https://beian.miit.gov.cn/",
|
||||
|
||||
@@ -56,9 +56,12 @@ async function fetchGithubMeta() {
|
||||
if (githubToken) {
|
||||
headers.Authorization = `Bearer ${githubToken}`;
|
||||
}
|
||||
const resp = await fetch(`https://api.github.com/users/${github.username}/repos?per_page=100&sort=updated`, {
|
||||
const resp = await fetch(
|
||||
`https://api.github.com/users/${github.username}/repos?per_page=100&sort=updated`,
|
||||
{
|
||||
headers,
|
||||
});
|
||||
}
|
||||
);
|
||||
const data = await resp.json();
|
||||
if (!Array.isArray(data)) return;
|
||||
type GithubRepo = { language?: string };
|
||||
|
||||
82
app/pages/comments.vue
Normal file
82
app/pages/comments.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<section class="container mx-auto py-8">
|
||||
<p v-if="!giscus || !giscus.repo" class="text-sm text-red-500 mb-4">Giscus 未配置。</p>
|
||||
<ClientOnly>
|
||||
<section class="card panel flex flex-col gap-2.5">
|
||||
<h2 class="m-0 mb-1 text-lg font-semibold">留言板</h2>
|
||||
<p class="text-sm text-white/60 mb-3">在这里留下想说的话吧 · Comments</p>
|
||||
<div class="giscus-wrapper">
|
||||
<component
|
||||
:is="GiscusComponent"
|
||||
v-if="GiscusComponent"
|
||||
:repo="giscus.repo"
|
||||
:repo-id="giscus.repoId"
|
||||
:category="giscus.category"
|
||||
:category-id="giscus.categoryId"
|
||||
:mapping="giscus.mapping"
|
||||
:strict="giscus.strict"
|
||||
:reactions-enabled="giscus.reactionsEnabled"
|
||||
:emit-metadata="giscus.emitMetadata"
|
||||
:input-position="giscus.inputPosition"
|
||||
:theme="'/css/giscus.css'"
|
||||
lang="zh-CN"
|
||||
class="giscus"
|
||||
/>
|
||||
<div v-else id="giscus-container" class="giscus" />
|
||||
</div>
|
||||
</section>
|
||||
</ClientOnly>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { definePageMeta } from "#imports";
|
||||
import { onMounted, shallowRef, markRaw } from "vue";
|
||||
import type { Component } from "vue";
|
||||
import siteConfig from "~/config/siteConfig";
|
||||
|
||||
const giscus = siteConfig.comments.giscus || {};
|
||||
const GiscusComponent = shallowRef<Component | null>(null);
|
||||
|
||||
async function tryUseOfficialComponent() {
|
||||
try {
|
||||
const mod = await import("@giscus/vue");
|
||||
const comp = mod.default ?? mod;
|
||||
if (comp) GiscusComponent.value = markRaw(comp);
|
||||
return !!comp;
|
||||
} catch (e) {
|
||||
console.error("Failed to import Giscus component:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 如果没有配置 giscus,显示提示(由模板处理)
|
||||
if (!siteConfig.comments.enable) return;
|
||||
if (!giscus || !giscus.repo) return;
|
||||
const ok = await tryUseOfficialComponent();
|
||||
if (!ok) {
|
||||
console.error("Failed to load Giscus component.");
|
||||
}
|
||||
});
|
||||
|
||||
definePageMeta({
|
||||
order: 5,
|
||||
label: "留言",
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
max-width: 880px;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #e6eef8;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- <style src="../styles/giscus.css"></style> -->
|
||||
@@ -1,6 +1,15 @@
|
||||
import { defineNuxtPlugin } from "#app";
|
||||
import siteConfig from "~/config/siteConfig";
|
||||
|
||||
type NeteaseMiniPlayerGlobal = {
|
||||
init?: () => void;
|
||||
};
|
||||
|
||||
type NeteaseWindow = Window & {
|
||||
NeteaseMiniPlayer?: NeteaseMiniPlayerGlobal;
|
||||
__NETEASE_MUSIC_CONFIG__?: unknown;
|
||||
};
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
if (import.meta.server) return;
|
||||
|
||||
@@ -35,13 +44,15 @@ export default defineNuxtPlugin(() => {
|
||||
const ensureScript = () =>
|
||||
new Promise<void>((resolve) => {
|
||||
// 检查全局对象是否已存在,表示脚本已加载
|
||||
const anyWin = window as any;
|
||||
const anyWin = window as NeteaseWindow;
|
||||
if (anyWin.NeteaseMiniPlayer) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = document.querySelector(`script[src="${scriptSrc}"]`) as HTMLScriptElement | null;
|
||||
const existing = document.querySelector(
|
||||
`script[src="${scriptSrc}"]`
|
||||
) as HTMLScriptElement | null;
|
||||
if (existing) {
|
||||
// 脚本已存在但未加载,等待它加载
|
||||
existing.addEventListener("load", () => resolve(), { once: true });
|
||||
@@ -62,7 +73,7 @@ export default defineNuxtPlugin(() => {
|
||||
});
|
||||
|
||||
const initPlayer = () => {
|
||||
const anyWin = window as any;
|
||||
const anyWin = window as NeteaseWindow;
|
||||
|
||||
// 将 siteConfig 的音乐配置传递给全局 window 对象
|
||||
if (!anyWin.__NETEASE_MUSIC_CONFIG__) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Router } from "vue-router";
|
||||
import siteConfig from "~/config/siteConfig";
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
if (!process.client) return;
|
||||
if (!import.meta.client) return;
|
||||
if (!siteConfig.umami?.enable) return;
|
||||
|
||||
// 跳过在 localhost 环境下加载 Umami
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Font Awesome 字体优化 */
|
||||
@font-face {
|
||||
font-family: "Font Awesome 6 Solid";
|
||||
src: url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-solid-900.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Font Awesome 6 Brands";
|
||||
src: url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-brands-400.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
height: 100%;
|
||||
@@ -123,7 +106,11 @@
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, border-color 0.18s ease;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
background 0.18s ease,
|
||||
border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.background-toggle:hover,
|
||||
|
||||
19
eslint.config.mjs
Normal file
19
eslint.config.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
// @ts-check
|
||||
import withNuxt from "./.nuxt/eslint.config.mjs";
|
||||
// import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
|
||||
export default withNuxt([eslintPluginPrettierRecommended], {
|
||||
files: ["src/**/*.ts", "src/**/*.vue"],
|
||||
ignores: [".nuxt/", "node_modules/"],
|
||||
language: "vue",
|
||||
|
||||
// plugins: {
|
||||
// prettier: prettierPlugin,
|
||||
// },
|
||||
// rules: {
|
||||
// "prettier/prettier": "error",
|
||||
// "simple-import-sort/imports": "error",
|
||||
// "simple-import-sort/exports": "error",
|
||||
// },
|
||||
});
|
||||
2
netlify.toml
Normal file
2
netlify.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[images]
|
||||
remote_images = ["https:\\/\\/.*"]
|
||||
@@ -4,54 +4,97 @@ import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-12-12",
|
||||
srcDir: "app/",
|
||||
srcDir: "app",
|
||||
|
||||
modules: [
|
||||
"@nuxt/image",
|
||||
"@nuxt/eslint",
|
||||
"@nuxtjs/robots",
|
||||
"@nuxtjs/sitemap",
|
||||
"@nuxt/icon",
|
||||
"@nuxtjs/seo",
|
||||
],
|
||||
|
||||
// 禁用 Vue Router 的非关键警告
|
||||
vue: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag.startsWith("ion-"),
|
||||
},
|
||||
},
|
||||
|
||||
// Tailwind CSS 集成
|
||||
css: ["~/styles.global.css"],
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
build: {
|
||||
sourcemap: false,
|
||||
chunkSizeWarningLimit: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
postcss: {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
},
|
||||
|
||||
routeRules: {
|
||||
"/": { prerender: true },
|
||||
"/about": { isr: 3600 },
|
||||
"/sites": { prerender: true },
|
||||
"/projects": { prerender: true },
|
||||
"/friends": { prerender: true },
|
||||
},
|
||||
|
||||
robots: { groups: [{ userAgent: ["GPTBot", "ChatGPT-User"], disallow: ["/"] }] },
|
||||
|
||||
sitemap: {
|
||||
zeroRuntime: true,
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
charset: "utf-8",
|
||||
viewport: "width=device-width,initial-scale=1,maximum-scale=5",
|
||||
title: siteConfig.siteMeta.title,
|
||||
titleTemplate: `%s - ${siteConfig.siteMeta.title}`,
|
||||
meta: [
|
||||
{ name: "author", content: siteConfig.siteMeta.author },
|
||||
{ name: "language", content: "zh-CN" },
|
||||
{ name: "description", content: siteConfig.siteMeta.description },
|
||||
],
|
||||
link: [
|
||||
{ rel: "icon", href: siteConfig.siteMeta.icon },
|
||||
// Font Awesome CDN 预加载和优化
|
||||
{
|
||||
rel: "preload",
|
||||
as: "style",
|
||||
href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?font-display=swap",
|
||||
crossorigin: "anonymous",
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-solid-900.woff2?font-display=swap",
|
||||
type: "font/woff2",
|
||||
crossorigin: "anonymous",
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-brands-400.woff2?font-display=swap",
|
||||
type: "font/woff2",
|
||||
crossorigin: "anonymous",
|
||||
},
|
||||
{ rel: "icon", href: siteConfig.siteMeta.favicon, type: "image/svg+xml" },
|
||||
{ rel: "canonical", href: siteConfig.siteMeta.url },
|
||||
{ rel: "alternate", hreflang: siteConfig.siteMeta.lang, href: siteConfig.siteMeta.url },
|
||||
{ rel: "dns-prefetch", href: siteConfig.siteMeta.url },
|
||||
{ rel: "preconnect", href: siteConfig.siteMeta.url },
|
||||
{ rel: "icon", href: siteConfig.siteMeta.favicon },
|
||||
],
|
||||
},
|
||||
pageTransition: { name: "page", mode: "out-in" },
|
||||
layoutTransition: { name: "layout", mode: "out-in" },
|
||||
},
|
||||
|
||||
nitro: {
|
||||
prerender: {
|
||||
crawlLinks: true,
|
||||
// routes: ["/sitemap.xml", "/rss.xml"],
|
||||
},
|
||||
minify: true,
|
||||
externals: {
|
||||
inline: ["unhead"],
|
||||
},
|
||||
},
|
||||
|
||||
site: {
|
||||
url: siteConfig.siteMeta.url,
|
||||
name: siteConfig.siteMeta.title,
|
||||
description: siteConfig.siteMeta.description,
|
||||
author: siteConfig.siteMeta.author,
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
smtpHost: process.env.SMTP_HOST ?? "",
|
||||
smtpPort: Number(process.env.SMTP_PORT ?? 465),
|
||||
@@ -60,11 +103,11 @@ export default defineNuxtConfig({
|
||||
senderEmail: process.env.SENDER_EMAIL ?? "",
|
||||
adminEmail: process.env.ADMIN_EMAIL ?? "",
|
||||
smtpSecure: process.env.SMTP_SECURE ? process.env.SMTP_SECURE === "true" : undefined,
|
||||
githubToken: process.env.NUXT_PUBLIC_GITHUB_TOKEN ?? "",
|
||||
umamiApiKey: process.env.UMAMI_API_KEY ?? "",
|
||||
public: {
|
||||
wakatimeApiKey: process.env.WAKATIME_API_KEY ?? "",
|
||||
wakatimeApiUrl: process.env.WAKATIME_API_URL ?? "https://wakatime.com/api/v1",
|
||||
public: {
|
||||
githubToken: process.env.NUXT_PUBLIC_GITHUB_TOKEN ?? "",
|
||||
umamiApiKey: process.env.NUXT_PUBLIC_UMAMI_API_KEY ?? "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
36
package.json
36
package.json
@@ -1,24 +1,48 @@
|
||||
{
|
||||
"name": "cloud-home",
|
||||
"author": {
|
||||
"name": "RhenCloud",
|
||||
"email": "i@rhen.cloud",
|
||||
"url": "https://rhen.cloud"
|
||||
},
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
"build": "nuxt build",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview"
|
||||
"preview": "nuxt preview",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@giscus/vue": "^3.1.1",
|
||||
"@jaseeey/vue-umami-plugin": "^1.4.0",
|
||||
"nodemailer": "^7.0.11",
|
||||
"@nuxt/icon": "^2.2.0",
|
||||
"@nuxt/image": "2.0.0",
|
||||
"@nuxtjs/icon": "^2.6.0",
|
||||
"@nuxtjs/robots": "^5.6.7",
|
||||
"@nuxtjs/seo": "3.3.0",
|
||||
"@nuxtjs/sitemap": "^7.5.2",
|
||||
"nodemailer": "^7.0.12",
|
||||
"nuxt": "^4.2.2",
|
||||
"vite-tsconfig-paths": "^6.0.1"
|
||||
"vite-tsconfig-paths": "^6.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/fa6-brands": "^1.2.6",
|
||||
"@iconify-json/simple-icons": "^1.2.66",
|
||||
"@nuxt/eslint": "1.12.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"@types/nodemailer": "^7.0.5",
|
||||
"@typescript-eslint/parser": "^8.53.0",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"prettier": "^3.8.0",
|
||||
"prettier-eslint": "^16.4.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
12552
pnpm-lock.yaml
generated
Normal file
12552
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
ignoredBuiltDependencies:
|
||||
- core-js
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>404 - Not Found</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
font-family:
|
||||
"Inter",
|
||||
"Segoe UI",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -20,9 +24,9 @@
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: radial-gradient(circle at 20% 20%, rgba(244, 151, 218, 0.14), transparent 32%),
|
||||
radial-gradient(circle at 80% 10%, rgba(124, 193, 255, 0.16), transparent 32%),
|
||||
#0f1116;
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(244, 151, 218, 0.14), transparent 32%),
|
||||
radial-gradient(circle at 80% 10%, rgba(124, 193, 255, 0.16), transparent 32%), #0f1116;
|
||||
color: #e8eefc;
|
||||
}
|
||||
|
||||
@@ -58,7 +62,10 @@
|
||||
color: #e8eefc;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
}
|
||||
|
||||
a.btn:hover {
|
||||
@@ -67,14 +74,13 @@
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<main class="card">
|
||||
<h1>404</h1>
|
||||
<p>页面不见了,或已被移除。</p>
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
public/avatar-1.webp
Normal file
BIN
public/avatar-1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 587 KiB |
167
public/css/giscus.css
Normal file
167
public/css/giscus.css
Normal file
@@ -0,0 +1,167 @@
|
||||
/*!
|
||||
* Modified from GitHub's Dark Dimmed theme, adapted for Cloud-Home
|
||||
* License: MIT (see original project primer/primitives)
|
||||
* Modified from GitHub's Dark Dimmed theme, licensed under the MIT License
|
||||
* Copyright (c) 2018 GitHub Inc.
|
||||
* https://github.com/primer/primitives/blob/main/LICENSE
|
||||
*/
|
||||
|
||||
:root {
|
||||
--ch-bg: #0a0c14; /* 深色卡片背景 */
|
||||
--ch-bg-2: rgba(6, 8, 15, 0.55);
|
||||
--ch-border: rgba(255, 255, 255, 0.04);
|
||||
--muted: #768390;
|
||||
--fg: #e6eef8;
|
||||
|
||||
/* 添加 1.css 中的颜色变量 */
|
||||
--color-prettylights-syntax-comment: #768390;
|
||||
--color-prettylights-syntax-constant: #6cb6ff;
|
||||
--color-prettylights-syntax-entity: #dcbdfb;
|
||||
--color-prettylights-syntax-storage-modifier-import: #adbac7;
|
||||
--color-prettylights-syntax-entity-tag: #8ddb8c;
|
||||
--color-prettylights-syntax-keyword: #f47067;
|
||||
--color-prettylights-syntax-string: #96d0ff;
|
||||
--color-prettylights-syntax-variable: #f69d50;
|
||||
--color-prettylights-syntax-brackethighlighter-unmatched: #e5534b;
|
||||
--color-prettylights-syntax-invalid-illegal-text: #cdd9e5;
|
||||
--color-prettylights-syntax-invalid-illegal-bg: #922323;
|
||||
--color-prettylights-syntax-carriage-return-text: #cdd9e5;
|
||||
--color-prettylights-syntax-carriage-return-bg: #ad2e2c;
|
||||
--color-prettylights-syntax-string-regexp: #8ddb8c;
|
||||
--color-prettylights-syntax-markup-list: #eac55f;
|
||||
--color-prettylights-syntax-markup-heading: #316dca;
|
||||
--color-prettylights-syntax-markup-italic: #adbac7;
|
||||
--color-prettylights-syntax-markup-bold: #adbac7;
|
||||
--color-prettylights-syntax-markup-deleted-text: #ffd8d3;
|
||||
--color-prettylights-syntax-markup-deleted-bg: #78191b;
|
||||
--color-prettylights-syntax-markup-inserted-text: #b4f1b4;
|
||||
--color-prettylights-syntax-markup-inserted-bg: #1b4721;
|
||||
--color-prettylights-syntax-markup-changed-text: #ffddb0;
|
||||
--color-prettylights-syntax-markup-changed-bg: #682d0f;
|
||||
--color-prettylights-syntax-markup-ignored-text: #adbac7;
|
||||
--color-prettylights-syntax-markup-ignored-bg: #255ab2;
|
||||
--color-prettylights-syntax-meta-diff-range: #dcbdfb;
|
||||
--color-prettylights-syntax-brackethighlighter-angle: #768390;
|
||||
--color-prettylights-syntax-sublimelinter-gutter-mark: #545d68;
|
||||
--color-prettylights-syntax-constant-other-reference-link: #96d0ff;
|
||||
--color-btn-text: #adbac7;
|
||||
--color-btn-bg: #373e47;
|
||||
--color-btn-border: #cdd9e51a;
|
||||
--color-btn-shadow: 0 0 #0000;
|
||||
--color-btn-inset-shadow: 0 0 #0000;
|
||||
--color-btn-hover-bg: #444c56;
|
||||
--color-btn-hover-border: #768390;
|
||||
--color-btn-active-bg: #3d444d;
|
||||
--color-btn-active-border: #636e7b;
|
||||
--color-btn-selected-bg: #2d333b;
|
||||
--color-btn-primary-text: #fff;
|
||||
--color-btn-primary-bg: #347d39;
|
||||
--color-btn-primary-border: #cdd9e51a;
|
||||
--color-btn-primary-shadow: 0 0 #0000;
|
||||
--color-btn-primary-inset-shadow: 0 0 #0000;
|
||||
--color-btn-primary-hover-bg: #46954a;
|
||||
--color-btn-primary-hover-border: #cdd9e51a;
|
||||
--color-btn-primary-selected-bg: #347d39;
|
||||
--color-btn-primary-selected-shadow: 0 0 #0000;
|
||||
--color-btn-primary-disabled-text: #cdd9e580;
|
||||
--color-btn-primary-disabled-bg: #347d3999;
|
||||
--color-btn-primary-disabled-border: #cdd9e51a;
|
||||
--color-action-list-item-default-hover-bg: #909dab1f;
|
||||
--color-segmented-control-bg: #636e7b1a;
|
||||
--color-segmented-control-button-bg: #22272e;
|
||||
--color-segmented-control-button-selected-border: #636e7b;
|
||||
}
|
||||
|
||||
/* 外部容器(在组件中也有一层) */
|
||||
.giscus-wrapper {
|
||||
background: linear-gradient(180deg, rgba(6, 8, 15, 0.55), rgba(10, 12, 20, 0.45));
|
||||
border: 1px solid var(--ch-border);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem;
|
||||
box-shadow: 0 8px 24px rgba(2, 6, 23, 0.6);
|
||||
backdrop-filter: blur(6px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(6px) saturate(120%);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.giscus {
|
||||
width: 100%;
|
||||
color-scheme: dark;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 兼容非组件 fallback 容器 */
|
||||
#giscus-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* GitHub Dark Dimmed inspired tweaks adapted to site theme */
|
||||
.gsc-reactions-count {
|
||||
display: none !important;
|
||||
}
|
||||
.gsc-timeline {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.gsc-header {
|
||||
padding-bottom: 1rem;
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
.gsc-comments > .gsc-comment-box {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.gsc-comments > .gsc-header {
|
||||
order: 1;
|
||||
}
|
||||
.gsc-comments > .gsc-timeline {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
/* 卡片风格:半透明+圆角+内阴影,契合站点 */
|
||||
.gsc-comment,
|
||||
.gsc-comment-body,
|
||||
.gsc-comment .gsc-comment-body {
|
||||
background: linear-gradient(180deg, rgba(6, 8, 15, 0.5), rgba(10, 12, 20, 0.45)) !important;
|
||||
border: 1px solid var(--ch-border) !important;
|
||||
border-radius: 10px !important;
|
||||
padding: 0.75rem !important;
|
||||
box-shadow: 0 8px 20px rgba(2, 6, 23, 0.55) !important;
|
||||
}
|
||||
|
||||
/* 评论作者/元信息颜色 */
|
||||
.gsc-comment .gsc-comment-header,
|
||||
.gsc-comment .gsc-comment-meta {
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
/* 按钮 / 交互控件微调 */
|
||||
.gsc-reaction-button,
|
||||
.gsc-input button {
|
||||
background: var(--color-btn-bg) !important;
|
||||
border: var(--color-btn-border) !important;
|
||||
color: var(--color-btn-text) !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: var(--color-btn-shadow) !important;
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.gsc-input textarea,
|
||||
.gsc-input input {
|
||||
background: rgba(0, 0, 0, 0.35) !important;
|
||||
color: var(--fg) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
/* 加载图像和首页背景微调 */
|
||||
main .gsc-loading-image {
|
||||
background-image: url(https://github.githubassets.com/images/mona-loading-dimmed.gif) !important;
|
||||
}
|
||||
.gsc-homepage-bg {
|
||||
background-color: var(--color-canvas-subtle) !important;
|
||||
}
|
||||
|
||||
/* 语义辅助:让嵌入内容更好地适配窄屏 */
|
||||
.giscus {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -38,7 +38,8 @@
|
||||
--shadow-outset: 6px 6px 12px var(--shadow-dark), -6px -6px 12px var(--shadow-light);
|
||||
--border-radius: 12px;
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--theme-transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
|
||||
--theme-transition:
|
||||
background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
|
||||
|
||||
--flow-color-1: rgba(255, 107, 53, 0.32);
|
||||
--flow-color-2: rgba(86, 151, 227, 0.26);
|
||||
@@ -158,10 +159,15 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-radius 0.5s cubic-bezier(0.4, 0, 0.2, 1), padding 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transition:
|
||||
width 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-radius 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
padding 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
var(--theme-transition);
|
||||
background: 0.5s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background:
|
||||
0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.netease-mini-player::before {
|
||||
@@ -170,14 +176,25 @@
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(circle at 15% 20%, var(--flow-color-1) 0%, transparent 60%),
|
||||
background:
|
||||
radial-gradient(circle at 15% 20%, var(--flow-color-1) 0%, transparent 60%),
|
||||
radial-gradient(circle at 80% 25%, var(--flow-color-2) 0%, transparent 60%),
|
||||
radial-gradient(circle at 30% 85%, var(--flow-color-3) 0%, transparent 60%),
|
||||
radial-gradient(circle at 10% 75%, var(--flow-color-4) 0%, transparent 55%),
|
||||
radial-gradient(circle at 85% 80%, var(--flow-color-5) 0%, transparent 55%);
|
||||
opacity: var(--flow-opacity);
|
||||
background-size: 220% 220%, 220% 220%, 220% 220%, 220% 220%, 220% 220%;
|
||||
background-position: 0% 0%, 100% 0%, 0% 100%, 20% 80%, 80% 20%;
|
||||
background-size:
|
||||
220% 220%,
|
||||
220% 220%,
|
||||
220% 220%,
|
||||
220% 220%,
|
||||
220% 220%;
|
||||
background-position:
|
||||
0% 0%,
|
||||
100% 0%,
|
||||
0% 100%,
|
||||
20% 80%,
|
||||
80% 20%;
|
||||
filter: saturate(1.02) brightness(1.01);
|
||||
transform: scale(1);
|
||||
animation: flow-breathe var(--flow-speed) ease-in-out infinite;
|
||||
@@ -190,17 +207,32 @@
|
||||
|
||||
@keyframes flow-breathe {
|
||||
0% {
|
||||
background-position: 0% 0%, 100% 0%, 0% 100%, 20% 80%, 80% 20%;
|
||||
background-position:
|
||||
0% 0%,
|
||||
100% 0%,
|
||||
0% 100%,
|
||||
20% 80%,
|
||||
80% 20%;
|
||||
transform: scale(1);
|
||||
filter: saturate(1.02) brightness(1.01);
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%, 0% 100%, 50% 0%, 35% 65%, 65% 35%;
|
||||
background-position:
|
||||
100% 50%,
|
||||
0% 100%,
|
||||
50% 0%,
|
||||
35% 65%,
|
||||
65% 35%;
|
||||
transform: scale(1.03);
|
||||
filter: saturate(1.15) brightness(1.06);
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 100%, 100% 50%, 100% 0%, 20% 80%, 80% 20%;
|
||||
background-position:
|
||||
0% 100%,
|
||||
100% 50%,
|
||||
100% 0%,
|
||||
20% 80%,
|
||||
80% 20%;
|
||||
transform: scale(1);
|
||||
filter: saturate(1.02) brightness(1.01);
|
||||
}
|
||||
@@ -211,10 +243,13 @@
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
background: radial-gradient(120px 120px at 30% 30%, rgba(255, 138, 80, 0.25), transparent 60%),
|
||||
background:
|
||||
radial-gradient(120px 120px at 30% 30%, rgba(255, 138, 80, 0.25), transparent 60%),
|
||||
radial-gradient(120px 120px at 70% 70%, rgba(0, 210, 255, 0.22), transparent 60%),
|
||||
linear-gradient(135deg, #151515, #1e1e1e);
|
||||
box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.4), -2px -2px 8px rgba(255, 255, 255, 0.1),
|
||||
box-shadow:
|
||||
4px 4px 12px rgba(0, 0, 0, 0.4),
|
||||
-2px -2px 8px rgba(255, 255, 255, 0.1),
|
||||
inset 0 0 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@@ -223,13 +258,17 @@
|
||||
.netease-mini-player.minimized .playlist-container {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized .player-bottom {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized .minimize-btn {
|
||||
@@ -261,7 +300,9 @@
|
||||
.netease-mini-player .player-bottom {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 0.3s ease 0.2s, visibility 0.3s ease 0.2s;
|
||||
transition:
|
||||
opacity 0.3s ease 0.2s,
|
||||
visibility 0.3s ease 0.2s;
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized .album-cover-container {
|
||||
@@ -272,8 +313,11 @@
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: none;
|
||||
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
top 0.5s cubic-bezier(0.4, 0, 0.2, 1), left 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transition:
|
||||
width 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
top 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
left 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@@ -282,7 +326,9 @@
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
filter: brightness(0.8) contrast(1.1);
|
||||
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transition:
|
||||
width 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
filter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@@ -306,18 +352,27 @@
|
||||
rgba(0, 0, 0, 0.15) 46%,
|
||||
transparent 46%
|
||||
);
|
||||
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.5), inset 0 0 5px rgba(0, 0, 0, 0.8);
|
||||
transition: background 0.5s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow:
|
||||
inset 0 0 15px rgba(0, 0, 0, 0.5),
|
||||
inset 0 0 5px rgba(0, 0, 0, 0.8);
|
||||
transition:
|
||||
background 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized .vinyl-center {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: radial-gradient(circle at 30% 30%, #666 0%, #333 50%, #111 100%);
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.8), inset 1px 1px 2px rgba(255, 255, 255, 0.2);
|
||||
box-shadow:
|
||||
0 0 3px rgba(0, 0, 0, 0.8),
|
||||
inset 1px 1px 2px rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid #000;
|
||||
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background 0.5s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transition:
|
||||
width 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@@ -367,8 +422,11 @@
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background: var(--secondary-bg);
|
||||
box-shadow: inset 2px 2px 4px var(--shadow-dark), inset -2px -2px 4px var(--shadow-light),
|
||||
2px 2px 6px rgba(0, 0, 0, 0.2), -1px -1px 3px rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
inset 2px 2px 4px var(--shadow-dark),
|
||||
inset -2px -2px 4px var(--shadow-light),
|
||||
2px 2px 6px rgba(0, 0, 0, 0.2),
|
||||
-1px -1px 3px rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||
@@ -437,7 +495,9 @@
|
||||
rgba(0, 0, 0, 0.2) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.3), inset 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
inset 0 0 10px rgba(0, 0, 0, 0.3),
|
||||
inset 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vinyl-center {
|
||||
@@ -449,7 +509,10 @@
|
||||
background: radial-gradient(circle at center, #1a1a1a 0%, #333 50%, #1a1a1a 100%);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5), inset 0 0 3px rgba(0, 0, 0, 0.8), inset 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 0 4px rgba(0, 0, 0, 0.5),
|
||||
inset 0 0 3px rgba(0, 0, 0, 0.8),
|
||||
inset 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@@ -536,7 +599,9 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
transition:
|
||||
opacity 0.2s ease-in-out,
|
||||
transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.lyric-line.current {
|
||||
@@ -602,8 +667,18 @@
|
||||
}
|
||||
|
||||
.lyric-line.word-lyric-active {
|
||||
-webkit-mask: linear-gradient(90deg, #000 0%, #000 var(--line-progress, 0%), transparent var(--line-progress, 0%));
|
||||
mask: linear-gradient(90deg, #000 0%, #000 var(--line-progress, 0%), transparent var(--line-progress, 0%));
|
||||
-webkit-mask: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
#000 var(--line-progress, 0%),
|
||||
transparent var(--line-progress, 0%)
|
||||
);
|
||||
mask: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
#000 var(--line-progress, 0%),
|
||||
transparent var(--line-progress, 0%)
|
||||
);
|
||||
}
|
||||
|
||||
.netease-mini-player[data-theme="dark"] .lyric-line.word-lyric-active {
|
||||
@@ -640,14 +715,18 @@
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
box-shadow: 2px 2px 4px var(--shadow-dark), -2px -2px 4px var(--shadow-light);
|
||||
box-shadow:
|
||||
2px 2px 4px var(--shadow-dark),
|
||||
-2px -2px 4px var(--shadow-light);
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light);
|
||||
box-shadow:
|
||||
inset 1px 1px 2px var(--shadow-dark),
|
||||
inset -1px -1px 2px var(--shadow-light);
|
||||
background: var(--secondary-bg);
|
||||
}
|
||||
|
||||
@@ -656,7 +735,9 @@
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
background: var(--primary-bg);
|
||||
box-shadow: 2px 2px 4px var(--shadow-dark), -2px -2px 4px var(--shadow-light);
|
||||
box-shadow:
|
||||
2px 2px 4px var(--shadow-dark),
|
||||
-2px -2px 4px var(--shadow-light);
|
||||
}
|
||||
|
||||
.play-icon,
|
||||
@@ -671,11 +752,15 @@
|
||||
|
||||
.play-btn:hover {
|
||||
background: var(--secondary-bg);
|
||||
box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light);
|
||||
box-shadow:
|
||||
inset 1px 1px 2px var(--shadow-dark),
|
||||
inset -1px -1px 2px var(--shadow-light);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
box-shadow: inset 2px 2px 4px var(--shadow-dark), inset -2px -2px 4px var(--shadow-light);
|
||||
box-shadow:
|
||||
inset 2px 2px 4px var(--shadow-dark),
|
||||
inset -2px -2px 4px var(--shadow-light);
|
||||
background: var(--accent-gradient);
|
||||
color: white;
|
||||
}
|
||||
@@ -731,7 +816,9 @@
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light);
|
||||
box-shadow:
|
||||
inset 1px 1px 2px var(--shadow-dark),
|
||||
inset -1px -1px 2px var(--shadow-light);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
@@ -753,7 +840,10 @@
|
||||
background: var(--accent-color-3);
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
box-shadow: 0 0 8px rgba(0, 210, 255, 0.6), 1px 1px 2px var(--shadow-dark), -1px -1px 2px var(--shadow-light);
|
||||
box-shadow:
|
||||
0 0 8px rgba(0, 210, 255, 0.6),
|
||||
1px 1px 2px var(--shadow-dark),
|
||||
-1px -1px 2px var(--shadow-light);
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
@@ -785,7 +875,9 @@
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light);
|
||||
box-shadow:
|
||||
inset 1px 1px 2px var(--shadow-dark),
|
||||
inset -1px -1px 2px var(--shadow-light);
|
||||
}
|
||||
|
||||
.volume-bar {
|
||||
@@ -943,7 +1035,9 @@
|
||||
top: 100%;
|
||||
background: var(--primary-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 4px 4px 8px var(--shadow-dark), -4px -4px 8px var(--shadow-light);
|
||||
box-shadow:
|
||||
4px 4px 8px var(--shadow-dark),
|
||||
-4px -4px 8px var(--shadow-light);
|
||||
max-height: 200px;
|
||||
overflow: hidden;
|
||||
z-index: 1001;
|
||||
@@ -960,14 +1054,25 @@
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(circle at 20% 15%, var(--flow-color-1) 0%, transparent 60%),
|
||||
background:
|
||||
radial-gradient(circle at 20% 15%, var(--flow-color-1) 0%, transparent 60%),
|
||||
radial-gradient(circle at 75% 30%, var(--flow-color-2) 0%, transparent 55%),
|
||||
radial-gradient(circle at 35% 85%, var(--flow-color-3) 0%, transparent 55%),
|
||||
radial-gradient(circle at 25% 70%, var(--flow-color-4) 0%, transparent 50%),
|
||||
radial-gradient(circle at 85% 75%, var(--flow-color-5) 0%, transparent 50%);
|
||||
opacity: var(--playlist-flow-opacity);
|
||||
background-size: 220% 220%, 220% 220%, 220% 220%, 220% 220%, 220% 220%;
|
||||
background-position: 0% 0%, 100% 0%, 0% 100%, 20% 80%, 80% 20%;
|
||||
background-size:
|
||||
220% 220%,
|
||||
220% 220%,
|
||||
220% 220%,
|
||||
220% 220%,
|
||||
220% 220%;
|
||||
background-position:
|
||||
0% 0%,
|
||||
100% 0%,
|
||||
0% 100%,
|
||||
20% 80%,
|
||||
80% 20%;
|
||||
filter: saturate(1.02) brightness(1.01);
|
||||
transform: scale(1);
|
||||
animation: flow-breathe var(--flow-speed) ease-in-out infinite;
|
||||
@@ -1021,7 +1126,9 @@
|
||||
|
||||
.playlist-item:hover {
|
||||
background: rgba(255, 107, 53, 0.08);
|
||||
box-shadow: inset 1px 1px 2px var(--shadow-dark), inset -1px -1px 2px var(--shadow-light);
|
||||
box-shadow:
|
||||
inset 1px 1px 2px var(--shadow-dark),
|
||||
inset -1px -1px 2px var(--shadow-light);
|
||||
transform: translateX(2px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -1193,8 +1300,8 @@
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized.fading-in {
|
||||
animation: player-fade-in var(--opacity-duration-up, 0.25s) var(--opacity-ease-in, cubic-bezier(0.4, 0, 0.2, 1))
|
||||
forwards;
|
||||
animation: player-fade-in var(--opacity-duration-up, 0.25s)
|
||||
var(--opacity-ease-in, cubic-bezier(0.4, 0, 0.2, 1)) forwards;
|
||||
}
|
||||
|
||||
@keyframes player-fade-out {
|
||||
@@ -1223,23 +1330,27 @@
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized.fading-out.docked-right {
|
||||
animation: player-fade-out var(--opacity-duration-down, 0.6s)
|
||||
animation:
|
||||
player-fade-out var(--opacity-duration-down, 0.6s)
|
||||
var(--opacity-ease-out, cubic-bezier(0.22, 1, 0.36, 1)) forwards,
|
||||
player-dock-right var(--dock-duration, 0.45s) var(--dock-ease-out, cubic-bezier(0.18, 0.9, 0.2, 1)) forwards;
|
||||
player-dock-right var(--dock-duration, 0.45s)
|
||||
var(--dock-ease-out, cubic-bezier(0.18, 0.9, 0.2, 1)) forwards;
|
||||
}
|
||||
.netease-mini-player.minimized.fading-out.docked-left {
|
||||
animation: player-fade-out var(--opacity-duration-down, 0.6s)
|
||||
animation:
|
||||
player-fade-out var(--opacity-duration-down, 0.6s)
|
||||
var(--opacity-ease-out, cubic-bezier(0.22, 1, 0.36, 1)) forwards,
|
||||
player-dock-left var(--dock-duration, 0.45s) var(--dock-ease-out, cubic-bezier(0.18, 0.9, 0.2, 1)) forwards;
|
||||
player-dock-left var(--dock-duration, 0.45s)
|
||||
var(--dock-ease-out, cubic-bezier(0.18, 0.9, 0.2, 1)) forwards;
|
||||
}
|
||||
|
||||
.netease-mini-player.minimized.popping-right {
|
||||
animation: player-popout-right var(--popout-duration, 0.28s) var(--popout-ease, cubic-bezier(0.4, 0, 0.2, 1))
|
||||
forwards;
|
||||
animation: player-popout-right var(--popout-duration, 0.28s)
|
||||
var(--popout-ease, cubic-bezier(0.4, 0, 0.2, 1)) forwards;
|
||||
}
|
||||
.netease-mini-player.minimized.popping-left {
|
||||
animation: player-popout-left var(--popout-duration, 0.28s) var(--popout-ease, cubic-bezier(0.4, 0, 0.2, 1))
|
||||
forwards;
|
||||
animation: player-popout-left var(--popout-duration, 0.28s)
|
||||
var(--popout-ease, cubic-bezier(0.4, 0, 0.2, 1)) forwards;
|
||||
}
|
||||
|
||||
@keyframes player-dock-right {
|
||||
|
||||
@@ -318,7 +318,10 @@ class NeteaseMiniPlayer {
|
||||
this.elements.minimizeBtn.addEventListener("click", () => this.toggleMinimize());
|
||||
}
|
||||
document.addEventListener("click", (e) => {
|
||||
if (this.elements.playlistContainer && this.elements.playlistContainer.classList.contains("show")) {
|
||||
if (
|
||||
this.elements.playlistContainer &&
|
||||
this.elements.playlistContainer.classList.contains("show")
|
||||
) {
|
||||
if (!this.element.contains(e.target)) {
|
||||
this.togglePlaylist(false);
|
||||
}
|
||||
@@ -463,14 +466,16 @@ class NeteaseMiniPlayer {
|
||||
const isiOS = isiPhone || isiPadUA || isIOSLikePad;
|
||||
const isAndroid = /android/.test(ua);
|
||||
const isHarmonyOS = /harmonyos/.test(uaRaw) || /huawei|honor/.test(ua);
|
||||
const isMobileToken = /mobile/.test(ua) || /sm-|mi |redmi|huawei|honor|oppo|vivo|oneplus/.test(ua);
|
||||
const isMobileToken =
|
||||
/mobile/.test(ua) || /sm-|mi |redmi|huawei|honor|oppo|vivo|oneplus/.test(ua);
|
||||
const isHarmonyDesktop = isHarmonyOS && !isMobileToken && !isAndroid && !isiOS;
|
||||
const isPWA =
|
||||
(typeof window !== "undefined" &&
|
||||
((window.matchMedia && window.matchMedia("(display-mode: standalone)").matches) ||
|
||||
nav.standalone === true)) ||
|
||||
false;
|
||||
const isMobile = isiOS || isAndroid || (isHarmonyOS && !isHarmonyDesktop) || isMobileToken || isInAppWebView;
|
||||
const isMobile =
|
||||
isiOS || isAndroid || (isHarmonyOS && !isHarmonyDesktop) || isMobileToken || isInAppWebView;
|
||||
const info = {
|
||||
isMobile,
|
||||
isiOS,
|
||||
@@ -497,7 +502,10 @@ class NeteaseMiniPlayer {
|
||||
if (shouldHideVolume) {
|
||||
this.elements.volumeContainer.classList.add("sr-visually-hidden");
|
||||
this.elements.volumeContainer.setAttribute("aria-hidden", "false");
|
||||
this.elements.volumeSlider?.setAttribute("aria-label", "音量控制(移动端隐藏,仅无障碍可见)");
|
||||
this.elements.volumeSlider?.setAttribute(
|
||||
"aria-label",
|
||||
"音量控制(移动端隐藏,仅无障碍可见)"
|
||||
);
|
||||
} else {
|
||||
this.elements.volumeContainer.classList.remove("sr-visually-hidden");
|
||||
this.elements.volumeContainer.removeAttribute("aria-hidden");
|
||||
@@ -765,7 +773,8 @@ class NeteaseMiniPlayer {
|
||||
tempElement.style.visibility = "hidden";
|
||||
tempElement.style.position = "absolute";
|
||||
tempElement.style.fontSize = "12px";
|
||||
tempElement.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||
tempElement.style.fontFamily =
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||
tempElement.textContent = artistText;
|
||||
document.body.appendChild(tempElement);
|
||||
const fullWidth = tempElement.offsetWidth;
|
||||
@@ -830,7 +839,9 @@ class NeteaseMiniPlayer {
|
||||
// 如果没有原始 URL,尝试从 API 获取
|
||||
if (!urlData) {
|
||||
const baseUrls = this.config.apiUrls;
|
||||
const apiUrls = baseUrls.map((baseUrl) => `${baseUrl}?server=netease&type=song&id=${songId}`);
|
||||
const apiUrls = baseUrls.map(
|
||||
(baseUrl) => `${baseUrl}?server=netease&type=song&id=${songId}`
|
||||
);
|
||||
|
||||
for (const url of apiUrls) {
|
||||
try {
|
||||
@@ -925,8 +936,10 @@ class NeteaseMiniPlayer {
|
||||
return;
|
||||
}
|
||||
// 处理 lrc 数据可能是字符串或对象的情况
|
||||
const lrcContent = typeof lyricData.lrc === "string" ? lyricData.lrc : lyricData.lrc?.lyric || "";
|
||||
const tlyricContent = typeof lyricData.tlyric === "string" ? lyricData.tlyric : lyricData.tlyric?.lyric || "";
|
||||
const lrcContent =
|
||||
typeof lyricData.lrc === "string" ? lyricData.lrc : lyricData.lrc?.lyric || "";
|
||||
const tlyricContent =
|
||||
typeof lyricData.tlyric === "string" ? lyricData.tlyric : lyricData.tlyric?.lyric || "";
|
||||
const lrcLines = lrcContent.split("\n");
|
||||
const tlyricLines = tlyricContent ? tlyricContent.split("\n") : [];
|
||||
const lrcMap = new Map();
|
||||
@@ -957,7 +970,9 @@ class NeteaseMiniPlayer {
|
||||
}
|
||||
}
|
||||
});
|
||||
const allTimes = Array.from(new Set([...lrcMap.keys(), ...tlyricMap.keys()])).sort((a, b) => a - b);
|
||||
const allTimes = Array.from(new Set([...lrcMap.keys(), ...tlyricMap.keys()])).sort(
|
||||
(a, b) => a - b
|
||||
);
|
||||
this.lyrics = allTimes.map((time) => ({
|
||||
time,
|
||||
text: lrcMap.get(time) || "",
|
||||
@@ -1018,7 +1033,9 @@ class NeteaseMiniPlayer {
|
||||
|
||||
let newIndex;
|
||||
if (this.playMode === "shuffle") {
|
||||
const availableIndices = this.playlist.map((_, i) => i).filter((i) => i !== this.currentIndex);
|
||||
const availableIndices = this.playlist
|
||||
.map((_, i) => i)
|
||||
.filter((i) => i !== this.currentIndex);
|
||||
|
||||
if (availableIndices.length === 0) {
|
||||
newIndex = this.currentIndex;
|
||||
@@ -1409,9 +1426,18 @@ class NeteaseMiniPlayer {
|
||||
const hex = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
|
||||
if (hex) {
|
||||
const hexValue = hex[1];
|
||||
const r = parseInt(hexValue.length === 3 ? hexValue[0] + hexValue[0] : hexValue.substr(0, 2), 16);
|
||||
const g = parseInt(hexValue.length === 3 ? hexValue[1] + hexValue[1] : hexValue.substr(2, 2), 16);
|
||||
const b = parseInt(hexValue.length === 3 ? hexValue[2] + hexValue[2] : hexValue.substr(4, 2), 16);
|
||||
const r = parseInt(
|
||||
hexValue.length === 3 ? hexValue[0] + hexValue[0] : hexValue.substr(0, 2),
|
||||
16
|
||||
);
|
||||
const g = parseInt(
|
||||
hexValue.length === 3 ? hexValue[1] + hexValue[1] : hexValue.substr(2, 2),
|
||||
16
|
||||
);
|
||||
const b = parseInt(
|
||||
hexValue.length === 3 ? hexValue[2] + hexValue[2] : hexValue.substr(4, 2),
|
||||
16
|
||||
);
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return brightness < 128;
|
||||
}
|
||||
@@ -1564,13 +1590,17 @@ class NMPv2ShortcodeParser {
|
||||
}
|
||||
|
||||
processExistingElements(container) {
|
||||
container.querySelectorAll(".netease-mini-player:not([data-shortcode-processed])").forEach((element) => {
|
||||
container
|
||||
.querySelectorAll(".netease-mini-player:not([data-shortcode-processed])")
|
||||
.forEach((element) => {
|
||||
element.setAttribute("data-shortcode-processed", "true");
|
||||
});
|
||||
}
|
||||
|
||||
initializePlayers(container) {
|
||||
container.querySelectorAll(".netease-mini-player:not([data-initialized])").forEach((element) => {
|
||||
container
|
||||
.querySelectorAll(".netease-mini-player:not([data-initialized])")
|
||||
.forEach((element) => {
|
||||
element.setAttribute("data-initialized", "true");
|
||||
NeteaseMiniPlayer.initPlayer(element);
|
||||
});
|
||||
|
||||
@@ -22,7 +22,8 @@ type SendMailPayload = {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const ensureValue = (value?: string, fallback = "未填写") => (value?.trim() ? value.trim() : fallback);
|
||||
const ensureValue = (value?: string, fallback = "未填写") =>
|
||||
value?.trim() ? value.trim() : fallback;
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const method = event.node.req.method;
|
||||
@@ -46,7 +47,15 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const config = useRuntimeConfig() as MailConfig;
|
||||
const { smtpHost, smtpPort: configSmtpPort, smtpUser, smtpPass, senderEmail, adminEmail, smtpSecure } = config;
|
||||
const {
|
||||
smtpHost,
|
||||
smtpPort: configSmtpPort,
|
||||
smtpUser,
|
||||
smtpPass,
|
||||
senderEmail,
|
||||
adminEmail,
|
||||
smtpSecure,
|
||||
} = config;
|
||||
|
||||
const smtpPort = Number(configSmtpPort ?? 465);
|
||||
if (!smtpHost || !smtpUser || !smtpPass || !senderEmail || !adminEmail) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: ["./app/**/*.{vue,js,ts}", "./app/components/**/*.vue", "./app/pages/**/*.vue", "./app/layouts/**/*.vue"],
|
||||
content: [
|
||||
"./app/**/*.{vue,js,ts}",
|
||||
"./app/components/**/*.vue",
|
||||
"./app/pages/**/*.vue",
|
||||
"./app/layouts/**/*.vue",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
@@ -15,7 +20,14 @@ export default {
|
||||
"text-muted": "rgb(104, 120, 152)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['"Inter"', "system-ui", "-apple-system", "BlinkMacSystemFont", '"Segoe UI"', "sans-serif"],
|
||||
sans: [
|
||||
'"Inter"',
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
'"Segoe UI"',
|
||||
"sans-serif",
|
||||
],
|
||||
},
|
||||
spacing: {
|
||||
"safe-x": "max(1rem, env(safe-area-inset-left))",
|
||||
|
||||
Reference in New Issue
Block a user