Initial commit

This commit is contained in:
Jeffrey Hsu 2024-11-17 11:45:08 +08:00
commit def2aee990
55 changed files with 12216 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

8
api/blog.ts Normal file
View File

@ -0,0 +1,8 @@
import {request} from "~/composables/network";
export async function getBlogRecentPost(): Promise<IBlogResponse<IPostsData>> {
return request({
url: "/blog/index.php/api/posts",
method: "get"
})
}

16
api/git.ts Normal file
View File

@ -0,0 +1,16 @@
import {request} from "~/composables/network";
const TOKEN: string = "fb8aec429ea7d0a36f7238dbffda9d2d66c7b045"
export async function getActivity(): Promise<IActivity[]> {
return request({
url: '/git/api/v1/users/cantyonion/activities/feeds',
method: 'get',
headers: {
Authorization: ` ${TOKEN}`,
},
params: {
limit: 10
}
})
}

23
app.vue Normal file
View File

@ -0,0 +1,23 @@
<script setup lang="ts">
const loading = ref<HTMLDivElement | null>(null)
emitter.on('startLoading', on => {
if (on) {
loading.value?.classList.remove('stop')
removeMobileTopColor()
} else {
loading.value?.classList.add('stop')
setMobileTopColor()
}
})
</script>
<template>
<div ref="loading" class="loading"></div>
<div class="relative flex min-h-screen flex-col overflow-hidden bg-blue-50">
<nav-bar/>
<NuxtPage/>
<footer-main/>
</div>
</template>

33
assets/css/main.scss Normal file
View File

@ -0,0 +1,33 @@
@import '@callmebill/lxgw-wenkai-web/style.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'BaconyScript';
src: url("assets/fonts/BaconyScript.otf") format("opentype");
}
body {
font-family: "LXGW WenKai", sans-serif;
}
pre, code {
font-family: "LXGW WenKai Mono", sans-serif;
}
.loading {
@apply fixed h-screen w-screen flex justify-center items-center z-[999] bg-blue-50 backdrop-blur-3xl bg-opacity-45 transition-all duration-500 opacity-100;
&::before {
content: 'CantyOni_on\'s Index';
font-family: 'BaconyScript', sans-serif;
@apply text-5xl lg:text-7xl text-gray-600;
}
}
.stop {
@apply invisible opacity-0;
}

Binary file not shown.

