Compare commits

..

No commits in common. "4c0d15b255d6953626125f82ba149a36f4d36cb0" and "fb109cd9ba74e0957602e0b8d8c7455dd511572f" have entirely different histories.

50 changed files with 2138 additions and 3558 deletions

24
app.vue
View File

@ -1,13 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
const loading = ref<HTMLDivElement | null>(null), const loading = ref<HTMLDivElement | null>(null)
{ $mitt } = useNuxtApp() const {$mitt} = useNuxtApp()
$mitt.on('startLoading', (on) => { $mitt.on('startLoading', on => {
if (on) { if (on) {
loading.value?.classList.remove('stop') loading.value?.classList.remove('stop')
removeMobileTopColor() removeMobileTopColor()
} } else {
else {
loading.value?.classList.add('stop') loading.value?.classList.add('stop')
setMobileTopColor() setMobileTopColor()
} }
@ -15,14 +14,11 @@ $mitt.on('startLoading', (on) => {
</script> </script>
<template> <template>
<div <div ref="loading" class="loading"></div>
ref="loading"
class="loading"
/>
<div class="relative flex min-h-screen flex-col overflow-hidden bg-blue-50"> <div class="relative flex min-h-screen flex-col overflow-hidden bg-blue-50">
<nav-bar /> <nav-bar/>
<nuxt-page /> <nuxt-page/>
<footer-main /> <footer-main/>
<alert-notification /> <alert-notification/>
</div> </div>
</template> </template>

View File

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

View File

@ -1,43 +1,42 @@
<script lang="ts" setup> <script lang="ts" setup>
const { $mitt } = useNuxtApp(), const {$mitt} = useNuxtApp()
notifications = ref<INotification[]>([]), const notifications = ref<INotification[]>([]);
typeClasses = { const typeClasses = {
success: 'bg-green-100 text-green-800 border border-green-400', success: 'bg-green-100 text-green-800 border border-green-400',
error: 'bg-red-100 text-red-800 border border-red-400', error: 'bg-red-100 text-red-800 border border-red-400',
warning: 'bg-yellow-100 text-yellow-800 border border-yellow-400', warning: 'bg-yellow-100 text-yellow-800 border border-yellow-400',
info: 'bg-blue-100 text-blue-800 border border-blue-400', info: 'bg-blue-100 text-blue-800 border border-blue-400',
}, };
addNotification = (notification: INotification) => { const addNotification = (notification: INotification) => {
notifications.value.push(notification) notifications.value.push(notification);
// //
setTimeout(() => { setTimeout(() => {
notifications.value.shift() notifications.value.shift();
}, 5000) }, 5000);
console.log(1) console.log(1)
}
};
$mitt.on('eventBus', addNotification) $mitt.on('eventBus', addNotification)
onBeforeUnmount(() => { onBeforeUnmount(() => {
$mitt.off('eventBus', addNotification) $mitt.off('eventBus', addNotification);
}) });
</script> </script>
<template> <template>
<div class="fixed top-5 right-5 z-50 space-y-4"> <div class="fixed top-5 right-5 z-50 space-y-4">
<div <div
v-for="(notification, index) in notifications" v-for="(notification, index) in notifications"
:key="index" :key="index"
:class="[ :class="[
'p-4 rounded-lg shadow-lg transition-all duration-300', 'p-4 rounded-lg shadow-lg transition-all duration-300',
typeClasses[notification.level], typeClasses[notification.level]
]" ]"
> >
<h3 class="text-lg font-bold"> <h3 class="text-lg font-bold">{{ notification.title }}</h3>
{{ notification.title }}
</h3>
<p>{{ notification.message }}</p> <p>{{ notification.message }}</p>
</div> </div>
</div> </div>

View File

@ -1,34 +1,30 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import {computed} from "vue";
import {pos} from "ipx";
const props = defineProps<{ const props = defineProps<{
post: IPost post: IPost
}>(), }>()
pid = useAsyncData('pid', async () => (Math.floor(Math.random() * (7 - 1 + 1)) + 1).toString()), const pid = useAsyncData('pid', async () => (Math.floor(Math.random() * (7 - 1 + 1)) + 1).toString())
thumbUrl = computed(() => props.post.fields.thumb.value), const thumbUrl = computed(() => props.post.fields.thumb.value)
const randomId = computed(() => pid.data.value!)
randomId = computed(() => pid.data.value!)
</script> </script>
<template> <template>
<nuxt-link <nuxt-link :title="post.title"
:title="post.title" class="container relative overflow-hidden rounded-2xl bg-white transition-all duration-300 ease-in-out h-[31rem] hover:-translate-y-2 hover:cursor-pointer hover:shadow"
class="container relative overflow-hidden rounded-2xl bg-white transition-all duration-300 ease-in-out h-[31rem] hover:-translate-y-2 hover:cursor-pointer hover:shadow" :to="{name: 'article-cid', params: {cid: post.cid}}">
:to="{ name: 'article-cid', params: { cid: post.cid } }"
>
<!-- 图片 --> <!-- 图片 -->
<div class="relative"> <div class="relative">
<!-- 渐变 --> <!-- 渐变 -->
<div class="absolute top-0 right-0 bottom-0 left-0 bg-gradient-to-t from-white" /> <div class="absolute top-0 right-0 bottom-0 left-0 bg-gradient-to-t from-white"></div>
<img <img v-if="thumbUrl" :src="thumbUrl" alt=""
v-if="thumbUrl" class="aspect-video w-full h-auto object-cover object-center">
:src="thumbUrl" <card-article-default-image :id="randomId"/>
alt=""
class="aspect-video w-full h-auto object-cover object-center"
>
<card-article-default-image :id="randomId" />
</div> </div>
<!-- 文字部分 --> <!-- 文字部分 -->
<div class="px-5"> <div class="px-5">
@ -41,4 +37,4 @@ const props = defineProps<{
<style scoped> <style scoped>
</style> </style>

View File

@ -5,45 +5,17 @@ defineProps<{
</script> </script>
<template> <template>
<img <img src="~/assets/images/common/1.jpg" alt="" v-if="id === '1'"/>
v-if="id === '1'" <img src="~/assets/images/common/2.jpg" alt="" v-else-if="id === '2'"/>
src="~/assets/images/common/1.jpg" <img src="~/assets/images/common/3.jpg" alt="" v-else-if="id === '3'"/>
alt="" <img src="~/assets/images/common/4.jpg" alt="" v-else-if="id === '4'"/>
> <img src="~/assets/images/common/5.jpg" alt="" v-else-if="id === '5'"/>
<img <img src="~/assets/images/common/6.jpg" alt="" v-else-if="id === '6'"/>
v-else-if="id === '2'" <img src="~/assets/images/common/7.jpg" alt="" v-else-if="id === '7'"/>
src="~/assets/images/common/2.jpg"
alt=""
>
<img
v-else-if="id === '3'"
src="~/assets/images/common/3.jpg"
alt=""
>
<img
v-else-if="id === '4'"
src="~/assets/images/common/4.jpg"
alt=""
>
<img
v-else-if="id === '5'"
src="~/assets/images/common/5.jpg"
alt=""
>
<img
v-else-if="id === '6'"
src="~/assets/images/common/6.jpg"
alt=""
>
<img
v-else-if="id === '7'"
src="~/assets/images/common/7.jpg"
alt=""
>
</template> </template>
<style scoped> <style scoped>
img { img {
@apply aspect-[16/10] w-full h-auto object-cover object-center @apply aspect-[16/10] w-full h-auto object-cover object-center
} }
</style> </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

@ -7,15 +7,14 @@ defineProps<{
<template> <template>
<div class="my-16 relative"> <div class="my-16 relative">
<h3 class="text-5xl font-bold text-blue-400 relative z-10"> <h3 class="text-5xl font-bold text-blue-400 relative z-10">{{ title }}</h3>
{{ title }} <div class="h-12 w-auto text-5xl text-blue-200 absolute left-48 top-0 -translate-y-6">
</h3> <font-awesome-icon :icon="icon"/>
<div class="h-12 w-auto text-5xl text-blue-200 absolute left-48 top-0 -translate-y-6"> </div>
<font-awesome-icon :icon="icon" />
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

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

View File

@ -1,114 +1,85 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import {computed} from "vue";
const props = defineProps<{ const props = defineProps<{
commit: IStrictActivity commit: IActivity<IContent>
}>(), }>()
commit_ref_name = computed(() => props.commit.ref_name.substring(props.commit.ref_name.lastIndexOf('/') + 1)), 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']
}
})
icon = computed(() => { const time = computed(() => timeDifference(props.commit.created))
switch (props.commit.op_type) {
case 'merge_pull_request':
return ['fas', 'code-merge']
case 'create_repo':
return ['fas', 'book-medical']
case 'delete_branch':
case 'delete_tag':
case 'commit_repo':
default:
return ['fas', 'code-commit']
}
}),
time = computed(() => timeDifference(props.commit.created)), const timeDifference = (dateString: string): string => {
const targetDate = new Date(dateString);
const currentDate = new Date();
timeDifference = (dateString: string): string => { const diffInMillis = currentDate.getTime() - targetDate.getTime();
const targetDate = new Date(dateString),
currentDate = new Date(),
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));
minutes = Math.floor(diffInMillis / (1000 * 60)), if (minutes < 60) {
hours = Math.floor(diffInMillis / (1000 * 60 * 60)), return `${minutes} 分钟前`;
days = Math.floor(diffInMillis / (1000 * 60 * 60 * 24)), } else if (hours < 24) {
months = Math.floor(diffInMillis / (1000 * 60 * 60 * 24 * 30)), return `${hours} 小时前`;
years = Math.floor(diffInMillis / (1000 * 60 * 60 * 24 * 365)) } else if (days < 30) {
return `${days} 天前`;
} else if (months < 12) {
return `${months} 个月前`;
} else {
return `${years} 年前`;
}
}
if (minutes < 60) { const handleClick = () => {
return `${minutes} 分钟前` window.open(props.commit.repo.html_url, '_self')
} }
else if (hours < 24) {
return `${hours} 小时前`
}
else if (days < 30) {
return `${days} 天前`
}
else if (months < 12) {
return `${months} 个月前`
}
return `${years} 年前`
},
calcHeight = (n: number) => `${n * 2.5}rem`
</script> </script>
<template> <template>
<div class="relative bg-transparent p-4 transition-all duration-500 group lg:hover:scale-105 lg:hover:rounded-xl lg:hover:shadow"> <div class="group container relative flex h-36 w-full overflow-hidden rounded-2xl transition-all
<!-- 标题部分 --> hover:cursor-pointer hover:shadow-md"
<div class="relative flex items-center gap-4 text-lg"> @click="handleClick"
<font-awesome-icon >
class="h-8 w-10" <!-- 提交图标 -->
:icon="icon" <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
<p v-if="commit.op_type === 'commit_repo' && commit.content"> before:blur before:transition-all group-hover:before:opacity-100"
推送到了仓库 {{ commit.repo.owner.login }}/{{ commit.repo.name }} <span>{{ commit_ref_name }}</span> 分支
</p>
<p v-else-if="commit.op_type === 'commit_repo' && commit.content === null">
{{ commit.repo.owner.login }}/{{ commit.repo.name }} 创建了分支 {{ commit_ref_name }}
</p>
<p v-else-if="commit.op_type === 'create_repo'">
创建了仓库 {{ commit.repo.owner.login }}/{{ commit.repo.name }}
</p>
<span class="absolute top-0 right-0 hidden lg:block">{{ time }}</span>
</div>
<!-- 提交内容 -->
<div
v-if="commit.content"
class="ml-14 overflow-hidden transition-all duration-500 lg:invisible lg:opacity-0
lg:h-0 lg:group-hover:h-[999px] lg:group-hover:visible lg:group-hover:opacity-100"
:style="{ maxHeight: calcHeight(commit.content.Commits.length) }"
> >
<div <font-awesome-icon :icon="icon" class="transition-all group-hover:scale-110"/>
v-for="content in commit.content.Commits" </div>
:key="content.Sha1" <!-- 内容 -->
class="my-2 flex items-center gap-2" <div class="flex-1 overflow-hidden bg-white p-4">
> <div class="mr-12 truncate text-2xl font-bold">
<font-awesome-icon 仓库{{ commit.repo.name }}
class="h-4 w-5" </div>
:icon="icon" <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">
<!-- SHA1 --> {{ commit.content.HeadCommit.Sha1.slice(0, 10) }}
<nuxt-link </span>
:to="`https://cantyonion.site/git/${commit.repo.owner.login}/${commit.repo.name}/commit/${content.Sha1}`"
class="rounded bg-gray-200 px-2 transition-all hover:bg-gray-400" <span class="absolute right-0 bottom-0 m-4 rounded bg-gray-300 px-2">
style="font-family: 'LXGW WenKai Mono', monospace" <font-awesome-icon :icon="['far', 'clock']" class="mr-1 text-sm"/>
>{{ content.Sha1.substring(0, 10) }}</nuxt-link> {{ time }}
<!-- 提交信息 --> </span>
<span class="truncate text-nowrap">{{ content.Message }}</span>
</div>
<!-- 比较链接 -->
<nuxt-link
v-if="commit.content.Commits.length > 1"
:to="`https://cantyonion.site/git/${commit.content.CompareURL}`"
class="text-blue-500 hover:text-blue-700"
style="font-family: 'LXGW WenKai Mono', monospace"
>比较 {{ commit.content.Commits.length }} 提交 &gt;&gt;</nuxt-link>
</div> </div>
<div class="absolute top-0 right-0 bottom-0 left-0 -z-10 transition-all duration-500 lg:group-hover:rounded-xl lg:group-hover:bg-white/10 lg:group-hover:backdrop-blur-2xl" />
</div> </div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

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

View File

@ -6,89 +6,46 @@ defineProps<{
errMsg?: object errMsg?: object
}>() }>()
const emit = defineEmits(['reload']), const emit = defineEmits(['reload'])
handleReload = () => { const handleReload = () => {
emit('reload') emit('reload')
} }
</script> </script>
<template> <template>
<div <div :class="{'justify-center': (empty || error)}" class="container flex">
:class="{ 'justify-center': (empty || error) }"
class="container flex"
>
<!-- 加载骨架 --> <!-- 加载骨架 -->
<div <div v-if="loading" class="h-56 w-full text-2xl font-bold">
v-if="loading"
class="h-56 w-full text-2xl font-bold"
>
<slot name="skeleton"> <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 class="grid w-full grid-cols-1 gap-4 overflow-hidden sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div <div v-for="index of 3" :key="index" class="h-56 animate-pulse rounded-2xl bg-gray-200"></div>
v-for="index of 3"
:key="index"
class="h-56 animate-pulse rounded-2xl bg-gray-200"
/>
</div> </div>
</slot> </slot>
</div> </div>
<div <div v-else class="w-full">
v-else
class="w-full"
>
<!-- 错误处理 --> <!-- 错误处理 -->
<div <div v-if="error" @click="handleReload">
v-if="error"
@click="handleReload"
>
<div class="flex items-center justify-center gap-8"> <div class="flex items-center justify-center gap-8">
<p class="text-2xl text-gray-700 border-2 border-black py-2 px-8 rounded-full rounded-br-lg"> <p class="text-2xl text-gray-700 border-2 border-black py-2 px-8 rounded-full rounded-br-lg">载入错误</p>
载入错误 <img class="h-32 w-auto relative" src="~/assets/images/common/plana_e.png" alt="">
</p>
<img
class="h-32 w-auto relative"
src="~/assets/images/common/plana_e.png"
alt=""
>
</div> </div>
<div <div v-if="errMsg" class="bg-white mt-4 p-4 border border-dashed rounded-md" style="font-family: 'LXGW WenKai Mono', monospace">
v-if="errMsg"
class="bg-white mt-4 p-4 border border-dashed rounded-md"
style="font-family: 'LXGW WenKai Mono', monospace"
>
{{ errMsg }} {{ errMsg }}
</div> </div>
<p class="text-right text-xs"> <p class="text-right text-xs">(点击重试)</p>
(点击重试)
</p>
</div> </div>
<div v-else> <div v-else>
<!-- 正式内容 --> <!-- 正式内容 -->
<div v-if="empty"> <div v-if="empty">
<div <div v-if="empty" class="flex items-center justify-center gap-8" @click="handleReload">
v-if="empty" <p class="text-2xl text-gray-700 border-2 border-black py-2 px-8 rounded-full rounded-br-lg">最近没有动态~</p>
class="flex items-center justify-center gap-8" <img class="h-32 w-auto relative" src="~/assets/images/common/plana_e.png" alt="">
@click="handleReload"
>
<p class="text-2xl text-gray-700 border-2 border-black py-2 px-8 rounded-full rounded-br-lg">
最近没有动态~
</p>
<img
class="h-32 w-auto relative"
src="~/assets/images/common/plana_e.png"
alt=""
>
</div> </div>
<p class="text-right text-xs"> <p class="text-right text-xs">(点击重试)</p>
(点击重试)
</p>
</div> </div>
<slot <slot v-else name="default"/>
v-else
name="default"
/>
</div> </div>
</div> </div>
</div> </div>
@ -96,4 +53,4 @@ const emit = defineEmits(['reload']),
<style scoped> <style scoped>
</style> </style>

View File

@ -8,41 +8,22 @@
<div class="grid-cols-4 gird min-h-48"> <div class="grid-cols-4 gird min-h-48">
<div> <div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<img <img alt="" class="h-20 w-20" src="~/assets/images/logo/c.png">
alt="" <div class="text-4xl leading-7 text-gray-500" style="font-family: BaconyScript, sans-serif">
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>CantyOni_on's</p>
<p>Index</p> <p>Index</p>
</div> </div>
</div> </div>
<p class="mt-20 leading-8 text-gray-500"> <p class="mt-20 leading-8 text-gray-500">&copy;2024 All rights reserved.</p>
&copy;2024 All rights reserved.
</p>
</div> </div>
</div> </div>
<div class="flex flex-col text-gray-500 md:flex-row"> <div class="flex flex-col text-gray-500 md:flex-row">
<a <a class="mr-4 hover:text-black" href="https://beian.miit.gov.cn/"
class="mr-4 hover:text-black" target="_blank">苏ICP备2022016243号-1</a>
href="https://beian.miit.gov.cn/"
target="_blank"
>苏ICP备2022016243号-1</a>
<div> <div>
<img <img alt="" class="inline-block w-4 pb-1" src="~/assets/images/common/beian.png"/>
alt="" <a class="hover:text-black" href="https://beian.mps.gov.cn/#/query/webSearch?code=32050602011641/"
class="inline-block w-4 pb-1" target="_blank">苏公网安备32050602011641</a>
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> </div>
</div> </div>
@ -56,4 +37,4 @@
background-color: white; background-color: white;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
</style> </style>

View File

@ -1,46 +1,45 @@
<template> <template>
<div class="loader"> <div class="loader">
<svg <svg
class="icon" class="icon"
height="200" height="200"
p-id="1166" p-id="1166"
t="1676267704832" t="1676267704832"
version="1.1" version="1.1"
viewBox="0 0 1024 1024" viewBox="0 0 1024 1024"
width="200" width="200"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <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" 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" fill="#ffffff"
p-id="1167" p-id="1167"
/> ></path>
</svg> </svg>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch } from 'vue' import {watch} from "vue";
const props = defineProps({ const props = defineProps({
loading: { loading: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) });
watch( watch(
() => props.loading, () => props.loading,
(v) => { (v) => {
const el: HTMLElement = document.querySelector('body') as HTMLElement const el: HTMLElement = document.querySelector("body") as HTMLElement;
if (v) { if (v) {
el.classList.add('loading') el.classList.add("loading");
} else {
el.classList.remove("loading");
}
} }
else { );
el.classList.remove('loading')
}
},
)
</script> </script>
<style scoped> <style scoped>

View File

@ -5,12 +5,10 @@ defineProps<{
</script> </script>
<template> <template>
<div <div class="markdown-body" v-html="$mdRender.render(content)"/>
class="markdown-body"
v-html="$mdRender.render(content)"
/>
</template> </template>
<style lang="scss"> <style lang="scss">
.markdown-body { .markdown-body {
font-family: "LXGW WenKai", sans-serif; font-family: "LXGW WenKai", sans-serif;
@ -36,4 +34,4 @@ defineProps<{
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
} }
</style> </style>

View File

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

View File

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

View File

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

View File

@ -8,17 +8,13 @@ defineProps<{
<template> <template>
<div class="flex gap-4 text-xl"> <div class="flex gap-4 text-xl">
<a <a :key="url" :href=url :title=title
:key="url" class="flex items-center justify-center transition-all hover:scale-125">
:href="url" <font-awesome-icon :icon="icon"/>
:title="title"
class="flex items-center justify-center transition-all hover:scale-125"
>
<font-awesome-icon :icon="icon" />
</a> </a>
</div> </div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,16 +1,14 @@
export const getAssetURL = (image: string) => { export const getAssetURL = (image: string) => {
// 参数一: 相对路径 // 参数一: 相对路径
// 参数二: 当前路径的URL // 参数二: 当前路径的URL
if (import.meta.client) { if (import.meta.client)
return new URL(`../assets/images/${image}`, import.meta.url).href return new URL(`../assets/images/${image}`, import.meta.url).href
}
return `/_nuxt/assets/images/${image}` return `/_nuxt/assets/images/${image}`
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export const debounce = (fn: Function, delay: number) => { export const debounce = (fn: Function, delay: number) => {
let timeoutId: NodeJS.Timeout let timeoutId: NodeJS.Timeout
return (...args: unknown[]) => { return (...args: any[]) => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
fn(...args) fn(...args)
@ -19,20 +17,20 @@ export const debounce = (fn: Function, delay: number) => {
} }
export const setMobileTopColor = () => { export const setMobileTopColor = () => {
const lightMeta = document.createElement('meta') const lightMeta = document.createElement('meta');
lightMeta.setAttribute('name', 'theme-color') lightMeta.setAttribute('name', 'theme-color');
lightMeta.setAttribute('media', '(prefers-color-scheme: light)') lightMeta.setAttribute('media', '(prefers-color-scheme: light)');
lightMeta.setAttribute('content', '#60a5fa') lightMeta.setAttribute('content', '#60a5fa');
const darkMeta = document.createElement('meta') const darkMeta = document.createElement('meta');
darkMeta.setAttribute('name', 'theme-color') darkMeta.setAttribute('name', 'theme-color');
darkMeta.setAttribute('media', '(prefers-color-scheme: dark)') darkMeta.setAttribute('media', '(prefers-color-scheme: dark)');
darkMeta.setAttribute('content', '#60a5fa') darkMeta.setAttribute('content', '#60a5fa');
document.head.appendChild(lightMeta) document.head.appendChild(lightMeta);
document.head.appendChild(darkMeta) document.head.appendChild(darkMeta);
} }
export const removeMobileTopColor = () => { export const removeMobileTopColor = () => {
document.querySelectorAll('meta[name="theme-color"]').forEach(meta => meta.remove()) document.querySelectorAll('meta[name="theme-color"]').forEach(meta => meta.remove());
} }

View File

@ -1,28 +1,28 @@
import type { AsyncData, UseFetchOptions } from '#app' import type { AsyncData, UseFetchOptions } from '#app'
export function requestCore<DataT>(url: string, options: UseFetchOptions<DataT>) { export function requestCore<DataT>(url: string, options: UseFetchOptions<DataT>) {
// Const config = useRuntimeConfig() // const config = useRuntimeConfig()
const { $mitt } = useNuxtApp() const {$mitt} = useNuxtApp()
return useFetch(url, { return useFetch(url, {
baseURL: 'https://cantyonion.site', baseURL: "https://cantyonion.site",
retry: false, retry: false,
timeout: 3000, timeout: 3000,
onRequest({ options }) { onRequest({ options }) {
let token = '' let token: string = ""
if (import.meta.client) { if (import.meta.client) {
token = localStorage.getItem('token') || '' token = localStorage.getItem("token") || ""
} }
if (!options.headers.get('Authorization')) { if (!options.headers.get('Authorization'))
options.headers.set('Authorization', `Bearer ${token}`) options.headers.set('Authorization', `Bearer ${token}`)
}
}, },
onResponse({ response }) { onResponse({ response }) {
// HTTP状态码2XX/3XX执行否则不执行 // HTTP状态码2XX/3XX执行否则不执行
if (response.status < 200 || response.status >= 400) { if (response.status < 200 || response.status >= 400) {
console.error(`HTTP 错误: ${response.status}`) console.error(`HTTP 错误: ${response.status}`)
return
} }
// 业务code状态码判断 // 业务code状态码判断
// If (response._data.code !== 200) { // if (response._data.code !== 200) {
// //
// } // }
}, },
@ -30,10 +30,10 @@ export function requestCore<DataT>(url: string, options: UseFetchOptions<DataT>)
$mitt.emit('eventBus', { $mitt.emit('eventBus', {
level: 'error', level: 'error',
title: `HTTP错误${response.status}`, title: `HTTP错误${response.status}`,
message: response.statusText, message: response.statusText
}) })
}, },
...options, ...options
}) })
} }
@ -43,9 +43,8 @@ export function get<DataT, ErrorT>(url: string, options?: UseFetchOptions<DataT>
return async (): Promise<DataT> => { return async (): Promise<DataT> => {
if (result != null) { if (result != null) {
await result.refresh() await result.refresh()
if (result.status.value === 'success') { if (result.status.value === 'success')
return result.data.value return result.data.value
}
throw result.error throw result.error
} }
@ -55,4 +54,4 @@ export function get<DataT, ErrorT>(url: string, options?: UseFetchOptions<DataT>
}) as AsyncData<DataT, ErrorT> }) as AsyncData<DataT, ErrorT>
return result.data.value return result.data.value
} }
} }

View File

@ -3,49 +3,49 @@ import type { NuxtError } from '#app'
import { setMobileTopColor } from '~/composables/function' import { setMobileTopColor } from '~/composables/function'
const props = defineProps({ const props = defineProps({
error: Object as () => NuxtError, error: Object as () => NuxtError
}), })
{ $mitt } = useNuxtApp(), const {$mitt} = useNuxtApp()
errorMessages: Record<string, { icon: string, title: string, message: string }> = { const errorMessages: Record<string, { icon: string; title: string; message: string }> = {
400: { '400': {
icon: '❓', icon: '❓',
title: '哦豁!请求有点问题', title: '哦豁!请求有点问题',
message: '请求似乎有点小问题,服务器君有点摸不着头脑呢!', message: '请求似乎有点小问题,服务器君有点摸不着头脑呢!',
},
403: {
icon: '🔒',
title: '哎呀!大门被锁住了',
message: '你没有权限访问这个角落,试试返回首页吧!',
},
404: {
icon: '🌌',
title: '哎呀!你发现了超级基地的秘密角落',
message: '看起来你找的页面藏到了未知的时空层里!',
},
500: {
icon: '🚀',
title: '糟糕!超级基地出了一点故障',
message: '服务器君正在努力解决问题,请稍等片刻!',
},
502: {
icon: '👽',
title: '糟糕!基地信号被外星人拦截了',
message: '网络通道似乎遇到了问题,请稍后再试!',
},
default: {
icon: '⚠️',
title: '哦豁!此时遇到了点小麻烦',
message: '请求在穿越洋葱星球时迷路了,请返回首页重新探索!',
},
}, },
'403': {
icon: '🔒',
title: '哎呀!大门被锁住了',
message: '你没有权限访问这个角落,试试返回首页吧!',
},
'404': {
icon: '🌌',
title: '哎呀!你发现了超级基地的秘密角落',
message: '看起来你找的页面藏到了未知的时空层里!',
},
'500': {
icon: '🚀',
title: '糟糕!超级基地出了一点故障',
message: '服务器君正在努力解决问题,请稍等片刻!',
},
'502': {
icon: '👽',
title: '糟糕!基地信号被外星人拦截了',
message: '网络通道似乎遇到了问题,请稍后再试!',
},
default: {
icon: '⚠️',
title: '哦豁!此时遇到了点小麻烦',
message: '请求在穿越洋葱星球时迷路了,请返回首页重新探索!',
},
};
errorContent = computed(() => errorMessages[props.error!.statusCode] || errorMessages.default) const errorContent = computed(() => errorMessages[props.error!.statusCode] || errorMessages['default']);
console.error(props.error) console.error(props.error)
onMounted(() => { onMounted(() => {
$mitt.emit('startLoading', false) $mitt.emit("startLoading", false)
setMobileTopColor() setMobileTopColor()
}) })
@ -56,17 +56,13 @@ onUnmounted(() => {
<template> <template>
<div class="relative flex min-h-screen flex-col overflow-hidden bg-blue-50"> <div class="relative flex min-h-screen flex-col overflow-hidden bg-blue-50">
<nav-bar :nav-color="true" /> <nav-bar :nav-color="true"/>
<alert <alert :title="errorContent.title" :message="errorContent.message" :icon="errorContent.icon"/>
:title="errorContent.title" <footer-main/>
:message="errorContent.message" <alert-notification/>
:icon="errorContent.icon"
/>
<footer-main />
<alert-notification />
</div> </div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,3 +0,0 @@
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt()

View File

@ -1,4 +1,4 @@
export default defineNuxtRouteMiddleware(() => { export default defineNuxtRouteMiddleware(() => {
const { $mitt } = useNuxtApp() const {$mitt} = useNuxtApp()
$mitt.emit('startLoading', true) $mitt.emit('startLoading', true)
}) })

View File

@ -1,6 +1,5 @@
export default defineNuxtRouteMiddleware((to) => { export default defineNuxtRouteMiddleware((to, _) => {
if (to.meta.maintenance === true && import.meta.env.DEV) { if (to.meta.maintenance === true && import.meta.env.DEV)
return navigateTo('/error/maintenance') return navigateTo('/error/maintenance')
}
return true return true
}) })

View File

@ -1,12 +1,9 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
// See https://blog.csdn.net/m0_53022813/article/details/137379480
import eslintPlugin from 'vite-plugin-eslint'
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: ['@pinia/nuxt', '@pinia-plugin-persistedstate/nuxt', '@nuxt/image', '@nuxt/eslint'], compatibilityDate: '2024-04-03',
components: true,
devtools: { enabled: true }, devtools: { enabled: true },
modules: ['@pinia/nuxt', '@pinia-plugin-persistedstate/nuxt', '@nuxt/image'],
app: { app: {
head: { head: {
title: 'CANTYONION.SITE', title: 'CANTYONION.SITE',
@ -18,50 +15,40 @@ export default defineNuxtConfig({
], ],
link: [ link: [
{ rel: 'icon', href: '/favicon.png' }, { rel: 'icon', href: '/favicon.png' },
], ]
}, }
}, },
components: true,
css: [ css: [
'@fortawesome/fontawesome-svg-core/styles.css', '@fortawesome/fontawesome-svg-core/styles.css',
'github-markdown-css/github-markdown-light.css', 'github-markdown-css/github-markdown-light.css',
'~/assets/css/main.scss', '~/assets/css/main.scss'
], ],
runtimeConfig: {
public: {
gitApiKey: 'fb8aec429ea7d0a36f7238dbffda9d2d66c7b045',
baseURL: 'https://cantyonion.site',
},
},
build: {
transpile: ['@fortawesome/vue-fontawesome', 'vue3-typed-js', 'pinia-plugin-persistedstate'],
},
devServer: {
host: '0.0.0.0',
},
compatibilityDate: '2024-04-03',
vite: {
plugins: [
eslintPlugin({
include: ['**/*.ts', '**/*.vue'],
}),
],
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler', // Or "modern"
},
},
},
},
postcss: { postcss: {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}, },
eslint: { build: {
config: { transpile: ['@fortawesome/vue-fontawesome', 'vue3-typed-js', 'pinia-plugin-persistedstate']
stylistic: true, },
vite: {
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler' // or "modern"
}
}
}, },
}, },
}) runtimeConfig: {
public: {
gitApiKey: 'fb8aec429ea7d0a36f7238dbffda9d2d66c7b045',
baseURL: 'https://cantyonion.site'
}
},
devServer: {
host: '0.0.0.0'
}
})

