feat:移植工程
This commit is contained in:
95
src/layout/admin/components/NavBar/index.vue
Normal file
95
src/layout/admin/components/NavBar/index.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<section class="layout-navbar">
|
||||
<div class="left-container">
|
||||
<!-- 控制菜单栏的显示 -->
|
||||
<hamburger
|
||||
id="hamburger-container"
|
||||
:is-active="sidebar.opened"
|
||||
class="hamburger-container"
|
||||
@toggleClick="toggleSideBar" />
|
||||
</div>
|
||||
|
||||
<h3 class="title">Sweet Hut</h3>
|
||||
|
||||
<!-- 右上角菜单栏 -->
|
||||
<div class="right-container">
|
||||
<!-- <div class="note-wrapper">-->
|
||||
<!-- <el-tooltip effect="dark" content="警告信息" placement="bottom">-->
|
||||
<!-- <el-badge is-dot class="item">-->
|
||||
<!-- <svg-icon icon-class="danger" />-->
|
||||
<!-- </el-badge>-->
|
||||
<!-- </el-tooltip>-->
|
||||
|
||||
<!-- <el-tooltip effect="dark" content="待办事项" placement="bottom">-->
|
||||
<!-- <el-badge is-dot class="item">-->
|
||||
<!-- <svg-icon icon-class="todo" @click="viewTodoList"/>-->
|
||||
<!-- </el-badge>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 下拉框组件 -->
|
||||
<el-dropdown>
|
||||
<div class="avatar-wrapper">
|
||||
<svg-icon name="admin-user" class="user-avatar" style="color: #0d9488;"/>
|
||||
<span class="username">管理员</span>
|
||||
<el-icon><caret-bottom /></el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 下拉框菜单 -->
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="updatePasswordClick">
|
||||
<span style="display:block;">修改密码</span>
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item @click="logoutClick">
|
||||
<span style="display:block;">退出登录</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Hamburger, SvgIcon } from 'vue3-common'
|
||||
import 'vue3-common/styles/Hamburger.css'
|
||||
import { CaretBottom } from '@element-plus/icons-vue'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/store/app'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const sidebar = computed(() => appStore.sidebar)
|
||||
|
||||
/**
|
||||
* 侧边栏控制
|
||||
*/
|
||||
const toggleSideBar = () => {
|
||||
appStore.toggleSideBar()
|
||||
}
|
||||
|
||||
const viewTodoList = () => {
|
||||
console.log('待办事项')
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
const updatePasswordClick = () => {
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
const logoutClick = () => {
|
||||
// userStore.removeAll()
|
||||
router.push('/login')
|
||||
// 刷新页面 否则会出现切换用户 丢失动态路由的现象
|
||||
// location.reload()
|
||||
}
|
||||
</script>
|
||||
39
src/layout/admin/components/SideBar/Logo.vue
Normal file
39
src/layout/admin/components/SideBar/Logo.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div :class="{'collapse':collapse}">
|
||||
<!-- 跳转到首页 -->
|
||||
<transition name="sidebarLogoFade">
|
||||
<!-- 根据collapse控制Logo组件,即是否显示标题 -->
|
||||
<router-link v-if="collapse" key="collapse" class="logo" to="/admin/dashboard">
|
||||
<!-- <img :src="logoInfo.logo" alt="logo">-->
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="logo" to="/admin/dashboard">
|
||||
<img :src="getAssetsImageFile('logo/vue.png')"
|
||||
alt="logo">
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue'
|
||||
import { getAssetsImageFile } from '@/utils'
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
require: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 动画样式
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-enter,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
23
src/layout/admin/components/SideBar/index.vue
Normal file
23
src/layout/admin/components/SideBar/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<aside class="layout-sidebar">
|
||||
<logo :collapse="isCollapse" class="layout-logo"/>
|
||||
|
||||
<menu-list :route="useRoute()"
|
||||
:routers="adminRoutes"
|
||||
:collapse="isCollapse"
|
||||
class="layout-menu"/>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Logo from './Logo.vue'
|
||||
import { MenuList } from 'vue3-common'
|
||||
import adminRoutes from '@/router/admin'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAppStore } from '@/store/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isCollapse = computed(() => !appStore.sidebar.opened)
|
||||
</script>
|
||||
|
||||
37
src/layout/admin/index.vue
Normal file
37
src/layout/admin/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<section :class="classObj" class="layout-admin">
|
||||
<side-bar/>
|
||||
|
||||
<section class="layout-main">
|
||||
<nav-bar/>
|
||||
|
||||
<section class="app-main">
|
||||
<router-view />
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SideBar from './components/SideBar/index.vue'
|
||||
import NavBar from './components/NavBar/index.vue'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/store/app'
|
||||
|
||||
const app = useAppStore()
|
||||
|
||||
const classObj = computed(() => {
|
||||
// 控制侧边栏的显示状态
|
||||
return {
|
||||
hideSidebar: !app.sidebar.opened,
|
||||
openSidebar: app.sidebar.opened,
|
||||
withoutAnimation: app.sidebar.withoutAnimation,
|
||||
mobile: app.device === 'mobile'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@/styles/admin.layout.module.scss';
|
||||
</style>
|
||||
20
src/layout/blog/components/FooterBar/index.vue
Normal file
20
src/layout/blog/components/FooterBar/index.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<footer class="blog-site-footer">
|
||||
<span>© {{ new Date(startDay).getFullYear() }}-{{ new Date().getFullYear() }} Cxx</span>
|
||||
<span>本网站已安全运行 {{ getBlogRunTotalTime(startDay) }}</span>
|
||||
<span>博客全站共 {{ formatBlogWordCount(blogStore.blogStats.wordCount) }} 字</span>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBlogStore } from '@/store/blog.ts'
|
||||
import { formatBlogWordCount, getBlogRunTotalTime } from '@/utils/blog/blogUtil.ts'
|
||||
|
||||
const startDay = '2025-01-07'
|
||||
|
||||
const blogStore = useBlogStore()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "@/styles/blog/blog.footbar.module.scss";
|
||||
</style>
|
||||
30
src/layout/blog/components/Sidebar/BlogMenu.vue
Normal file
30
src/layout/blog/components/Sidebar/BlogMenu.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<el-menu router class="blog-menu-container">
|
||||
<el-menu-item index="/blog/overview">
|
||||
<i class="fa fa-fw fa-home" />
|
||||
<span class="menu-title">首页</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/blog/category">
|
||||
<i class="fa fa-fw fa-th" />
|
||||
<span class="menu-title">分类</span>
|
||||
<span class="value">{{ blogStore.blogStats.categoryCount }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/blog/archive">
|
||||
<i class="fa fa-fw fa-archive" />
|
||||
<span class="menu-title">归档</span>
|
||||
<span class="value">{{ blogStore.blogStats.blogCount }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item>
|
||||
<i class="fa fa-search fa-fw" />
|
||||
<span class="menu-title">搜索</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBlogStore } from '@/store/blog.ts'
|
||||
const blogStore = useBlogStore()
|
||||
</script>
|
||||
82
src/layout/blog/components/Sidebar/BlogSiteBrief.vue
Normal file
82
src/layout/blog/components/Sidebar/BlogSiteBrief.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="blog-brief">
|
||||
<div class="avatar">
|
||||
<img :src="getAssetsImageFile('avatar.jpg')" alt="暂无图片"/>
|
||||
<span>Cxx</span>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="item" @click="viewBlogArchiveClick">
|
||||
<span class="value">{{ blogStore.blogStats.blogCount }}</span>
|
||||
<span class="name">日志</span>
|
||||
</div>
|
||||
|
||||
<div class="item" @click="viewBlogCategoryClick">
|
||||
<span class="value">{{ blogStore.blogStats.categoryCount }}</span>
|
||||
<span class="name">分类</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="social">
|
||||
<div class="item">
|
||||
<i class="fa fab fa-github"></i>
|
||||
<span class="name">Github</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="fa fab fa-weixin"></i>
|
||||
<span class="name">微信</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="latest-blog">
|
||||
<div class="title">
|
||||
<i class="fa fa-history fa-" />
|
||||
<span>近期文章</span>
|
||||
</div>
|
||||
|
||||
<div class="blog-list">
|
||||
<span v-for="(item, index) in blogStore.latestBlogList"
|
||||
:key="index" @click="viewBlogClick(index)">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="blog-time">-->
|
||||
<!-- <blog-time />-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BlogTime from '@/layout/blog/components/Tools/BlogTime.vue'
|
||||
import { getAssetsImageFile } from '@/utils'
|
||||
import { useBlogStore } from '@/store/blog.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const blogStore = useBlogStore()
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* 点击查看博客归档按钮
|
||||
*/
|
||||
const viewBlogArchiveClick = () => {
|
||||
router.push('/blog/archive')
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击查看博客分类按钮
|
||||
*/
|
||||
const viewBlogCategoryClick = () => {
|
||||
router.push('/blog/category')
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击查看博客按钮
|
||||
* @param index 索引
|
||||
*/
|
||||
const viewBlogClick = async (index: number) => {
|
||||
const blogId = blogStore.latestBlogList[index].id
|
||||
await router.push({ name: 'BlogDetail', params: { id: blogId } })
|
||||
}
|
||||
</script>
|
||||
73
src/layout/blog/components/Sidebar/BlogSummary.vue
Normal file
73
src/layout/blog/components/Sidebar/BlogSummary.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="blog-summary">
|
||||
<div v-if="blogSummaryInfo.isBlogDetail" class="blog-summary-tab">
|
||||
<span @click="changeTabClick('Catalog', $event)" class="tab-active">
|
||||
文章目录
|
||||
</span>
|
||||
|
||||
<span @click="changeTabClick('Summary', $event)">
|
||||
站点概览
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<blog-site-brief v-if="blogSummaryInfo.activePanelName === 'Summary'
|
||||
|| !blogSummaryInfo.isBlogDetail"/>
|
||||
<MdCatalog v-if="blogSummaryInfo.activePanelName === 'Catalog'
|
||||
&& blogSummaryInfo.isBlogDetail"
|
||||
editorId="blog-id" :scrollElement="scrollElement"/>
|
||||
|
||||
<div class="scroll-container" @click="back2topClick()">
|
||||
<i class="fa fa-arrow-up"></i>
|
||||
<span>{{ blogStore.progressValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BlogSiteBrief from '@/layout/blog/components/Sidebar/BlogSiteBrief.vue'
|
||||
import { MdCatalog } from 'md-editor-v3'
|
||||
import { setActiveClass } from '@/utils'
|
||||
import { useBlogStore } from '@/store/blog.ts'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const blogStore = useBlogStore()
|
||||
const router = useRouter()
|
||||
|
||||
const scrollElement = document.documentElement
|
||||
|
||||
const blogSummaryInfo = reactive({
|
||||
activePanelName: 'Catalog',
|
||||
isBlogDetail: false
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听路由变化 如果当前是查看博客 则需要显示博客目录
|
||||
*/
|
||||
watch(() => router.currentRoute.value.path, (path) => {
|
||||
blogSummaryInfo.isBlogDetail = path.includes('/blog/detail')
|
||||
if (blogSummaryInfo.isBlogDetail) {
|
||||
blogSummaryInfo.activePanelName = 'Catalog'
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
/**
|
||||
* 切换tab标签
|
||||
* @param panelName panel名称
|
||||
* @param e 点击事件
|
||||
*/
|
||||
const changeTabClick = (panelName: string, e: Event) => {
|
||||
blogSummaryInfo.activePanelName = panelName
|
||||
const element = e.target as Element
|
||||
setActiveClass('blog-summary-tab', 'span', element, 'tab-active')
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击返回顶部按钮
|
||||
*/
|
||||
const back2topClick = () => {
|
||||
document.body.scrollTop = document.documentElement.scrollTop = 0
|
||||
}
|
||||
</script>
|
||||
15
src/layout/blog/components/Sidebar/BlogTitle.vue
Normal file
15
src/layout/blog/components/Sidebar/BlogTitle.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="title-container">
|
||||
<span class="title">{{ blogTitleInfo.title }}</span>
|
||||
<span class="sub-title">{{ blogTitleInfo.subTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const blogTitleInfo = reactive({
|
||||
title: 'NJCxx0822',
|
||||
subTitle: '不要因为别人5%的负面评价而否定自己100%的努力。'
|
||||
})
|
||||
</script>
|
||||
28
src/layout/blog/components/Sidebar/index.vue
Normal file
28
src/layout/blog/components/Sidebar/index.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<aside class="blog-aside">
|
||||
<div class="top-container card-container">
|
||||
<blog-title />
|
||||
<blog-menu />
|
||||
</div>
|
||||
|
||||
<blog-summary class="card-container"/>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BlogTitle from '@/layout/blog/components/Sidebar/BlogTitle.vue'
|
||||
import BlogMenu from '@/layout/blog/components/Sidebar/BlogMenu.vue'
|
||||
import BlogSummary from '@/layout/blog/components/Sidebar/BlogSummary.vue'
|
||||
import { useBlogStore } from '@/store/blog.ts'
|
||||
import { onMounted } from 'vue'
|
||||
const blogStore = useBlogStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await blogStore.queryBlogStats()
|
||||
await blogStore.queryLatestBlog()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/styles/blog/blog.sidebar.module.scss";
|
||||
</style>
|
||||
123
src/layout/blog/components/Tools/BlogCube.vue
Normal file
123
src/layout/blog/components/Tools/BlogCube.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="cube-container">
|
||||
<div class="cube">
|
||||
<div class="front">
|
||||
<span>欢迎光临</span>
|
||||
</div>
|
||||
|
||||
<div class="back">
|
||||
<span>❤</span>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<span>Cxx0822</span>
|
||||
</div>
|
||||
|
||||
<div class="left">
|
||||
<span>请多关照</span>
|
||||
</div>
|
||||
|
||||
<div class="top">
|
||||
<span>❤</span>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<span>❤</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cube-container {
|
||||
width: 50px;
|
||||
height: 60px;
|
||||
margin: 0 auto;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
-webkit-perspective: 1000px;
|
||||
perspective: 1000px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.cube {
|
||||
width: 0; /* 大角度旋转 */
|
||||
position: absolute;
|
||||
-webkit-transform-style: preserve-3d;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(-15deg) rotateY(-20deg) translateZ(-100px);
|
||||
-webkit-transform-origin: center center -100px;
|
||||
transform-origin: center center -100px;
|
||||
-webkit-animation: around 5s cubic-bezier(0.94, -0.6, 0.45, 1.31) infinite;
|
||||
animation: around 5s cubic-bezier(0.94, -0.6, 0.45, 1.31) infinite;
|
||||
|
||||
div {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: block;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
|
||||
span {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 45%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.front {
|
||||
transform: rotateY(0deg) translateZ(25px);
|
||||
background-color: rgba(0, 191, 255, 0.7);
|
||||
border: 1px solid rgba(0, 191, 255, 0.7);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateX(180deg) translateZ(25px);
|
||||
background-color: rgba(124, 252, 0, 0.7);
|
||||
border: 1px solid rgba(124, 252, 0, 0.7);
|
||||
}
|
||||
|
||||
.left {
|
||||
transform: rotateY(-90deg) translateZ(25px);
|
||||
background-color: rgba(255, 215, 0, 0.7);
|
||||
border: 1px solid rgba(255, 215, 0, 0.7);
|
||||
}
|
||||
.right {
|
||||
transform: rotateY(90deg) translateZ(25px);
|
||||
background-color: rgba(255, 69, 0, 0.7);
|
||||
border: 1px solid rgba(255, 69, 0, 0.7);
|
||||
}
|
||||
|
||||
.top {
|
||||
transform: rotateX(90deg) translateZ(25px);
|
||||
background-color: rgba(255, 0, 157, 0.7);
|
||||
border: 1px solid rgba(255, 0, 157, 0.7);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
transform: rotateX(-90deg) translateZ(25px);
|
||||
background-color: rgba(184, 111, 220, 0.7);
|
||||
border: 1px solid rgba(184, 111, 220, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes around {
|
||||
100% {
|
||||
transform: rotateX(-15deg) rotateY(-380deg) translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes around {
|
||||
100% {
|
||||
transform: rotateX(-15deg) rotateY(-380deg) translateZ(-100px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
src/layout/blog/components/Tools/BlogTime.vue
Normal file
29
src/layout/blog/components/Tools/BlogTime.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<canvas id="time-canvas"></canvas>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { TimeCanvas } from '@/utils/blog/timeCanvas'
|
||||
|
||||
let timeInterval: any = null
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = document.getElementById('time-canvas') as HTMLCanvasElement
|
||||
canvas.width = canvas.parentElement?.offsetWidth as number
|
||||
canvas.height = canvas.parentElement?.offsetHeight as number
|
||||
const context = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||
const timeCanvas = new TimeCanvas(context, canvas.width, canvas.height)
|
||||
|
||||
timeInterval = setInterval(() => {
|
||||
// 清空整个Canvas,重新绘制内容
|
||||
context.clearRect(0, 0, context.canvas.width, context.canvas.height)
|
||||
timeCanvas.drawDatetime()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timeInterval)
|
||||
})
|
||||
|
||||
</script>
|
||||
196
src/layout/blog/components/Tools/index.vue
Normal file
196
src/layout/blog/components/Tools/index.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="blog-tools">
|
||||
<a class="book-mark-link fa fa-bookmark"/>
|
||||
|
||||
<a href="https://github.com/Cxx0822" class="github-corner" aria-label="View source on GitHub" target="_blank">
|
||||
<svg width="80" height="80" viewBox="0 0 250 250"
|
||||
style="fill:#FD6C6C; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
|
||||
aria-hidden="true">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||
<path
|
||||
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6
|
||||
C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3
|
||||
C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
|
||||
fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
|
||||
<path
|
||||
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6
|
||||
C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0
|
||||
C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1
|
||||
C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4
|
||||
C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9
|
||||
C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5
|
||||
C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5
|
||||
C139.8,137.7 141.6,141.9 141.8,141.8 Z"
|
||||
fill="currentColor" class="octo-body"></path>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- <vue-particles id="particles" :options="particlesOptions"/>-->
|
||||
<progress class="blog-progress" id="blog_progress" value="0" />
|
||||
<blog-cube/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BlogCube from '@/layout/blog/components/Tools/BlogCube.vue'
|
||||
import { createHeartDiv } from '@/utils/blog/blogUtil.ts'
|
||||
import { Ribbon } from '@/utils/blog/ribbon'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useBlogStore } from '@/store/blog.ts'
|
||||
|
||||
const ribbon = new Ribbon()
|
||||
const blogStore = useBlogStore()
|
||||
|
||||
/**
|
||||
* 处理点击事件
|
||||
* @param event 事件
|
||||
*/
|
||||
const handleClick = (event) => {
|
||||
createHeartDiv(event.clientX, event.clientY)
|
||||
|
||||
if (event.target.classList.contains('ribbon-canvas')) {
|
||||
ribbon.reDraw()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('scroll', () => {
|
||||
const progressBar = document.getElementById('blog_progress') as HTMLProgressElement
|
||||
progressBar.max = document.documentElement.scrollHeight - window.innerHeight
|
||||
progressBar.value = window.scrollY
|
||||
blogStore.progressValue = `${(progressBar.value / progressBar.max * 100).toFixed(0)}%`
|
||||
})
|
||||
|
||||
document.addEventListener('click', handleClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
ribbon.clear()
|
||||
document.removeEventListener('click', handleClick)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/styles/blog/blog.variable.module.scss" as blog;
|
||||
|
||||
.blog-tools {
|
||||
position: absolute;
|
||||
|
||||
.book-mark-link {
|
||||
border-bottom: none;
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: -2px;
|
||||
left: 20px;
|
||||
color: #222;
|
||||
font-size: 26px;
|
||||
transition: .3s;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.github-corner {
|
||||
border-bottom: none;
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: #222;
|
||||
font-size: 26px;
|
||||
transition: .3s;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.github-corner:hover .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out
|
||||
}
|
||||
|
||||
@keyframes octocat-wave {
|
||||
0%, 100% {
|
||||
transform: rotate(0)
|
||||
}
|
||||
20%, 60% {
|
||||
transform: rotate(-25deg)
|
||||
}
|
||||
40%, 80% {
|
||||
transform: rotate(10deg)
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.github-corner:hover .octo-arm {
|
||||
animation: none
|
||||
}
|
||||
.github-corner .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out
|
||||
}
|
||||
}
|
||||
|
||||
#particles {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.heart {
|
||||
position: fixed;
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: red;
|
||||
transform: rotate(45deg);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.heart:after, .heart:before {
|
||||
content: "";
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
background: inherit;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.heart:after {
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.heart:before {
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
100% {
|
||||
transform: translateY(-20px) rotate(45deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-progress {
|
||||
/* Positioning */
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: blog.$theme-color;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.blog-progress::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.blog-progress::-webkit-progress-value {
|
||||
background-color: blog.$theme-color;
|
||||
}
|
||||
|
||||
.blog-progress::-moz-progress-bar {
|
||||
background-color: blog.$theme-color;
|
||||
}
|
||||
</style>
|
||||
23
src/layout/blog/index.vue
Normal file
23
src/layout/blog/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<section class="blog-container" id="blog-container">
|
||||
<tools />
|
||||
|
||||
<sidebar />
|
||||
|
||||
<section>
|
||||
<router-view/>
|
||||
|
||||
<footer-bar />
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tools from '@/layout/blog/components/Tools/index.vue'
|
||||
import Sidebar from '@/layout/blog/components/Sidebar/index.vue'
|
||||
import FooterBar from '@/layout/blog/components/FooterBar/index.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/styles/blog/blog.module.scss";
|
||||
</style>
|
||||
10
src/layout/home/components/HomeTitle.vue
Normal file
10
src/layout/home/components/HomeTitle.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="title-container">
|
||||
<span class="title">{{ appStore.homeTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
9
src/layout/home/components/Navbar/HomeMenu.vue
Normal file
9
src/layout/home/components/Navbar/HomeMenu.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<menu-list :route="useRoute()" :routers="homeRoutes" :mode="'horizontal'" class="layout-menu"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
import { MenuList } from 'vue3-common'
|
||||
import homeRoutes from '@/router/home'
|
||||
</script>
|
||||
43
src/layout/home/components/Navbar/index.vue
Normal file
43
src/layout/home/components/Navbar/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<aside class="layout-navbar">
|
||||
<div class="title-container">
|
||||
|
||||
</div>
|
||||
|
||||
<home-menu />
|
||||
|
||||
<div class="right-container">
|
||||
<home-query />
|
||||
|
||||
<el-dropdown placement="bottom">
|
||||
<el-avatar :src="getAssetsImageFile('avatar.jpg')" ></el-avatar>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="profileClick">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item @click="logoutClick">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HomeMenu from '@/layout/home/components/Navbar/HomeMenu.vue'
|
||||
import HomeQuery from '@/components/Home/HomeQuery.vue'
|
||||
import { getAssetsImageFile } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const profileClick = () => {
|
||||
router.push('/home/profile')
|
||||
}
|
||||
|
||||
const logoutClick = () => {
|
||||
commonElMessageBox('请确认是否退出登录?').then(() => {
|
||||
router.push('/login')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
17
src/layout/home/index.vue
Normal file
17
src/layout/home/index.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<section class="layout-home">
|
||||
<navbar/>
|
||||
|
||||
<section class="layout-main">
|
||||
<router-view />
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Navbar from '@/layout/home/components/Navbar/index.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/styles/home.layout.module.scss";
|
||||
</style>
|
||||
156
src/layout/mobile-home/TabBar.vue
Normal file
156
src/layout/mobile-home/TabBar.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="tab-bar">
|
||||
<router-link to="/mobile-home" @click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-house"></i>
|
||||
<span>主页</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('bill')" to="/mobile-home/bill/data"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>日常</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('bill')" to="/mobile-home/bill/chart"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-chart-pie"></i>
|
||||
<span>统计</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('food')" to="/mobile-home/food/recipe"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-utensils"></i>
|
||||
<span>菜谱</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('food')" to="/mobile-home/food/record"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>记录</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('travel')" to="/mobile-home/travel/spot"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="iconfont icon-jingdian"></i>
|
||||
<span>景点</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('travel')" to="/mobile-home/travel/record"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>记录</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('plan')" to="/mobile-home/plan/calendar"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>日程</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('plan')" to="/mobile-home/plan/list"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('anniversary')" to="/mobile-home/anniversary/list"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
<span>纪念日</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('anniversary')" to="/mobile-home/anniversary/category"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-table-cells"></i>
|
||||
<span>分类</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('period')" to="/mobile-home/period/calendar"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>记录</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('period')" to="/mobile-home/period/setting"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span>设置</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('product')" to="/mobile-home/product/list"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-list-ul"></i>
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('journal')" to="/mobile-home/journal/record"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>记录</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('journal')" to="/mobile-home/journal/timeline"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-timeline"></i>
|
||||
<span>时间轴</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('kpi')" to="/mobile-home/kpi/record"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
<span>记录</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('asset')" to="/mobile-home/asset/list"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-list-ul"></i>
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('ledger')" to="/mobile-home/ledger/list"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-list-ul"></i>
|
||||
<span>清单</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('server')" to="/mobile-home/server/monitor"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-desktop"></i>
|
||||
<span>监控</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('server')" to="/mobile-home/server/file"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-folder"></i>
|
||||
<span>文件</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="path.includes('blog')" to="/mobile-home/blog/overview"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-book-open"></i>
|
||||
<span>列表</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('blog')" to="/mobile-home/blog/category"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-table-cells"></i>
|
||||
<span>分类</span>
|
||||
</router-link>
|
||||
<router-link v-if="path.includes('blog')" to="/mobile-home/blog/archive"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-box-archive"></i>
|
||||
<span>归档</span>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/mobile-home/profile/index"
|
||||
@click="routerClick()" class="item">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span>我的</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const path = computed(() => router.currentRoute.value.path)
|
||||
|
||||
const routerClick = () => {
|
||||
appStore.isShowMobileBack = false
|
||||
}
|
||||
</script>
|
||||
72
src/layout/mobile-home/index.vue
Normal file
72
src/layout/mobile-home/index.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<section class="layout-mobile-home">
|
||||
<div class="title-bar">
|
||||
<div class="left-container">
|
||||
<el-button v-if="appStore.isShowMobileBack"
|
||||
type="info" :icon="Back" size="small"
|
||||
circle color="#FFFFFF"
|
||||
@click="goBackClick" />
|
||||
<div v-else class="logo-container">
|
||||
<img :src="getAssetsImageFile('logo/logo.png')" alt="暂无图片" class="logo"/>
|
||||
<span>Sweet Hut</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="title-container">{{ title }}</span>
|
||||
|
||||
<div class="right-container">
|
||||
<svg-icon v-if="appStore.isWebSocket"
|
||||
name="home-websocket" style="color: #FFFFFF;"></svg-icon>
|
||||
<el-button type="info" :icon="Setting" size="small"
|
||||
circle color="#FFFFFF" @click="openSettingClick"/>
|
||||
|
||||
<el-drawer
|
||||
v-model="appStore.isShowSetting"
|
||||
title="系统设置" :show-close="false"
|
||||
size="50%" class="setting-drawer">
|
||||
<mobile-setting />
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-view />
|
||||
|
||||
<tab-bar />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { SvgIcon } from 'vue3-common'
|
||||
import MobileSetting from '@/components/Mobile/MobileSetting.vue'
|
||||
import { Back } from '@element-plus/icons-vue'
|
||||
import { Setting } from '@element-plus/icons-vue'
|
||||
import TabBar from '@/layout/mobile-home/TabBar.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { computed } from 'vue'
|
||||
import { getAssetsImageFile } from '@/utils'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const title = computed(() => router.currentRoute.value.meta.title)
|
||||
|
||||
const goBackClick = () => {
|
||||
appStore.isShowMobileBack = false
|
||||
history.back()
|
||||
}
|
||||
|
||||
const openSettingClick = () => {
|
||||
appStore.isShowSetting = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/styles/mobile.home.layout.module.scss";
|
||||
|
||||
.setting-drawer {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user