BIN
assets/images/common/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
assets/images/common/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/images/common/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/images/common/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
assets/images/common/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
assets/images/common/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
assets/images/common/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
assets/images/common/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,5 @@
<!-- 由 https://getwaves.io/ 生成 -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
<path fill="rgb(239 246 255)" fill-opacity="1"
d="M0,32L26.7,26.7C53.3,21,107,11,160,26.7C213.3,43,267,85,320,85.3C373.3,85,427,43,480,32C533.3,21,587,43,640,48C693.3,53,747,43,800,53.3C853.3,64,907,96,960,106.7C1013.3,117,1067,107,1120,117.3C1173.3,128,1227,160,1280,144C1333.3,128,1387,64,1413,32L1440,0L1440,0L1413.3,0C1386.7,0,1333,0,1280,0C1226.7,0,1173,0,1120,0C1066.7,0,1013,0,960,0C906.7,0,853,0,800,0C746.7,0,693,0,640,0C586.7,0,533,0,480,0C426.7,0,373,0,320,0C266.7,0,213,0,160,0C106.7,0,53,0,27,0L0,0Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
assets/images/logo/c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
assets/images/logo/w.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,30 @@
<script lang="ts" setup>
defineProps<{
icon: string
title: string
message: string
}>()
</script>
<template>
<div class="flex flex-grow flex-col items-center justify-center p-4 text-center min-h-[30rem]">
<div class="max-w-md w-full p-6 bg-white rounded-xl shadow-lg">
<div class="text-6xl mb-4">
<span v-if="icon" class="block text-blue-500">{{ icon }}</span>
<span v-else></span>
</div>
<h1 class="text-2xl font-bold mb-2">{{ title }}</h1>
<p class="text-gray-600 mb-6">{{ message }}</p>
<router-link
to="/"
class="inline-block px-6 py-2 bg-blue-500 text-white rounded-lg shadow hover:bg-blue-600 transition duration-200"
>
返回首页
</router-link>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,51 @@
<script lang="ts" setup>
import {computed} from "vue";
const props = defineProps<{
post: IPost
}>()
const thumbUrl = computed(() => {
//
props.post.fields.thumb.value = props.post.fields.thumb.value ? props.post.fields.thumb.value : getAssetURL(
`/common/${Math.floor(Math.random() * (7 - 1 + 1)) + 1}.jpg`
)
return props.post.fields.thumb.value
})
const handleOnCardClick = () => {
window.open(props.post.url, '_self')
}
</script>
<template>
<div :title="post.title"
class="container relative h-56 overflow-hidden rounded-2xl text-white hover:cursor-pointer hover:text-amber-200 hover:shadow-lg"
@click="handleOnCardClick">
<!-- 图片 -->
<div class="container h-full">
<img :src="thumbUrl" alt=""
class="h-full w-full object-cover object-center transition-all duration-300 hover:scale-110">
</div>
<!-- 文字部分 -->
<div class="container relative">
<div class="absolute bottom-0 h-12 w-full truncate bg-black bg-opacity-60 p-2 leading-8 backdrop-blur-3xl">
<span class="px-1 text-xl font-bold transition-all">{{ post.title }}</span>
</div>
</div>
<!-- 分类 -->
<div class="absolute top-0 left-0 m-2 rounded-lg bg-black bg-opacity-60 px-2 text-white backdrop-blur-3xl">
<span>{{ post.category }}</span>
</div>
<!-- 日期 -->
<div class="absolute right-0 bottom-12 m-2 rounded-lg bg-black bg-opacity-60 px-2 text-white backdrop-blur-3xl">
<font-awesome-icon :icon="['far', 'clock']" class="mr-1 text-sm"/>
<span>{{ post.date.year }}-{{ post.date.month }}-{{ post.date.day }}</span>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,86 @@
<script lang="ts" setup>
import {computed, onMounted, onUnmounted, ref} from "vue";
const currentPost = ref<IPost | null>(null)
const onOpenPost = (e: IPost) => {
currentPost.value = e;
}
const onClosePost = () => {
currentPost.value = null
}
const thumbUrl = computed(() => {
if (currentPost.value && currentPost.value.fields.thumb.value) {
return currentPost.value.fields.thumb.value
}
return getAssetURL(`${Math.floor(Math.random() * (7 - 1 + 1)) + 1}.jpg`)
})
onMounted(() => {
emitter.on('openPost', onOpenPost)
})
onUnmounted(() => {
emitter.off('openPost', onOpenPost)
})
</script>
<template>
<div v-if="currentPost" class="fixed top-0 right-0 bottom-0 left-0 z-20 bg-black bg-opacity-60">
<div class="container mx-auto my-10 max-h-full overflow-hidden overscroll-y-contain rounded-3xl bg-white">
<!-- 标题栏 -->
<div class="relative w-full border-b-2 border-b-gray-200">
<!-- 文章头图 -->
<div class="min-h-52">
<img :src="thumbUrl" alt="" class="max-h-64 w-full object-cover object-center"/>
</div>
<!-- 预览标题 -->
<div
class="absolute top-0 right-0 left-0 h-12 truncate bg-black bg-opacity-60 pr-16 pl-8 text-white backdrop-blur-3xl py-1.5">
<span :title="currentPost.title" class="text-3xl font-bold">
文章预览 /
</span>
</div>
<!-- 关闭按钮 -->
<div
class="absolute top-0 right-2 flex h-9 w-9 cursor-pointer items-center justify-center rounded-full text-white transition-all my-1.5 hover:bg-gray-200 hover:text-black"
@click="onClosePost"
>
<font-awesome-icon :icon="['fas', 'xmark']" class="p-2"/>
</div>
</div>
<!-- 正文 -->
<div class="container overflow-y-auto p-8 h-[60vh] preview" v-html="currentPost.digest"></div>
</div>
</div>
</template>
<style lang="scss">
.preview {
h1 {
@apply mb-2 border-l-8 border-l-blue-400 text-3xl font-bold;
}
h2 {
@apply mb-2 border-l-4 border-l-blue-400 text-2xl font-bold;
}
h3 {
@apply mb-2 border-l-2 border-l-blue-400 text-xl font-bold;
}
h4 {
@apply text-lg font-bold;
}
h5 {
@apply text-sm font-bold;
}
ul {
@apply line-clamp-1;
}
}
</style>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
defineProps<{
title: string
mainColor: string
subColor: string
url: string
icon: string[]
}>()
</script>
<template>
<div class="mb-4 flex justify-between text-2xl font-bold text-white">
<div :class="[mainColor]" class="flex w-max items-center justify-center overflow-hidden rounded-2xl">
<div :class="[subColor]" class="flex h-full w-14 items-center justify-center rounded-2xl px-4 py-2 text-center">
<font-awesome-icon :icon="icon"/>
</div>
<span class="px-4">{{ title }}</span>
</div>
<a :class="[mainColor]" :href="url" class="rounded-2xl px-4 py-2">
<font-awesome-icon :icon="['fas', 'chevron-right']"/>
</a>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,105 @@
<script lang="ts" setup>
import {computed, ref} from "vue";
// 使 ref x y
const mouseX = ref(0)
const mouseY = ref(0)
const typedString = ['Hello, Welcome to this site!^1000', '欢迎访问本网站!^1000']
const littleWidget = [
{
icon: ['fab', 'qq'],
url: "https://qm.qq.com/q/9fhtO8JJt0",
title: "QQ",
},
{
icon: ['fab', 'github-alt'],
url: "https://github.com/Aurora1949",
title: "Github",
},
{
icon: ['fab', 'weibo'],
url: "https://weibo.com/u/7923648952",
title: "微博"
},
{
icon: ['fab', 'steam-symbol'],
url: "https://steamcommunity.com/id/cantyonion/",
title: "Steam"
}
]
// const characterUrl = "~/assets/images/common/hoshino.png"
//
const handleMouseMove = (event: MouseEvent) => {
//
mouseX.value = -(event.clientX / window.innerWidth - 0.5) * 100
mouseY.value = -(event.clientY / window.innerHeight - 0.5) * 100
}
const debouncedMouseMove = debounce(handleMouseMove, 5)
//
const backgroundStyle = computed(() => ({
backgroundPosition: `${mouseX.value}px ${mouseY.value}px`
}))
</script>
<template>
<div :style="backgroundStyle"
class="relative flex h-screen w-full items-center justify-center bg-blue-400 bg-fixed bg min-h-[40rem]"
@mousemove="debouncedMouseMove"
>
<div class="relative mx-4 w-full rounded-xl bg-white p-8 transition-transform duration-300 min-h-96
md:mx-0 md:max-w-screen-md"
>
<p class="text-gray-500">你好欢迎来到</p>
<div class="relative mt-4 h-72 text-5xl text-blue-400 transition-all md:text-6xl lg:text-7xl">
<h1 style="font-family: BaconyScript,sans-serif">CantyOni_on's</h1>
<h1 class="font-bold">超级基地</h1>
<div class="absolute -z-10 text-blue-100 top-[3px] left-[3px]">
<h1 class="" style="font-family: BaconyScript,sans-serif">CantyOni_on's</h1>
<h1 class="font-bold">超级基地</h1>
</div>
</div>
<!-- TypedJS -->
<div class="flex flex-row text-gray-500">
<vue-typed :auto-insert-css="true" :backSpeed="30"
:loop="true"
:showCursor="true"
:strings="typedString"
:typeSpeed="50"
/>
</div>
<!-- 社交媒体组件 -->
<div class="flex flex-row text-gray-500">
<widget-social-media-widget v-for="item in littleWidget"
:key="item.url"
:icon="item.icon"
:title="item.title"
:url="item.url"
class="h-10 w-10"
/>
</div>
<!-- 人物背景 -->
<div class="invisible absolute top-0 -right-6 -bottom-6 opacity-0 transition-all
sm:visible sm:opacity-100 lg:-top-28 lg:-right-28 lg:-bottom-10"
>
<img src="~/assets/images/common/hoshino.png" alt="" class="float-right h-full" draggable="false"/>
</div>
</div>
<!-- 下箭头 -->
<font-awesome-icon :icon="['fas', 'chevron-down']" beat class="absolute bottom-12 h-auto w-6 text-white"/>
</div>
</template>
<style scoped>
.bg {
background-image: url("assets/images/common/bg.png");
background-size: 150px 150px;
}
</style>