View File

@ -7,9 +7,7 @@
"dev": "nuxt dev", "dev": "nuxt dev",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare", "postinstall": "nuxt prepare"
"lint": "eslint .",
"stylefix": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"@callmebill/lxgw-wenkai-web": "^1.501.0", "@callmebill/lxgw-wenkai-web": "^1.501.0",
@ -32,20 +30,11 @@
"vue3-typed-js": "^0.0.1" "vue3-typed-js": "^0.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.18.0",
"@nuxt/eslint": "^0.7.5",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.18.0",
"eslint-plugin-vue": "^9.32.0",
"globals": "^15.14.0",
"jiti": "^2.4.2",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"sass-embedded": "^1.81.0", "sass-embedded": "^1.81.0",
"tailwindcss": "^3.4.5", "tailwindcss": "^3.4.15"
"typescript": "^5.7.3",
"typescript-eslint": "^8.19.1",
"vite-plugin-eslint": "^1.8.1"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

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

View File

@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
const { $mitt } = useNuxtApp() const {$mitt} = useNuxtApp()
onMounted(() => { onMounted( () => {
$mitt.emit('startLoading', false) $mitt.emit('startLoading', false)
}) })
</script> </script>
<template> <template>
<nuxt-page /> <nuxt-page/>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,23 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute(), const route = useRoute()
cid = route.params.cid as string, const cid = route.params.cid as string
content = ref<IBlogResponse<IPostFull>>(), const content = ref<IBlogResponse<IPostFull>>()
getPost = get<IBlogResponse<IPostFull>, unknown>('/blog/index.php/api/post', { const getPost = get<IBlogResponse<IPostFull>, any>('/blog/index.php/api/post', {
params: { params: {
cid, cid: cid,
md: false, md: false
}, }
}), })
title = computed(() => content.value?.data.title || 'Can not see me... can not see me...'), const title = computed(() => content.value?.data.title || 'Can not see me... can not see me...')
text = computed(() => content.value?.data.text || '## Ops!'), const text = computed(() => content.value?.data.text || '## Ops!')
date = computed(() => new Date((content.value?.data.date.timeStamp || 0) * 1000) const date = computed(() => new Date((content.value?.data.date.timeStamp || 0) * 1000)
.toISOString() .toISOString()
.split('T')[0], .split('T')[0]
), )
category = computed(() => content.value?.data.category || 'default'), const category = computed(() => content.value?.data.category || 'default')
url = computed(() => content.value?.data.url || '/error/404') const url = computed(() => content.value?.data.url || '/error/404')
content.value = await getPost() content.value = await getPost()
</script> </script>
@ -25,9 +25,7 @@ content.value = await getPost()
<template> <template>
<section class="container m-4 mx-auto rounded-xl bg-white p-8 shadow-md lg:max-w-screen-lg"> <section class="container m-4 mx-auto rounded-xl bg-white p-8 shadow-md lg:max-w-screen-lg">
<div class="mb-4 border-b-2 border-dashed border-gray-200 pb-4"> <div class="mb-4 border-b-2 border-dashed border-gray-200 pb-4">
<h1 class="text-center text-3xl font-bold"> <h1 class="text-center text-3xl font-bold">{{ title }}</h1>
{{ title }}
</h1>
<div class="flex w-full items-center justify-center gap-4 text-gray-600"> <div class="flex w-full items-center justify-center gap-4 text-gray-600">
<!-- 日期 --> <!-- 日期 -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -45,10 +43,10 @@ content.value = await getPost()
</div> </div>
</div> </div>
</div> </div>
<markdown-section :content="text" /> <markdown-section :content="text"/>
</section> </section>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
maintenance: true, maintenance: true
}) })
</script> </script>
<template> <template>
<NuxtWelcome /> <NuxtWelcome/>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -3,9 +3,9 @@
</script> </script>
<template> <template>
<h1>auth view</h1>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
maintenance: true, maintenance: true
}) })
</script> </script>
<template> <template>
<NuxtWelcome /> <NuxtWelcome/>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,15 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute(), const route = useRoute()
errorCode = parseInt(route.params.code as string, 10) const errorCode = parseInt(route.params.code as string, 10)
if (import.meta.server) { if (import.meta.server)
throw createError({ throw createError({
statusCode: errorCode, statusCode: errorCode,
}) })
} else
else { showError({ statusCode: errorCode }) } showError({statusCode: errorCode})
</script> </script>
<template>
</template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,19 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
const { $mitt } = useNuxtApp() const {$mitt} = useNuxtApp()
onMounted(() => { onMounted(() => {
$mitt.emit('startLoading', false) $mitt.emit("startLoading", false)
}) })
</script> </script>
<template> <template>
<alert <alert icon="🚧" title="建设中" message="此页面正在建设中,请稍后再来查看更新。"/>
icon="🚧"
title="建设中"
message="此页面正在建设中,请稍后再来查看更新。"
/>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,95 +1,96 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue' import {computed, onMounted, ref} from "vue";
interface errObj { type errObj = {
blog?: object blog?: object,
git?: object git?: object
} }
const { $mitt } = useNuxtApp(), const {$mitt} = useNuxtApp()
recentPosts = ref<IPost[] | null>(null), const recentPosts = ref<IPost[] | null>(null);
recentActivities = ref<IActivity[] | null>(null), const recentActivities = ref<IActivity[] | null>(null);
isLoading = ref({ const isLoading = ref({
blog: true, blog: true,
git: true, git: true
}), });
isError = ref({ const isError = ref({
blog: false, blog: false,
git: false, git: false
}), });
errMsg = ref<errObj>({ const errMsg = ref<errObj>({
blog: undefined, blog: undefined,
git: undefined, git: undefined
}), })
getBlogRecentPost = get<IBlogResponse<IPostsData>, unknown>('/blog/index.php/api/posts', { const config = useRuntimeConfig()
params: { const getBlogRecentPost = get<IBlogResponse<IPostsData>, any>('/blog/index.php/api/posts', {
showDigest: 'excerpt', params: {
limit: 100, showDigest: "excerpt",
}, limit: 100
}),
getActivity = get<IActivity[], unknown>('/git/api/v1/users/cantyonion/activities/feeds', {
params: {
'limit': 10,
'only-performed-by': true,
},
}),
hasPosts = computed(() => (recentPosts.value ?? []).length > 0),
hasActivities = computed(() => (recentActivities.value ?? []).length > 0),
postsData = computed(() => {
if (!recentPosts.value) {
return []
}
if (recentPosts.value.length > 4) {
return recentPosts.value.slice(0, 4)
}
return recentPosts.value
}),
activitiesData = computed((): IStrictActivity[] => {
if (!Array.isArray(recentActivities.value)) {
return []
}
return recentActivities.value.map((item) => {
return {
...item,
content: typeof item.content === 'string' && item.content ? JSON.parse(item.content) : null,
}
})
}),
reloadPosts = async () => {
try {
isLoading.value.blog = true
isError.value.blog = false
const postData = await getBlogRecentPost()
if (postData !== null) {
recentPosts.value = postData.data.dataSet
}
}
catch (e) {
isError.value.blog = true
errMsg.value.blog = e as object
}
finally {
isLoading.value.blog = false
}
},
reloadActivities = async () => {
try {
isLoading.value.git = true
isError.value.git = false
recentActivities.value = await getActivity()
}
catch (e) {
isError.value.git = true
errMsg.value.git = e as object
}
finally {
isLoading.value.git = false
}
} }
})
const getActivity = get<IActivity[], any>('/git/api/v1/users/cantyonion/activities/feeds', {
headers: {
Authorization: ` ${config.public.gitApiKey}`
},
params: {
limit: 10,
}
})
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 (!Array.isArray(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
isError.value.blog = false
const postData = await getBlogRecentPost()
if (postData !== null)
recentPosts.value = postData.data.dataSet
} catch (e) {
isError.value.blog = true
errMsg.value.blog = e as object
}
finally {
isLoading.value.blog = false
}
}
const reloadActivities = async () => {
try {
isLoading.value.git = true
isError.value.git = false
recentActivities.value = await getActivity()
} catch (e) {
isError.value.git = true
errMsg.value.git = e as object
}
finally {
isLoading.value.git = false
}
}
await Promise.allSettled([reloadPosts(), reloadActivities()]) await Promise.allSettled([reloadPosts(), reloadActivities()])
@ -99,67 +100,39 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="main"> <!-- 个人介绍 -->
<!-- 个人介绍 --> <card-full-screen-intro-card/>
<card-full-screen-intro-card />
<div class="container mx-auto xl:max-w-screen-xl"> <div class="container mx-auto xl:max-w-screen-xl">
<!-- 博客部分 --> <!-- 博客部分 -->
<section class="w-full px-4 sm:px-0"> <section class="w-full px-4 sm:px-0">
<!-- 标题部分 --> <!-- 标题部分 -->
<card-title <card-title :icon="['fas', 'blog']" title="最近文章"/>
:icon="['fas', 'blog']"
title="最近文章"
/>
<card-section-card <card-section-card :empty="!hasPosts" :error="isError.blog" :loading="isLoading.blog" @reload="reloadPosts" :err-msg="errMsg.blog">
:empty="!hasPosts" <template v-slot:default>
:error="isError.blog" <div class="grid w-full grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
:loading="isLoading.blog" <card-article-card v-for="item in postsData" :key="item.cid" :post="item"/>
:err-msg="errMsg.blog" </div>
@reload="reloadPosts" </template>
> </card-section-card>
<template #default> </section>
<div class="grid w-full grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
<card-article-card
v-for="item in postsData"
:key="item.cid"
:post="item"
/>
</div>
</template>
</card-section-card>
</section>
<!-- Git部分 --> <!-- Git部分 -->
<section class="my-4 w-full px-4 sm:px-0"> <section class="my-4 w-full px-4 sm:px-0">
<card-title <card-title :icon="['fas', 'code']" title="最近活动"/>
:icon="['fas', 'code']"
title="最近活动"
/>
<card-section-card <card-section-card :empty="!hasActivities" :error="isError.git" :loading="isLoading.git" @reload="reloadActivities" :err-msg="errMsg.git">
:empty="!hasActivities" <template v-slot:default>
:error="isError.git" <div class="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
:loading="isLoading.git" <card-git-card v-for="item in activitiesData" :key="item.id" :commit="item"/>
:err-msg="errMsg.git" </div>
@reload="reloadActivities" </template>
> </card-section-card>
<template #default> </section>
<div class="w-full rounded-xl bg-white divide-y divide-dashed">
<card-git-card
v-for="item in activitiesData"
:key="item.id"
:commit="item"
/>
</div>
</template>
</card-section-card>
</section>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
maintenance: true, maintenance: true
}) })
</script> </script>
<template> <template>
<NuxtWelcome /> <NuxtWelcome/>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
maintenance: true, maintenance: true
}) })
</script> </script>
<template> <template>
<NuxtWelcome /> <NuxtWelcome/>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

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

View File

@ -1,23 +1,48 @@
import md, { type Options } from 'markdown-it' import md, { type Options } from 'markdown-it'
// @ts-ignore
import mm from 'markdown-it-mathjax3' import mm from 'markdown-it-mathjax3'
const options: Options = { const options: Options = {
breaks: true, // Enable HTML tags in source
html: true, html: true,
langPrefix: 'language-',
linkify: true, // Use '/' to close single tags (<br />).
quotes: '“”‘’', // This is only for full CommonMark compatibility.
typographer: true, xhtmlOut: true,
xhtmlOut: true,
// Convert '\n' in paragraphs into <br>
breaks: true,
// CSS language prefix for fenced blocks. Can be
// useful for external highlighters.
langPrefix: 'language-',
// Autoconvert URL-like text to links
linkify: true,
// Enable some language-neutral replacement + quotes beautification
// For the full list of replacements, see https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/replacements.mjs
typographer: true,
// Double + single quotes replacement pairs, when typographer enabled,
// and smartquotes on. Could be either a String or an Array.
//
// For example, you can use '«»„“' for Russian, '„“‚‘' for German,
// and ['«\xA0', '\xA0»', '\xA0', '\xA0'] for French (including nbsp).
quotes: '“”‘’',
// Highlighter function. Should return escaped HTML,
// or '' if the source string is not changed and should be escaped externally.
// If result starts with <pre... internal wrapper is skipped.
highlight: function (/*str, lang*/) { return ''; }
} }
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const render = md(options) const render = md(options)
.use(mm, { .use(mm, {
}) })
return { return {
provide: { provide: {
mdRender: render, mdRender: render
}, }
} }
}) })