View File

@ -0,0 +1,85 @@
<script lang="ts" setup>
import {computed} from "vue";
const props = defineProps<{
commit: IActivity<IContent>
}>()
const icon = computed(() => {
switch (props.commit.op_type) {
case "merge_pull_request":
return ['fas', 'code-merge']
case "create_repo":
return ['fab', 'git-alt']
case "commit_repo":
default:
return ['fas', 'code-commit']
}
})
const time = computed(() => timeDifference(props.commit.created))
const timeDifference = (dateString: string): string => {
const targetDate = new Date(dateString);
const currentDate = new Date();
const diffInMillis = currentDate.getTime() - targetDate.getTime();
const minutes = Math.floor(diffInMillis / (1000 * 60));
const hours = Math.floor(diffInMillis / (1000 * 60 * 60));
const days = Math.floor(diffInMillis / (1000 * 60 * 60 * 24));
const months = Math.floor(diffInMillis / (1000 * 60 * 60 * 24 * 30));
const years = Math.floor(diffInMillis / (1000 * 60 * 60 * 24 * 365));
if (minutes < 60) {
return `${minutes} 分钟前`;
} else if (hours < 24) {
return `${hours} 小时前`;
} else if (days < 30) {
return `${days} 天前`;
} else if (months < 12) {
return `${months} 个月前`;
} else {
return `${years} 年前`;
}
}
const handleClick = () => {
window.open(props.commit.repo.html_url, '_self')
}
</script>
<template>
<div class="group container relative flex h-36 w-full overflow-hidden rounded-2xl transition-all
hover:cursor-pointer hover:shadow-md"
@click="handleClick"
>
<!-- 提交图标 -->
<div class="before:absolute relative before:top-0 before:right-0 before:bottom-0 flex h-36 before:w-2 w-36
items-center justify-center bg-pink-300 before:bg-pink-300 text-5xl text-white before:opacity-0
before:blur before:transition-all group-hover:before:opacity-100"
>
<font-awesome-icon :icon="icon" class="transition-all group-hover:scale-110"/>
</div>
<!-- 内容 -->
<div class="flex-1 overflow-hidden bg-white p-4">
<div class="mr-12 truncate text-2xl font-bold">
仓库{{ commit.repo.name }}
</div>
<span class="text-xl line-clamp-2">{{ commit.content.HeadCommit.Message }}</span>
<span class="absolute top-0 right-0 m-4 hidden rounded bg-gray-300 px-2 sm:block">
{{ commit.content.HeadCommit.Sha1.slice(0, 10) }}
</span>
<span class="absolute right-0 bottom-0 m-4 rounded bg-gray-300 px-2">
<font-awesome-icon :icon="['far', 'clock']" class="mr-1 text-sm"/>
{{ time }}
</span>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,60 @@
<script lang="ts" setup>
type Widget = {
icon: string[]
url: string
title: string
}
// const welcomeWords = ['👋']
const littleWidget: Widget[] = [
{
icon: ['fab', 'qq'],
url: "tencent://message/?uin=1922471905&Site=&Menu=yes",
title: "QQ",
},
{
icon: ['fab', 'github-alt'],
url: "https://github.com/Aurora1949",
title: "Github",
},
{
icon: ['fab', 'weibo'],
url: "https://weibo.com/u/7923648952",
title: "微博"
},
{
icon: ['fab', 'steam-symbol'],
url: "https://steamcommunity.com/id/cantyonion/",
title: "Steam"
}
]
</script>
<template>
<div class="container my-16 flex w-full">
<!-- 头像部分 -->
<div class="hidden h-32 w-32 overflow-hidden rounded-full bg-white hover:animate-pulse sm:block">
<img alt="" src="https://q.qlogo.cn/g?b=qq&nk=1922471905&s=640">
</div>
<!-- 文字部分 -->
<div class="container flex flex-1 flex-col justify-between px-4 sm:px-8">
<div class="text-4xl">Jeffrey Hsu</div>
<div class="text-2xl text-gray-400">你好👋很高兴认识你</div>
<!-- <vue-typed-js :strings="['First text', 'Second Text']">-->
<!-- <h1 class="typing"></h1>-->
<!-- </vue-typed-js>-->
<div class="flex gap-4 text-xl">
<a v-for="item in littleWidget"
:key="item.url" :href=item.url :title=item.title
class="flex h-10 w-10 items-center justify-center rounded-full bg-green-400 text-white transition-all hover:scale-125">
<font-awesome-icon :icon="item.icon"/>
</a>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
defineProps<{
loading: boolean
error: boolean
empty: boolean
}>()
const emit = defineEmits(['reload'])
const handleReload = () => {
emit('reload')
}
</script>
<template>
<div :class="{'justify-center': (empty || error)}" class="container flex">
<!-- 加载骨架 -->
<div v-if="loading" class="h-56 w-full text-2xl font-bold">
<slot name="skeleton">
<div class="grid w-full grid-cols-1 gap-4 overflow-hidden sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div v-for="index of 3" :key="index" class="h-56 animate-pulse rounded-2xl bg-gray-200"></div>
</div>
</slot>
</div>
<div v-else class="w-full">
<!-- 错误处理 -->
<div v-if="error" class="flex h-56 flex-col items-center justify-center" @click="handleReload">
<span class="mb-4 text-8xl"></span>
<span class="text-center text-2xl font-bold">载入错误</span>
</div>
<div v-else>
<!-- 正式内容 -->
<span v-if="empty" class="text-2xl font-bold">最近没有动态</span>
<slot v-else name="default"/>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
</script>
<template>
<footer class="mt-8 items-end pt-48 pb-8 min-h-48 bg">
<div class="container mx-auto px-4 md:px-0 xl:max-w-screen-xl">
<div class="grid-cols-4 gird min-h-48">
<div>
<div class="flex items-center gap-4">
<img alt="" class="h-20 w-20" src="~/assets/images/logo/c.png">
<div class="text-4xl leading-7 text-gray-500" style="font-family: BaconyScript, sans-serif">
<p>CantyOni_on's</p>
<p>Index</p>
</div>
</div>
<p class="mt-20 leading-8 text-gray-500">&copy;2024 All rights reserved.</p>
</div>
</div>
<div class="flex flex-col text-gray-500 md:flex-row">
<a class="mr-4 hover:text-black" href="https://beian.miit.gov.cn/"
target="_blank">苏ICP备2022016243号-1</a>
<div>
<img alt="" class="inline-block w-4 pb-1" src="~/assets/images/common/beian.png"/>
<a class="hover:text-black" href="https://beian.mps.gov.cn/#/query/webSearch?code=32050602011641/"
target="_blank">苏公网安备32050602011641</a>
</div>
</div>
</div>
</footer>
</template>
<style scoped lang="scss">
.bg {
background: url("assets/images/common/footer-bg.svg");
background-size: 100% auto;
background-color: white;
background-repeat: no-repeat;
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="loader">
<svg
class="icon"
height="200"
p-id="1166"
t="1676267704832"
version="1.1"
viewBox="0 0 1024 1024"
width="200"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M787.009641 293.979897c21.792821-0.183795 51.922051-0.459487 53.39241-0.722051a12.826256 12.826256 0 0 0 6.642872-3.610256l58.092308-57.737846a12.589949 12.589949 0 0 0 3.662769-6.603488c0.393846-2.376205-0.131282-4.424205-1.496615-5.802666l-67.872821-67.413334c-1.352205-1.378462-3.452718-1.890462-5.77641-1.522871a12.616205 12.616205 0 0 0-6.695385 3.610256l-58.092307 57.737846a12.826256 12.826256 0 0 0-3.675898 6.669128c-0.236308 1.575385 4.030359 35.997538 3.977846 57.803488-54.350769-40.539897-114.346667-68.096-184.595692-78.336V116.36841h25.915077c2.691282 0 10.660103-3.990974 12.524308-5.356307 2.021744-1.378462 8.388923-6.445949 8.388923-8.375795V7.286154c0-1.929846-6.419692-3.807179-8.310154-5.172513-1.929846-1.352205-9.885538-2.113641-12.603077-2.113641H419.052308c-2.743795 0-11.106462 0.774564-13.049436 2.139897-1.890462 1.352205-8.756513 3.21641-8.756513 5.146257v95.310769c0 1.969231 6.892308 7.036718 8.795897 8.402051 1.929846 1.339077 10.266256 5.369436 13.036308 5.369436h24.996103v81.657436C256.761436 227.511795 91.897436 400.108308 91.897436 608.518564 91.897436 837.618872 281.888821 1024 512.380718 1024 742.859487 1024 931.577436 837.632 931.577436 608.518564c-0.026256-125.702564-55.492923-238.316308-144.567795-314.538667z m-67.413333 146.116924L530.825846 627.698872a25.665641 25.665641 0 0 1-35.551179-0.590769 25.337436 25.337436 0 0 1-0.577641-35.341129l188.783589-187.602051a25.521231 25.521231 0 0 1 18.064411-7.443692 25.665641 25.665641 0 0 1 18.051282 7.443692 25.403077 25.403077 0 0 1 0 35.905641z"
fill="#ffffff"
p-id="1167"
></path>
</svg>
</div>
</template>
<script lang="ts" setup>
import {watch} from "vue";
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
});
watch(
() => props.loading,
(v) => {
const el: HTMLElement = document.querySelector("body") as HTMLElement;
if (v) {
el.classList.add("loading");
} else {
el.classList.remove("loading");
}
}
);
</script>
<style scoped>
.loader::before {
top: 50%;
left: 50%;
z-index: 2;
content: "";
width: 100vmax;
height: 100vmax;
position: fixed;
border-radius: 66%;
background: var(--lime-soap);
transition: transform 0.5s cubic-bezier(0, 0, 0.5, 1.25);
transform: translate(-50%, -50%) scale(0);
}
.loader svg {
top: 50%;
left: 50%;
opacity: 0;
z-index: 3;
height: 8em;
color: white;
position: fixed;
visibility: hidden;
transform: translate(-50%, -50%) scale(0);
transition: opacity 0.3s, transform 0.5s cubic-bezier(0.5, 0, 0.5, 1.5),
visibility 0.3s;
}
</style>