View File

@ -1,17 +1,19 @@
import mitt from 'mitt' import mitt from 'mitt'
interface Events { type Events = {
openPost: IPost openPost: IPost
startLoading: boolean startLoading: boolean
eventBus: INotification eventBus: INotification
} }
export default defineNuxtPlugin(() => {
const emitter = mitt<Events>()
return {
provide: { export default defineNuxtPlugin(() => {
mitt: emitter, const emitter = mitt<Events>()
},
} return {
}) provide: {
mitt: emitter
}
}
})

View File

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

View File

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

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

@ -0,0 +1,69 @@
declare interface IBlogResponse<T> {
status: string
message: string
data: T
}
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
}
declare interface IPostFull extends IPost {
text: string
}

View File

@ -1,72 +0,0 @@
declare global {
interface IBlogResponse<T> {
status: string
message: string
data: T
}
interface IPostsData {
page: number
pageSize: number
pages: number
count: number
dataSet: []
}
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: unknown
}
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
}
interface IDate {
timeStamp: number
year: string
month: string
day: string
}
interface IPostFull extends IPost {
text: 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
}

View File

@ -1,37 +0,0 @@
declare global {
interface IActivity {
id: string
content: null | string | IContent
op_type: 'commit_repo' | 'create_repo' | 'delete_branch' | 'delete_tag' | 'merge_pull_request'
repo: {
name: string
html_url: string
owner: {
login: string
}
}
created: string
ref_name: string
}
interface IStrictActivity extends IActivity {
content: IContent | null
}
interface IContent {
Commits: ICommit[]
HeadCommit: ICommit
CompareURL: string
Len: number
}
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
}

View File

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

View File

@ -1,7 +1,5 @@
declare global { declare interface INotification {
interface INotification {
level: 'warning' | 'error' | 'info' | 'success' level: 'warning' | 'error' | 'info' | 'success'
title: string title: string
message: string message: string
} }
}

3599
yarn.lock

File diff suppressed because it is too large Load Diff