162
components/nav/NavBar.vue Normal file
View File

@ -0,0 +1,162 @@
<script lang="ts" setup>
import {computed, onMounted, onUnmounted, ref} from "vue";
type Widget = {
icon: string[]
url: string
title: string
}
// 使 ref
const scrolled = ref(false)
const littleWidget: Widget[] = [
{
icon: ['fas', 'gauge'],
url: "/netdata/",
title: "Pi Dashboard",
},
{
icon: ['fas', 'cloud'],
url: "/nas/",
title: "Nextcloud",
},
{
icon: ['fas', 'code-branch'],
url: "/git/",
title: "Git Repository"
}
]
//const logoUrl = getAssetURL("w.png")
const logoType: string = "logo";
const showLogo = computed(() => logoType === 'text')
const isExpended = ref<boolean>(false)
const route = useRoute()
const entry = [
{
title: '主页',
icon: ['fas', 'home'],
entry: [],
to: 'home'
},
{
title: '文章',
icon: ['fas', 'pen'],
entry: [
{
title: '博客',
url: false,
to: 'blog'
},
{
title: '日记',
url: false,
to: 'diary'
},
],
},
{
title: '作品',
icon: ['fas', 'brush'],
entry: [
{
title: '软件',
url: false,
to: 'soft'
},
{
title: '仓库',
url: true,
to: 'https://cantyonion.site/git/'
},
],
},
{
title: '杂谈',
icon: ['fas', 'chess-rook'],
entry: [],
to: 'talk'
}
]
const menuExpanded = () => {
isExpended.value = !isExpended.value
}
const menuClose = () => {
isExpended.value = false
}
// scroll
const handleScroll = () => {
scrolled.value = window.scrollY > 0
}
const debouncedScroll = debounce(handleScroll, 10)
const alwaysBlueBackground = computed(() => route.name !== 'index')
//
onMounted(() => {
window.addEventListener('scroll', debouncedScroll)
})
onUnmounted(() => {
window.removeEventListener('scroll', debouncedScroll)
})
</script>
<template>
<nav :class="{'bg-transparent': !scrolled && !alwaysBlueBackground,
'bg-blue-400': scrolled || alwaysBlueBackground,
'shadow': scrolled}"
class="fixed top-0 z-20 h-16 w-full text-white transition-all duration-300"
>
<div class="container mx-auto flex h-16 w-full items-center justify-between px-4 md:px-0 xl:max-w-screen-xl">
<!-- 下拉菜单按钮 -->
<div class="flex h-8 w-10 items-center justify-center rounded-md border-gray-600 md:hidden"
@click="menuExpanded"
>
<font-awesome-icon :icon="['fas', 'bars']" class="h-6"/>
</div>
<!-- Logo -->
<div class="absolute left-1/2 -translate-x-1/2 md:relative md:left-0 md:translate-x-0">
<!-- 文字模式 -->
<div v-if="showLogo" class="h-full text-4xl min-w-16 logo py-3.5">
<span>CantyOni_on's Index</span>
</div>
<!-- 图片模式 -->
<div v-else class="py-2">
<img src="~/assets/images/logo/w.png" alt="" class="h-12 w-12"/>
</div>
</div>
<!-- Entry -->
<div :class="{'h-screen opacity-100 visible': isExpended, 'h-0 opacity-0 invisible': !isExpended}"
class="absolute top-full right-0 left-0 bg-white transition-all md:visible md:relative md:top-0 md:flex
md:h-auto md:bg-transparent md:opacity-100"
>
<nav-bar-entry-item v-for="item in entry" :key="item.title"
:entry="item.entry" :icon="item.icon"
:title="item.title" :to="item.to"
@on-go="menuClose"
/>
</div>
<!-- 快速跳转小组件 -->
<div class="flex items-center justify-between gap-4 text-xl">
<a v-for="item in littleWidget"
:key="item.url" :href=item.url :title=item.title
class="transition-all hover:scale-125">
<font-awesome-icon :icon="item.icon"/>
</a>
</div>
</div>
</nav>
</template>
<style scoped>
.logo {
font-family: BaconyScript, serif;
}
</style>

View File

@ -0,0 +1,69 @@
<script lang="ts" setup>
const router = useRouter()
type Entry = {
title: string,
url: boolean,
to: string,
}
const emit = defineEmits<{
(e: 'onGo'): void
}>();
defineProps<{
title: string
icon: string[]
to?: string
entry: Entry[]
}>()
const onClick = (to: string | undefined, url: boolean) => {
if (to === undefined)
return
if (url)
window.open(to)
emit('onGo')
router.push({name: to})
}
</script>
<template>
<div class="container relative w-full max-w-max flex-auto shrink-0 cursor-pointer group md:w-16 md:hover:w-full"
@click="onClick(to, false)"
>
<div class="my-1 flex h-14 w-screen items-center rounded-md pr-4 pl-2 text-gray-500
md:w-auto md:text-white md:group-hover:bg-gray-100 md:group-hover:bg-opacity-30"
>
<font-awesome-icon :icon="icon" class="mx-2 h-5 w-5 md:h-8 md:w-8"/>
<span class="w-full max-w-max overflow-hidden text-lg transition-all duration-300 text-nowrap font-bold
md:font-normal md:w-0 md:text-xl md:group-hover:w-full"
>
{{ title }}
</span>
</div>
<!-- 下拉菜单 -->
<div v-if="entry"
class="grid grid-cols-2 overflow-hidden transition-all duration-300 min-w-32 item md:invisible md:absolute
md:top-full md:left-1/2 md:block md:rounded-xl md:bg-white md:opacity-0 md:shadow-md
md:group-hover:visible md:group-hover:-translate-x-1/2 md:group-hover:opacity-100"
>
<div v-for="item in entry" :key="item.title"
class="px-11 py-2 text-gray-500 text-md md:px-0 md:text-center md:text-lg md:hover:bg-gray-100"
@click="onClick(item.to, item.url)"
>
{{ item.title }}
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.item {
@media (min-width: 768px) {
transform: perspective(500px) rotateX(-45deg) translateX(-50%);
}
}
</style>

View File

@ -0,0 +1,13 @@
<script lang="ts" setup>
</script>
<template>
<a class="flex items-center justify-center" href="https://www.upyun.com/?utm_source=lianmeng&utm_medium=referral">
本网站由<img :src="getAssetURL('upyun.png')" alt="又拍云" class="h-8 w-auto">提供CDN加速/云储存服务
</a>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
defineProps<{
icon: string[]
url: string
title: string
}>()
</script>
<template>
<div class="flex gap-4 text-xl">
<a :key="url" :href=url :title=title
class="flex items-center justify-center transition-all hover:scale-125">
<font-awesome-icon :icon="icon"/>
</a>
</div>
</template>
<style scoped>
</style>

9
composables/emitter.ts Normal file
View File

@ -0,0 +1,9 @@
import mitt from 'mitt'
type Events = {
openPost: IPost
startLoading: boolean
}
const emitter = mitt<Events>()
export default emitter

34
composables/function.ts Normal file
View File

@ -0,0 +1,34 @@
export const getAssetURL = (image: string) => {
// 参数一: 相对路径
// 参数二: 当前路径的URL
return new URL(`~/assets/images/${image}`, import.meta.url).href
}
export const debounce = (fn: Function, delay: number) => {
let timeoutId: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
fn(...args)
}, delay)
}
}
export const setMobileTopColor = () => {
const lightMeta = document.createElement('meta');
lightMeta.setAttribute('name', 'theme-color');
lightMeta.setAttribute('media', '(prefers-color-scheme: light)');
lightMeta.setAttribute('content', '#60a5fa');
const darkMeta = document.createElement('meta');
darkMeta.setAttribute('name', 'theme-color');
darkMeta.setAttribute('media', '(prefers-color-scheme: dark)');
darkMeta.setAttribute('content', '#60a5fa');
document.head.appendChild(lightMeta);
document.head.appendChild(darkMeta);
}
export const removeMobileTopColor = () => {
document.querySelectorAll('meta[name="theme-color"]').forEach(meta => meta.remove());
}

24
composables/network.ts Normal file
View File

@ -0,0 +1,24 @@
import axios from 'axios'
import type {AxiosRequestConfig, AxiosError} from 'axios'
export async function request<T = unknown>(config: AxiosRequestConfig<any>): Promise<T> {
const instance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 5000,
headers: {},
})
instance.interceptors.request.use(config => {
return config
}, error => error)
// bug fixed on csdn https://blog.csdn.net/qq_45325810/article/details/120704910
instance.interceptors.response.use(resource => {
if (resource.status === 200) return resource
return Promise.reject(new Error(resource.data))
}, (error: AxiosError) => {
return Promise.reject(error.response ? error.response.data : error.code)
})
return instance.request<T>(config).then(res => res.data)
}

38
nuxt.config.ts Normal file
View File

@ -0,0 +1,38 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
components: true,
css: [
'@fortawesome/fontawesome-svg-core/styles.css',
'~/assets/css/main.scss'
],
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
build: {
transpile: ['@fortawesome/vue-fontawesome']
},
vite: {
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler' // or "modern"
}
}
},
server: {
proxy: {
'/api': {
target: 'https://cantyonion.site',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
}
})

15
nuxt.run.xml Normal file
View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="client: chrome" type="JavascriptDebugType" uri="https://127.0.0.1:3000" useFirstLineBreakpoints="true">
<method v="2" />
</configuration>
<configuration default="false" name="server: nuxt" type="NodeJSConfigurationType" application-parameters="dev" path-to-js-file="$PROJECT_DIR$/node_modules/nuxi/bin/nuxi.mjs" working-dir="$PROJECT_DIR$">
<method v="2" />
</configuration>
<configuration default="false" name="fullstack: nuxt" type="CompoundRunConfigurationType">
<toRun name="client: chrome" type="JavascriptDebugType" />
<toRun name="server: nuxt" type="NodeJSConfigurationType" />
<method v="2" />
</configuration>
</component>

10692
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@callmebill/lxgw-wenkai-web": "^1.501.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/vue-fontawesome": "^3.0.8",
"axios": "^1.7.7",
"mitt": "^3.0.1",
"nuxt": "^3.14.159",
"vue": "latest",
"vue-router": "latest",
"vue3-typed-js": "^0.0.1"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"sass-embedded": "^1.81.0",
"tailwindcss": "^3.4.15"
}
}

11
pages/[...default].vue Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
navigateTo("/error/404")
</script>
<template>
</template>
<style scoped>
</style>

50
pages/error/[code].vue Normal file
View File

@ -0,0 +1,50 @@
<script setup lang="ts">
const route = useRoute()
const errorCode = route.params.code as string
const errorMessages: Record<string, { icon: string; title: string; message: string }> = {
'400': {
icon: '❓',
title: '哦豁!请求有点问题',
message: '请求似乎有点小问题,服务器君有点摸不着头脑呢!',
},
'403': {
icon: '🔒',
title: '哎呀!大门被锁住了',
message: '你没有权限访问这个角落,试试返回首页吧!',
},
'404': {
icon: '🌌',
title: '哎呀!你发现了超级基地的秘密角落',
message: '看起来你找的页面藏到了未知的时空层里!',
},
'500': {
icon: '🚀',
title: '糟糕!超级基地出了一点故障',
message: '服务器君正在努力解决问题,请稍等片刻!',
},
'502': {
icon: '👽',
title: '糟糕!基地信号被外星人拦截了',
message: '网络通道似乎遇到了问题,请稍后再试!',
},
default: {
icon: '⚠️',
title: '哦豁!此时遇到了点小麻烦',
message: '请求在穿越洋葱星球时迷路了,请返回首页重新探索!',
},
};
const errorContent = computed(() => errorMessages[errorCode] || errorMessages['default']);
onMounted(() => {
emitter.emit("startLoading", false)
})
</script>
<template>
<alert :title="errorContent.title" :message="errorContent.message" :icon="errorContent.icon"/>
</template>
<style scoped>
</style>

110
pages/index.vue Normal file
View File

@ -0,0 +1,110 @@
<script lang="ts" setup>
import {computed, onMounted, ref} from "vue";
import {getBlogRecentPost} from "~/api/blog";
import {getActivity} from "~/api/git"
const recentPosts = ref<IPost[] | null>(null);
const recentActivities = ref<IActivity[] | null>(null);
const isLoading = ref({
blog: true,
git: true
});
const isError = ref({
blog: false,
git: false
});
const hasPosts = computed(() => (recentPosts.value ?? []).length > 0);
const hasActivities = computed(() => (recentActivities.value ?? []).length > 0);
const postsData = computed(() => {
if (!recentPosts.value) return [];
if (recentPosts.value.length > 4)
return recentPosts.value.slice(0, 4)
return recentPosts.value
})
const activitiesData = computed((): IActivity<IContent>[] => {
if (!recentActivities.value) return [];
return recentActivities.value.map(item => {
try {
if (item.op_type === "commit_repo")
return {
...item,
content: JSON.parse(item.content)
}
else return null
} catch (e) {
return null
}
}).filter(item => item !== null)
})
const reloadPosts = async () => {
try {
isLoading.value.blog = true
const postData = await getBlogRecentPost();
recentPosts.value = postData.data.dataSet
isLoading.value.blog = false
} catch (e) {
isLoading.value.blog = false
isError.value.blog = true
}
}
const reloadActivities = async () => {
try {
isLoading.value.git = true
recentActivities.value = await getActivity()
isLoading.value.git = false
} catch (e) {
isLoading.value.git = false
isError.value.git = true
}
}
onMounted(async () => {
await reloadPosts()
await reloadActivities()
emitter.emit('startLoading', false)
})
</script>
<template>
<!-- 个人介绍 -->
<card-full-screen-intro-card/>
<div class="container mx-auto mt-8 xl:max-w-screen-xl">
<!-- 博客部分 -->
<section class="w-full px-4 sm:px-0">
<!-- 标题部分 -->
<card-title :icon="['fas', 'blog']" main-color="bg-rose-500" sub-color="bg-rose-600" title="最新博文"
url="/blog/"/>
<card-section-card :empty="!hasPosts" :error="isError.blog" :loading="isLoading.blog" @reload="reloadPosts">
<template v-slot:default>
<div class="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<card-article-card v-for="item in postsData" :key="item.cid" :post="item"/>
</div>
</template>
</card-section-card>
</section>
<!-- Git部分 -->
<section class="my-4 w-full px-4 sm:px-0">
<card-title :icon="['fas', 'code']" main-color="bg-green-500" sub-color="bg-green-600" title="最近活动"
url="/git/"/>
<card-section-card :empty="!hasActivities" :error="isError.git" :loading="isLoading.git" @reload="reloadActivities">
<template v-slot:default>
<div class="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
<card-git-card v-for="item in activitiesData" :key="item.id" :commit="item"/>
</div>
</template>
</card-section-card>
</section>
</div>
</template>
<style scoped>
</style>

32
plugins/fontawsome.ts Normal file
View File

@ -0,0 +1,32 @@
import {library, config} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import {faClock} from '@fortawesome/free-regular-svg-icons'
import {
faXmark,
faBlog,
faGauge,
faCodeBranch,
faCloud,
faChevronRight,
faCodeCommit,
faCode,
faHouse,
faPen,
faBrush,
faChessRook,
faBars,
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
import {faWeibo, faQq, faGithubAlt, faSteamSymbol} from '@fortawesome/free-brands-svg-icons'
// 因为默认添加了 nuxt会造成一些错误所以不自动添加样式
config.autoAddCss = false
export default defineNuxtPlugin((nuxtApp) => {
library.add(
faClock, faXmark, faBlog, faGauge, faCodeBranch, faCloud, faWeibo, faQq, faGithubAlt, faSteamSymbol, faChevronRight,
faCodeCommit, faCode, faHouse, faPen, faBrush, faChessRook, faBars, faChevronDown
)
nuxtApp.vueApp.component('font-awesome-icon', FontAwesomeIcon)
})

6
plugins/typedjs.ts Normal file
View File

@ -0,0 +1,6 @@
// @ts-expect-error
import VueTyped from 'vue3-typed-js'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueTyped)
})

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/robots.txt Normal file
View File

@ -0,0 +1 @@

3
server/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

17
tailwind.config.ts Normal file
View File

@ -0,0 +1,17 @@
import type { Config } from 'tailwindcss'
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
],
theme: {
extend: {},
},
plugins: [],
} satisfies Config

4
tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
}

59
types/blog.d.ts vendored Normal file
View File

@ -0,0 +1,59 @@
declare interface IPostsData {
page: number
pageSize: number
pages: number
count: number
dataSet: []
}
declare interface IPost {
cid: string
title: string
created: string
modified: string
slug: string
commentsNum: string
type: string
digest: string
password: string
categories: ICategory[]
category: string
directory: string[]
date: IDate
year: string
month: string
day: string
hidden: boolean
pathinfo: string
permalink: string
url: string
isMarkdown: boolean
feedUrl: string
feedRssUrl: string
feedAtomUrl: string
fields: any
}
declare interface ICategory {
mid: string
name: string
slug: string
type: string
description: string
count: string
order: string
parent: string
cid: string
directory: string[]
url: string
feedUrl: string
feedRssUrl: string
feedAtomUrl: string
}
declare interface IDate {
timestamp: number
year: string
month: string
day: string
}

27
types/git.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
declare interface IActivity<T = string> {
id: string;
content: T
op_type: string
repo: {
name: string
html_url: string
}
created: string
}
declare interface IContent {
Commits: ICommit[]
HeadCommit: ICommit
CompareURL: string
Len: number
}
declare interface ICommit {
Sha1: string,
Message: string,
AuthorEmail: string,
AuthorName: string,
CommitterEmail: string,
CommitterName: string,
Timestamp: string
}

5
types/index.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare interface IBlogResponse<T = null> {
status: string
message: string
data: T
}