更新 package.json,添加 typescript-eslint 依赖;更新 pnpm-lock.yaml;优化多个组件的代码风格,统一引号使用,提升代码可读性和一致性。

This commit is contained in:
宇阳
2025-07-22 15:51:23 +08:00
parent e11e452b43
commit dc95fd75a3
116 changed files with 5430 additions and 3816 deletions

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"singleQuote": true,
"jsxSingleQuote": false,
"printWidth": 999,
"semi": true,
"bracketSpacing": true,
"arrowParens": "always"
}

40
eslint.config.mjs Normal file
View File

@@ -0,0 +1,40 @@
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import { defineConfig } from 'eslint/config';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default defineConfig([
{
files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
plugins: { js },
extends: ['js/recommended'],
},
{
files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
languageOptions: { globals: globals.browser },
},
{
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
},
tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-vars': 'off', // 关闭未使用变量的检查
'react-refresh/only-export-components': 'off',
'react/display-name': 'off',
// 约束js使用单引号允许jsx双引号
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
'jsx-quotes': ['error', 'prefer-double'],
'react-hooks/exhaustive-deps': 'off',
'react/react-in-jsx-scope': 'off',
},
},
]);

View File

@@ -30,6 +30,8 @@
"dayjs": "^1.11.13",
"dompurify": "^3.2.6",
"echarts": "^5.5.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"feed": "^4.2.2",
"framer-motion": "^12.7.4",
"github-markdown-css": "^5.6.1",
@@ -64,15 +66,20 @@
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/aos": "^3.0.7",
"@types/canvas-confetti": "^1.6.4",
"@types/markdown-navbar": "^1.4.4",
"@types/node": "^20",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"eslint": "^9.31.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
"typescript": "^5",
"typescript-eslint": "^8.38.0"
},
"overrides": {
"@types/react": "19.0.10",

1914
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,8 @@ import { Photo, Cate } from '@/types/app/album'
import Request from '@/utils/request'
// 分页获取相册列表
export const getAlbumCatePagingAPI = (page: number = 1, size: number = 10) => Request<Paginate<Cate[]>>("POST", `/album/cate/paging?page=${page}&size=${size}`)
export const getAlbumCatePagingAPI = (page: number = 1, size: number = 10) => Request<Paginate<Cate[]>>('POST', `/album/cate/paging?page=${page}&size=${size}`)
// 获取指定相册中的所有照片
export const getImagesByAlbumIdAPI = (id: number, page: number = 1, size: number = 10) =>
Request<Paginate<Photo[]>>("GET", `/album/cate/${id}/images?page=${page}&size=${size}`)
Request<Paginate<Photo[]>>('GET', `/album/cate/${id}/images?page=${page}&size=${size}`)

View File

@@ -1,32 +1,32 @@
import Request from "@/utils/request";
import { Article } from "@/types/app/article";
import Request from '@/utils/request';
import { Article } from '@/types/app/article';
// 获取指定文章数据
export const getArticleDataAPI = async (id: number, password?: string) => {
return await Request<Article>("GET", `/article${!password ? `/${id}` : `/${id}?password=${password}`}`);
return await Request<Article>('GET', `/article${!password ? `/${id}` : `/${id}?password=${password}`}`);
}
// 获取文章列表
export const getArticleListAPI = async () => {
return await Request<Article[]>("POST", `/article/list`);
return await Request<Article[]>('POST', `/article/list`);
}
// 分页获取文章数据
export const getArticlePagingAPI = async (data: QueryData) => {
return await Request<Paginate<Article[]>>("POST", `/article/paging?page=${data.pagination?.page}&size=${data.pagination?.size ? data.pagination?.size : 8}`, data.query);
return await Request<Paginate<Article[]>>('POST', `/article/paging?page=${data.pagination?.page}&size=${data.pagination?.size ? data.pagination?.size : 8}`, data.query);
}
// 获取随机文章列表
export const getRandomArticleListAPI = async () => {
return await Request<Article[]>("GET", "/article/random");
return await Request<Article[]>('GET', '/article/random');
}
// 获取推荐文章列表
export const getRecommendedArticleListAPI = async () => {
return await Request<Article[]>("GET", "/article/hot");
return await Request<Article[]>('GET', '/article/hot');
}
// 递增浏览量
export const recordViewAPI = async (id: number) => {
return await Request<void>("GET", `/article/view/${id}`);
return await Request<void>('GET', `/article/view/${id}`);
}

View File

@@ -4,15 +4,15 @@ import Request from '@/utils/request'
// 获取分类列表
export const getCateListAPI = async () => {
return await Request<Cate[]>("POST", "/cate/list")
return await Request<Cate[]>('POST', '/cate/list')
}
// 获取指定分类中的所有文章
export const getCateArticleListAPI = async (id: number, page: number) => {
return await Request<Paginate<Article[]>>("GET", `/article/cate/${id}?page=${page}`)
return await Request<Paginate<Article[]>>('GET', `/article/cate/${id}?page=${page}`)
}
// 获取每个分类的文章数量
export const getCateArticleCountAPI = async () => {
return await Request<CateArticleCount[]>("GET", "/cate/article/count")
return await Request<CateArticleCount[]>('GET', '/cate/article/count')
}

View File

@@ -1,22 +1,22 @@
import Request from "@/utils/request";
import { Comment } from "@/types/app/comment";
import Request from '@/utils/request';
import { Comment } from '@/types/app/comment';
// 新增评论
export const addCommentDataAPI = async (data: Comment) => {
return await Request("POST", `/comment`, data);
return await Request('POST', `/comment`, data);
}
// 获取评论列表
export const getCommentListAPI = async () => {
return await Request<Comment[]>("POST", `/comment/list`);
return await Request<Comment[]>('POST', `/comment/list`);
}
// 分页获取评论数据
export const getCommentPagingAPI = async () => {
return await Request<Paginate<Comment[]>>("POST", `/comment/paging`);
return await Request<Paginate<Comment[]>>('POST', `/comment/paging`);
}
// 获取当前文章中所有评论
export const getArticleCommentListAPI = async (articleId: number, paginate: Page) => {
return await Request<Paginate<Comment[]>>("POST", `/comment/article/${articleId}?page=${paginate.page}&pageSize=${paginate.size}`);
return await Request<Paginate<Comment[]>>('POST', `/comment/article/${articleId}?page=${paginate.page}&pageSize=${paginate.size}`);
}

View File

@@ -1,15 +1,15 @@
import Request from "@/utils/request";
import { Config } from "@/types/app/config";
import Request from '@/utils/request';
import { Config } from '@/types/app/config';
// 获取网站配置
export const getWebConfigDataAPI = <T>(name: string) => Request<T>("GET", `/web_config/name/${name}`)
export const getWebConfigDataAPI = <T>(name: string) => Request<T>('GET', `/web_config/name/${name}`)
// 修改网站配置
export const editWebConfigDataAPI = (name: string, data: object) => Request<Config>("PATCH", `/web_config/json/name/${name}`, { data })
export const editWebConfigDataAPI = (name: string, data: object) => Request<Config>('PATCH', `/web_config/json/name/${name}`, { data })
// 获取高德地图配置
export const getGaodeMapConfigDataAPI = () => Request("GET", `/env_config/gaode_map`)
export const getGaodeMapConfigDataAPI = () => Request('GET', `/env_config/gaode_map`)
// 根据名称获取页面配置
export const getPageConfigDataByNameAPI = (name: string) => Request<Config>("GET", `/page_config/name/${name}`)
export const getPageConfigDataByNameAPI = (name: string) => Request<Config>('GET', `/page_config/name/${name}`)

View File

@@ -1,7 +1,7 @@
import Request from "@/utils/request";
import { CommentEmail } from "@/types/app/email";
import Request from '@/utils/request';
import { CommentEmail } from '@/types/app/email';
// 发送评论邮件
export const sendCommentEmailAPI = async (data: CommentEmail) => {
return await Request<string>("POST", `/email/comment`, data);
return await Request<string>('POST', `/email/comment`, data);
}

View File

@@ -2,7 +2,7 @@ import Request from '@/utils/request'
import { Footprint } from '@/types/app/footprint'
// 获取足迹
export const getFootprintDataAPI = (id?: number) => Request<Footprint>("GET", `/footprint/${id}`)
export const getFootprintDataAPI = (id?: number) => Request<Footprint>('GET', `/footprint/${id}`)
// 获取足迹列表
export const getFootprintListAPI = (data?: QueryData) => Request<Footprint[]>("POST", "/footprint/list");
export const getFootprintListAPI = () => Request<Footprint[]>('POST', '/footprint/list');

View File

@@ -2,21 +2,21 @@ import Request from '@/utils/request'
import { Record } from '@/types/app/record'
// 新增说说
export const addRecordDataAPI = (data: Record) => Request("POST", "/record", { data })
export const addRecordDataAPI = (data: Record) => Request('POST', '/record', { data })
// 删除说说
export const delRecordDataAPI = (id: number) => Request<Record>("DELETE", `/record/${id}`)
export const delRecordDataAPI = (id: number) => Request<Record>('DELETE', `/record/${id}`)
// 修改说说
export const editRecordDataAPI = (data: Record) => Request<Record>("PATCH", "/record", { data })
export const editRecordDataAPI = (data: Record) => Request<Record>('PATCH', '/record', { data })
// 获取说说
export const getRecordDataAPI = (id?: number) => Request<Record>("GET", `/record/${id}`)
export const getRecordDataAPI = (id?: number) => Request<Record>('GET', `/record/${id}`)
// 获取说说列表
export const getRecordListAPI = (data?: QueryData) => Request<Record[]>("POST", `/record/list`, {
export const getRecordListAPI = (data?: QueryData) => Request<Record[]>('POST', `/record/list`, {
data: { ...data?.query },
})
// 分页获取说说列表
export const getRecordPagingAPI = (data?: QueryData) => Request<Paginate<Record[]>>("POST", `/record/paging?page=${data?.pagination?.page}&size=${data?.pagination?.size ? data.pagination?.size : 8}`)
export const getRecordPagingAPI = (data?: QueryData) => Request<Paginate<Record[]>>('POST', `/record/paging?page=${data?.pagination?.page}&size=${data?.pagination?.size ? data.pagination?.size : 8}`)

View File

@@ -2,12 +2,12 @@ import { Rss } from '@/types/app/rss';
import Request from '@/utils/request';
// 获取订阅数据列表
export const getRssListAPI = (data?: QueryData) => Request<Rss[]>("GET", `/rss/list`, {
export const getRssListAPI = (data?: QueryData) => Request<Rss[]>('GET', `/rss/list`, {
data: { ...data?.query },
})
// 分页获取订阅列表
export const getRssPagingAPI = (data?: QueryData) => Request<Paginate<Rss[]>>("POST", `/rss/paging`, {
export const getRssPagingAPI = (data?: QueryData) => Request<Paginate<Rss[]>>('POST', `/rss/paging`, {
data: { ...data?.query },
params: {
...data?.pagination

View File

@@ -2,7 +2,7 @@ import Request from '@/utils/request'
import { Swiper } from '@/types/app/swiper'
// 获取轮播图
export const getSwiperDataAPI = (id?: number) => Request<Swiper>("GET", `/swiper/${id}`)
export const getSwiperDataAPI = (id?: number) => Request<Swiper>('GET', `/swiper/${id}`)
// 获取轮播图数据列表
export const getSwiperListAPI = () => Request<Swiper[]>("POST", `/swiper/list`)
export const getSwiperListAPI = () => Request<Swiper[]>('POST', `/swiper/list`)

View File

@@ -4,20 +4,20 @@ import { Article } from '@/types/app/article';
// 获取标签列表
export const getTagListAPI = async () => {
return await Request<Tag[]>("POST", `/tag/list`);
return await Request<Tag[]>('POST', `/tag/list`);
}
// 获取标签列表+文章数量统计
export const getTagListWithArticleCountAPI = async () => {
return await Request<Tag[]>("GET", `/tag/article/count`);
return await Request<Tag[]>('GET', `/tag/article/count`);
}
// 获取指定标签中的所有文章
export const getTagArticleListAPI = async (id: number, page: number) => {
return await Request<Paginate<Article[]>>("GET", `/article/tag/${id}?page=${page}`)
return await Request<Paginate<Article[]>>('GET', `/article/tag/${id}?page=${page}`)
}
// 分页获取标签数据
export const getTagPagingAPI = async (data: QueryData) => {
return await Request<Paginate<Tag[]>>("POST", `/tag/paging?page=${data.pagination?.page}&&size=8`, data.query);
return await Request<Paginate<Tag[]>>('POST', `/tag/paging?page=${data.pagination?.page}&&size=8`, data.query);
}

View File

@@ -3,5 +3,5 @@ import Request from '@/utils/request'
// 获取作者信息
export const getUserDataAPI = async () => {
return await Request<User>("GET", "/user/author")
return await Request<User>('GET', '/user/author')
}

View File

@@ -1,22 +1,22 @@
import Request from "@/utils/request";
import { Wall, Cate } from "@/types/app/wall";
import Request from '@/utils/request';
import { Wall, Cate } from '@/types/app/wall';
// 新增留言
export const addWallDataAPI = async (data: Wall) => {
return await Request("POST", `/wall`, data);
return await Request('POST', `/wall`, data);
}
// 获取留言列表
export const getWallListAPI = async () => {
return await Request<Paginate<Wall[]>>("POST", `/wall/paging`);
return await Request<Paginate<Wall[]>>('POST', `/wall/paging`);
}
// 获取留言分类列表
export const getCateListAPI = async () => {
return await Request<Cate[]>("GET", `/wall/cate`);
return await Request<Cate[]>('GET', `/wall/cate`);
}
// 获取当前分类中所有留言
export const getCateWallListAPI = async (cateId: number, page: number, size = 8) => {
return await Request<Paginate<Wall[]>>("POST", `/wall/cate/${cateId}?page=${page}&size=${size}`, undefined, false);
return await Request<Paginate<Wall[]>>('POST', `/wall/cate/${cateId}?page=${page}&size=${size}`, undefined, false);
}

View File

@@ -1,17 +1,17 @@
import Request from "@/utils/request";
import { Web, WebType } from "@/types/app/web";
import Request from '@/utils/request';
import { Web, WebType } from '@/types/app/web';
// 获取网站类型列表
export const getWebTypeListAPI = async () => {
return await Request<WebType[]>("GET", `/link/type`);
return await Request<WebType[]>('GET', `/link/type`);
}
// 获取网站列表
export const getWebListAPI = async () => {
return await Request<Web[]>("POST", `/link/list`, undefined, false);
return await Request<Web[]>('POST', `/link/list`, undefined, false);
}
// 新增网站
export const addWebDataAPI = async (data: Web) => {
return await Request("POST", `/link`, data);
return await Request('POST', `/link`, data);
}

View File

@@ -1,37 +1,37 @@
"use client"
'use client';
import { useEffect, useState, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { IoChevronBack, IoChevronForward } from 'react-icons/io5'
import { BsCalendar } from 'react-icons/bs'
import { Photo } from '@/types/app/album'
import { getImagesByAlbumIdAPI } from '@/api/album'
import Masonry from "react-masonry-css"
import Empty from '@/components/Empty'
import dayjs from 'dayjs'
import "./page.scss"
import { useEffect, useState, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { IoChevronBack, IoChevronForward } from 'react-icons/io5';
import { BsCalendar } from 'react-icons/bs';
import { Photo } from '@/types/app/album';
import { getImagesByAlbumIdAPI } from '@/api/album';
import Masonry from 'react-masonry-css';
import Empty from '@/components/Empty';
import dayjs from 'dayjs';
import './page.scss';
const breakpointColumnsObj = {
default: 4,
1024: 3,
700: 2
700: 2,
};
interface Props {
params: Promise<{ id: number }>;
searchParams: Promise<{ page: number; name: string }>;
};
}
export default function AlbumPage(props: Props) {
const [list, setList] = useState<Photo[]>([])
const [currentPhotoIndex, setCurrentPhotoIndex] = useState<number | null>(null)
const [showModal, setShowModal] = useState(false)
const [isImageLoading, setIsImageLoading] = useState(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
const [albumName, setAlbumName] = useState('')
const [albumId, setAlbumId] = useState<number>(0)
const [list, setList] = useState<Photo[]>([]);
const [currentPhotoIndex, setCurrentPhotoIndex] = useState<number | null>(null);
const [showModal, setShowModal] = useState(false);
const [isImageLoading, setIsImageLoading] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const [albumName, setAlbumName] = useState('');
const [albumId, setAlbumId] = useState<number>(0);
useEffect(() => {
const initData = async () => {
@@ -47,77 +47,77 @@ export default function AlbumPage(props: Props) {
const getImagesByAlbumId = async (id: number, page: number = 1, isLoadMore: boolean = false) => {
try {
setLoading(true)
const response = await getImagesByAlbumIdAPI(id, page)
if (!response) return
setLoading(true);
const response = await getImagesByAlbumIdAPI(id, page);
const { data } = response
if (!response) return;
const { data } = response;
if (isLoadMore) {
setList(prev => [...prev, ...data.result])
setList((prev) => [...prev, ...data.result]);
} else {
setList(data.result)
setList(data.result);
}
setHasMore(data.result.length === 10)
setHasMore(data.result.length === 10);
} catch (error) {
console.error('Failed to fetch images:', error)
console.error('Failed to fetch images:', error);
} finally {
setLoading(false)
setLoading(false);
}
}
};
const handleScroll = useCallback(() => {
if (loading || !hasMore) return
if (loading || !hasMore) return;
const scrollHeight = document.documentElement.scrollHeight
const scrollTop = document.documentElement.scrollTop
const clientHeight = document.documentElement.clientHeight
const scrollHeight = document.documentElement.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
if (scrollHeight - scrollTop - clientHeight < 300) {
setPage(prev => prev + 1)
getImagesByAlbumId(albumId, page + 1, true)
setPage((prev) => prev + 1);
getImagesByAlbumId(albumId, page + 1, true);
}
}, [loading, hasMore, page, albumId])
}, [loading, hasMore, page, albumId]);
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [handleScroll])
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
const openPhoto = async (index: number) => {
setCurrentPhotoIndex(index)
setIsImageLoading(true)
setCurrentPhotoIndex(index);
setIsImageLoading(true);
const img = new Image()
img.src = list[index].image
const img = new Image();
img.src = list[index].image;
await new Promise((resolve) => {
img.onload = () => {
resolve(true)
}
})
resolve(true);
};
});
setIsImageLoading(false)
setShowModal(true)
}
setIsImageLoading(false);
setShowModal(true);
};
const closeModal = () => {
setShowModal(false)
setCurrentPhotoIndex(null)
}
setShowModal(false);
setCurrentPhotoIndex(null);
};
const nextPhoto = () => {
if (currentPhotoIndex !== null) {
setCurrentPhotoIndex((currentPhotoIndex + 1) % list.length)
setCurrentPhotoIndex((currentPhotoIndex + 1) % list.length);
}
}
};
const prevPhoto = () => {
if (currentPhotoIndex !== null) {
setCurrentPhotoIndex((currentPhotoIndex - 1 + list.length) % list.length)
setCurrentPhotoIndex((currentPhotoIndex - 1 + list.length) % list.length);
}
}
};
return (
<>
@@ -131,26 +131,11 @@ export default function AlbumPage(props: Props) {
<Empty info="暂无照片" />
) : (
<>
<Masonry
breakpointCols={breakpointColumnsObj}
className="masonry-grid"
columnClassName="masonry-grid_column"
>
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid" columnClassName="masonry-grid_column">
{list?.map((photo, index) => (
<motion.div
key={photo.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="relative group overflow-hidden rounded-lg shadow-lg mb-6"
onClick={() => openPhoto(index)}
>
<motion.div key={photo.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: index * 0.1 }} className="relative group overflow-hidden rounded-lg shadow-lg mb-6" onClick={() => openPhoto(index)}>
<div className="w-full cursor-pointer">
<img
src={photo.image || "https://images.unsplash.com/photo-1501785888041-af3ef285b470?ixlib=rb-1.2.1&auto=format&fit=crop&w=3840&q=100"}
alt={photo.name}
className="w-full h-auto object-cover transform transition-transform group-hover:scale-110"
/>
<img src={photo.image || 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?ixlib=rb-1.2.1&auto=format&fit=crop&w=3840&q=100'} alt={photo.name} className="w-full h-auto object-cover transform transition-transform group-hover:scale-110" />
</div>
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4">
<h3 className="text-white font-medium text-lg">{photo.name}</h3>
@@ -163,9 +148,7 @@ export default function AlbumPage(props: Props) {
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900"></div>
</div>
)}
{!hasMore && list.length > 0 && (
<div className="text-center text-gray-500 py-4"></div>
)}
{!hasMore && list.length > 0 && <div className="text-center text-gray-500 py-4"></div>}
</>
)}
</div>
@@ -173,21 +156,8 @@ export default function AlbumPage(props: Props) {
{/* 照片查看模态框 */}
<AnimatePresence>
{showModal && currentPhotoIndex !== null && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 backdrop-blur-md bg-black/30 flex items-center justify-center z-50"
onClick={closeModal}
>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className="relative max-w-4xl w-full mx-4"
onClick={(e) => e.stopPropagation()}
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 backdrop-blur-md bg-black/30 flex items-center justify-center z-50" onClick={closeModal}>
<motion.div initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.3, ease: 'easeInOut' }} className="relative max-w-4xl w-full mx-4" onClick={(e) => e.stopPropagation()}>
<div className="relative rounded-2xl overflow-hidden">
{isImageLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
@@ -195,26 +165,15 @@ export default function AlbumPage(props: Props) {
</div>
)}
<motion.div
key={currentPhotoIndex}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className="relative"
>
<img
src={list[currentPhotoIndex].image}
alt={list[currentPhotoIndex].name}
className="w-full h-auto max-h-[80vh] rounded-2xl object-cover"
/>
<motion.div key={currentPhotoIndex} initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} transition={{ duration: 0.3, ease: 'easeInOut' }} className="relative">
<img src={list[currentPhotoIndex].image} alt={list[currentPhotoIndex].name} className="w-full h-auto max-h-[80vh] rounded-2xl object-cover" />
{/* 导航按钮 */}
<button
className="flex justify-center items-center absolute left-4 top-1/2 z-10 -translate-y-1/2 p-2 rounded-full bg-[#fff3] hover:bg-black/50 backdrop-blur-md duration-200"
onClick={(e) => {
e.stopPropagation()
prevPhoto()
e.stopPropagation();
prevPhoto();
}}
>
<IoChevronBack className="w-8 h-8 text-white" />
@@ -223,8 +182,8 @@ export default function AlbumPage(props: Props) {
<button
className="flex justify-center items-center absolute right-4 top-1/2 z-10 -translate-y-1/2 p-2 rounded-full bg-[#fff3] hover:bg-black/10 backdrop-blur-md duration-200"
onClick={(e) => {
e.stopPropagation()
nextPhoto()
e.stopPropagation();
nextPhoto();
}}
>
<IoChevronForward className="w-8 h-8 text-white" />
@@ -232,19 +191,10 @@ export default function AlbumPage(props: Props) {
{/* 照片信息 */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-6">
<motion.div
key={currentPhotoIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<h3 className="text-white text-2xl font-medium mb-2">
{list[currentPhotoIndex].name}
</h3>
<motion.div key={currentPhotoIndex} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: 0.2 }}>
<h3 className="text-white text-2xl font-medium mb-2">{list[currentPhotoIndex].name}</h3>
<p className="text-white/50 leading-relaxed mb-3">
{list[currentPhotoIndex].description}
</p>
<p className="text-white/50 leading-relaxed mb-3">{list[currentPhotoIndex].description}</p>
<div className="flex items-center space-x-2 text-gray-400">
<BsCalendar className="w-4 h-4 text-gray-400" />
@@ -260,5 +210,5 @@ export default function AlbumPage(props: Props) {
</AnimatePresence>
</div>
</>
)
}
);
}

View File

@@ -1,36 +1,36 @@
"use client"
'use client';
import { useEffect, useState } from "react"
import { motion } from "framer-motion"
import { useRouter } from "next/navigation"
import { Cate } from "@/types/app/album"
import { getAlbumCatePagingAPI } from "@/api/album"
import Masonry from "react-masonry-css"
import "./page.scss"
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { Cate } from '@/types/app/album';
import { getAlbumCatePagingAPI } from '@/api/album';
import Masonry from 'react-masonry-css';
import './page.scss';
const breakpointColumnsObj = {
default: 4,
1024: 3,
700: 2
700: 2,
};
export default function AlbumPage() {
const router = useRouter()
const router = useRouter();
const [list, setList] = useState<Cate[]>([])
const [list, setList] = useState<Cate[]>([]);
const getAlbumCatePaging = async () => {
const { data } = await getAlbumCatePagingAPI(1, 9999) || { data: {} as Paginate<Cate[]> }
setList(data.result)
}
const { data } = (await getAlbumCatePagingAPI(1, 9999)) || { data: {} as Paginate<Cate[]> };
setList(data.result);
};
useEffect(() => {
getAlbumCatePaging()
}, [])
getAlbumCatePaging();
}, []);
const handleClick = (data: Cate) => {
router.push(`/album/${data.id}?name=${data.name}`)
}
router.push(`/album/${data.id}?name=${data.name}`);
};
return (
<>
@@ -38,36 +38,17 @@ export default function AlbumPage() {
<meta name="description" content="📷 照片墙" />
<div className="container mx-auto px-4 py-8 pt-[90px]">
<Masonry
breakpointCols={breakpointColumnsObj}
className="masonry-grid mb-12"
columnClassName="masonry-grid_column"
>
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid mb-12" columnClassName="masonry-grid_column">
{list.map((cate, index) => (
<motion.div
key={cate.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="mb-6"
>
<div
className="relative group overflow-hidden rounded-lg shadow-lg cursor-pointer"
onClick={() => handleClick(cate)}
>
<motion.div key={cate.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: index * 0.1 }} className="mb-6">
<div className="relative group overflow-hidden rounded-lg shadow-lg cursor-pointer" onClick={() => handleClick(cate)}>
{/* 图片容器 */}
<div className="aspect-w-1 aspect-h-1 w-full">
<img
src={cate.cover || "https://images.unsplash.com/photo-1501785888041-af3ef285b470?ixlib=rb-1.2.1&auto=format&fit=crop&w=3840&q=100"}
alt={cate.name}
className="w-full h-full object-cover transform transition-transform group-hover:scale-110"
/>
<img src={cate.cover || 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?ixlib=rb-1.2.1&auto=format&fit=crop&w=3840&q=100'} alt={cate.name} className="w-full h-full object-cover transform transition-transform group-hover:scale-110" />
</div>
{/* 分类标签 */}
<div className="absolute top-4 left-4 bg-black/20 backdrop-blur-md text-white px-3 py-1 rounded-full text-sm">
{cate.name}
</div>
<div className="absolute top-4 left-4 bg-black/20 backdrop-blur-md text-white px-3 py-1 rounded-full text-sm">{cate.name}</div>
{/* 标题遮罩 */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4">
@@ -79,5 +60,5 @@ export default function AlbumPage() {
</Masonry>
</div>
</>
)
}
);
}

View File

@@ -12,7 +12,7 @@ import { getUserDataAPI } from '@/api/user'
import { getRecordPagingAPI } from '@/api/record';
export async function GET() {
const { data: { value: web } } = (await getWebConfigDataAPI<{ value: Web }>("web")) || { data: { value: {} as Web } };
const { data: { value: web } } = (await getWebConfigDataAPI<{ value: Web }>('web')) || { data: { value: {} as Web } };
const { data: user } = await getUserDataAPI() || { data: {} as User }
const { data: article } = await getArticlePagingAPI({ pagination: { page: 1, size: 8 } }) || { data: {} as Paginate<Article[]> }
const { data: record } = await getRecordPagingAPI({ pagination: { page: 1, size: 8 } }) || { data: {} as Paginate<Record[]> }
@@ -34,7 +34,7 @@ export async function GET() {
copyright: 'ThriveX 现代化博客管理系统',
updated: new Date(),
generator: '为爱发电',
docs: "https://github.com/LiuYuYang01/ThriveX-Blog",
docs: 'https://github.com/LiuYuYang01/ThriveX-Blog',
author: {
name: user?.name,
email: user?.email,
@@ -52,7 +52,7 @@ export async function GET() {
description: 'title' in item ? item?.description : item?.content,
content: item?.content,
copyright: 'ThriveX 现代化博客管理系统',
date: new Date(+item?.createTime!)
date: new Date(+item?.createTime)
});
});

View File

@@ -1,20 +1,20 @@
import { getArticleDataAPI, recordViewAPI } from '@/api/article'
import { getArticleDataAPI, recordViewAPI } from '@/api/article';
import Starry from "@/components/Starry"
import Slide from "@/components/Slide"
import Starry from '@/components/Starry';
import Slide from '@/components/Slide';
import Tag from "../components/Tag";
import Copyright from "../components/Copyright";
import UpAndDown from "../components/UpAndDown";
import Comment from "../components/Comment";
import MD from "../components/MD";
import Summary from "../components/Summary";
import Nav from "../components/Nav";
import Tag from '../components/Tag';
import Copyright from '../components/Copyright';
import UpAndDown from '../components/UpAndDown';
import Comment from '../components/Comment';
import MD from '../components/MD';
import Summary from '../components/Summary';
import Nav from '../components/Nav';
import { IoMdPricetags } from "react-icons/io";
import { FaHotjar } from "react-icons/fa";
import { AiOutlineComment } from "react-icons/ai";
import { LuTimer } from "react-icons/lu";
import { IoMdPricetags } from 'react-icons/io';
import { FaHotjar } from 'react-icons/fa';
import { AiOutlineComment } from 'react-icons/ai';
import { LuTimer } from 'react-icons/lu';
import dayjs from 'dayjs';
import { Article } from '@/types/app/article';
@@ -22,86 +22,94 @@ import Encrypt from '@/components/Encrypt';
import NotFound from '@/app/not-found';
interface Props {
params: Promise<{ id: number }>;
searchParams: Promise<{ password: string }>
};
params: Promise<{ id: number }>;
searchParams: Promise<{ password: string }>;
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
const params = await props.params;
const id = params.id
const password = searchParams.password
const searchParams = await props.searchParams;
const params = await props.params;
const id = params.id;
const password = searchParams.password;
const { code, data } = password ? (await getArticleDataAPI(id, password)) || { data: {} as Article } : (await getArticleDataAPI(id)) || { data: {} as Article }
const { code, data } = password ? (await getArticleDataAPI(id, password)) || { data: {} as Article } : (await getArticleDataAPI(id)) || { data: {} as Article };
const errorCodes = [400, 404, 611]
const errorCodes = [400, 404, 611];
if (errorCodes.includes(code ?? 200)) {
return <NotFound />
}
if (errorCodes.includes(code ?? 200)) {
return <NotFound />;
}
// 记录文章访问量
await recordViewAPI(id)
// 记录文章访问量
await recordViewAPI(id);
// 图标样式
const iconSty = "flex justify-center items-center w-5 h-5 rounded-full text-xs mr-1"
// 图标样式
const iconSty = 'flex justify-center items-center w-5 h-5 rounded-full text-xs mr-1';
if ((data && data.config.isEncrypt !== 1) || password && data.config.isEncrypt === 1) {
return (
<>
<title>{data.title}</title>
<meta name="description" content={data.description} />
if ((data && data.config.isEncrypt !== 1) || (password && data.config.isEncrypt === 1)) {
return (
<>
<title>{data.title}</title>
<meta name="description" content={data.description} />
<div className="ArticlePage">
<Slide>
{/* 星空背景组件 */}
<Starry />
<div className="ArticlePage">
<Slide>
{/* 星空背景组件 */}
<Starry />
<div className="absolute w-[80%] sm:w-[70%] lg:w-[60%] xl:w-[50%] top-[60%] md:top-1/2 left-1/2 -translate-x-1/2 -translate-y-[65%] text-white custom_text_shadow">
<div className="text-xl mb-5 sm:text-2xl lg:text-3xl xl:text-4xl text-center sm:mb-7 md:mb-10">{data?.title}</div>
<div className="absolute w-[80%] sm:w-[70%] lg:w-[60%] xl:w-[50%] top-[60%] md:top-1/2 left-1/2 -translate-x-1/2 -translate-y-[65%] text-white custom_text_shadow">
<div className="text-xl mb-5 sm:text-2xl lg:text-3xl xl:text-4xl text-center sm:mb-7 md:mb-10">{data?.title}</div>
<div className="flex flex-wrap justify-between text-xs sm:text-sm">
<div className="flex mb-2">
<span className={`${iconSty} bg-[#A543E6]`}><IoMdPricetags /></span>
<span>{data?.cateList[0]?.name}</span>
</div>
<div className="flex mb-2">
<span className={`${iconSty} bg-[#EA3B24]`}><FaHotjar /></span>
<span>{data?.view}</span>
</div>
<div className="flex mb-2">
<span className={`${iconSty} bg-[#4FA759]`}><AiOutlineComment /></span>
<span>{data?.comment}</span>
</div>
<div className="flex mb-2">
<span className={`${iconSty} bg-[#5A9CF8]`}><LuTimer /></span>
<span>{dayjs(+data?.createTime!).format('YYYY-MM-DD HH:mm')}</span>
</div>
</div>
</div>
</Slide>
<div className="w-[90%] xl:w-6/12 mx-auto mt-12 relative">
<Summary content={data?.description || ""} />
<MD data={data?.content} />
<div className="w-full">
<Tag data={data?.tagList} />
<Copyright />
<UpAndDown id={id} prev={data?.prev} next={data?.next} />
<Comment articleId={id} articleTitle={data.title} />
</div>
</div>
<Nav />
<div className="flex flex-wrap justify-between text-xs sm:text-sm">
<div className="flex mb-2">
<span className={`${iconSty} bg-[#A543E6]`}>
<IoMdPricetags />
</span>
<span>{data?.cateList[0]?.name}</span>
</div>
</>
)
} else {
return !password && <Encrypt id={id} />
}
};
<div className="flex mb-2">
<span className={`${iconSty} bg-[#EA3B24]`}>
<FaHotjar />
</span>
<span>{data?.view}</span>
</div>
<div className="flex mb-2">
<span className={`${iconSty} bg-[#4FA759]`}>
<AiOutlineComment />
</span>
<span>{data?.comment}</span>
</div>
<div className="flex mb-2">
<span className={`${iconSty} bg-[#5A9CF8]`}>
<LuTimer />
</span>
<span>{dayjs(+data?.createTime).format('YYYY-MM-DD HH:mm')}</span>
</div>
</div>
</div>
</Slide>
<div className="w-[90%] xl:w-6/12 mx-auto mt-12 relative">
<Summary content={data?.description || ''} />
<MD data={data?.content} />
<div className="w-full">
<Tag data={data?.tagList} />
<Copyright />
<UpAndDown id={id} prev={data?.prev} next={data?.next} />
<Comment articleId={id} articleTitle={data.title} />
</div>
</div>
<Nav />
</div>
</>
);
} else {
return !password && <Encrypt id={id} />;
}
};

View File

@@ -3,221 +3,191 @@ import Link from 'next/link';
import Show from '@/components/Show';
import Empty from '@/components/Empty';
import RandomAvatar from '@/components/RandomAvatar';
import { Comment } from '@/types/app/comment'
import { RiMessage3Line } from "react-icons/ri";
import { Comment } from '@/types/app/comment';
import { RiMessage3Line } from 'react-icons/ri';
import dayjs from 'dayjs';
import { getArticleCommentListAPI } from '@/api/comment';
import { Pagination } from "@heroui/react";
import "./index.scss"
import { Pagination } from '@heroui/react';
import './index.scss';
interface Props {
id: number,
reply: (id: number, name: string) => void
id: number;
reply: (id: number, name: string) => void;
}
const CommentList = forwardRef(({ id, reply }: Props, ref) => {
const [data, setData] = useState<Paginate<Comment[]>>({} as Paginate<Comment[]>)
const getCommentList = async (page: number = 1) => {
const { data } = (await getArticleCommentListAPI(+id!, { page, size: 8 })) || { data: {} as Paginate<Comment[]> }
setData(data)
}
const [data, setData] = useState<Paginate<Comment[]>>({} as Paginate<Comment[]>);
const getCommentList = async (page: number = 1) => {
const { data } = (await getArticleCommentListAPI(+id!, { page, size: 8 })) || { data: {} as Paginate<Comment[]> };
setData(data);
};
useEffect(() => {
getCommentList()
}, [])
useEffect(() => {
getCommentList();
}, []);
const [page, setPage] = useState(1)
const onPaginateChange = (page: number) => {
setPage(page)
getCommentList(page)
}
const [page, setPage] = useState(1);
const onPaginateChange = (page: number) => {
setPage(page);
getCommentList(page);
};
// 回复评论
const replyComment = (id: number, name: string) => {
reply(id, name)
}
// 回复评论
const replyComment = (id: number, name: string) => {
reply(id, name);
};
useImperativeHandle(ref, () => ({
getCommentList
}))
useImperativeHandle(ref, () => ({
getCommentList,
}));
// 这里的逻辑有点乱,暂时先这样,有空再优化!!!
return (
<div className='CommentListComponent'>
<Show is={!!data.result?.length} children={
<ul className="list">
{data.result?.map(one => (
<li className="item" key={one.id}>
<div className="comment_user_one">
{
one.avatar ? <img src={one.avatar} alt="" className="avatar" /> : <RandomAvatar className="avatar" />
}
<div className="comment_user_one_info">
{
one.url
? <a href={one.url} className="name active" target="_blank" rel="noopener noreferrer">{one.name}</a>
: <span className="name">{one.name}</span>
}
<span className="time">{dayjs(+one.createTime).format('YYYY-MM-DD HH:mm')}</span>
</div>
// 这里的逻辑有点乱,暂时先这样,有空再优化!!!
return (
<div className="CommentListComponent">
<Show is={!!data.result?.length}>
<ul className="list">
{data.result?.map((one) => (
<li className="item" key={one.id}>
<div className="comment_user_one">
{one.avatar ? <img src={one.avatar} alt="" className="avatar" /> : <RandomAvatar className="avatar" />}
<div className="comment_user_one_info">
{one.url ? (
<a href={one.url} className="name active" target="_blank" rel="noopener noreferrer">
{one.name}
</a>
) : (
<span className="name">{one.name}</span>
)}
<span className="time">{dayjs(+one.createTime).format('YYYY-MM-DD HH:mm')}</span>
</div>
<div className="reply" onClick={() => replyComment(one.id!, one.name)}>
<RiMessage3Line />
</div>
<div className="reply" onClick={() => replyComment(one.id!, one.name)}>
<RiMessage3Line />
</div>
</div>
<div className="comment_main">{one.content}</div>
{one?.children?.length
? one.children?.map((two) => (
<div className="comment_user_two !ml-5 sm:!ml-12" key={two.id}>
<div className="comment_user_two_info">
{two.avatar ? <img src={two.avatar} alt="" className="avatar" /> : <RandomAvatar className="avatar" />}
{two.url ? (
<a href={two.url} className="name active !text-primary" target="_blank" rel="noopener noreferrer">
{two.name}
</a>
) : (
<span className="name">{two.name}</span>
)}
<span className="time">{dayjs(+two.createTime).format('YYYY-MM-DD HH:mm')}</span>
<div className="reply" onClick={() => replyComment(two.id!, two.name)}>
<RiMessage3Line />
</div>
</div>
<div className="comment_main">
<Link href="#">@{one.name}</Link>
<span>{two.content}</span>
</div>
{two.children?.map((three) => (
<div key={three.id}>
<div className="comment_user_three !ml-5 sm:!ml-12">
<div className="comment_user_three_info">
{three.avatar ? <img src={three.avatar} alt="" className="avatar" /> : <RandomAvatar className="avatar" />}
{three.url ? (
<a href={three.url} className="name active !text-primary" target="_blank" rel="noopener noreferrer">
{three.name}
</a>
) : (
<span className="name">{three.name}</span>
)}
<span className="time">{dayjs(+three.createTime).format('YYYY-MM-DD HH:mm')}</span>
<div className="reply" onClick={() => replyComment(three.id!, three.name)}>
<RiMessage3Line />
</div>
</div>
<div className="comment_main">{one.content}</div>
<div className="comment_main">
<Link href="#">@{two.name}</Link>
<span>{three.content}</span>
</div>
</div>
{one?.children?.length ? (
one.children?.map(two => (
<div className="comment_user_two !ml-5 sm:!ml-12" key={two.id}>
<div className="comment_user_two_info">
{
two.avatar
? <img src={two.avatar} alt="" className="avatar" />
: <RandomAvatar className="avatar" />
}
{three.children?.map((four) => (
<div key={four.id}>
<div className="comment_user_three !ml-5 sm:!ml-12">
<div className="comment_user_three_info">
{four.avatar ? <img src={four.avatar} alt="" className="avatar" /> : <RandomAvatar className="avatar" />}
{two.url ? (
<a href={two.url} className="name active !text-primary" target="_blank" rel="noopener noreferrer">
{two.name}
</a>
) : (
<span className="name">{two.name}</span>
)}
{four.url ? (
<a href={four.url} className="name active !text-primary" target="_blank" rel="noopener noreferrer">
{four.name}
</a>
) : (
<span className="name">{four.name}</span>
)}
<span className="time">{dayjs(+two.createTime).format('YYYY-MM-DD HH:mm')}</span>
<div className="reply" onClick={() => replyComment(two.id!, two.name)}>
<RiMessage3Line />
</div>
</div>
<span className="time">{dayjs(+four.createTime).format('YYYY-MM-DD HH:mm')}</span>
<div className="comment_main">
<Link href="#">@{one.name}</Link>
<span>{two.content}</span>
</div>
<div className="reply" onClick={() => replyComment(four.id!, four.name)}>
<RiMessage3Line />
</div>
</div>
{two.children?.map(three => (
<div key={three.id}>
<div className="comment_user_three !ml-5 sm:!ml-12">
<div className="comment_user_three_info">
{
three.avatar
? <img src={three.avatar} alt="" className="avatar" />
: <RandomAvatar className="avatar" />
}
<div className="comment_main">
<Link href="#">@{three.name}</Link>
<span>{four.content}</span>
</div>
</div>
{three.url ? (
<a href={three.url} className="name active !text-primary" target="_blank" rel="noopener noreferrer">
{three.name}
</a>
) : (
<span className="name">{three.name}</span>
)}
{four.children?.map((five) => (
<div key={five.id} className="comment_user_three !ml-5 sm:!ml-12">
<div className="comment_user_three_info">
{five.avatar ? <img src={five.avatar} alt="" className="avatar" /> : <RandomAvatar className="avatar" />}
<span className="time">{dayjs(+three.createTime).format('YYYY-MM-DD HH:mm')}</span>
{five.url ? (
<a href={five.url} className="name active !text-primary" target="_blank" rel="noopener noreferrer">
{five.name}
</a>
) : (
<span className="name">{five.name}</span>
)}
<div className="reply" onClick={() => replyComment(three.id!, three.name)}>
<RiMessage3Line />
</div>
</div>
<span className="time">{dayjs(+five.createTime).format('YYYY-MM-DD HH:mm')}</span>
<div className="comment_main">
<Link href="#">@{two.name}</Link>
<span>{three.content}</span>
</div>
</div>
{three.children?.map(four => (
<div key={four.id}>
<div className="comment_user_three !ml-5 sm:!ml-12">
<div className="comment_user_three_info">
{
four.avatar
? <img src={four.avatar} alt="" className="avatar" />
: <RandomAvatar className="avatar" />
}
{four.url ? (
<a href={four.url} className="name active !text-primary" target="_blank" rel="noopener noreferrer">
{four.name}
</a>
) : (
<span className="name">{four.name}</span>
)}
<span className="time">{dayjs(+four.createTime).format('YYYY-MM-DD HH:mm')}</span>
<div className="reply" onClick={() => replyComment(four.id!, four.name)}>
<RiMessage3Line />
</div>
</div>
<div className="comment_main">
<Link href="#">@{three.name}</Link>
<span>{four.content}</span>
</div>
</div>
{four.children?.map(five => (
<div key={five.id} className="comment_user_three !ml-5 sm:!ml-12">
<div className="comment_user_three_info">
{
five.avatar
? <img src={five.avatar} alt="" className="avatar" />
: <RandomAvatar className="avatar" />
}
{five.url ? (
<a href={five.url} className="name active !text-primary" target="_blank" rel="noopener noreferrer">
{five.name}
</a>
) : (
<span className="name">{five.name}</span>
)}
<span className="time">{dayjs(+five.createTime).format('YYYY-MM-DD HH:mm')}</span>
<div className="reply" onClick={() => replyComment(five.id!, five.name)}>
<RiMessage3Line />
</div>
</div>
<div className="comment_main">
<Link href="#">@{four.name}</Link>
<span>{five.content}</span>
</div>
</div>
))}
</div>
))}
</div>
))}
<div className="reply" onClick={() => replyComment(five.id!, five.name)}>
<RiMessage3Line />
</div>
))
) : null}
</li>
))}
</ul>
} />
</div>
<div className="comment_main">
<Link href="#">@{four.name}</Link>
<span>{five.content}</span>
</div>
</div>
))}
</div>
))}
</div>
))}
</div>
))
: null}
</li>
))}
</ul>
</Show>
{
!data.result?.length
? <Empty info='评论列表为空~'></Empty>
: (
<Pagination
showControls
total={data.pages}
page={page}
onChange={onPaginateChange}
className='flex justify-center mt-2'
classNames={{ item: "shadow-none bg-transparent dark:hover:!bg-black-b ", prev: "dark:bg-black-b ", next: "dark:bg-black-b " }}
/>
)
}
</div >
);
})
{!data.result?.length ? <Empty info="评论列表为空~"></Empty> : <Pagination showControls total={data.pages} page={page} onChange={onPaginateChange} className="flex justify-center mt-2" classNames={{ item: 'shadow-none bg-transparent dark:hover:!bg-black-b ', prev: 'dark:bg-black-b ', next: 'dark:bg-black-b ' }} />}
</div>
);
});
export default CommentList;
export default CommentList;

View File

@@ -1,129 +1,142 @@
"use client"
'use client';
import { useState, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { addCommentDataAPI } from '@/api/comment';
import { ToastContainer, toast } from 'react-toastify';
import { Spinner } from "@heroui/react";
import { Spinner } from '@heroui/react';
import List from './components/List';
import 'react-toastify/dist/ReactToastify.css';
import "./index.scss"
import './index.scss';
interface Props {
articleId: number,
articleTitle: string
articleId: number;
articleTitle: string;
}
interface CommentForm {
content: string,
name: string,
email: string,
url: string,
avatar: string
content: string;
name: string;
email: string;
url: string;
avatar: string;
}
const CommentForm = ({ articleId }: Props) => {
const contentRef = useRef<HTMLTextAreaElement>(null);
const [commentId, setCommentId] = useState(articleId);
const [placeholder, setPlaceholder] = useState("来发一针见血的评论吧~");
const contentRef = useRef<HTMLTextAreaElement>(null);
const [commentId, setCommentId] = useState(articleId);
const [placeholder, setPlaceholder] = useState('来发一针见血的评论吧~');
const [loading, setLoading] = useState(false)
const [loading, setLoading] = useState(false);
const commentRef = useRef<{ getCommentList: () => void }>(null)
const commentRef = useRef<{ getCommentList: () => void }>(null);
const { register, control, formState: { errors }, handleSubmit, reset, setValue } = useForm<CommentForm>({});
const {
register,
formState: { errors },
handleSubmit,
setValue,
} = useForm<CommentForm>({});
// 如果之前评论过,就从本地取数据,不需要再重新填写
useEffect(() => {
const info = JSON.parse(localStorage.getItem("comment_data") || '{}');
setValue('name', info.name || '');
setValue('email', info.email || '');
setValue('avatar', info.avatar || '');
setValue('url', info.url || '');
}, [setValue]);
// 如果之前评论过,就从本地取数据,不需要再重新填写
useEffect(() => {
const info = JSON.parse(localStorage.getItem('comment_data') || '{}');
setValue('name', info.name || '');
setValue('email', info.email || '');
setValue('avatar', info.avatar || '');
setValue('url', info.url || '');
}, [setValue]);
const onSubmit = async (data: CommentForm) => {
setLoading(true)
const onSubmit = async (data: CommentForm) => {
setLoading(true);
// 判断是不是QQ邮箱如果是就把QQ截取出来然后用QQ当做头像
const email_index = data.email.lastIndexOf("@qq.com")
if (email_index !== -1) {
const qq = data.email.substring(0, email_index)
// 判断是不是QQ邮箱如果是就把QQ截取出来然后用QQ当做头像
const email_index = data.email.lastIndexOf('@qq.com');
if (email_index !== -1) {
const qq = data.email.substring(0, email_index);
// 判断是否是纯数字的QQ
if (!isNaN(+qq)) data.avatar = `https://q1.qlogo.cn/g?b=qq&nk=${qq}&s=640`
};
const { code, message } = (await addCommentDataAPI({ ...data, articleId, commentId: commentId === articleId ? 0 : commentId, createTime: Date.now().toString() })) || { code: 0, message: "" }
if (code !== 200) return alert("发布评论失败:" + message);
toast("🎉 提交成功, 请等待审核!")
// 发布成功后初始化表单
setCommentId(articleId)
setValue('content', "");
setPlaceholder("来发一针见血的评论吧~");
commentRef.current?.getCommentList()
setLoading(false)
// 提交成功后把评论的数据持久化到本地
localStorage.setItem("comment_data", JSON.stringify(data))
};
// 回复评论
const replyComment = (id: number, name: string) => {
contentRef.current?.focus();
setCommentId(id);
setPlaceholder(`回复评论给:${name}`);
// 判断是否是纯数字的QQ
if (!isNaN(+qq)) data.avatar = `https://q1.qlogo.cn/g?b=qq&nk=${qq}&s=640`;
}
return (
(<div className='CommentComponent'>
<div className="mt-[70px]">
<div className="title relative top-0 left-0 w-full h-[1px] mb-10 bg-[#f7f7f7] dark:bg-black-b "></div>
const { code, message } = (await addCommentDataAPI({ ...data, articleId, commentId: commentId === articleId ? 0 : commentId, createTime: Date.now().toString() })) || { code: 0, message: '' };
if (code !== 200) return alert('发布评论失败:' + message);
<form className="flex flex-wrap justify-between mt-4 space-y-2 text-xs xs:text-sm" onSubmit={handleSubmit(onSubmit)}>
<div className='w-full'>
<textarea
{...register("content", { required: "请输入内容" })}
placeholder={placeholder}
className="tw_form w-full p-4 min-h-36"
ref={(e) => {
register("content").ref(e);
(contentRef as any).current = e;
}}
/>
<span className='text-red-400 text-sm pl-3'>{errors.content?.message}</span>
</div>
toast('🎉 提交成功, 请等待审核!');
<div className='flex flex-col w-[32%]'>
<input type="text" className="tw_form w-full h-9 pl-4" placeholder="你的名称" {...register("name", { required: "请输入名称" })} />
<span className='text-red-400 text-sm pl-3 mt-1'>{errors.name?.message}</span>
</div>
// 发布成功后初始化表单
setCommentId(articleId);
setValue('content', '');
setPlaceholder('来发一针见血的评论吧~');
commentRef.current?.getCommentList();
setLoading(false);
<div className='flex flex-col w-[32%]'>
<input type="text" className="tw_form w-full h-9 pl-4" placeholder="你的邮箱(选填)" {...register("email", { pattern: { value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: "请输入正确的邮箱" } })} />
<span className='text-red-400 text-sm pl-3 mt-1'>{errors.email?.message}</span>
</div>
// 提交成功后把评论的数据持久化到本地
localStorage.setItem('comment_data', JSON.stringify(data));
};
<div className='flex flex-col w-[32%]'>
<input type="text" className="tw_form w-full h-9 pl-4" placeholder="头像(选填)" {...register("avatar", { pattern: { value: /^https?:\/\//, message: "请输入正确的头像链接" } })} />
<span className='text-red-400 text-sm pl-3 mt-1'>{errors.avatar?.message}</span>
</div>
// 回复评论
const replyComment = (id: number, name: string) => {
contentRef.current?.focus();
setCommentId(id);
setPlaceholder(`回复评论给:${name}`);
};
<div className='w-full flex flex-col'>
<input type="text" className="tw_form w-full h-9 pl-4" placeholder="你的站点(选填)" {...register("url", { pattern: { value: /^https?:\/\//, message: "请输入正确的网站链接" } })} />
<span className='text-red-400 text-sm pl-3 mt-1'>{errors.url?.message}</span>
</div>
return (
<div className="CommentComponent">
<div className="mt-[70px]">
<div className="title relative top-0 left-0 w-full h-[1px] mb-10 bg-[#f7f7f7] dark:bg-black-b "></div>
{loading ? <div className='w-full h-10 flex justify-center !mt-4'><Spinner /></div> : <button className="w-full h-10 !mt-4 text-white rounded-md bg-primary text-center" type="submit">发表评论</button>}
</form>
<form className="flex flex-wrap justify-between mt-4 space-y-2 text-xs xs:text-sm" onSubmit={handleSubmit(onSubmit)}>
<div className="w-full">
<textarea
{...register('content', { required: '请输入内容' })}
placeholder={placeholder}
className="tw_form w-full p-4 min-h-36"
ref={(e) => {
register('content').ref(e);
(contentRef as any).current = e;
}}
/>
<span className="text-red-400 text-sm pl-3">{errors.content?.message}</span>
</div>
<List ref={commentRef} id={articleId} reply={replyComment} />
<div className="flex flex-col w-[32%]">
<input type="text" className="tw_form w-full h-9 pl-4" placeholder="你的名称" {...register('name', { required: '请输入名称' })} />
<span className="text-red-400 text-sm pl-3 mt-1">{errors.name?.message}</span>
</div>
<div className="flex flex-col w-[32%]">
<input type="text" className="tw_form w-full h-9 pl-4" placeholder="你的邮箱(选填)" {...register('email', { pattern: { value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: '请输入正确的邮箱' } })} />
<span className="text-red-400 text-sm pl-3 mt-1">{errors.email?.message}</span>
</div>
<div className="flex flex-col w-[32%]">
<input type="text" className="tw_form w-full h-9 pl-4" placeholder="头像(选填)" {...register('avatar', { pattern: { value: /^https?:\/\//, message: '请输入正确的头像链接' } })} />
<span className="text-red-400 text-sm pl-3 mt-1">{errors.avatar?.message}</span>
</div>
<div className="w-full flex flex-col">
<input type="text" className="tw_form w-full h-9 pl-4" placeholder="你的站点(选填)" {...register('url', { pattern: { value: /^https?:\/\//, message: '请输入正确的网站链接' } })} />
<span className="text-red-400 text-sm pl-3 mt-1">{errors.url?.message}</span>
</div>
{loading ? (
<div className="w-full h-10 flex justify-center !mt-4">
<Spinner />
</div>
<ToastContainer />
</div>)
);
) : (
<button className="w-full h-10 !mt-4 text-white rounded-md bg-primary text-center" type="submit">
发表评论
</button>
)}
</form>
<List ref={commentRef} id={articleId} reply={replyComment} />
</div>
<ToastContainer />
</div>
);
};
export default CommentForm;
export default CommentForm;

View File

@@ -1,15 +1,15 @@
import { getUserDataAPI } from "@/api/user";
import { User } from "@/types/app/user";
import { getUserDataAPI } from '@/api/user';
import { User } from '@/types/app/user';
const Copyright = async () => {
const { data } = await getUserDataAPI() || { data: {} as User }
const { data } = (await getUserDataAPI()) || { data: {} as User };
return (
<div className="p-3 space-y-2 border-l-[3px] border-primary bg-[#ecf7fe] rounded-md text-sm text-black-b">
<p>{data?.name}</p>
<p> {data?.name} !</p>
</div>
);
return (
<div className="p-3 space-y-2 border-l-[3px] border-primary bg-[#ecf7fe] rounded-md text-sm text-black-b">
<p>{data?.name}</p>
<p> {data?.name} !</p>
</div>
);
};
export default Copyright;

View File

@@ -1,29 +1,29 @@
"use client";
'use client';
import React, { useEffect, useRef, useState, useMemo } from "react";
import ReactMarkdown from "react-markdown";
import { useConfigStore } from "@/stores";
import { PhotoProvider, PhotoView } from "react-photo-view";
import { ToastContainer, toast } from "react-toastify";
import "react-photo-view/dist/react-photo-view.css";
import "react-toastify/dist/ReactToastify.css";
import "katex/dist/katex.min.css";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { remarkMark } from "remark-mark-highlight";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import rehypeSemanticBlockquotes from "rehype-semantic-blockquotes";
import rehypeCallouts from "rehype-callouts";
import "rehype-callouts/theme/obsidian";
import Skeleton from "@/components/Skeleton";
import { BiCopy } from "react-icons/bi";
import React, { useEffect, useRef, useState, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { useConfigStore } from '@/stores';
import { PhotoProvider, PhotoView } from 'react-photo-view';
import { ToastContainer, toast } from 'react-toastify';
import 'react-photo-view/dist/react-photo-view.css';
import 'react-toastify/dist/ReactToastify.css';
import 'katex/dist/katex.min.css';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { remarkMark } from 'remark-mark-highlight';
import rehypeKatex from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import rehypeSemanticBlockquotes from 'rehype-semantic-blockquotes';
import rehypeCallouts from 'rehype-callouts';
import 'rehype-callouts/theme/obsidian';
import Skeleton from '@/components/Skeleton';
import { BiCopy } from 'react-icons/bi';
import "./index.scss";
import './index.scss';
import hljs from "highlight.js";
import hljs from 'highlight.js';
// 主题样式,换成你喜欢的
import "highlight.js/styles/atom-one-dark.css";
import 'highlight.js/styles/atom-one-dark.css';
interface Props {
data: string;
@@ -36,11 +36,11 @@ const ContentMD = ({ data }: Props) => {
useEffect(() => {
setIsClient(true);
document.body.style.backgroundColor = isDark ? "#0f0f0f" : "#fff";
document.body.style.backgroundColor = isDark ? '#0f0f0f' : '#fff';
// 处理波浪色假设页面有波浪SVG
let color = isDark ? "36, 41, 48" : "255, 255, 255";
const waves = document.querySelectorAll<SVGUseElement>(".waves use");
const color = isDark ? '36, 41, 48' : '255, 255, 255';
const waves = document.querySelectorAll<SVGUseElement>('.waves use');
if (waves.length) {
waves[0].style.fill = `rgba(${color}, 0.7)`;
waves[1].style.fill = `rgba(${color}, 0.5)`;
@@ -49,12 +49,12 @@ const ContentMD = ({ data }: Props) => {
}
return () => {
document.body.style.backgroundColor = "#f9f9f9";
document.body.style.backgroundColor = '#f9f9f9';
if (waves) {
waves[0].style.fill = "rgba(249, 249, 249, 0.7)";
waves[1].style.fill = "rgba(249, 249, 249, 0.5)";
waves[2].style.fill = "rgba(249, 249, 249, 0.3)";
waves[3].style.fill = "rgba(249, 249, 249)";
waves[0].style.fill = 'rgba(249, 249, 249, 0.7)';
waves[1].style.fill = 'rgba(249, 249, 249, 0.5)';
waves[2].style.fill = 'rgba(249, 249, 249, 0.3)';
waves[3].style.fill = 'rgba(249, 249, 249)';
}
};
}, [isDark]);
@@ -84,33 +84,35 @@ const ContentMD = ({ data }: Props) => {
// 代码块组件,带行号、折叠、复制
const CodeBlock = ({ language, value }: { language: string; value: string }) => {
const [expanded, setExpanded] = useState(false);
const isLong = value.split("\n").length > 10;
const isLong = value.split('\n').length > 10;
const highlightedLines = useMemo(() => {
try {
if (hljs.getLanguage(language)) {
return hljs.highlight(value, { language }).value.split("\n");
return hljs.highlight(value, { language }).value.split('\n');
}
} catch { }
return hljs.highlightAuto(value).value.split("\n");
} catch (error) {
console.error(error);
}
return hljs.highlightAuto(value).value.split('\n');
}, [value, language]);
const linesToRender = highlightedLines;
if (linesToRender.length > 1 && linesToRender[linesToRender.length - 1] === "") {
if (linesToRender.length > 1 && linesToRender[linesToRender.length - 1] === '') {
linesToRender.pop();
}
const handleCopy = () => {
navigator.clipboard.writeText(value).then(
() => toast.success("代码已复制 🎉"),
() => toast.error("复制失败 😖")
() => toast.success('代码已复制 🎉'),
() => toast.error('复制失败 😖')
);
};
return (
<pre
className={`mac-style with-line-number ${isLong ? (expanded ? "expanded" : "collapsed") : ""}`}
className={`mac-style with-line-number ${isLong ? (expanded ? 'expanded' : 'collapsed') : ''}`}
onClick={() => {
if (isLong && !expanded) setExpanded(true);
}}
@@ -134,10 +136,7 @@ const ContentMD = ({ data }: Props) => {
{linesToRender.map((line, idx) => (
<div key={idx} className="code-line">
<span className="line-number">{idx + 1}</span>
<span
className="line-content"
dangerouslySetInnerHTML={{ __html: line || "\u200B" }}
/>
<span className="line-content" dangerouslySetInnerHTML={{ __html: line || '\u200B' }} />
</div>
))}
</code>
@@ -151,9 +150,7 @@ const ContentMD = ({ data }: Props) => {
}}
type="button"
>
{expanded
? "收起代码"
: `展开代码 (${value.split("\n").length - 1} 行)`}
{expanded ? '收起代码' : `展开代码 (${value.split('\n').length - 1} 行)`}
</button>
)}
</pre>
@@ -174,7 +171,7 @@ const ContentMD = ({ data }: Props) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setTimeout(() => {
img.style.filter = "blur(0px)";
img.style.filter = 'blur(0px)';
}, 400);
observer.unobserve(img);
}
@@ -191,7 +188,7 @@ const ContentMD = ({ data }: Props) => {
}, []);
return (
<PhotoView src={src || ""}>
<PhotoView src={src || ''}>
<span className="flex justify-center my-4 dark:brightness-90">
<img ref={imgRef} alt={alt} src={src} className="max-h-[500px]" />
</span>
@@ -199,23 +196,18 @@ const ContentMD = ({ data }: Props) => {
);
},
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => {
if (children === "douyin-video" && href) {
const videoId = href.split("/").pop();
if (children === 'douyin-video' && href) {
const videoId = href.split('/').pop();
return (
<div className="flex justify-center">
<iframe
src={`https://open.douyin.com/player/video?vid=${videoId}&autoplay=0`}
referrerPolicy="unsafe-url"
allowFullScreen
className="douyin"
/>
<iframe src={`https://open.douyin.com/player/video?vid=${videoId}&autoplay=0`} referrerPolicy="unsafe-url" allowFullScreen className="douyin" />
</div>
);
}
return <a href={href}>{children}</a>;
},
code: ({ node, inline, className = "", children, ...props }: any) => {
const match = /language-(\w+)/.exec(className || "");
code: ({ node, inline, className = '', children, ...props }: any) => {
const match = /language-(\w+)/.exec(className || '');
if (inline || !match) {
return (
@@ -234,28 +226,11 @@ const ContentMD = ({ data }: Props) => {
return (
<div className="ContentMdComponent">
<ToastContainer
theme={isDark ? "dark" : "light"}
autoClose={1000}
hideProgressBar
/>
<ToastContainer theme={isDark ? 'dark' : 'light'} autoClose={1000} hideProgressBar />
<PhotoProvider>
<div className="content markdown-body">
<ReactMarkdown
components={renderers}
remarkPlugins={[
[remarkGfm, { singleTilde: false }],
remarkMath,
remarkMark,
]}
rehypePlugins={[
rehypeRaw,
rehypeKatex,
rehypeCallouts,
rehypeSemanticBlockquotes,
]}
>
<ReactMarkdown components={renderers} remarkPlugins={[[remarkGfm, { singleTilde: false }], remarkMath, remarkMark]} rehypePlugins={[rehypeRaw, rehypeKatex, rehypeCallouts, rehypeSemanticBlockquotes]}>
{data}
</ReactMarkdown>
</div>

View File

@@ -1,12 +1,12 @@
"use client";
'use client';
import Image from "next/image";
import { useEffect, useState } from "react";
import { MdOutlineKeyboardDoubleArrowLeft } from "react-icons/md";
import directory from "@/assets/svg/other/directory.svg";
import { motion, AnimatePresence } from "framer-motion";
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { MdOutlineKeyboardDoubleArrowLeft } from 'react-icons/md';
import directory from '@/assets/svg/other/directory.svg';
import { motion, AnimatePresence } from 'framer-motion';
import "./index.scss";
import './index.scss';
interface NavItem {
name: string;
@@ -26,13 +26,13 @@ const ContentNav = () => {
useEffect(() => {
setTimeout(() => {
const list = document.querySelectorAll<HTMLHeadingElement>(
".content h1, .content h2, .content h3, .content h4, .content h5, .content h6"
'.content h1, .content h2, .content h3, .content h4, .content h5, .content h6'
);
list?.forEach((nav, index) => {
const tag = nav.tagName.toLowerCase(); // "h1"~"h6"
nav.setAttribute("id", nav.textContent! + index);
nav.setAttribute("class", tag);
nav.setAttribute('id', nav.textContent! + index);
nav.setAttribute('class', tag);
});
const titles = Array.from(list).map((t) => {
@@ -66,8 +66,8 @@ const ContentNav = () => {
};
onScroll();
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, 0);
}, []);
@@ -77,7 +77,7 @@ const ContentNav = () => {
const top = element.getBoundingClientRect().top + window.scrollY - OFFSET;
window.scrollTo({
top,
behavior: "instant", // 可改为 "smooth" 实现平滑滚动
behavior: 'instant', // 可改为 "smooth" 实现平滑滚动
});
setActive(index);
}
@@ -158,8 +158,8 @@ const ContentNav = () => {
}}
className={`nav_item overflow-hidden relative block p-1 pl-5 mb-[5px] hover:text-primary ${
active === index
? "text-primary pl-[30px] rounded-[10px] text-[15px] dark:bg-[#313d4e99] before:!left-4"
: ""
? 'text-primary pl-[30px] rounded-[10px] text-[15px] dark:bg-[#313d4e99] before:!left-4'
: ''
} ${item.className}`}
>
{item.name}

View File

@@ -1,24 +1,24 @@
"use client";
import { useEffect, useState } from "react";
import "./index.scss";
'use client';
import { useEffect, useState } from 'react';
import './index.scss';
interface SummaryProps {
content: string;
}
const suggestions = [
"谁是刘宇阳?",
"这篇文章讲了什么?",
"带我去看看其他文章",
'谁是刘宇阳?',
'这篇文章讲了什么?',
'带我去看看其他文章',
];
export default function Summary({ content }: SummaryProps) {
const [displayText, setDisplayText] = useState("");
const [displayText, setDisplayText] = useState('');
const [currentIndex, setCurrentIndex] = useState(0);
const [showSuggestions, setShowSuggestions] = useState(false);
const [showSuggestions] = useState(false);
const handleThriveGPTClick = () => {
// TODO: 调用逻辑
};
useEffect(() => {

View File

@@ -3,18 +3,18 @@ import Image from 'next/image';
import tagSvg from '@/assets/svg/other/tag.svg';
import Link from 'next/link';
import { Tag } from '@/types/app/tag';
import "./index.scss"
import './index.scss'
const TagComponent = ({ data }: { data: Tag[] }) => {
return (
<div className='TagComponent'>
<div className="TagComponent">
<div className="tag">
<Image src={tagSvg} alt="标签" />
{/* 标签列表 */}
<div className="list">
{data?.map((item, index) => (
<Link href={`/tag/${item?.id}?name=${item?.name}`} target='_blank' key={index}>{item.name}</Link>
<Link href={`/tag/${item?.id}?name=${item?.name}`} target="_blank" key={index}>{item.name}</Link>
))}
</div>
</div>

View File

@@ -1,3 +0,0 @@
.UpAndDownComponent {
}

View File

@@ -1,6 +1,5 @@
import React from 'react';
import Link from 'next/link';
import './index.scss';
interface info {
id: number,
@@ -13,21 +12,21 @@ interface Props {
next: info,
};
const btnSty = "group w-full border hover:border-primary hover:bg-[#f8fbff] dark:bg-black-b dark:border-black-b dark:hover:border-primary transition rounded-md"
const titleSty = "group-hover:text-primary text-center "
const btnSty = 'group w-full border hover:border-primary hover:bg-[#f8fbff] dark:bg-black-b dark:border-black-b dark:hover:border-primary transition rounded-md'
const titleSty = 'group-hover:text-primary text-center '
export default ({ id, prev, next }: Props) => {
return (
<div className='UpAndDownComponent'>
<div className="UpAndDownComponent">
<div className="flex justify-between mt-8 space-x-3">
<Link href={`/article/${prev ? prev.id : id}`} className={`${btnSty} py-2 sm:py-4`}>
<p className={`${titleSty} text-lg sm:text-xl`}></p>
<p className='text-center dark:text-[#8c9ab1] text-sm px-2.5 sm:text-base sm:p-0 line-clamp-1 mt-1 sm:mt-3'>{prev ? prev.title : '没有上一篇文章了~'}</p>
<p className="text-center dark:text-[#8c9ab1] text-sm px-2.5 sm:text-base sm:p-0 line-clamp-1 mt-1 sm:mt-3">{prev ? prev.title : '没有上一篇文章了~'}</p>
</Link>
<Link href={`/article/${next ? next.id : id}`} className={`${btnSty} py-2 sm:py-4`}>
<p className={`${titleSty} text-lg sm:text-xl`}></p>
<p className='text-center dark:text-[#8c9ab1] text-sm px-2.5 sm:text-base sm:p-0 line-clamp-1 mt-1 sm:mt-3'>{next ? next.title : '没有下一篇文章了~'}</p>
<p className="text-center dark:text-[#8c9ab1] text-sm px-2.5 sm:text-base sm:p-0 line-clamp-1 mt-1 sm:mt-3">{next ? next.title : '没有下一篇文章了~'}</p>
</Link>
</div>
</div>

View File

@@ -1,14 +1,14 @@
import { getCateArticleListAPI } from "@/api/cate";
import Starry from "@/components/Starry"
import Slide from "@/components/Slide"
import Classics from "@/components/ArticleLayout/Classics";
import Pagination from "@/components/Pagination";
import { Article } from "@/types/app/article";
import { getCateArticleListAPI } from '@/api/cate';
import Starry from '@/components/Starry';
import Slide from '@/components/Slide';
import Classics from '@/components/ArticleLayout/Classics';
import Pagination from '@/components/Pagination';
import { Article } from '@/types/app/article';
interface Props {
params: Promise<{ id: number }>;
searchParams: Promise<{ page: number; name: string }>;
};
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
@@ -17,7 +17,7 @@ export default async (props: Props) => {
const page = searchParams.page || 1;
const name = searchParams.name;
const { data } = (await getCateArticleListAPI(id, page)) || { data: {} as Paginate<Article[]> }
const { data } = (await getCateArticleListAPI(id, page)) || { data: {} as Paginate<Article[]> };
return (
<>
@@ -31,7 +31,9 @@ export default async (props: Props) => {
{/* 分类信息 */}
<div className="absolute top-[40%] left-[50%] transform -translate-x-1/2 w-[80%] text-center text-white text-[20px] xs:text-[25px] sm:text-[30px] custom_text_shadow">
<span>{name} ~ {data?.total}</span>
<span>
{name} ~ {data?.total}
</span>
</div>
</Slide>
@@ -42,5 +44,5 @@ export default async (props: Props) => {
</div>
</div>
</>
)
};
);
};

View File

@@ -1,167 +1,176 @@
"use client"
'use client';
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react"
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { Article } from "@/types/app/article"
import { Article } from '@/types/app/article';
import { Accordion, AccordionItem, Spinner } from "@heroui/react";
import { Accordion, AccordionItem, Spinner } from '@heroui/react';
import archiving from './svg/archiving.svg'
import { AiOutlineEye } from "react-icons/ai";
import dayjs from "dayjs";
import archiving from './svg/archiving.svg';
import { AiOutlineEye } from 'react-icons/ai';
import dayjs from 'dayjs';
interface MonthData {
total: number;
list: Article[];
wordCount: number;
total: number;
list: Article[];
wordCount: number;
}
interface YearData {
year: number;
total: number;
month: Record<number, MonthData>;
wordCount: number;
year: number;
total: number;
month: Record<number, MonthData>;
wordCount: number;
}
const Title = ({ data }: { data: YearData }) => {
return (
<div>
<div className="text-xl font-sans inline-block text_markSty">{data.year} {(data.wordCount / 1000) > 50 && '🔥'}</div>
<div className="dark:text-[#86909c]"><span className="text-primary">{data.total}</span> </div>
<div className="dark:text-[#86909c]"><span className="text-primary">{(data.wordCount / 1000).toFixed(2)}</span> K</div>
</div>
)
}
return (
<div>
<div className="text-xl font-sans inline-block text_markSty">
{data.year} {data.wordCount / 1000 > 50 && '🔥'}
</div>
<div className="dark:text-[#86909c]">
<span className="text-primary">{data.total}</span>
</div>
<div className="dark:text-[#86909c]">
<span className="text-primary">{(data.wordCount / 1000).toFixed(2)}</span> K
</div>
</div>
);
};
export default ({ list }: { list: Article[] }) => {
const [result, setResult] = useState<YearData[]>([])
const getArticleList = async () => {
const result = groupByYearAndMonth(list);
// 从早到晚排序
result.sort((a, b) => b.year - a.year)
setResult(result)
}
const [result, setResult] = useState<YearData[]>([]);
const getArticleList = async () => {
const result = groupByYearAndMonth(list);
// 从早到晚排序
result.sort((a, b) => b.year - a.year);
setResult(result);
};
// 将文章进行分组
function groupByYearAndMonth(data: Article[]): YearData[] {
const groupedData: Record<number, YearData> = {};
// 将文章进行分组
function groupByYearAndMonth(data: Article[]): YearData[] {
const groupedData: Record<number, YearData> = {};
data.forEach(item => {
const date = new Date(+item.createTime!);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const wordCount = item.content ? item.content.length : 0;
data.forEach((item) => {
const date = new Date(+item.createTime!);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const wordCount = item.content ? item.content.length : 0;
if (!groupedData[year]) {
groupedData[year] = { year, total: 0, month: {}, wordCount: 0 };
}
if (!groupedData[year]) {
groupedData[year] = { year, total: 0, month: {}, wordCount: 0 };
}
if (!groupedData[year].month[month]) {
groupedData[year].month[month] = { total: 0, list: [], wordCount: 0 };
}
if (!groupedData[year].month[month]) {
groupedData[year].month[month] = { total: 0, list: [], wordCount: 0 };
}
groupedData[year].month[month].list.push(item);
groupedData[year].month[month].total++;
groupedData[year].total++;
groupedData[year].wordCount += wordCount;
groupedData[year].month[month].wordCount += wordCount;
});
groupedData[year].month[month].list.push(item);
groupedData[year].month[month].total++;
groupedData[year].total++;
groupedData[year].wordCount += wordCount;
groupedData[year].month[month].wordCount += wordCount;
});
return Object.values(groupedData);
}
return Object.values(groupedData);
}
useEffect(() => {
getArticleList()
}, [list])
useEffect(() => {
getArticleList();
}, [list]);
return (
<>
{/* <div className="w-3/6 mx-auto"> */}
<div className="">
<h3 className="flex justify-center items-center text-2xl mb-3"><Image src={archiving.src} alt="归档" width={36} height={36} className="mr-3" /> </h3>
return (
<>
{/* <div className="w-3/6 mx-auto"> */}
<div className="">
<h3 className="flex justify-center items-center text-2xl mb-3">
<Image src={archiving.src} alt="归档" width={36} height={36} className="mr-3" />
</h3>
{
!!result.length
? (
<Accordion
className="[&>hr]:bg-[#eee] !px-0 [&>hr]:dark:bg-[#4e5969] [&>hr]: "
motionProps={{
variants: {
enter: {
y: 0,
opacity: 1,
height: "auto",
transition: {
height: {
type: "spring",
stiffness: 500,
damping: 30,
duration: 1,
},
opacity: {
ease: "easeInOut",
duration: 1,
},
},
},
exit: {
y: -10,
opacity: 0,
height: 0,
transition: {
height: {
ease: "easeInOut",
duration: 0.25,
},
opacity: {
ease: "easeInOut",
duration: 0.3,
},
},
},
},
}}
>
{
result.map((item, index) => (
<AccordionItem key={index} aria-label={item.year + '年'} title={<Title data={item} />}>
{
Object.keys(item.month).map((month, index) => (
<div key={index} className="ml-3">
<div className="relative border-l border-gray-300 dark:border-[#4e5969] ">
<div className="mb-8 ml-4">
<div className="absolute w-3 h-3 bg-blue-500 rounded-full -left-1.5 border border-white"></div>
<div className="ml-2 sm:ml-6">
<div className="flex items-center space-x-4">
<div className="text-2xl text-gray-600 dark:text-primary">{month} {((item.month[+month].wordCount / 1000) > 10) && '🔥'}</div>
<div>{item.month[+month].total} </div>
<div>{(item.month[+month].wordCount / 1000).toFixed(2)} K字</div>
</div>
{result.length ? (
<Accordion
className="[&>hr]:bg-[#eee] !px-0 [&>hr]:dark:bg-[#4e5969] [&>hr]: "
motionProps={{
variants: {
enter: {
y: 0,
opacity: 1,
height: 'auto',
transition: {
height: {
type: 'spring',
stiffness: 500,
damping: 30,
duration: 1,
},
opacity: {
ease: 'easeInOut',
duration: 1,
},
},
},
exit: {
y: -10,
opacity: 0,
height: 0,
transition: {
height: {
ease: 'easeInOut',
duration: 0.25,
},
opacity: {
ease: 'easeInOut',
duration: 0.3,
},
},
},
},
}}
>
{result.map((item, index) => (
<AccordionItem key={index} aria-label={item.year + '年'} title={<Title data={item} />}>
{Object.keys(item.month).map((month, index) => (
<div key={index} className="ml-3">
<div className="relative border-l border-gray-300 dark:border-[#4e5969] ">
<div className="mb-8 ml-4">
<div className="absolute w-3 h-3 bg-blue-500 rounded-full -left-1.5 border border-white"></div>
<div className="ml-2 sm:ml-6">
<div className="flex items-center space-x-4">
<div className="text-2xl text-gray-600 dark:text-primary">
{month} {item.month[+month].wordCount / 1000 > 10 && '🔥'}
</div>
<div>{item.month[+month].total} </div>
<div>{(item.month[+month].wordCount / 1000).toFixed(2)} K字</div>
</div>
{
item.month[+month].list.map((article: Article, index) => (
<div key={index} className="group flex justify-between py-2">
<Link href={`/article/${article.id}`} target="_blank" className="dark:text-[#bfbfbf] group-hover:text-primary ">{dayjs(+article.createTime!).format('MM-DD')} {article.title}</Link>
<span className="hidden sm:flex items-center min-w-24 text-sm text-white group-hover:text-gray-400 "><AiOutlineEye className="mr-1" />{article.view}</span>
</div>
))
}
</div>
</div>
</div>
</div>
))
}
</AccordionItem>
))
}
</Accordion>
)
: <div className="flex justify-center w-full my-10"><Spinner /></div>
}
</div>
</>
)
}
{item.month[+month].list.map((article: Article, index) => (
<div key={index} className="group flex justify-between py-2">
<Link href={`/article/${article.id}`} target="_blank" className="dark:text-[#bfbfbf] group-hover:text-primary ">
{dayjs(+article.createTime!).format('MM-DD')} {article.title}
</Link>
<span className="hidden sm:flex items-center min-w-24 text-sm text-white group-hover:text-gray-400 ">
<AiOutlineEye className="mr-1" />
{article.view}
</span>
</div>
))}
</div>
</div>
</div>
</div>
))}
</AccordionItem>
))}
</Accordion>
) : (
<div className="flex justify-center w-full my-10">
<Spinner />
</div>
)}
</div>
</>
);
};

View File

@@ -10,87 +10,81 @@ import { LabelLayout } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { CateArticleCount } from '@/types/app/cate';
echarts.use([
TooltipComponent,
LegendComponent,
PieChart,
CanvasRenderer,
LabelLayout
]);
echarts.use([TooltipComponent, LegendComponent, PieChart, CanvasRenderer, LabelLayout]);
export default () => {
const chartRef = useRef<HTMLDivElement | null>(null);
const [list, setList] = useState<{ value: number, name: string }[]>([])
const chartRef = useRef<HTMLDivElement | null>(null);
const [list, setList] = useState<{ value: number; name: string }[]>([]);
const getCateArticleCount = async () => {
const { data } = await getCateArticleCountAPI() || { data: [] as CateArticleCount[] }
setList(data.map(({ count, name }) => ({ value: count, name })))
const getCateArticleCount = async () => {
const { data } = (await getCateArticleCountAPI()) || { data: [] as CateArticleCount[] };
setList(data.map(({ count, name }) => ({ value: count, name })));
};
useEffect(() => {
getCateArticleCount();
}, []);
useEffect(() => {
if (chartRef.current) {
const myChart = echarts.init(chartRef.current);
const option = {
tooltip: {
trigger: 'item',
},
legend: {
show: false,
},
series: [
{
name: '数量统计',
type: 'pie',
radius: ['5%', '70%'],
avoidLabelOverlap: false,
padAngle: 5,
itemStyle: {
borderRadius: 10,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
},
},
labelLine: {
show: false,
},
data: list,
},
],
};
myChart.setOption(option);
const handleResize = () => {
myChart.resize();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
myChart.dispose();
};
}
}, [list]);
useEffect(() => {
getCateArticleCount()
}, [])
return (
<div className="flex flex-col items-center mb-5 md:mb-0">
<h3 className="flex items-center text-xl mb-5">
<Image src={cate.src} alt="分类一览" width={25} height={25} className="mr-3" />
</h3>
useEffect(() => {
if (chartRef.current) {
const myChart = echarts.init(chartRef.current);
const option = {
tooltip: {
trigger: 'item'
},
legend: {
show: false
},
series: [
{
name: '数量统计',
type: 'pie',
radius: ['5%', '70%'],
avoidLabelOverlap: false,
padAngle: 5,
itemStyle: {
borderRadius: 10
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: false,
}
},
labelLine: {
show: false
},
data: list
}
]
};
myChart.setOption(option);
const handleResize = () => {
myChart.resize();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
myChart.dispose();
};
}
}, [list]);
return (
<div className='flex flex-col items-center mb-5 md:mb-0'>
<h3 className="flex items-center text-xl mb-5">
<Image src={cate.src} alt="分类一览" width={25} height={25} className="mr-3" />
</h3>
<div ref={chartRef} className='min-w-[300px] h-[300px]'></div>
</div>
);
}
<div ref={chartRef} className="min-w-[300px] h-[300px]"></div>
</div>
);
};

View File

@@ -1,58 +1,62 @@
import Image from 'next/image'
import { useEffect, useState } from 'react'
import { getTagListAPI } from '@/api/tag'
import { Tag } from '@/types/app/tag'
import { getRandom } from '@/utils'
import tag from './svg/tag.svg'
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { getTagListAPI } from '@/api/tag';
import { Tag } from '@/types/app/tag';
import { getRandom } from '@/utils';
import tag from './svg/tag.svg';
export default () => {
const [list, setList] = useState<Tag[]>([])
const getTagData = async () => {
const { data } = await getTagListAPI() || { data: [] as Tag[] }
setList(data)
}
const [list, setList] = useState<Tag[]>([]);
const getTagData = async () => {
const { data } = (await getTagListAPI()) || { data: [] as Tag[] };
setList(data);
};
useEffect(() => {
getTagData()
}, [])
useEffect(() => {
getTagData();
}, []);
const colors = [
{
color: "#0d6efd",
backgroundColor: "rgba(13, 110, 253, .2)"
},
{
color: "#6610f2",
backgroundColor: "rgba(102, 16, 242, .2)"
},
{
color: "#20c997",
backgroundColor: "rgba(32, 201, 151, .2)"
},
{
color: "#dc3545",
backgroundColor: "rgba(220, 53, 69, .2)"
},
{
color: "#fd7e14",
backgroundColor: "rgba(253, 126, 20, .2)"
}
]
const colors = [
{
color: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, .2)',
},
{
color: '#6610f2',
backgroundColor: 'rgba(102, 16, 242, .2)',
},
{
color: '#20c997',
backgroundColor: 'rgba(32, 201, 151, .2)',
},
{
color: '#dc3545',
backgroundColor: 'rgba(220, 53, 69, .2)',
},
{
color: '#fd7e14',
backgroundColor: 'rgba(253, 126, 20, .2)',
},
];
return (
<>
<div className='flex flex-col items-center'>
<h3 className="flex items-center text-xl mb-5"><Image src={tag.src} alt="标签墙" width={25} height={25} className="mr-3" /> </h3>
return (
<>
<div className="flex flex-col items-center">
<h3 className="flex items-center text-xl mb-5">
<Image src={tag.src} alt="标签墙" width={25} height={25} className="mr-3" />
</h3>
<div className='overflow-auto h-[270px] pr-1 grid grid-cols-6 gap-2 hide_sliding'>
{
list.map((item, index) => {
const { color, backgroundColor } = colors[getRandom(0, colors.length - 1)]
return <span key={index} className='flex justify-center items-center px-4 h-8 text-xs rounded-md whitespace-nowrap line-clamp-1' style={{ color, backgroundColor }}>{item.name}</span>
})
}
</div>
</div>
</>
)
}
<div className="overflow-auto h-[270px] pr-1 grid grid-cols-6 gap-2 hide_sliding">
{list.map((item, index) => {
const { color, backgroundColor } = colors[getRandom(0, colors.length - 1)];
return (
<span key={index} className="flex justify-center items-center px-4 h-8 text-xs rounded-md whitespace-nowrap line-clamp-1" style={{ color, backgroundColor }}>
{item.name}
</span>
);
})}
</div>
</div>
</>
);
};

View File

@@ -1,97 +1,96 @@
"use client"
'use client';
import Image from "next/image";
import { useEffect, useState } from "react"
import Image from 'next/image';
import { useEffect, useState } from 'react';
import statis from './svg/statis.svg'
import article from './svg/article.svg'
import cate from './svg/cate.svg'
import comment from './svg/comment.svg'
import friend from './svg/friend.svg'
import statis from './svg/statis.svg';
import article from './svg/article.svg';
import cate from './svg/cate.svg';
import comment from './svg/comment.svg';
import friend from './svg/friend.svg';
import { Cate } from "@/types/app/cate";
import { Comment } from "@/types/app/comment";
import { Web } from "@/types/app/web";
import { Cate } from '@/types/app/cate';
import { Comment } from '@/types/app/comment';
import { Web } from '@/types/app/web';
import { getCateListAPI } from "@/api/cate";
import { getCommentListAPI } from "@/api/comment";
import { getWebListAPI } from "@/api/web";
import { getCateListAPI } from '@/api/cate';
import { getCommentListAPI } from '@/api/comment';
import { getWebListAPI } from '@/api/web';
import CateStatis from "./components/CateStatis";
import TagStatis from "./components/TagStatus";
import CateStatis from './components/CateStatis';
import TagStatis from './components/TagStatus';
interface Props {
aTotal: number
aTotal: number;
}
export default ({ aTotal }: Props) => {
const [cateList, setCateList] = useState<Cate[]>([])
const [commentList, setCommentList] = useState<Comment[]>([])
const [linkList, setLinkList] = useState<Web[]>([])
const getData = async () => {
await Promise.all([
getCateListAPI(),
getCommentListAPI(),
getWebListAPI()
]).then(([cateList, commentList, linkList]) => {
setCateList((cateList as null | { data: Cate[] })?.data || [])
setCommentList((commentList as null | { data: Comment[] })?.data || [])
setLinkList((linkList as null | { data: Web[] })?.data || [])
})
}
const [cateList, setCateList] = useState<Cate[]>([]);
const [commentList, setCommentList] = useState<Comment[]>([]);
const [linkList, setLinkList] = useState<Web[]>([]);
useEffect(() => {
getData()
}, [])
const getData = async () => {
await Promise.all([getCateListAPI(), getCommentListAPI(), getWebListAPI()]).then(([cateList, commentList, linkList]) => {
setCateList((cateList as null | { data: Cate[] })?.data || []);
setCommentList((commentList as null | { data: Comment[] })?.data || []);
setLinkList((linkList as null | { data: Web[] })?.data || []);
});
};
return (
<>
<h3 className="flex items-center text-2xl mb-3"><Image src={statis.src} alt="统计" width={36} height={36} className="mr-3" /> </h3>
useEffect(() => {
getData();
}, []);
<div className="mb-10 mt-5">
<div className="grid grid-cols-1 gap-2 xs:grid-cols-2 md:grid-cols-4 md:gap-4">
<div className="flex justify-between items-center px-4 sm:px-5 h-20 sm:h-24 border-2 border-[#0EA5E9] rounded-lg bg-[#F0F9FF]">
<Image src={article} alt="文章" />
return (
<>
<h3 className="flex items-center text-2xl mb-3">
<Image src={statis.src} alt="统计" width={36} height={36} className="mr-3" />
</h3>
<div className="flex flex-col">
<h3 className="text-2xl sm:text-3xl font-sans text-[#0EA5E9] text-end">{aTotal}</h3>
<p className="text-[#0EA5E9]"></p>
</div>
</div>
<div className="mb-10 mt-5">
<div className="grid grid-cols-1 gap-2 xs:grid-cols-2 md:grid-cols-4 md:gap-4">
<div className="flex justify-between items-center px-4 sm:px-5 h-20 sm:h-24 border-2 border-[#0EA5E9] rounded-lg bg-[#F0F9FF]">
<Image src={article} alt="文章" />
<div className="flex justify-between items-center px-4 sm:px-5 h-20 sm:h-24 border-2 border-[#F59E0B] rounded-lg bg-[#FFFBEB]">
<Image src={comment} alt="" />
<div className="flex flex-col">
<h3 className="text-2xl sm:text-3xl font-sans text-[#F59E0B] text-end">{commentList.length}</h3>
<p className="text-[#F59E0B]"></p>
</div>
</div>
<div className="flex justify-between items-center px-4 sm:px-5 h-20 sm:h-24 border-2 border-[#0E9F6E] rounded-lg bg-[#F3FAF7]">
<Image src={cate} alt="分类" />
<div className="flex flex-col">
<h3 className="text-2xl sm:text-3xl font-sans text-[#0E9F6E] text-end">{cateList.length}</h3>
<p className="text-[#0E9F6E]"></p>
</div>
</div>
<div className="flex justify-between items-center px-4 sm:px-5 h-20 sm:h-24 border-2 border-[#EC160F] rounded-lg bg-[#FFF0F0]">
<Image src={friend} alt="友联" />
<div className="flex flex-col">
<h3 className="text-2xl sm:text-3xl font-sans text-[#EC160F] text-end">{linkList.length}</h3>
<p className="text-[#EC160F]"></p>
</div>
</div>
</div>
<div className="flex flex-col md:flex-row justify-between my-14">
<CateStatis />
<TagStatis />
</div>
<div className="flex flex-col">
<h3 className="text-2xl sm:text-3xl font-sans text-[#0EA5E9] text-end">{aTotal}</h3>
<p className="text-[#0EA5E9]"></p>
</div>
</>
)
}
</div>
<div className="flex justify-between items-center px-4 sm:px-5 h-20 sm:h-24 border-2 border-[#F59E0B] rounded-lg bg-[#FFFBEB]">
<Image src={comment} alt="" />
<div className="flex flex-col">
<h3 className="text-2xl sm:text-3xl font-sans text-[#F59E0B] text-end">{commentList.length}</h3>
<p className="text-[#F59E0B]"></p>
</div>
</div>
<div className="flex justify-between items-center px-4 sm:px-5 h-20 sm:h-24 border-2 border-[#0E9F6E] rounded-lg bg-[#F3FAF7]">
<Image src={cate} alt="分类" />
<div className="flex flex-col">
<h3 className="text-2xl sm:text-3xl font-sans text-[#0E9F6E] text-end">{cateList.length}</h3>
<p className="text-[#0E9F6E]"></p>
</div>
</div>
<div className="flex justify-between items-center px-4 sm:px-5 h-20 sm:h-24 border-2 border-[#EC160F] rounded-lg bg-[#FFF0F0]">
<Image src={friend} alt="友联" />
<div className="flex flex-col">
<h3 className="text-2xl sm:text-3xl font-sans text-[#EC160F] text-end">{linkList.length}</h3>
<p className="text-[#EC160F]"></p>
</div>
</div>
</div>
<div className="flex flex-col md:flex-row justify-between my-14">
<CateStatis />
<TagStatis />
</div>
</div>
</>
);
};

View File

@@ -1,31 +1,31 @@
import Slide from "@/components/Slide";
import Starry from "@/components/Starry";
import Statis from './components/Statis'
import Archiving from './components/Archiving'
import { Article } from "@/types/app/article";
import { getArticleListAPI } from "@/api/article";
import Slide from '@/components/Slide';
import Starry from '@/components/Starry';
import Statis from './components/Statis';
import Archiving from './components/Archiving';
import { Article } from '@/types/app/article';
import { getArticleListAPI } from '@/api/article';
export default async () => {
const { data } = await getArticleListAPI() || { data: [] as Article[] }
const { data } = (await getArticleListAPI()) || { data: [] as Article[] };
return (
<>
<title>📊 </title>
<meta name="description" content="📊 数据统计" />
return (
<>
<title>📊 </title>
<meta name="description" content="📊 数据统计" />
<Slide isRipple={false} src="https://bu.dusays.com/2023/11/10/654e2da1d80f8.jpg">
{/* 星空背景组件 */}
<Starry />
<Slide isRipple={false} src="https://bu.dusays.com/2023/11/10/654e2da1d80f8.jpg">
{/* 星空背景组件 */}
<Starry />
<div className="absolute top-[45%] left-[50%] transform -translate-x-1/2 flex flex-col items-center">
<div className="text-white text-[20px] xs:text-[25px] sm:text-[30px] whitespace-nowrap custom_text_shadow"></div>
</div>
</Slide>
<div className="absolute top-[45%] left-[50%] transform -translate-x-1/2 flex flex-col items-center">
<div className="text-white text-[20px] xs:text-[25px] sm:text-[30px] whitespace-nowrap custom_text_shadow"></div>
</div>
</Slide>
<div className="w-[90%] xl:w-[1200px] my-10 mx-auto bg-white dark:bg-black-b p-6 sm:p-10 rounded-xl border dark:border-black-b ">
<Statis aTotal={data?.length} />
<Archiving list={data} />
</div>
</>
)
}
<div className="w-[90%] xl:w-[1200px] my-10 mx-auto bg-white dark:bg-black-b p-6 sm:p-10 rounded-xl border dark:border-black-b ">
<Statis aTotal={data?.length} />
<Archiving list={data} />
</div>
</>
);
};

View File

@@ -1,47 +1,47 @@
import { getPageConfigDataByNameAPI } from "@/api/config";
import { Config } from "@/types/app/config";
import { getPageConfigDataByNameAPI } from '@/api/config';
import { Config } from '@/types/app/config';
interface Equipment {
category: string
description: string
items: { name: string, description: string, price: string, image: string, color: string }[]
category: string;
description: string;
items: { name: string; description: string; price: string; image: string; color: string }[];
}
export default async () => {
const { data } = await getPageConfigDataByNameAPI("equipment") || { data: {} as Config }
const { list } = data.value as { list: Equipment[] }
const { data } = (await getPageConfigDataByNameAPI('equipment')) || { data: {} as Config };
const { list } = data.value as { list: Equipment[] };
return (
<>
<title>🔭 - </title>
<meta name="description" content="🔭 分享我的生产力工具" />
return (
<>
<title>🔭 - </title>
<meta name="description" content="🔭 分享我的生产力工具" />
<div className="pt-20 pb-10">
<div className="w-[90%] lg:w-[1200px] mx-auto mt-10 space-y-20 md:space-y-24">
{list.map((group, index) => (
<div key={index}>
<h2 className="text-xl">{group.category}</h2>
<p className="text-gray-600 mb-6">{group.description}</p>
<div className="pt-20 pb-10">
<div className="w-[90%] lg:w-[1200px] mx-auto mt-10 space-y-20 md:space-y-24">
{list.map((group, index) => (
<div key={index}>
<h2 className="text-xl">{group.category}</h2>
<p className="text-gray-600 mb-6">{group.description}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{group.items.map((item, idx) => (
<div key={idx} className="group overflow-hidden border rounded-lg bg-white dark:bg-black-a transform transition-transform hover:scale-105 cursor-pointer">
<div className="flex justify-center h-40" style={{ backgroundColor: item.color }}>
<img src={item.image} alt={item.name} className="h-full object-cover" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{group.items.map((item, idx) => (
<div key={idx} className="group overflow-hidden border rounded-lg bg-white dark:bg-black-a transform transition-transform hover:scale-105 cursor-pointer">
<div className="flex justify-center h-40" style={{ backgroundColor: item.color }}>
<img src={item.image} alt={item.name} className="h-full object-cover" />
</div>
<div className="p-4">
<h3 className="group-hover:text-primary ">{item.name}</h3>
<p className="text-gray-500 text-sm pt-2 mb-4 line-clamp-2">{item.description}</p>
<span className="mt-2 py-1 px-1.5 rounded-md text-white bg-gray-300 group-hover:bg-primary ">{item.price}</span>
</div>
</div>
))}
</div>
</div>
))}
</div>
<div className="p-4">
<h3 className="group-hover:text-primary ">{item.name}</h3>
<p className="text-gray-500 text-sm pt-2 mb-4 line-clamp-2">{item.description}</p>
<span className="mt-2 py-1 px-1.5 rounded-md text-white bg-gray-300 group-hover:bg-primary ">{item.price}</span>
</div>
</div>
))}
</div>
</div>
</>
)
}
))}
</div>
</div>
</>
);
};

View File

@@ -1,22 +1,22 @@
'use client'
'use client';
import { MdOutlineError } from "react-icons/md";
import { MdOutlineError } from 'react-icons/md';
interface Props {
error: Error & { digest?: string }
error: Error & { digest?: string };
}
function NotFoundPage({ error }: Props) {
return (
<html>
<body className='bg-white'>
<div className='mt-24 mx-auto flex flex-col items-center'>
<MdOutlineError className="text-[15vw] text-[#ff6262]" />
<h1 className='w-6/12 text-[2vw] text-[#888] font-medium mt-8 text-xl'>{error.message}</h1>
</div>
</body>
</html>
)
return (
<html>
<body className="bg-white">
<div className="mt-24 mx-auto flex flex-col items-center">
<MdOutlineError className="text-[15vw] text-[#ff6262]" />
<h1 className="w-6/12 text-[2vw] text-[#888] font-medium mt-8 text-xl">{error.message}</h1>
</div>
</body>
</html>
);
}
export default NotFoundPage
export default NotFoundPage;

View File

@@ -1,13 +1,11 @@
'use client';
import { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { Rss } from '@/types/app/rss';
import { getRssListAPI } from '@/api/rss';
import Loading from '@/components/Loading';
import Empty from '@/components/Empty';
import RandomAvatar from '@/components/RandomAvatar';
import dayjs from 'dayjs';
import parse from 'html-react-parser';
import { HTMLParser } from '@/utils/htmlParser';
import Masonry from 'react-masonry-css';
@@ -20,16 +18,14 @@ const breakpointColumnsObj = {
1450: 4,
1350: 3,
1024: 2,
768: 1
768: 1,
};
export default function FishpondPage() {
const [rssData, setRssData] = useState<Rss[] | null>(null);
const [loading, setLoading] = useState(true);
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
const fetchRssData = async (page: number = 1) => {
const getRssList = async () => {
try {
setLoading(true);
const response = await getRssListAPI();
@@ -45,38 +41,28 @@ export default function FishpondPage() {
};
useEffect(() => {
fetchRssData(currentPage);
}, [currentPage]);
getRssList();
}, []);
// 渲染内容组件
const ContentRenderer = ({ content, mode = 'html' }: { content: string, mode?: 'html' | 'text' }) => {
const ContentRenderer = ({ content, mode = 'html' }: { content: string; mode?: 'html' | 'text' }) => {
if (mode === 'text') {
const summary = HTMLParser.getSummary(content, 150);
return (
<p className="m-0 my-2 text-sm leading-6 text-[#666] text-justify">
{summary.text}
</p>
);
return <p className="m-0 my-2 text-sm leading-6 text-[#666] text-justify">{summary.text}</p>;
}
// HTML模式安全渲染
const cleanHTML = HTMLParser.sanitize(content, {
allowedTags: ['p', 'br', 'strong', 'em', 'u', 'a', 'span', 'div'],
allowedAttributes: ['href', 'target', 'rel'],
maxLength: 150
maxLength: 150,
});
return (
<div className="m-0 my-2 text-sm leading-6 text-[#666] dark:text-gray-400 text-justify">
{parse(cleanHTML)}
</div>
);
return <div className="m-0 my-2 text-sm leading-6 text-[#666] dark:text-gray-400 text-justify">{parse(cleanHTML)}</div>;
};
if (loading) {
return (
<Loading />
);
return <Loading />;
}
return (
@@ -89,32 +75,18 @@ export default function FishpondPage() {
<div>
{rssData && rssData.length > 0 ? (
<Masonry
breakpointCols={breakpointColumnsObj}
className="masonry-grid pl-10 pr-4"
columnClassName="masonry-grid_column"
>
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid pl-10 pr-4" columnClassName="masonry-grid_column">
{rssData.map((item, index) => {
return (
<div key={`${item.url}-${index}`} className="border border-[#eee] dark:border-black-b rounded-md transition-shadow hover:shadow-[0_2px_8px_rgba(186,186,186,0.15)] bg-white dark:bg-black-b p-5 pb-3 hover:-translate-y-0.5 transition-transform mb-3 break-inside-avoid">
<div className="flex justify-between items-center mb-3.75">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full overflow-hidden border-2 border-[#eee] dark:border-black-b">
{item.image ? (
<img src={item.image} alt="avatar" className="w-full h-full object-cover" />
) : (
<RandomAvatar className="w-full h-full rounded-full" />
)}
</div>
<div className="w-10 h-10 rounded-full overflow-hidden border-2 border-[#eee] dark:border-black-b">{item.image ? <img src={item.image} alt="avatar" className="w-full h-full object-cover" /> : <RandomAvatar className="w-full h-full rounded-full" />}</div>
<div className="text-sm font-medium text-[#333]">
{item.email ? item.email.split('@')[0] : '匿名用户'}
</div>
<div className="text-sm font-medium text-[#333]">{item.email ? item.email.split('@')[0] : '匿名用户'}</div>
</div>
<div className="text-xs text-[#666] dark:text-gray-300 opacity-80">
{item.createTime ? dayFormat(item.createTime) : ''}
</div>
<div className="text-xs text-[#666] dark:text-gray-300 opacity-80">{item.createTime ? dayFormat(item.createTime) : ''}</div>
</div>
<div>
@@ -129,9 +101,7 @@ export default function FishpondPage() {
<div className="flex justify-between items-center mt-2 pt-2 border-t border-[#eee] dark:border-black-b">
<div className="flex items-center gap-2">
<span className="inline-block px-3 py-1 bg-gray-100 text-gray-800 dark:bg-slate-600 dark:text-white text-xs rounded-full font-medium">
{item.type}
</span>
<span className="inline-block px-3 py-1 bg-gray-100 text-gray-800 dark:bg-slate-600 dark:text-white text-xs rounded-full font-medium">{item.type}</span>
</div>
</div>
</div>
@@ -145,4 +115,4 @@ export default function FishpondPage() {
</div>
</>
);
}
}

View File

@@ -1,84 +1,85 @@
"use client"
'use client';
import { useEffect, useState } from "react";
import { Modal, ModalContent, ModalHeader, ModalBody, useDisclosure } from "@heroui/react";
import { getFootprintListAPI } from "@/api/footprint";
import { Footprint } from "@/types/app/footprint";
import { useEffect, useState } from 'react';
import { Modal, ModalContent, ModalHeader, ModalBody, useDisclosure } from '@heroui/react';
import { getFootprintListAPI } from '@/api/footprint';
import { Footprint } from '@/types/app/footprint';
import { PhotoProvider, PhotoView } from 'react-photo-view';
import 'react-photo-view/dist/react-photo-view.css';
import dayjs from 'dayjs'
import Masonry from "react-masonry-css";
import "./page.scss";
import { getGaodeMapConfigDataAPI } from "@/api/config";
import dayjs from 'dayjs';
import Masonry from 'react-masonry-css';
import './page.scss';
import { getGaodeMapConfigDataAPI } from '@/api/config';
const breakpointColumnsObj = {
default: 4,
1024: 3,
700: 2
default: 4,
1024: 3,
700: 2,
};
export default function MapContainer() {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [isDismissable, setIsDismissable] = useState(true);
const [list, setList] = useState<Footprint[]>([])
const [data, setData] = useState<Footprint>({} as Footprint);
let map: any = null;
let infoWindow: any = null;
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [isDismissable, setIsDismissable] = useState(true);
const [list, setList] = useState<Footprint[]>([]);
const [data, setData] = useState<Footprint>({} as Footprint);
let map: any = null;
let infoWindow: any = null;
const getFootprintList = async () => {
const { data } = (await getFootprintListAPI()) || { data: [] as Footprint[] }
setList(data)
}
const getFootprintList = async () => {
const { data } = (await getFootprintListAPI()) || { data: [] as Footprint[] };
setList(data);
};
useEffect(() => {
getFootprintList()
}, [])
useEffect(() => {
getFootprintList();
}, []);
useEffect(() => {
if (!list.length) return
useEffect(() => {
if (!list.length) return;
// 确保代码仅在客户端执行
import('@amap/amap-jsapi-loader').then(async AMapLoader => {
const { data } = await getGaodeMapConfigDataAPI() || { data: {} }
const { key_code, security_code } = data as { key_code: string, security_code: string }
// 确保代码仅在客户端执行
import('@amap/amap-jsapi-loader').then(async (AMapLoader) => {
const { data } = (await getGaodeMapConfigDataAPI()) || { data: {} };
const { key_code, security_code } = data as { key_code: string; security_code: string };
// @ts-ignore
window._AMapSecurityConfig = {
securityJsCode: security_code,
};
(window as any)._AMapSecurityConfig = {
securityJsCode: security_code,
};
AMapLoader.load({
key: key_code,
version: "2.0",
plugins: ["AMap.Scale", "AMap.Marker", "AMap.InfoWindow"],
})
.then((AMap) => {
map = new AMap.Map("container", {
mapStyle: "amap://styles/grey",
viewMode: "3D",
zoom: 4.8,
center: [105.625368, 37.746599],
});
AMapLoader.load({
key: key_code,
version: '2.0',
plugins: ['AMap.Scale', 'AMap.Marker', 'AMap.InfoWindow'],
})
.then((AMap) => {
map = new AMap.Map('container', {
mapStyle: 'amap://styles/grey',
viewMode: '3D',
zoom: 4.8,
center: [105.625368, 37.746599],
});
// 创建信息窗体
infoWindow = new AMap.InfoWindow({
offset: new AMap.Pixel(0, -30),
autoMove: true,
anchor: 'bottom-center',
isCustom: true, // 使用自定义窗体
});
// 创建信息窗体
infoWindow = new AMap.InfoWindow({
offset: new AMap.Pixel(0, -30),
autoMove: true,
anchor: 'bottom-center',
isCustom: true, // 使用自定义窗体
});
// 点击地图任意位置时关闭信息窗体
map.on("click", () => {
infoWindow.close();
});
// 点击地图任意位置时关闭信息窗体
map.on('click', () => {
infoWindow.close();
});
// 遍历 locations 数组,创建标记
list?.forEach((data) => {
const marker = new AMap.Marker({
position: data?.position.split(","),
map: map,
content: data?.images[0] && `
// 遍历 locations 数组,创建标记
list?.forEach((data) => {
const marker = new AMap.Marker({
position: data?.position.split(','),
map: map,
content:
data?.images[0] &&
`
<div style="display: flex; justify-content: center; align-items: center; background-color: #fff; width: 35px; height: 35px; border-radius: 50%; overflow: hidden; box-shadow: 0 0 5px 1px rgba(255, 255, 255, 0.4); animation: pulse 2s infinite;">
<img src="${data?.images[0]}" alt="" style="width: 90%; height: 90%; border-radius: 50%;">
</div>
@@ -95,12 +96,12 @@ export default function MapContainer() {
}
}
</style>
`
});
`,
});
// 点击标记时,显示信息窗体并定位到该位置
marker.on("click", () => {
const content = `
// 点击标记时,显示信息窗体并定位到该位置
marker.on('click', () => {
const content = `
<div style="border-radius: 12px; overflow: hidden; width: 240px; margin-top: 25px; margin-left: 20px;">
<div style="position: relative; width: 100%; padding-bottom: 100%; overflow: hidden; border-radius: 12px;">
<img src="${data?.images[0]}" alt="" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;">
@@ -147,100 +148,94 @@ export default function MapContainer() {
</div>
</div>
`;
infoWindow.setContent(content);
infoWindow.open(map, marker.getPosition());
setData(data);
// 设置地图中心点和缩放级别
map.setCenter(marker.getPosition());
map.setZoom(15);
});
});
infoWindow.setContent(content);
infoWindow.open(map, marker.getPosition());
setData(data);
// 设置地图中心点和缩放级别
map.setCenter(marker.getPosition());
map.setZoom(15);
});
});
// 监听自定义事件来打开 Modal
document.addEventListener('openModal', (event) => {
const button = event.target as HTMLElement;
const id = button.getAttribute('data-id');
if (id) {
const targetData = list.find(item => item.id === Number(id));
if (targetData) {
setData(targetData);
onOpen();
}
}
});
})
.catch((e) => {
console.log(e);
});
return () => {
map?.destroy();
infoWindow?.destroy();
};
// 监听自定义事件来打开 Modal
document.addEventListener('openModal', (event) => {
const button = event.target as HTMLElement;
const id = button.getAttribute('data-id');
if (id) {
const targetData = list.find((item) => item.id === Number(id));
if (targetData) {
setData(targetData);
onOpen();
}
}
});
})
.catch((e) => {
console.log(e);
});
}, [list]);
return (
<>
<title> </title>
<meta name="description" content="⛳️ 那年走过的路" />
<div id="container"></div>
return () => {
map?.destroy();
infoWindow?.destroy();
};
});
}, [list]);
<Modal
size="4xl"
backdrop="opaque"
isDismissable={isDismissable}
isOpen={isOpen}
onOpenChange={(open) => {
if (isDismissable || !open) onOpenChange();
}}
classNames={{
backdrop: "bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20"
}}
>
<ModalContent className="bg-[rgba(36,40,45,0.9)]">
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1 text-center pb-2 text-white">{data?.title}</ModalHeader>
<ModalBody>
<div className="flex flex-col">
<div className="flex flex-col justify-between w-full mb-8">
<p className="overflow-auto max-h-[210px] text-[#d6d6d6] px-[5px]">{data?.content}</p>
<div className="text-sm text-end text-[#a5a5a5] pt-2">
<p>{dayjs(+data?.createTime).format('YYYY-MM-DD HH:mm')}</p>
<p>{data?.address}</p>
</div>
</div>
return (
<>
<title> </title>
<meta name="description" content="⛳️ 那年走过的路" />
<div id="container"></div>
<div className={`overflow-auto flex justify-center w-full ${data?.images.length !== 1 ? 'max-h-96' : ''} mb-5 hide_sliding`}>
<PhotoProvider
speed={() => 800}
easing={(type) => (type === 2 ? 'cubic-bezier(0.36, 0, 0.66, -0.56)' : 'cubic-bezier(0.34, 1.56, 0.64, 1)')}
onVisibleChange={(visible) => {
setIsDismissable(!visible);
}}
>
<Masonry
breakpointCols={breakpointColumnsObj}
className="masonry-grid mb-12"
columnClassName="masonry-grid_column"
>
{
data?.images?.map((item, index) => (
<PhotoView src={item} key={index}>
<img src={item} alt="" className="rounded-2xl w-full mb-3 cursor-pointer" />
</PhotoView>
))
}
</Masonry>
</PhotoProvider>
</div>
</div>
</ModalBody>
</>
)}
</ModalContent>
</Modal>
</>
);
}
<Modal
size="4xl"
backdrop="opaque"
isDismissable={isDismissable}
isOpen={isOpen}
onOpenChange={(open) => {
if (isDismissable || !open) onOpenChange();
}}
classNames={{
backdrop: 'bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20',
}}
>
<ModalContent className="bg-[rgba(36,40,45,0.9)]">
{() => (
<>
<ModalHeader className="flex flex-col gap-1 text-center pb-2 text-white">{data?.title}</ModalHeader>
<ModalBody>
<div className="flex flex-col">
<div className="flex flex-col justify-between w-full mb-8">
<p className="overflow-auto max-h-[210px] text-[#d6d6d6] px-[5px]">{data?.content}</p>
<div className="text-sm text-end text-[#a5a5a5] pt-2">
<p>{dayjs(+data?.createTime).format('YYYY-MM-DD HH:mm')}</p>
<p>{data?.address}</p>
</div>
</div>
<div className={`overflow-auto flex justify-center w-full ${data?.images.length !== 1 ? 'max-h-96' : ''} mb-5 hide_sliding`}>
<PhotoProvider
speed={() => 800}
easing={(type) => (type === 2 ? 'cubic-bezier(0.36, 0, 0.66, -0.56)' : 'cubic-bezier(0.34, 1.56, 0.64, 1)')}
onVisibleChange={(visible) => {
setIsDismissable(!visible);
}}
>
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid mb-12" columnClassName="masonry-grid_column">
{data?.images?.map((item, index) => (
<PhotoView src={item} key={index}>
<img src={item} alt="" className="rounded-2xl w-full mb-3 cursor-pointer" />
</PhotoView>
))}
</Masonry>
</PhotoProvider>
</div>
</div>
</ModalBody>
</>
)}
</ModalContent>
</Modal>
</>
);
}

View File

@@ -1,35 +1,35 @@
"use client"
'use client';
import { useEffect, useState } from "react";
import { Input, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure, Select, SelectItem, Textarea } from "@heroui/react";
import { Controller, SubmitHandler, useForm } from "react-hook-form";
import { Web, WebType } from "@/types/app/web";
import { addWebDataAPI, getWebTypeListAPI } from '@/api/web'
import { Bounce, toast, ToastOptions } from "react-toastify";
import { useEffect, useState } from 'react';
import { Input, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure, Select, SelectItem, Textarea } from '@heroui/react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { Web, WebType } from '@/types/app/web';
import { addWebDataAPI, getWebTypeListAPI } from '@/api/web';
import { Bounce, toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const toastConfig: ToastOptions = {
position: "top-right",
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
theme: 'colored',
transition: Bounce,
}
};
export default () => {
const [loading, setLoading] = useState(false)
const [loading, setLoading] = useState(false);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
// 获取网站类型列表
const [typeList, setTypeList] = useState<WebType[]>([])
const [typeList, setTypeList] = useState<WebType[]>([]);
const getWebTypeList = async () => {
const { data } = (await getWebTypeListAPI()) || { data: [] as WebType[] }
setTypeList(data.filter(item => !item.isAdmin))
}
const { data } = (await getWebTypeListAPI()) || { data: [] as WebType[] };
setTypeList(data.filter((item) => !item.isAdmin));
};
useEffect(() => {
// 页面加载后检查是否有需要显示的消息
@@ -39,204 +39,159 @@ export default () => {
localStorage.removeItem('toastMessage'); // 显示后删除消息
}
getWebTypeList()
}, [])
getWebTypeList();
}, []);
const { handleSubmit, control, formState: { errors }, trigger } = useForm<Web>({ defaultValues: {} as Web });
const {
handleSubmit,
control,
formState: { errors },
trigger,
} = useForm<Web>({ defaultValues: {} as Web });
const onSubmit: SubmitHandler<Web> = async (data, event) => {
event?.preventDefault();
setLoading(true)
const { code, message } = (await addWebDataAPI({ ...data, createTime: Date.now().toString() })) || { code: 0, message: "" }
setLoading(true);
const { code, message } = (await addWebDataAPI({ ...data, createTime: Date.now().toString() })) || { code: 0, message: '' };
if (code !== 200) return toast.error(message, toastConfig);
setLoading(false)
setLoading(false);
localStorage.setItem('toastMessage', '🎉 提交成功, 请等待审核!');
window.location.reload();
onOpenChange()
}
onOpenChange();
};
// 表单样式
const inputWrapper = "hover:!border-primary group-data-[focus=true]:border-primary rounded-md"
const inputWrapper = 'hover:!border-primary group-data-[focus=true]:border-primary rounded-md';
return (<>
<Button color="primary" variant="shadow" onPress={onOpen}></Button>
<Modal
size="lg"
backdrop="opaque"
isOpen={isOpen}
onOpenChange={onOpenChange}
classNames={{
backdrop: "bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20"
}}
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1"></ModalHeader>
return (
<>
<Button color="primary" variant="shadow" onPress={onOpen}>
</Button>
<ModalBody>
<div className="mx-auto mb-4 p-3 space-y-2 border-l-[3px] border-primary bg-[#ecf7fe] rounded-md text-sm text-black-b">
<p>1</p>
<p>210</p>
<p>3 80% ()</p>
</div>
<Modal
size="lg"
backdrop="opaque"
isOpen={isOpen}
onOpenChange={onOpenChange}
classNames={{
backdrop: 'bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20',
}}
>
<ModalContent>
{() => (
<>
<ModalHeader className="flex flex-col gap-1"></ModalHeader>
<Controller
name="title"
control={control}
rules={{ required: '请输入网站名称' }}
render={({ field }) => (
<>
<Input
{...field}
type="text"
label="网站名称"
variant="bordered"
placeholder="示例:宇阳"
isInvalid={!!errors.title?.message}
errorMessage={errors.title?.message}
onBlur={() => trigger('title')}
classNames={{ inputWrapper }}
/>
</>
)}
/>
<ModalBody>
<div className="mx-auto mb-4 p-3 space-y-2 border-l-[3px] border-primary bg-[#ecf7fe] rounded-md text-sm text-black-b">
<p>1</p>
<p>210</p>
<p>3 80% ()</p>
</div>
<Controller
name="description"
control={control}
rules={{ required: '请输入网站介绍' }}
render={({ field }) => (
<>
<Textarea
{...field}
label="网站介绍"
variant="bordered"
placeholder="示例:逐渐强大的全栈开发工程师"
isInvalid={!!errors.description?.message}
errorMessage={errors.description?.message}
onBlur={() => trigger('description')}
classNames={{ inputWrapper }}
/>
</>
)}
/>
<Controller
name="title"
control={control}
rules={{ required: '请输入网站名称' }}
render={({ field }) => (
<>
<Input {...field} type="text" label="网站名称" variant="bordered" placeholder="示例:宇阳" isInvalid={!!errors.title?.message} errorMessage={errors.title?.message} onBlur={() => trigger('title')} classNames={{ inputWrapper }} />
</>
)}
/>
<Controller
name="image"
control={control}
rules={{ required: '请输入图片地址', pattern: { value: /^https?:\/\//, message: "请输入正确的图片地址" } }}
render={({ field }) => (
<>
<Input
{...field}
type="text"
label="图片地址"
variant="bordered"
placeholder="示例https://liuyuyang.net/avatar.jpg"
isInvalid={!!errors.image?.message}
errorMessage={errors.image?.message}
onBlur={() => trigger('image')}
classNames={{ inputWrapper }}
/>
</>
)}
/>
<Controller
name="description"
control={control}
rules={{ required: '请输入网站介绍' }}
render={({ field }) => (
<>
<Textarea {...field} label="网站介绍" variant="bordered" placeholder="示例:逐渐强大的全栈开发工程师" isInvalid={!!errors.description?.message} errorMessage={errors.description?.message} onBlur={() => trigger('description')} classNames={{ inputWrapper }} />
</>
)}
/>
<Controller
name="url"
control={control}
rules={{ required: '请输入网站地址', pattern: { value: /^https?:\/\//, message: "请输入正确的网站地址" } }}
render={({ field }) => (
<>
<Input
{...field}
type="text"
label="网站地址"
variant="bordered"
placeholder="示例https://liuyuyang.net/"
isInvalid={!!errors.url?.message}
errorMessage={errors.url?.message}
onBlur={() => trigger('url')}
classNames={{ inputWrapper }}
/>
</>
)}
/>
<Controller
name="image"
control={control}
rules={{ required: '请输入图片地址', pattern: { value: /^https?:\/\//, message: '请输入正确的图片地址' } }}
render={({ field }) => (
<>
<Input {...field} type="text" label="图片地址" variant="bordered" placeholder="示例https://liuyuyang.net/avatar.jpg" isInvalid={!!errors.image?.message} errorMessage={errors.image?.message} onBlur={() => trigger('image')} classNames={{ inputWrapper }} />
</>
)}
/>
<Controller
name="email"
control={control}
rules={{ pattern: { value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: "请输入正确的邮箱" } }}
render={({ field }) => (
<>
<Input
{...field}
type="text"
label="邮箱(选填)"
variant="bordered"
placeholder="示例liuyuyang1024@yeah.net"
isInvalid={!!errors.email?.message}
errorMessage={errors.email?.message}
onBlur={() => trigger('email')}
classNames={{ inputWrapper }}
/>
</>
)}
/>
<Controller
name="url"
control={control}
rules={{ required: '请输入网站地址', pattern: { value: /^https?:\/\//, message: '请输入正确的网站地址' } }}
render={({ field }) => (
<>
<Input {...field} type="text" label="网站地址" variant="bordered" placeholder="示例https://liuyuyang.net/" isInvalid={!!errors.url?.message} errorMessage={errors.url?.message} onBlur={() => trigger('url')} classNames={{ inputWrapper }} />
</>
)}
/>
<Controller
name="rss"
control={control}
rules={{ pattern: { value: /^https?:\/\//, message: "请输入正确的订阅地址" } }}
render={({ field }) => (
<>
<Input
{...field}
type="text"
label="订阅地址(选填)"
variant="bordered"
placeholder="示例https://liuyuyang.net/index.php/feed/"
isInvalid={!!errors.rss?.message}
errorMessage={errors.rss?.message}
onBlur={() => trigger('rss')}
classNames={{ inputWrapper }}
/>
</>
)}
/>
<Controller
name="email"
control={control}
rules={{ pattern: { value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: '请输入正确的邮箱' } }}
render={({ field }) => (
<>
<Input {...field} type="text" label="邮箱(选填)" variant="bordered" placeholder="示例liuyuyang1024@yeah.net" isInvalid={!!errors.email?.message} errorMessage={errors.email?.message} onBlur={() => trigger('email')} classNames={{ inputWrapper }} />
</>
)}
/>
<Controller
name="typeId"
control={control}
rules={{ required: '请选择网站类型' }}
render={({ field }) => (
<>
<Select
{...field}
label="网站类型"
variant="bordered"
placeholder="示例:技术类"
isInvalid={!!errors.typeId?.message}
errorMessage={errors.typeId?.message}
classNames={{
trigger: "hover:!border-primary data-[focus=true]:!border-primary data-[open=true]:!border-primary rounded-md"
}}
>
{typeList?.map(item => <SelectItem key={item.id}>{item.name}</SelectItem>)}
</Select>
</>
)}
/>
</ModalBody>
<Controller
name="rss"
control={control}
rules={{ pattern: { value: /^https?:\/\//, message: '请输入正确的订阅地址' } }}
render={({ field }) => (
<>
<Input {...field} type="text" label="订阅地址(选填)" variant="bordered" placeholder="示例https://liuyuyang.net/index.php/feed/" isInvalid={!!errors.rss?.message} errorMessage={errors.rss?.message} onBlur={() => trigger('rss')} classNames={{ inputWrapper }} />
</>
)}
/>
<ModalFooter>
<Button color="primary" isLoading={loading} onPress={() => handleSubmit(onSubmit)()} className="w-full"></Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>);
}
<Controller
name="typeId"
control={control}
rules={{ required: '请选择网站类型' }}
render={({ field }) => (
<>
<Select
{...field}
label="网站类型"
variant="bordered"
placeholder="示例:技术类"
isInvalid={!!errors.typeId?.message}
errorMessage={errors.typeId?.message}
classNames={{
trigger: 'hover:!border-primary data-[focus=true]:!border-primary data-[open=true]:!border-primary rounded-md',
}}
>
{typeList?.map((item) => (
<SelectItem key={item.id}>{item.name}</SelectItem>
))}
</Select>
</>
)}
/>
</ModalBody>
<ModalFooter>
<Button color="primary" isLoading={loading} onPress={() => handleSubmit(onSubmit)()} className="w-full">
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
);
};

View File

@@ -1,52 +1,50 @@
'use client'
'use client';
import { toast } from 'react-toastify'
import { toast } from 'react-toastify';
interface CopyableTextProps {
text: string
children: React.ReactNode
className?: string
text: string;
children: React.ReactNode;
className?: string;
}
export default function CopyableText({ text, children, className = "" }: CopyableTextProps) {
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text)
toast.success('复制成功!', {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
})
} catch (err) {
// 降级处理,使用传统方法复制
const textArea = document.createElement('textarea')
textArea.value = text
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
toast.success('复制成功!', {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
})
}
}
export default function CopyableText({ text, children, className = '' }: CopyableTextProps) {
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
toast.success('复制成功!', {
position: 'top-right',
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
});
} catch (error) {
// 降级处理,使用传统方法复制
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
return (
<span
className={`hover:text-primary cursor-pointer transition-colors ${className}`}
onClick={handleCopy}
title="点击复制"
>
{children}
</span>
)
}
toast.success('复制成功!', {
position: 'top-right',
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
});
console.error(error);
}
};
return (
<span className={`hover:text-primary cursor-pointer transition-colors ${className}`} onClick={handleCopy} title="点击复制">
{children}
</span>
);
}

View File

@@ -1,122 +1,129 @@
import Link from "next/link";
import { Metadata } from "next";
import Link from 'next/link';
import { Metadata } from 'next';
import { getWebConfigDataAPI } from "@/api/config";
import { getWebListAPI, getWebTypeListAPI } from '@/api/web'
import { Web as WebLink, WebType } from "@/types/app/web";
import { getWebConfigDataAPI } from '@/api/config';
import { getWebListAPI, getWebTypeListAPI } from '@/api/web';
import { Web as WebLink, WebType } from '@/types/app/web';
import Slide from "@/components/Slide";
import Starry from "@/components/Starry";
import ApplyForAdd from "./components/ApplyForAdd";
import CopyableText from "./components/CopyableText";
import Slide from '@/components/Slide';
import Starry from '@/components/Starry';
import ApplyForAdd from './components/ApplyForAdd';
import CopyableText from './components/CopyableText';
import { ToastContainer } from "react-toastify";
import { getUserDataAPI } from "@/api/user";
import { User } from "@/types/app/user";
import { Web } from "@/types/app/config";
import { ToastContainer } from 'react-toastify';
import { getUserDataAPI } from '@/api/user';
import { User } from '@/types/app/user';
import { Web } from '@/types/app/config';
export const metadata: Metadata = {
title: "😇 朋友圈",
description: "😇 朋友圈",
title: '😇 朋友圈',
description: '😇 朋友圈',
};
export default async () => {
const { data: user } = await getUserDataAPI() || { data: {} as User }
const { data: { value: web } } = (await getWebConfigDataAPI<{ value: Web }>("web")) || { data: { value: {} as Web } };
const { data: linkList } = await getWebListAPI() || { data: [] as WebLink[] }
const { data: typeList } = await getWebTypeListAPI() || { data: [] as WebType[] }
const { data: user } = (await getUserDataAPI()) || { data: {} as User };
const {
data: { value: web },
} = (await getWebConfigDataAPI<{ value: Web }>('web')) || { data: { value: {} as Web } };
const { data: linkList } = (await getWebListAPI()) || { data: [] as WebLink[] };
const { data: typeList } = (await getWebTypeListAPI()) || { data: [] as WebType[] };
let data: { [string: string]: { order: number, list: WebLink[] } } = {}
let data: { [string: string]: { order: number; list: WebLink[] } } = {};
linkList.sort((a: WebLink, b: WebLink) => a.order - b.order)
linkList.sort((a: WebLink, b: WebLink) => a.order - b.order);
// 给每个数据进行分组处理
linkList?.forEach((item: WebLink) => {
if (data[item.type.name]) {
data[item.type.name].list.push(item)
} else {
// 查询出当前类型的排序
const order = typeList.find(({ name }) => name === item.type.name)?.order!
data[item.type.name] = { order, list: [] }
data[item.type.name].list = [item]
}
})
// 给每个数据进行分组处理
linkList?.forEach((item: WebLink) => {
if (data[item.type.name]) {
data[item.type.name].list.push(item);
} else {
// 查询出当前类型的排序
const order = typeList.find(({ name }) => name === item.type.name)?.order ?? 0;
data[item.type.name] = { order, list: [] };
data[item.type.name].list = [item];
}
});
// 根据order进行从小到大排序
const dataTemp = Object.entries(data);
dataTemp.sort((a, b) => a[1].order - b[1].order);
data = Object.fromEntries(dataTemp);
// 根据order进行从小到大排序
const dataTemp = Object.entries(data);
dataTemp.sort((a, b) => a[1].order - b[1].order);
data = Object.fromEntries(dataTemp);
return (
<>
<Slide isRipple={false}>
{/* 星空背景组件 */}
<Starry />
return (
<>
<Slide isRipple={false}>
{/* 星空背景组件 */}
<Starry />
<div className="absolute top-[30%] left-[50%] transform -translate-x-1/2 flex flex-col items-center">
<div className="text-white text-[20px] xs:text-[25px] sm:text-[30px] whitespace-nowrap custom_text_shadow"></div>
<div className="mt-4 sm:mt-8">
<ApplyForAdd />
</div>
</div>
</Slide>
<div className="absolute top-[30%] left-[50%] transform -translate-x-1/2 flex flex-col items-center">
<div className="text-white text-[20px] xs:text-[25px] sm:text-[30px] whitespace-nowrap custom_text_shadow"></div>
<div className="mt-4 sm:mt-8">
<ApplyForAdd />
</div>
</div>
</Slide>
<div className="bg-[linear-gradient(180deg,#edf6ff_0%,#ffffff_100%)] dark:bg-[linear-gradient(to_right,#232931_0%,#232931_100%)]">
<div className="relative -top-20 xs:-top-20 sm:-top-32 md:-top-36 w-[90%] xl:w-[1200px] p-10 pt-2 mx-auto bg-white dark:bg-black-b border dark:border-black-b rounded-2xl space-y-8 ">
<div>
<h3 className="w-full text-center text-xl p-4 dark:text-white "></h3>
<div className="bg-[linear-gradient(180deg,#edf6ff_0%,#ffffff_100%)] dark:bg-[linear-gradient(to_right,#232931_0%,#232931_100%)]">
<div className="relative -top-20 xs:-top-20 sm:-top-32 md:-top-36 w-[90%] xl:w-[1200px] p-10 pt-2 mx-auto bg-white dark:bg-black-b border dark:border-black-b rounded-2xl space-y-8 ">
<div>
<h3 className="w-full text-center text-xl p-4 dark:text-white "></h3>
<div className="mx-auto p-3 space-y-2 border-l-[3px] border-primary bg-[#ecf7fe] dark:bg-[#333b48] rounded-md text-sm text-black-b dark:text-gray-300">
<p><CopyableText text={web?.title}>{web?.title}</CopyableText></p>
<p><CopyableText text={web?.description}>{web?.description}</CopyableText></p>
<p><CopyableText text={user?.avatar || ''}>{user?.avatar}</CopyableText></p>
<p><CopyableText text={web?.url}>{web?.url}</CopyableText></p>
<p>Rss地址<CopyableText text={web?.url + '/api/rss'}>{web?.url + '/api/rss'}</CopyableText></p>
</div>
</div>
{
Object.keys(data)?.map((type, index) => (
<div key={index}>
<h3 className="w-full text-center text-xl p-4 dark:text-white ">{type}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{
type === "全站置顶" &&
<Link href="https://liuyuyang.net" target="_blank" className="group">
<div className="flex items-center p-3 border group-hover:border-2 dark:border-[#3d4653] group-hover:!border-primary group-hover:shadow-[0_10px_20px_1px_rgb(83,157,253,.1)] rounded-md ">
<img src="https://q1.qlogo.cn/g?b=qq&nk=3311118881&s=640" alt="项目作者" className="w-14 h-14 mr-4 rounded-full" />
<div className="flex flex-col space-y-2">
<h4 className="text-sm text-gray-700 dark:text-white group-hover:text-primary"></h4>
<p className="text-xs text-[#8c9ab1] line-clamp-2">ThriveX </p>
</div>
</div>
</Link>
}
{
data[type].list?.map((item: WebLink) => (
<Link key={item.id} href={item.url} target="_blank" className="group">
<div key={item.id} className="flex items-center p-3 border group-hover:border-2 dark:border-[#3d4653] group-hover:!border-primary group-hover:shadow-[0_10px_20px_1px_rgb(83,157,253,.1)] rounded-md ">
<img src={item.image} alt={item.title} className="w-14 h-14 mr-4 rounded-full" />
<div className="flex flex-col space-y-2">
<h4 className="text-sm text-gray-700 dark:text-white group-hover:text-primary">{item.title}</h4>
<p className="text-xs text-[#8c9ab1] line-clamp-2">{item.description}</p>
</div>
</div>
</Link>
))
}
</div>
</div>
))
}
</div>
<div className="mx-auto p-3 space-y-2 border-l-[3px] border-primary bg-[#ecf7fe] dark:bg-[#333b48] rounded-md text-sm text-black-b dark:text-gray-300">
<p>
<CopyableText text={web?.title}>{web?.title}</CopyableText>
</p>
<p>
<CopyableText text={web?.description}>{web?.description}</CopyableText>
</p>
<p>
<CopyableText text={user?.avatar || ''}>{user?.avatar}</CopyableText>
</p>
<p>
<CopyableText text={web?.url}>{web?.url}</CopyableText>
</p>
<p>
Rss地址<CopyableText text={web?.url + '/api/rss'}>{web?.url + '/api/rss'}</CopyableText>
</p>
</div>
</div>
<ToastContainer />
</>
)
}
{Object.keys(data)?.map((type, index) => (
<div key={index}>
<h3 className="w-full text-center text-xl p-4 dark:text-white ">{type}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{type === '全站置顶' && (
<Link href="https://liuyuyang.net" target="_blank" className="group">
<div className="flex items-center p-3 border group-hover:border-2 dark:border-[#3d4653] group-hover:!border-primary group-hover:shadow-[0_10px_20px_1px_rgb(83,157,253,.1)] rounded-md ">
<img src="https://q1.qlogo.cn/g?b=qq&nk=3311118881&s=640" alt="项目作者" className="w-14 h-14 mr-4 rounded-full" />
<div className="flex flex-col space-y-2">
<h4 className="text-sm text-gray-700 dark:text-white group-hover:text-primary"></h4>
<p className="text-xs text-[#8c9ab1] line-clamp-2">ThriveX </p>
</div>
</div>
</Link>
)}
{data[type].list?.map((item: WebLink) => (
<Link key={item.id} href={item.url} target="_blank" className="group">
<div key={item.id} className="flex items-center p-3 border group-hover:border-2 dark:border-[#3d4653] group-hover:!border-primary group-hover:shadow-[0_10px_20px_1px_rgb(83,157,253,.1)] rounded-md ">
<img src={item.image} alt={item.title} className="w-14 h-14 mr-4 rounded-full" />
<div className="flex flex-col space-y-2">
<h4 className="text-sm text-gray-700 dark:text-white group-hover:text-primary">{item.title}</h4>
<p className="text-xs text-[#8c9ab1] line-clamp-2">{item.description}</p>
</div>
</div>
</Link>
))}
</div>
</div>
))}
</div>
</div>
<ToastContainer />
</>
);
};

View File

@@ -1,36 +1,40 @@
import localFont from 'next/font/local'
import localFont from 'next/font/local';
import HeroUIProvider from "@/components/HeroUIProvider";
import HeroUIProvider from '@/components/HeroUIProvider';
import NProgress from '@/components/NProgress';
import Header from '@/components/Header'
import Footer from '@/components/Footer'
import Header from '@/components/Header';
import Footer from '@/components/Footer';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import Tools from '@/components/Tools';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import Confetti from '@/components/Confetti';
import RouteChangeHandler from '@/components/RouteChangeHandler'
import RouteChangeHandler from '@/components/RouteChangeHandler';
import { getWebConfigDataAPI } from '@/api/config'
import { getWebConfigDataAPI } from '@/api/config';
import { Web } from '@/types/app/config';
// 加载样式文件
import "@/styles/index.scss";
import "@/styles/tailwind.scss";
import '@/styles/index.scss';
import '@/styles/tailwind.scss';
import BaiduStatis from '@/components/BaiduStatis';
import FloatingBlock from '@/components/FloatingBlock';
// 加载本地字体
const LXGWWenKai = localFont({
src: '../assets/font/LXGWWenKai-Regular.ttf',
display: 'swap'
})
display: 'swap',
});
export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
const { data: { value: data } } = (await getWebConfigDataAPI<{ value: Web }>("web")) || { data: { value: {} as Web } };
const {
data: { value: data },
} = (await getWebConfigDataAPI<{ value: Web }>('web')) || { data: { value: {} as Web } };
// 尊重开源,禁止删除此版权信息!!!
console.log("🚀 欢迎使用 ThriveX 现代化博客管理系统")
console.log("🎉 开源地址https://github.com/LiuYuYang01/ThriveX-Blog")
console.log("🏕 作者主页https://liuyuyang.net")
console.log("🌟 觉得好用的话记得点个 Star 哦 🙏")
console.log('🚀 欢迎使用 ThriveX 现代化博客管理系统');
console.log('🎉 开源地址https://github.com/LiuYuYang01/ThriveX-Blog');
console.log('🏕 作者主页https://liuyuyang.net');
console.log('🌟 觉得好用的话记得点个 Star 哦 🙏');
return (
<html lang="zh-CN" className={LXGWWenKai.className}>
@@ -47,7 +51,7 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
{/* 监听路由变化 */}
<RouteChangeHandler />
<body id='root' className={`dark:!bg-black-a`}>
<body id="root" className={`dark:!bg-black-a`}>
{/* 🎉 礼花效果 */}
{/* <Confetti /> */}
@@ -58,9 +62,7 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
{/* 主体内容 */}
<HeroUIProvider>
<div className='min-h-[calc(100vh-300px)]'>
{children}
</div>
<div className="min-h-[calc(100vh-300px)]">{children}</div>
</HeroUIProvider>
{/* 底部组件 */}

View File

@@ -1,5 +1,5 @@
import Loading from '@/components/Loading'
import Loading from '@/components/Loading';
export default () => {
return <Loading />
}
return <Loading />;
};

View File

@@ -1,15 +1,15 @@
"use client"
'use client';
import { useConfigStore } from '@/stores'
import GitHubCalendar from "react-github-calendar"
import "./index.scss"
import { useConfigStore } from '@/stores';
import GitHubCalendar from 'react-github-calendar';
import './index.scss';
export default () => {
const isDark = useConfigStore(state => state.isDark)
const isDark = useConfigStore((state) => state.isDark);
return (
<>
<GitHubCalendar username="liuyuyang01" colorScheme={isDark ? "dark" : "light"} hideTotalCount />
</>
)
}
return (
<>
<GitHubCalendar username="liuyuyang01" colorScheme={isDark ? 'dark' : 'light'} hideTotalCount />
</>
);
};

View File

@@ -1,74 +1,71 @@
"use client"
'use client';
import { useEffect } from "react";
import Image from "next/image";
import Link from "next/link";
import { Progress, Tooltip } from "@heroui/react";
import INFJ from '@/assets/image/INFJ.png'
import { BiQuestionMark } from "react-icons/bi";
import { useEffect } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Progress, Tooltip } from '@heroui/react';
import INFJ from '@/assets/image/INFJ.png';
import { BiQuestionMark } from 'react-icons/bi';
import AOS from 'aos';
import 'aos/dist/aos.css';
interface Props {
data: {
value: number,
text1: string,
text2: string,
content: string,
color: string
}[]
data: {
value: number;
text1: string;
text2: string;
content: string;
color: string;
}[];
}
export default ({ data }: Props) => {
// 提前把颜色写好,否则会导致样式丢失
const colors = ["[&>div>div]:bg-[#4298b4]", "[&>div>div]:bg-[#e4ae3a]", "[&>div>div]:bg-[#33a474]", "[&>div>div]:bg-[#88619a]", "[&>div>div]:bg-[#f25e62]"]
// 提前把颜色写好,否则会导致样式丢失
const colors = ['[&>div>div]:bg-[#4298b4]', '[&>div>div]:bg-[#e4ae3a]', '[&>div>div]:bg-[#33a474]', '[&>div>div]:bg-[#88619a]', '[&>div>div]:bg-[#f25e62]'];
useEffect(() => {
AOS.init()
}, [])
useEffect(() => {
AOS.init();
}, []);
return (
<div data-aos="fade-down" className="w-full md:w-7/12 flex flex-col mr-0 md:mr-20">
<div className="text-center text-xl mb-8"></div>
return (
<div data-aos="fade-down" className="w-full md:w-7/12 flex flex-col mr-0 md:mr-20">
<div className="text-center text-xl mb-8"></div>
<div className="flex flex-col sm:flex-row justify-between items-center">
<div className="flex sm:block w-[40%]">
<div className="text-[30px] sm:text-[40px] text-[#33a474] font-medium font-sans"></div>
<div className="text-[#666] dark:text-[#8c9ab1] hidden sm:block">INFJ</div>
<Image src={INFJ} alt="性格" width={200}></Image>
<Link href="https://www.16personalities.com/ch/infj-人格" className="block w-full mt-2 text-center text-[#666] text-xs hover:text-[#33a474]"></Link>
</div>
<div className="w-full sm:w-[65%] mt-10 sm:mt-0 space-y-10">
{
data?.map(({ value, text1, text2, content, color }, index) => {
return (
<div key={index} className="flex justify-center items-center">
<span className="min-w-[60px] dark:text-[#8c9ab1] text-xs sm:text-base">{text1}</span>
<div className="relative w-full max-w-md">
<Progress
value={value}
className={`relative [&>div]:justify-center ${colors[index]}`}
/>
<div className="absolute -top-[25px] -translate-x-1/2 left-0 h-full flex items-center justify-center" style={{ left: `${value}%` }}>
<span className={`flex items-center text-[${color}]`}>
{value}%
<Tooltip content={content}>
<BiQuestionMark className="w-5 h-5 ml-2 rounded-full p-[2px] bg-[#eee] dark:bg-black-b cursor-pointer" />
</Tooltip>
</span>
</div>
</div>
<span className="text-end min-w-[60px] dark:text-[#8c9ab1] text-xs sm:text-base">{text2}</span>
</div>
);
})
}
</div>
</div>
<div className="flex flex-col sm:flex-row justify-between items-center">
<div className="flex sm:block w-[40%]">
<div className="text-[30px] sm:text-[40px] text-[#33a474] font-medium font-sans"></div>
<div className="text-[#666] dark:text-[#8c9ab1] hidden sm:block">INFJ</div>
<Image src={INFJ} alt="性格" width={200}></Image>
<Link href="https://www.16personalities.com/ch/infj-人格" className="block w-full mt-2 text-center text-[#666] text-xs hover:text-[#33a474]">
</Link>
</div>
)
}
<div className="w-full sm:w-[65%] mt-10 sm:mt-0 space-y-10">
{data?.map(({ value, text1, text2, content, color }, index) => {
return (
<div key={index} className="flex justify-center items-center">
<span className="min-w-[60px] dark:text-[#8c9ab1] text-xs sm:text-base">{text1}</span>
<div className="relative w-full max-w-md">
<Progress value={value} className={`relative [&>div]:justify-center ${colors[index]}`} />
<div className="absolute -top-[25px] -translate-x-1/2 left-0 h-full flex items-center justify-center" style={{ left: `${value}%` }}>
<span className={`flex items-center text-[${color}]`}>
{value}%
<Tooltip content={content}>
<BiQuestionMark className="w-5 h-5 ml-2 rounded-full p-[2px] bg-[#eee] dark:bg-black-b cursor-pointer" />
</Tooltip>
</span>
</div>
</div>
<span className="text-end min-w-[60px] dark:text-[#8c9ab1] text-xs sm:text-base">{text2}</span>
</div>
);
})}
</div>
</div>
</div>
);
};

View File

@@ -1,9 +0,0 @@
export default () => {
return (
<>
<div>
{/* <h1>Hello World!</h1> */}
</div>
</>
)
}

View File

@@ -1,44 +1,46 @@
"use client"
'use client';
import { useEffect } from "react";
import { Checkbox } from "@heroui/react"
import { useEffect } from 'react';
import { Checkbox } from '@heroui/react';
import AOS from 'aos';
import 'aos/dist/aos.css';
interface Props {
data: {
status: number,
value: string
}[]
data: {
status: number;
value: string;
}[];
}
export default ({ data }: Props) => {
useEffect(() => {
AOS.init()
}, [])
useEffect(() => {
AOS.init();
}, []);
return (
<>
<div data-aos="zoom-in" className="w-full md:w-5/12 flex flex-col mt-52 md:mt-0">
<div className="text-center text-xl mb-8">2025</div>
return (
<>
<div data-aos="zoom-in" className="w-full md:w-5/12 flex flex-col mt-52 md:mt-0">
<div className="text-center text-xl mb-8">2025</div>
<div className="flex flex-col space-y-2">
{data?.map((item, index) => (
<div key={index} className="flex flex-wrap justify-between items-center">
<Checkbox key={index} defaultSelected={item.status === 3} className="[&>input]:hidden space-x-2">{item.value}</Checkbox>
<div className="flex flex-col space-y-2">
{data?.map((item, index) => (
<div key={index} className="flex flex-wrap justify-between items-center">
<Checkbox key={index} defaultSelected={item.status === 3} className="[&>input]:hidden space-x-2">
{item.value}
</Checkbox>
{item.status === 1 && <span className="hidden xs:block text-xs text-yellow-400"></span>}
{item.status === 2 && <span className="hidden xs:block text-xs text-red-500"></span>}
{item.status === 3 && <span className="hidden xs:block text-xs text-green-500"></span>}
{item.status === 1 && <span className="hidden xs:block text-xs text-yellow-400"></span>}
{item.status === 2 && <span className="hidden xs:block text-xs text-red-500"></span>}
{item.status === 3 && <span className="hidden xs:block text-xs text-green-500"></span>}
{item.status === 1 && <span className="block xs:hidden overflow-hidden w-2 h-2 bg-yellow-400 rounded-full"></span>}
{item.status === 2 && <span className="block xs:hidden overflow-hidden w-2 h-2 bg-red-500 rounded-full"></span>}
{item.status === 3 && <span className="block xs:hidden overflow-hidden w-2 h-2 bg-green-500 rounded-full"></span>}
</div>
))}
</div>
{item.status === 1 && <span className="block xs:hidden overflow-hidden w-2 h-2 bg-yellow-400 rounded-full"></span>}
{item.status === 2 && <span className="block xs:hidden overflow-hidden w-2 h-2 bg-red-500 rounded-full"></span>}
{item.status === 3 && <span className="block xs:hidden overflow-hidden w-2 h-2 bg-green-500 rounded-full"></span>}
</div>
</>
)
}
))}
</div>
</div>
</>
);
};

View File

@@ -1,4 +1,4 @@
"use client"
'use client';
import { useEffect } from 'react';
@@ -7,25 +7,27 @@ import 'aos/dist/aos.css';
import { InfoOne } from '@/types/app/my';
export default ({ data }: { data: InfoOne }) => {
useEffect(() => {
AOS.init()
}, [])
useEffect(() => {
AOS.init();
}, []);
return (
<>
<div data-aos="zoom-in" className="mt-8 sm:mt-16 ">
<div className="flex flex-col-reverse sm:flex-row justify-between items-center">
<div className="w-full text-center sm:text-start sm:w-6/12 mt-6 sm:mt-0 text-[#353a40] dark:text-[#fff]">
<div className="text-xl lg:text-4xl my-0 lg:my-5 text-[#738bff]">I am <span className="name">{data?.name}</span></div>
<div className="text-xl lg:text-4xl my-2 sm:my-4 lg:my-5">{data?.profession}</div>
<div className="text-sm text-[#666] dark:text-[#8c9ab1] leading-6 lg:leading-8">{data?.introduction}</div>
</div>
<div className="overflow-hidden w-[40%] h-[40%] rounded-full shadow-lg">
<img src={data?.avatar} alt={data?.name} className="w-full h-full" />
</div>
</div>
return (
<>
<div data-aos="zoom-in" className="mt-8 sm:mt-16 ">
<div className="flex flex-col-reverse sm:flex-row justify-between items-center">
<div className="w-full text-center sm:text-start sm:w-6/12 mt-6 sm:mt-0 text-[#353a40] dark:text-[#fff]">
<div className="text-xl lg:text-4xl my-0 lg:my-5 text-[#738bff]">
I am <span className="name">{data?.name}</span>
</div>
</>
)
}
<div className="text-xl lg:text-4xl my-2 sm:my-4 lg:my-5">{data?.profession}</div>
<div className="text-sm text-[#666] dark:text-[#8c9ab1] leading-6 lg:leading-8">{data?.introduction}</div>
</div>
<div className="overflow-hidden w-[40%] h-[40%] rounded-full shadow-lg">
<img src={data?.avatar} alt={data?.name} className="w-full h-full" />
</div>
</div>
</div>
</>
);
};

View File

@@ -1,42 +1,48 @@
"use client"
'use client';
import { useEffect } from 'react';
import Link from 'next/link';
import AOS from 'aos';
import 'aos/dist/aos.css';
import { InfoTwo } from '@/types/app/my';
import "./index.scss"
import './index.scss';
export default ({ data }: { data: InfoTwo }) => {
useEffect(() => {
AOS.init()
}, [])
useEffect(() => {
AOS.init();
}, []);
return (
<section className='mt-16'>
<div className="about-me">
<div className="info-left">
{data.left_tags.map((t, index) => (
<span key={index} className="tag dark:text-white dark:bg-[#36404d] dark:border-[#4e5969] ">{t}</span>
))}
</div>
return (
<section className="mt-16">
<div className="about-me">
<div className="info-left">
{data.left_tags.map((t, index) => (
<span key={index} className="tag dark:text-white dark:bg-[#36404d] dark:border-[#4e5969] ">
{t}
</span>
))}
</div>
<div className="avatar">
<img src={data.avatar_url} alt={data.author} className="avatar-img dark:!border-[rgba(56,64,76)]" />
</div>
<div className="avatar">
<img src={data.avatar_url} alt={data.author} className="avatar-img dark:!border-[rgba(56,64,76)]" />
</div>
<div className="info-right">
{data.right_tags.map((t, index) => (
<span key={index} className="tag dark:text-white dark:bg-[#36404d] dark:border-[#4e5969] ">{t}</span>
))}
</div>
</div>
<div className="info-right">
{data.right_tags.map((t, index) => (
<span key={index} className="tag dark:text-white dark:bg-[#36404d] dark:border-[#4e5969] ">
{t}
</span>
))}
</div>
</div>
<div className="about-me-2 flex flex-col">
<button className="trigger dark:bg-black-b !border dark:border-[#4e5969] dark:text-white">{data.author}</button>
<div className="about-me-2 flex flex-col">
<button className="trigger dark:bg-black-b !border dark:border-[#4e5969] dark:text-white">{data.author}</button>
<Link href={data.know_me} target='_blank' className='text-xs text-[#2764b2] mt-3'></Link>
</div>
</section>
)
}
<Link href={data.know_me} target="_blank" className="text-xs text-[#2764b2] mt-3">
</Link>
</div>
</section>
);
};

View File

@@ -1,60 +1,59 @@
"use client";
'use client';
import { useEffect } from "react";
import { useEffect } from 'react';
import AOS from 'aos';
import 'aos/dist/aos.css';
import { getGaodeMapConfigDataAPI } from "@/api/config";
import { getGaodeMapConfigDataAPI } from '@/api/config';
export default function MapContainer() {
let map: any;
let map: any;
useEffect(() => {
AOS.init()
useEffect(() => {
AOS.init();
// 确保代码仅在客户端执行
import('@amap/amap-jsapi-loader').then(async AMapLoader => {
const { data } = await getGaodeMapConfigDataAPI() || { data: {} }
const { key_code, security_code } = data as { key_code: string, security_code: string }
// 确保代码仅在客户端执行
import('@amap/amap-jsapi-loader').then(async (AMapLoader) => {
const { data } = (await getGaodeMapConfigDataAPI()) || { data: {} };
const { key_code, security_code } = data as { key_code: string; security_code: string };
// @ts-ignore
window._AMapSecurityConfig = {
securityJsCode: security_code,
};
(window as any)._AMapSecurityConfig = {
securityJsCode: security_code,
};
AMapLoader.load({
key: key_code,
version: "2.0",
plugins: ["AMap.Scale", "AMap.Marker"],
})
.then((AMap) => {
map = new AMap.Map("container", {
viewMode: "3D", // 是否为3D地图模式
zoom: 7,
center: [113.625351, 34.746303], // 初始化地图中心点位置
});
AMapLoader.load({
key: key_code,
version: '2.0',
plugins: ['AMap.Scale', 'AMap.Marker'],
})
.then((AMap) => {
map = new AMap.Map('container', {
viewMode: '3D', // 是否为3D地图模式
zoom: 7,
center: [113.625351, 34.746303], // 初始化地图中心点位置
});
new AMap.Marker({
position: [113.625351, 34.746303], // 标记位置
map, // 将标记添加到地图
});
})
.catch((e) => {
console.log(e);
});
return () => map?.destroy();
new AMap.Marker({
position: [113.625351, 34.746303], // 标记位置
map, // 将标记添加到地图
});
})
}, []);
.catch((e) => {
console.log(e);
});
return (
<>
<div data-aos="zoom-in" className="w-full md:w-5/12 flex flex-col mr-0 md:mr-20">
<div className="text-center text-xl mb-8"></div>
return () => map?.destroy();
});
}, []);
<div id="container" className="w-full h-60 sm:h-80 border rounded-3xl"></div>
</div>
</>
);
}
return (
<>
<div data-aos="zoom-in" className="w-full md:w-5/12 flex flex-col mr-0 md:mr-20">
<div className="text-center text-xl mb-8"></div>
<div id="container" className="w-full h-60 sm:h-80 border rounded-3xl"></div>
</div>
</>
);
}

View File

@@ -1,91 +1,93 @@
"use client"
'use client';
import { useEffect } from "react";
import { Project } from "@/types/app/my"
import { Tabs, Tab, Card, CardBody } from "@heroui/react"
import { PhotoProvider, PhotoView } from "react-photo-view"
import "react-photo-view/dist/react-photo-view.css";
import { useEffect } from 'react';
import { Project } from '@/types/app/my';
import { Tabs, Tab, Card, CardBody } from '@heroui/react';
import { PhotoProvider, PhotoView } from 'react-photo-view';
import 'react-photo-view/dist/react-photo-view.css';
import AOS from 'aos';
import 'aos/dist/aos.css';
export default ({ data }: { data: Project[] }) => {
useEffect(() => {
AOS.init()
}, [])
useEffect(() => {
AOS.init();
}, []);
return (
<>
<div data-aos="zoom-in" className="character pb-20">
<div className="text-center text-xl mb-8"></div>
<div className="w-[80%] xl:w-[1200px] mx-auto">
<div className="flex w-full flex-col">
<Tabs aria-label="Options" placement="top" classNames={{ tabList: "dark:bg-black-b", tabWrapper: "flex flex-col", base: "justify-center", tab: "[&>span]:dark:bg-[#3a4250]" }}>
{
data?.map((item, index) => (
<Tab key={index} title={item.name}>
<Card>
<CardBody className="flex-col md:flex-row md:space-x-10 py-5 dark:bg-black-b ">
<div className="sticky top-0 w-full md:w-2/6 px-4">
<h3 className="text-[18px] mb-4"></h3>
<div className="grid grid-cols-2 gap-2 p-2.5 border dark:border-[#444e5d] rounded-xl ">
<PhotoProvider>
{
item.images?.map((img, index) => (
<PhotoView key={index} src={img || ''}>
<img src={img} alt="作品图片" className="border dark:border-[#444e5d] dark hover:scale-[1.2] rounded-lg cursor-pointer transition-transform" />
</PhotoView>
))
}
</PhotoProvider>
</div>
</div>
return (
<>
<div data-aos="zoom-in" className="character pb-20">
<div className="text-center text-xl mb-8"></div>
<div className="overflow-auto w-full md:w-4/6 h-60 pl-4 pr-2.5 pb-8 mt-6 md:mt-0 text-sm space-y-8">
<div>
<h3 className="text-[18px] mb-4"></h3>
<p className="text-gray-700 dark:text-[#8c9ab1]">{item.description}</p>
</div>
<div className="w-[80%] xl:w-[1200px] mx-auto">
<div className="flex w-full flex-col">
<Tabs aria-label="Options" placement="top" classNames={{ tabList: 'dark:bg-black-b', tabWrapper: 'flex flex-col', base: 'justify-center', tab: '[&>span]:dark:bg-[#3a4250]' }}>
{data?.map((item, index) => (
<Tab key={index} title={item.name}>
<Card>
<CardBody className="flex-col md:flex-row md:space-x-10 py-5 dark:bg-black-b ">
<div className="sticky top-0 w-full md:w-2/6 px-4">
<h3 className="text-[18px] mb-4"></h3>
<div className="grid grid-cols-2 gap-2 p-2.5 border dark:border-[#444e5d] rounded-xl ">
<PhotoProvider>
{item.images?.map((img, index) => (
<PhotoView key={index} src={img || ''}>
<img src={img} alt="作品图片" className="border dark:border-[#444e5d] dark hover:scale-[1.2] rounded-lg cursor-pointer transition-transform" />
</PhotoView>
))}
</PhotoProvider>
</div>
</div>
<div>
<h3 className="text-[18px] mb-4"></h3>
<div className="text-gray-700 dark:text-[#8c9ab1]">
<p className="text-xs">{item.front.technology}</p>
<p className="text-xs">{item.control.technology}</p>
<p className="text-xs">{item.backend.technology}</p>
</div>
</div>
<div className="overflow-auto w-full md:w-4/6 h-60 pl-4 pr-2.5 pb-8 mt-6 md:mt-0 text-sm space-y-8">
<div>
<h3 className="text-[18px] mb-4"></h3>
<p className="text-gray-700 dark:text-[#8c9ab1]">{item.description}</p>
</div>
<div>
<h3 className="text-[18px] mb-4">GitHub</h3>
<div className="space-y-2">
<div>
<span></span>
<a href={item.front.url} target="_blank" className="text-xs text-primary">{item.front.url}</a>
</div>
<div>
<h3 className="text-[18px] mb-4"></h3>
<div className="text-gray-700 dark:text-[#8c9ab1]">
<p className="text-xs">{item.front.technology}</p>
<p className="text-xs">{item.control.technology}</p>
<p className="text-xs">{item.backend.technology}</p>
</div>
</div>
<div>
<span></span>
<a href={item.control.url} target="_blank" className="text-xs text-primary">{item.control.url}</a>
</div>
<div>
<h3 className="text-[18px] mb-4">GitHub</h3>
<div className="space-y-2">
<div>
<span></span>
<a href={item.front.url} target="_blank" className="text-xs text-primary" rel="noreferrer">
{item.front.url}
</a>
</div>
<div>
<span></span>
<a href={item.backend.url} target="_blank" className="text-xs text-primary">{item.backend.url}</a>
</div>
</div>
</div>
</div>
</CardBody>
</Card>
</Tab>
))
}
</Tabs>
</div>
</div>
</div>
</>
)
}
<div>
<span></span>
<a href={item.control.url} target="_blank" className="text-xs text-primary" rel="noreferrer">
{item.control.url}
</a>
</div>
<div>
<span></span>
<a href={item.backend.url} target="_blank" className="text-xs text-primary" rel="noreferrer">
{item.backend.url}
</a>
</div>
</div>
</div>
</div>
</CardBody>
</Card>
</Tab>
))}
</Tabs>
</div>
</div>
</div>
</>
);
};

View File

@@ -1,44 +1,56 @@
"use client"
'use client';
import Image from 'next/image'
import Image from 'next/image';
import { useEffect } from 'react'
import { useEffect } from 'react';
import qdAdvanced from '@/assets/svg/technology/qd_advanced.svg'
import qdBasics from '@/assets/svg/technology/qd_basics.svg'
import qdTool from '@/assets/svg/technology/qd_tool.svg'
import rearEnd from '@/assets/svg/technology/rear_end.svg'
import tool from '@/assets/svg/technology/tool.svg'
import qdAdvanced from '@/assets/svg/technology/qd_advanced.svg';
import qdBasics from '@/assets/svg/technology/qd_basics.svg';
import qdTool from '@/assets/svg/technology/qd_tool.svg';
import rearEnd from '@/assets/svg/technology/rear_end.svg';
import tool from '@/assets/svg/technology/tool.svg';
import AOS from 'aos';
import 'aos/dist/aos.css';
export default () => {
useEffect(() => {
AOS.init()
}, [])
useEffect(() => {
AOS.init();
}, []);
return (
<>
<div data-aos="zoom-in" className="w-full md:w-7/12 flex flex-col mt-52 md:mt-0">
<div className="text-center text-xl mb-8"></div>
return (
<>
<div data-aos="zoom-in" className="w-full md:w-7/12 flex flex-col mt-52 md:mt-0">
<div className="text-center text-xl mb-8"></div>
<div className="flex flex-col items-center space-y-2">
<div><Image src={tool} alt="软件工具" /></div>
<div><Image src={qdBasics} alt="前端基础技术栈" /></div>
<div><Image src={qdAdvanced} alt="前端高级技术栈" /></div>
<div><Image src={qdTool} alt="前端工具" /></div>
<div><Image src={rearEnd} alt="后端技术栈" /></div>
</div>
</div>
</>
)
}
<div className="flex flex-col items-center space-y-2">
<div>
<Image src={tool} alt="软件工具" />
</div>
<div>
<Image src={qdBasics} alt="前端基础技术栈" />
</div>
<div>
<Image src={qdAdvanced} alt="前端高级技术栈" />
</div>
<div>
<Image src={qdTool} alt="前端工具" />
</div>
<div>
<Image src={rearEnd} alt="后端技术栈" />
</div>
</div>
</div>
</>
);
};
{/* <div className="flex flex-col items-center space-y-2">
{
/* <div className="flex flex-col items-center space-y-2">
<div><Image src="https://skillicons.dev/icons?i=html,css,javascript,typescript,jquery,less,scss,tailwind" alt="" /></div>
<div><Image src="https://skillicons.dev/icons?i=react,nextjs,remix,redux,vue,nuxt,pinia,electron" alt="" /></div>
<div><Image src="https://skillicons.dev/icons?i=webpack,vite,npm,yarn,pnpm,md,git,github" alt="" /></div>
<div><Image src="https://skillicons.dev/icons?i=java,spring,maven,python,flask,express,nodejs,nestjs,prisma,mysql,redis,vercel,docker,linux" alt="" /></div>
<div><Image src="https://skillicons.dev/icons?i=vscode,idea,webstorm,pycharm,postman,ps" alt="" /></div>
</div> */}
</div> */
}

View File

@@ -1,37 +1,30 @@
import bg from '@/assets/image/bg.png'
import bg from '@/assets/image/bg.png';
import Goals from './component/Goals'
import Character from './component/Character'
import Map from './component/Map'
import Technology from './component/Technology'
import Project from './component/Project'
import Calendar from "./component/Calendar"
import InfoTwo from './component/InfoTwo'
import { getPageConfigDataByNameAPI } from '@/api/config'
import { Config } from '@/types/app/config'
import { MyData } from '@/types/app/my'
import InfoOne from './component/InfoOne'
import Goals from './component/Goals';
import Character from './component/Character';
import Map from './component/Map';
import Technology from './component/Technology';
import Project from './component/Project';
import Calendar from './component/Calendar';
import InfoTwo from './component/InfoTwo';
import { getPageConfigDataByNameAPI } from '@/api/config';
import { Config } from '@/types/app/config';
import { MyData } from '@/types/app/my';
import InfoOne from './component/InfoOne';
export default async () => {
const { data } = await getPageConfigDataByNameAPI("my") || { data: {} as Config }
const { info_style, info_one, info_two, character, goals, project } = data.value as MyData
const { data } = (await getPageConfigDataByNameAPI('my')) || { data: {} as Config };
const { info_style, info_one, info_two, character, goals, project } = data.value as MyData;
return (
<>
<title>👋 </title>
<meta name="description" content="👋 关于我" />
<div className="bg-white dark:bg-black-a pt-20 bg-cover bg-center bg-fixed"
style={{ backgroundImage: `url(${bg.src})` }}>
<div className="w-[90%] lg:w-[950px] mx-auto">
{
info_style === 'info_one'
? <InfoOne data={info_one} />
: <InfoTwo data={info_two} />
}
</div>
<div className="bg-white dark:bg-black-a pt-20 bg-cover bg-center bg-fixed" style={{ backgroundImage: `url(${bg.src})` }}>
<div className="w-[90%] lg:w-[950px] mx-auto">{info_style === 'info_one' ? <InfoOne data={info_one} /> : <InfoTwo data={info_two} />}</div>
<div className='flex justify-center mt-24 px-10'>
<div className="flex justify-center mt-24 px-10">
<Calendar />
</div>
@@ -48,11 +41,7 @@ export default async () => {
<div className="mt-52">
<Project data={project} />
</div>
{/* <div className="mt-52">
<CurriculumVitae />
</div> */}
</div>
</>
)
}
);
};

View File

@@ -1,29 +1,31 @@
'use client'
'use client';
import { useRouter } from 'next/navigation'
import Image from 'next/image'
import NotFoundSvg from '@/assets/svg/other/404.svg'
import { Button } from "@heroui/react"
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import NotFoundSvg from '@/assets/svg/other/404.svg';
import { Button } from '@heroui/react';
export default function NotFound() {
const router = useRouter()
const router = useRouter();
return (
<>
<div className='absolute w-screen h-screen bg-white dark:bg-black-b z-[999]'>
<div className="absolute w-screen h-screen bg-white dark:bg-black-b z-[999]">
<div className="w-full h-[73vh] mt-20">
<div className="w-full h-full flex justify-center items-center flex-wrap">
<Image src={NotFoundSvg} alt="404" className='w-full xl:w-[35rem] lg:w-[35rem] md:w-[28rem]' />
<Image src={NotFoundSvg} alt="404" className="w-full xl:w-[35rem] lg:w-[35rem] md:w-[28rem]" />
<div className="xl:w-[32rem] lg:w-[26rem] md:w-[20rem] sm:text-start mx-4 text-center">
<h1 className='text-5xl sm:text-8xl font-bold'>404</h1>
<h2 className='text-3xl sm:text-3xl font-bold my-4'>Page not found</h2>
<h1 className="text-5xl sm:text-8xl font-bold">404</h1>
<h2 className="text-3xl sm:text-3xl font-bold my-4">Page not found</h2>
<p>The page you are looking for does not exist or has been removed.</p>
<Button className='mt-6' color="primary" variant="shadow" onPress={() => router.push("/")}></Button>
<Button className="mt-6" color="primary" variant="shadow" onPress={() => router.push('/')}>
</Button>
</div>
</div>
</div>
</div>
</>
);
}
}

View File

@@ -1,22 +1,24 @@
import Slide from "@/components/Slide";
import Typed from "@/components/Typed";
import Starry from "@/components/Starry"
import Container from "@/components/Container";
import ArticleLayout from "@/components/ArticleLayout";
import Sidebar from "@/components/Sidebar";
import Slide from '@/components/Slide';
import Typed from '@/components/Typed';
import Starry from '@/components/Starry';
import Container from '@/components/Container';
import ArticleLayout from '@/components/ArticleLayout';
import Sidebar from '@/components/Sidebar';
import { getWebConfigDataAPI } from '@/api/config'
import { Theme } from "@/types/app/config";
import { getWebConfigDataAPI } from '@/api/config';
import { Theme } from '@/types/app/config';
interface Props {
searchParams: Promise<{ page: number }>;
};
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
const page = searchParams.page || 1;
const { data: { value: data } } = (await getWebConfigDataAPI<{ value: Theme }>("theme")) || { data: { value: {} as Theme } };
const {
data: { value: data },
} = (await getWebConfigDataAPI<{ value: Theme }>('theme')) || { data: { value: {} as Theme } };
return (
<>
{/* <Lantern data={['新', '春', '快', '乐']} /> */}
@@ -36,4 +38,4 @@ export default async (props: Props) => {
</Container>
</>
);
};
};

View File

@@ -1,24 +1,24 @@
"use client"
'use client';
import { Accordion, AccordionItem } from "@heroui/react"
import { Accordion, AccordionItem } from '@heroui/react';
export default () => {
return (
<>
<Accordion>
<AccordionItem key="1" aria-label="评论" title="评论" className="[&>h2]:text-base [&>h2]:w-[10%] [&>h2]:mx-auto [&>h2>button]:pb-0">
<div className="space-y-2">
<div className="flex items-center w-full p-4 bg-[#fafafa] rounded-lg">
<img src="https://q1.qlogo.cn/g?b=qq&nk=3311118881&s=640" alt="Avatar" className="w-10 h-10 rounded-full mr-3" />
return (
<>
<Accordion>
<AccordionItem key="1" aria-label="评论" title="评论" className="[&>h2]:text-base [&>h2]:w-[10%] [&>h2]:mx-auto [&>h2>button]:pb-0">
<div className="space-y-2">
<div className="flex items-center w-full p-4 bg-[#fafafa] rounded-lg">
<img src="https://q1.qlogo.cn/g?b=qq&nk=3311118881&s=640" alt="Avatar" className="w-10 h-10 rounded-full mr-3" />
<div>
<div className="font-bold mb-1"></div>
<div className="text-gray-700"></div>
</div>
</div>
</div>
</AccordionItem>
</Accordion>
</>
)
}
<div>
<div className="font-bold mb-1"></div>
<div className="text-gray-700"></div>
</div>
</div>
</div>
</AccordionItem>
</Accordion>
</>
);
};

View File

@@ -1,43 +1,32 @@
"use client"
'use client';
import { useState, useEffect } from 'react'
import dynamic from 'next/dynamic'
import { IDomEditor, IEditorConfig } from '@wangeditor-next/editor'
import '@wangeditor-next/editor/dist/css/style.css'
import './index.scss'
import { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
import { IDomEditor, IEditorConfig } from '@wangeditor-next/editor';
import '@wangeditor-next/editor/dist/css/style.css';
import './index.scss';
const Editor = dynamic(
() => import('@wangeditor-next/editor-for-react').then(mod => mod.Editor),
{ ssr: false }
)
const Editor = dynamic(() => import('@wangeditor-next/editor-for-react').then((mod) => mod.Editor), { ssr: false });
export default ({ value }: { value: string }) => {
// editor 实例
const [editor, setEditor] = useState<IDomEditor | null>(null)
// editor 实例
const [editor, setEditor] = useState<IDomEditor | null>(null);
// 编辑器内容
const [html, setHtml] = useState(value)
// 编辑器内容
const [html, setHtml] = useState(value);
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
readOnly: true, // 设置为只读
}
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
readOnly: true, // 设置为只读
};
useEffect(() => {
return () => {
if (editor == null) return
editor.destroy()
setEditor(null)
}
}, [editor])
useEffect(() => {
return () => {
if (editor == null) return;
editor.destroy();
setEditor(null);
};
}, [editor]);
return (
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={setEditor}
onChange={editor => setHtml(editor.getHtml())}
mode="default"
/>
)
}
return <Editor defaultConfig={editorConfig} value={html} onCreated={setEditor} onChange={(editor) => setHtml(editor.getHtml())} mode="default" />;
};

View File

@@ -1,31 +1,26 @@
"use client"
'use client';
import { PhotoProvider, PhotoView } from "react-photo-view"
import { PhotoProvider, PhotoView } from 'react-photo-view';
import 'react-photo-view/dist/react-photo-view.css';
interface Props {
list: string[]
list: string[];
}
export default ({ list }: Props) => {
return (
<>
<div className={`flex justify-center ${list.length && 'mt-4'} w-full sm:w-3/6`}>
<PhotoProvider
speed={() => 800}
easing={(type) => (type === 2 ? 'cubic-bezier(0.36, 0, 0.66, -0.56)' : 'cubic-bezier(0.34, 1.56, 0.64, 1)')}
>
<div className={`grid gap-2 ${list.length === 1 ? 'justify-center' : 'grid-cols-2'}`}>
{
list?.map((url, index) => (
<PhotoView key={index} src={url}>
<img src={url} alt="闪念图片" className="rounded-2xl w-full h-full object-cover cursor-pointer" />
</PhotoView>
))
}
</div>
</PhotoProvider>
</div>
</>
)
}
return (
<>
<div className={`flex justify-center ${list.length && 'mt-4'} w-full sm:w-3/6`}>
<PhotoProvider speed={() => 800} easing={(type) => (type === 2 ? 'cubic-bezier(0.36, 0, 0.66, -0.56)' : 'cubic-bezier(0.34, 1.56, 0.64, 1)')}>
<div className={`grid gap-2 ${list.length === 1 ? 'justify-center' : 'grid-cols-2'}`}>
{list?.map((url, index) => (
<PhotoView key={index} src={url}>
<img src={url} alt="闪念图片" className="rounded-2xl w-full h-full object-cover cursor-pointer" />
</PhotoView>
))}
</div>
</PhotoProvider>
</div>
</>
);
};

View File

@@ -1,27 +1,29 @@
import ImageList from "./components/ImageList"
import { getRecordPagingAPI } from '@/api/record'
import ImageList from './components/ImageList';
import { getRecordPagingAPI } from '@/api/record';
import { getUserDataAPI } from '@/api/user';
import { Record } from "@/types/app/record"
import { User } from "@/types/app/user";
import { dayFormat } from '@/utils'
import Pagination from "@/components/Pagination";
import Empty from "@/components/Empty";
import Show from "@/components/Show";
import { getWebConfigDataAPI } from "@/api/config";
import { Theme } from "@/types/app/config";
import Editor from "./components/Editor";
import { Record } from '@/types/app/record';
import { User } from '@/types/app/user';
import { dayFormat } from '@/utils';
import Pagination from '@/components/Pagination';
import Empty from '@/components/Empty';
import Show from '@/components/Show';
import { getWebConfigDataAPI } from '@/api/config';
import { Theme } from '@/types/app/config';
import Editor from './components/Editor';
interface Props {
searchParams: Promise<{ page: number }>;
};
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
const page = searchParams.page || 1;
const { data: user } = (await getUserDataAPI()) || { data: {} as User }
const { data: record } = (await getRecordPagingAPI({ pagination: { page, size: 8 } })) || { data: {} as Paginate<Record[]> }
const { data: { value: theme } } = (await getWebConfigDataAPI<{ value: Theme }>("theme")) || { data: { value: {} as Theme } };
const { data: user } = (await getUserDataAPI()) || { data: {} as User };
const { data: record } = (await getRecordPagingAPI({ pagination: { page, size: 8 } })) || { data: {} as Paginate<Record[]> };
const {
data: { value: theme },
} = (await getWebConfigDataAPI<{ value: Theme }>('theme')) || { data: { value: {} as Theme } };
return (
<>
@@ -37,8 +39,8 @@ export default async (props: Props) => {
</div>
<div className="space-y-12">
{
!!record?.result?.length && record?.result.map(item => (
{!!record?.result?.length &&
record?.result.map((item) => (
<div key={item.id} className="flex flex-col sm:flex-row">
<img src={user?.avatar} alt="作者头像" width={56} height={56} className="hidden sm:block rounded-lg border dark:border-black-b h-14 mr-2 " />
@@ -60,20 +62,21 @@ export default async (props: Props) => {
<div className="w-full p-4 border dark:border-black-b rounded-3xl rounded-tl-none bg-[rgba(255,255,255,0.7)] dark:bg-[rgba(30,36,46,0.9)] backdrop-blur-sm ">
<Editor value={item?.content} />
<ImageList list={JSON.parse(item?.images as string || '[]')} />
<ImageList list={JSON.parse((item?.images as string) || '[]')} />
{/* <Comment /> */}
</div>
</div>
</div>
))
}
))}
<Show is={!record?.result?.length} children={<Empty info='闪念列表为空~' />} />
<Show is={!record?.result?.length}>
<Empty info="闪念为空~" />
</Show>
</div>
{record?.total && <Pagination total={record?.pages} page={page} className="flex justify-center mt-5" />}
</div>
</div>
</>
)
};
);
};

View File

@@ -1,13 +1,13 @@
import { Config } from '@/types/app/config'
import { getPageConfigDataByNameAPI } from '@/api/config'
import Resume from './resume'
import { Config } from '@/types/app/config';
import { getPageConfigDataByNameAPI } from '@/api/config';
import Resume from './resume';
export default async () => {
const { data } = await getPageConfigDataByNameAPI("resume") || { data: {} as Config }
const { data } = (await getPageConfigDataByNameAPI('resume')) || { data: {} as Config };
return (
<>
<Resume data={data.value} />
</>
)
}
);
};

View File

@@ -1,16 +1,16 @@
'use client'
'use client';
import { useEffect } from 'react'
import { motion } from 'framer-motion'
import { FaGithub, FaPhone, FaEnvelope } from 'react-icons/fa'
import { Resume } from '@/types/app/resume'
import { useEffect } from 'react';
import { motion } from 'framer-motion';
import { FaGithub, FaPhone, FaEnvelope } from 'react-icons/fa';
import { Resume } from '@/types/app/resume';
export default ({ data }: { data: Resume }) => {
const { personalInfo, advantages, links, skills, workExperience, projects, education } = data || {}
const { personalInfo, advantages, links, skills, workExperience, projects, education } = data || {};
useEffect(() => {
document.documentElement.style.scrollBehavior = 'smooth'
}, [])
document.documentElement.style.scrollBehavior = 'smooth';
}, []);
return (
<>
@@ -18,25 +18,11 @@ export default ({ data }: { data: Resume }) => {
<meta name="description" content={`💪 ${personalInfo?.name ?? ''} - ${personalInfo?.title ?? ''}`} />
<div className="min-h-screen py-12 mt-[60px] px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="max-w-4xl mx-auto bg-white dark:!bg-black-b rounded-2xl shadow-xl hover:shadow-2xl transition-shadow p-10"
>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="max-w-4xl mx-auto bg-white dark:!bg-black-b rounded-2xl shadow-xl hover:shadow-2xl transition-shadow p-10">
{/* 个人信息头部 */}
<div className="flex items-center space-x-8 mb-12">
<motion.div
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5 }}
className="relative w-48 h-48 rounded-full overflow-hidden group"
>
<img
src={personalInfo?.avatar}
alt={personalInfo?.name}
className="object-cover transition-transform group-hover:scale-110"
/>
<motion.div initial={{ scale: 0.8 }} animate={{ scale: 1 }} transition={{ duration: 0.5 }} className="relative w-48 h-48 rounded-full overflow-hidden group">
<img src={personalInfo?.avatar} alt={personalInfo?.name} className="object-cover transition-transform group-hover:scale-110" />
</motion.div>
<div>
@@ -59,12 +45,7 @@ export default ({ data }: { data: Resume }) => {
</div>
{/* 自我介绍 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="mb-12"
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.2 }} className="mb-12">
<h3 className="text-2xl font-bold text-gray-900 dark:!text-white mb-6 flex items-center">
<span className="w-1 h-8 bg-blue-600 mr-3 rounded-full"></span>
@@ -106,12 +87,7 @@ export default ({ data }: { data: Resume }) => {
</motion.div>
{/* 专业技能 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="mb-12"
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.4 }} className="mb-12">
<h3 className="text-2xl font-bold text-gray-900 dark:!text-white mb-6 flex items-center">
<span className="w-1 h-8 bg-blue-600 mr-3 rounded-full"></span>
@@ -130,12 +106,7 @@ export default ({ data }: { data: Resume }) => {
</motion.div>
{/* 工作经历 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="mb-12"
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.8 }} className="mb-12">
<h3 className="text-2xl font-bold text-gray-900 dark:!text-white mb-6 flex items-center">
<span className="w-1 h-8 bg-blue-600 mr-3 rounded-full"></span>
@@ -144,14 +115,16 @@ export default ({ data }: { data: Resume }) => {
<div className="space-y-4">
{workExperience?.map((job, index) => (
<div key={index} className="group bg-gray-50 dark:!bg-[#373f4b] p-6 rounded-xl">
<div className='flex justify-between items-center mb-3'>
<div className="flex justify-between items-center mb-3">
<h4 className="text-lg font-bold text-gray-800 dark:!text-white group-hover:text-blue-600 ">{job.company}</h4>
<p className="text-gray-600 dark:!text-gray-300 font-medium">{job.period}</p>
</div>
<p className="text-gray-700 dark:!text-gray-300 font-semibold mb-4">{job.position}</p>
<ul className="list-disc list-inside text-gray-600 dark:!text-gray-300 space-y-2">
{job.responsibilities?.map((responsibility, i) => (
<li key={i} className="text-base">{responsibility}</li>
<li key={i} className="text-base">
{responsibility}
</li>
))}
</ul>
</div>
@@ -160,12 +133,7 @@ export default ({ data }: { data: Resume }) => {
</motion.div>
{/* 项目经历 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 }}
className="mb-12"
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.7 }} className="mb-12">
<h3 className="text-2xl font-bold text-gray-900 dark:!text-white mb-6 flex items-center">
<span className="w-1 h-8 bg-blue-600 mr-3 rounded-full"></span>
@@ -174,7 +142,7 @@ export default ({ data }: { data: Resume }) => {
<div className="space-y-8">
{projects?.map((project, index) => (
<div key={index} className="group bg-gray-50 dark:!bg-[#373f4b] p-6 rounded-xl">
<div className='flex justify-between items-center mb-3'>
<div className="flex justify-between items-center mb-3">
<h4 className="text-lg font-bold text-gray-800 dark:!text-white group-hover:text-blue-600 ">{project.name}</h4>
<p className="text-gray-600 dark:!text-gray-300 font-medium">{project.period}</p>
</div>
@@ -184,9 +152,13 @@ export default ({ data }: { data: Resume }) => {
<div>
<h5 className="font-bold text-gray-800 dark:!text-white mb-3 text-base"></h5>
<div className="text-gray-600 dark:!text-gray-300 text-base">
{Array.isArray(project.description) ? project.description.map((desc, i) => (
<div key={i} className="mb-2">{desc}</div>
)) : project.description}
{Array.isArray(project.description)
? project.description.map((desc, i) => (
<div key={i} className="mb-2">
{desc}
</div>
))
: project.description}
</div>
</div>
@@ -194,7 +166,9 @@ export default ({ data }: { data: Resume }) => {
<div>
<h5 className="font-bold text-gray-800 dark:!text-white mb-3 text-base"></h5>
<div className="text-gray-600 dark:!text-gray-300 text-base">
{typeof project.techStack === 'string' ? project.techStack : (
{typeof project.techStack === 'string' ? (
project.techStack
) : (
<>
<div className="mb-2"> {project.techStack.frontend}</div>
<div className="mb-2"> {project.techStack.backend}</div>
@@ -221,7 +195,8 @@ export default ({ data }: { data: Resume }) => {
<h5 className="font-bold text-gray-800 dark:!text-white mb-3 text-base"></h5>
<div className="space-y-2 text-gray-600 dark:!text-gray-300 text-base">
{Object.entries(project.links).map(([key, value]) => (
<div key={key}> {key === 'preview' ? '项目预览' : key === 'website' ? '项目官网' : key === 'docs' ? '项目文档' : key === 'api' ? '项目接口' : key === 'dashboard' ? '项目后台' : key}
<div key={key}>
{key === 'preview' ? '项目预览' : key === 'website' ? '项目官网' : key === 'docs' ? '项目文档' : key === 'api' ? '项目接口' : key === 'dashboard' ? '项目后台' : key}
<a href={value as string} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800 font-medium ml-2">
{value as string}
</a>
@@ -236,7 +211,8 @@ export default ({ data }: { data: Resume }) => {
<h5 className="font-bold text-gray-800 dark:!text-white mb-3 text-base"></h5>
<div className="space-y-2 text-gray-600 dark:!text-gray-300 text-base">
{Object.entries(project.repositories).map(([key, value]) => (
<div key={key}> {key === 'frontend' ? '前端' : key === 'admin' ? '控制端' : '后端'}
<div key={key}>
{key === 'frontend' ? '前端' : key === 'admin' ? '控制端' : '后端'}
<a href={value as string} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800 font-medium ml-2">
{value as string}
</a>
@@ -249,11 +225,7 @@ export default ({ data }: { data: Resume }) => {
{project.achievements && (
<div>
<h5 className="font-bold text-gray-800 dark:!text-white mb-3 text-base"></h5>
<div className="space-y-2 text-gray-600 dark:!text-gray-300 text-base">
{Array.isArray(project.achievements) ? project.achievements.map((achievement, i) => (
<div key={i}> {achievement}</div>
)) : <div> {project.achievements}</div>}
</div>
<div className="space-y-2 text-gray-600 dark:!text-gray-300 text-base">{Array.isArray(project.achievements) ? project.achievements.map((achievement, i) => <div key={i}> {achievement}</div>) : <div> {project.achievements}</div>}</div>
</div>
)}
@@ -281,12 +253,7 @@ export default ({ data }: { data: Resume }) => {
</motion.div>
{/* 教育背景 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="mb-12"
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }} className="mb-12">
<h3 className="text-2xl font-bold text-gray-900 dark:!text-white mb-6 flex items-center">
<span className="w-1 h-8 bg-blue-600 mr-3 rounded-full"></span>
@@ -294,10 +261,8 @@ export default ({ data }: { data: Resume }) => {
<div className="group bg-gray-50 dark:!bg-[#373f4b] p-6 rounded-xl">
<div>
<div className='flex justify-between items-center mb-3'>
<h4 className="text-lg font-bold text-gray-800 dark:!text-white group-hover:!text-blue-600">
{education?.school}
</h4>
<div className="flex justify-between items-center mb-3">
<h4 className="text-lg font-bold text-gray-800 dark:!text-white group-hover:!text-blue-600">{education?.school}</h4>
<p className="text-gray-600 dark:!text-gray-300 font-medium">
{education?.major} | {education?.degree} | {education?.period}
</p>
@@ -314,5 +279,5 @@ export default ({ data }: { data: Resume }) => {
</motion.div>
</div>
</>
)
}
);
};

View File

@@ -1,14 +1,14 @@
import Starry from "@/components/Starry"
import Slide from "@/components/Slide"
import Classics from "@/components/ArticleLayout/Classics";
import Pagination from "@/components/Pagination";
import { Article } from "@/types/app/article";
import { getTagArticleListAPI } from "@/api/tag";
import Starry from '@/components/Starry';
import Slide from '@/components/Slide';
import Classics from '@/components/ArticleLayout/Classics';
import Pagination from '@/components/Pagination';
import { Article } from '@/types/app/article';
import { getTagArticleListAPI } from '@/api/tag';
interface Props {
params: Promise<{ id: number }>;
searchParams: Promise<{ page: number; name: string }>;
};
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
@@ -17,7 +17,7 @@ export default async (props: Props) => {
const page = searchParams.page || 1;
const name = searchParams.name;
const { data } = (await getTagArticleListAPI(id, page)) || { data: {} as Paginate<Article[]> }
const { data } = (await getTagArticleListAPI(id, page)) || { data: {} as Paginate<Article[]> };
return (
<>
@@ -31,7 +31,9 @@ export default async (props: Props) => {
{/* 标签信息 */}
<div className="absolute top-[40%] left-[50%] transform -translate-x-1/2 w-[80%] text-center text-white text-[20px] xs:text-[25px] sm:text-[30px] custom_text_shadow">
<span>{name} ~ {data?.total}</span>
<span>
{name} ~ {data?.total}
</span>
</div>
</Slide>
@@ -42,5 +44,5 @@ export default async (props: Props) => {
</div>
</div>
</>
)
};
);
};

View File

@@ -18,14 +18,8 @@ interface TagItemProps {
}
const TagItem: React.FC<TagItemProps> = ({ icon: Icon, text, isLeft, delay }) => (
<motion.div
initial={{ opacity: 0, x: isLeft ? 100 : -100 }}
animate={{ opacity: [0, 0.8, 0], x: isLeft ? [-100, 0, 100] : [100, 0, -100] }}
transition={{ duration: 5, delay, repeat: Infinity, repeatType: 'loop', ease: 'linear' }}
>
<div
className={clsx('inline-flex items-center space-x-2 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-400 px-3 py-1 text-sm shadow-sm')}
>
<motion.div initial={{ opacity: 0, x: isLeft ? 100 : -100 }} animate={{ opacity: [0, 0.8, 0], x: isLeft ? [-100, 0, 100] : [100, 0, -100] }} transition={{ duration: 5, delay, repeat: Infinity, repeatType: 'loop', ease: 'linear' }}>
<div className={clsx('inline-flex items-center space-x-2 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-400 px-3 py-1 text-sm shadow-sm')}>
<Icon size={14} />
<span>{text}</span>
</div>

View File

@@ -2,12 +2,12 @@
import React from 'react';
import Link from 'next/link';
import { LiaTagsSolid } from "react-icons/lia";
import { LiaTagsSolid } from 'react-icons/lia';
import { Tag } from '@/types/app/tag';
import { clsx } from 'clsx';
import { motion } from 'framer-motion';
const TagItemCard = ({ data, count, index }: { data: Tag, count: number, index: number }) => {
const TagItemCard = ({ data, count, index }: { data: Tag; count: number; index: number }) => {
const colors = ['bg-blue-400', 'bg-green-400', 'bg-yellow-400', 'bg-red-400', 'bg-indigo-400', 'bg-purple-400', 'bg-pink-400'];
const color = colors[index % colors.length];
@@ -21,13 +21,17 @@ const TagItemCard = ({ data, count, index }: { data: Tag, count: number, index:
transition: { duration: 0.2 },
}}
>
<Link href={`/tag/${data?.id}?name=${data?.name}`} className={clsx('flex h-10 bg-opacity-20 backdrop-blur', color)} style={{
borderRadius: '0.5rem',
backdropFilter: 'blur(10px)',
margin: '0.5rem',
padding: '0 1rem',
alignItems: 'center',
}}>
<Link
href={`/tag/${data?.id}?name=${data?.name}`}
className={clsx('flex h-10 bg-opacity-20 backdrop-blur', color)}
style={{
borderRadius: '0.5rem',
backdropFilter: 'blur(10px)',
margin: '0.5rem',
padding: '0 1rem',
alignItems: 'center',
}}
>
<LiaTagsSolid className="h-4 w-4 text-gray-400" aria-hidden="true" />
<span className="ml-2">{data?.name}</span>
<span className="ml-4 text-sm text-gray-400 dark:text-gray-500">{count}</span>

View File

@@ -1,16 +1,16 @@
import { getTagListWithArticleCountAPI } from "@/api/tag"
import { Tag } from "@/types/app/tag"
import TagCloudBackground from "@/app/tags/components/TagCloudBackground"
import TagItemCard from "./components/TagItemCard"
import { Metadata } from "next";
import { getTagListWithArticleCountAPI } from '@/api/tag';
import { Tag } from '@/types/app/tag';
import TagCloudBackground from '@/app/tags/components/TagCloudBackground';
import TagItemCard from './components/TagItemCard';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: "🏷️ 标签墙",
description: "🏷️ 标签墙",
title: '🏷️ 标签墙',
description: '🏷️ 标签墙',
};
export default async () => {
const { data } = await getTagListWithArticleCountAPI() || { data: {} as Tag[] }
const { data } = (await getTagListWithArticleCountAPI()) || { data: {} as Tag[] };
return (
<div className="py-[50px] mt-[60px] h-screen overflow-scroll hide_sliding">
@@ -24,5 +24,5 @@ export default async () => {
<TagCloudBackground tags={data?.map((item: Tag) => item.name) || []} />
</div>
)
}
);
};

View File

@@ -1,76 +1,73 @@
import Link from 'next/link';
import Pagination from '@/components/Pagination';
import AddWallInfo from '../components/AddWallInfo';
import { getCateListAPI, getCateWallListAPI } from "@/api/wall";
import { getCateListAPI, getCateWallListAPI } from '@/api/wall';
import dayjs from 'dayjs';
import { Cate } from '@/types/app/cate';
import { Wall } from '@/types/app/wall';
interface Props {
params: Promise<{ cate: string }>;
searchParams: Promise<{ page: number }>
params: Promise<{ cate: string }>;
searchParams: Promise<{ page: number }>;
}
export default async (props: Props) => {
const searchParams = await props.searchParams;
const params = await props.params;
const cate = params.cate
const page = searchParams.page || 1;
const searchParams = await props.searchParams;
const params = await props.params;
const cate = params.cate;
const page = searchParams.page || 1;
const active = "!text-primary !border-primary"
const active = '!text-primary !border-primary';
// 提前把颜色写好,否则会导致样式丢失
const colors = ["bg-[#fcafa24d]", "bg-[#a8ed8a4d]", "bg-[#caa7f74d]", "bg-[#ffe3944d]", "bg-[#92e6f54d]"]
// 提前把颜色写好,否则会导致样式丢失
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const colors = ['bg-[#fcafa24d]', 'bg-[#a8ed8a4d]', 'bg-[#caa7f74d]', 'bg-[#ffe3944d]', 'bg-[#92e6f54d]'];
const { data: cateList } = (await getCateListAPI()) || { data: [] as Cate[] }
const { data: cateList } = (await getCateListAPI()) || { data: [] as Cate[] };
const id = cateList.find(item => item.mark === cate)?.id!
const { data: tallList } = (await getCateWallListAPI(id, page)) || { data: {} as Paginate<Wall[]> }
const id = cateList.find((item) => item.mark === cate)?.id ?? 0;
const { data: tallList } = (await getCateWallListAPI(id, page)) || { data: {} as Paginate<Wall[]> };
cateList.sort((a, b) => a.order - b.order);
cateList.sort((a, b) => a.order - b.order);
return (
<>
<title>💌 </title>
<meta name="description" content="💌 留言墙" />
return (
<>
<title>💌 </title>
<meta name="description" content="💌 留言墙" />
<div className='py-16 border-b dark:border-[#4e5969] bg-[linear-gradient(to_right,#fff1eb_0%,#d0edfb_100%)] dark:bg-[linear-gradient(to_right,#232931_0%,#232931_100%)] '>
<div className="flex flex-col items-center">
<h2 className="text-5xl pt-24"></h2>
<p className="text-sm text-gray-600 my-10"></p>
</div>
<div className="py-16 border-b dark:border-[#4e5969] bg-[linear-gradient(to_right,#fff1eb_0%,#d0edfb_100%)] dark:bg-[linear-gradient(to_right,#232931_0%,#232931_100%)] ">
<div className="flex flex-col items-center">
<h2 className="text-5xl pt-24"></h2>
<p className="text-sm text-gray-600 my-10"></p>
</div>
<ul className="flex flex-col md:flex-row justify-center text-sm space-y-1 md:space-y-0">
{
cateList?.map(item => (
<li key={item.id} className={`py-2 px-4 mx-1 dark:text-[#8c9ab1] border-2 border-transparent rounded-full hover:!text-primary hover:border-primary ${item.mark === cate ? active : ''} `}>
<Link href={`/wall/${item.mark}`}>{item.name}</Link>
</li>
))
}
</ul>
<ul className="flex flex-col md:flex-row justify-center text-sm space-y-1 md:space-y-0">
{cateList?.map((item) => (
<li key={item.id} className={`py-2 px-4 mx-1 dark:text-[#8c9ab1] border-2 border-transparent rounded-full hover:!text-primary hover:border-primary ${item.mark === cate ? active : ''} `}>
<Link href={`/wall/${item.mark}`}>{item.name}</Link>
</li>
))}
</ul>
<div className='w-[90%] xl:w-[1200px] mx-auto mt-12 grid grid-cols-1 gap-1 xs:grid-cols-2 xs:gap-2 md:grid-cols-3 md:gap-3 lg:grid-cols-4 lg:gap-4'>
{
tallList.result?.map(item => (
<div key={item.id} className={`relative flex flex-col py-2 px-4 bg-[${item.color}] rounded-lg top-0 hover:-top-2 transition-[top]`}>
<div className='flex justify-between items-center mt-2 text-xs text-gray-500 dark:text-[#8c9ab1]'>
<span>{dayjs(+item.createTime!).format('YYYY-MM-DD HH:mm')}</span>
<span>{item.cate.name}</span>
</div>
<div className="w-[90%] xl:w-[1200px] mx-auto mt-12 grid grid-cols-1 gap-1 xs:grid-cols-2 xs:gap-2 md:grid-cols-3 md:gap-3 lg:grid-cols-4 lg:gap-4">
{tallList.result?.map((item) => (
<div key={item.id} className={`relative flex flex-col py-2 px-4 bg-[${item.color}] rounded-lg top-0 hover:-top-2 transition-[top]`}>
<div className="flex justify-between items-center mt-2 text-xs text-gray-500 dark:text-[#8c9ab1]">
<span>{dayjs(+item.createTime!).format('YYYY-MM-DD HH:mm')}</span>
<span>{item.cate.name}</span>
</div>
<div className='hide_sliding overflow-auto h-32 text-sm my-4 text-gray-700 dark:text-[#cecece]'>{item.content}</div>
<div className="hide_sliding overflow-auto h-32 text-sm my-4 text-gray-700 dark:text-[#cecece]">{item.content}</div>
<div className='text-end text-[#5b5b5b] dark:text-[#A0A0A0]'>{item.name ? item.name : "匿名"}</div>
</div>
))
}
</div>
{tallList.total && <Pagination total={tallList.pages} page={page} className="flex justify-center mt-5" />}
<AddWallInfo />
<div className="text-end text-[#5b5b5b] dark:text-[#A0A0A0]">{item.name ? item.name : '匿名'}</div>
</div>
</>
)
};
))}
</div>
{tallList.total && <Pagination total={tallList.pages} page={page} className="flex justify-center mt-5" />}
<AddWallInfo />
</div>
</>
);
};

View File

@@ -1,35 +1,35 @@
"use client"
'use client';
import { useEffect, useState } from "react";
import { Input, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure, Select, SelectItem, Textarea, RadioGroup, Radio } from "@heroui/react";
import { Controller, SubmitHandler, useForm } from "react-hook-form";
import { Wall, Cate } from "@/types/app/wall";
import { addWallDataAPI, getCateListAPI } from '@/api/wall'
import { Bounce, toast, ToastContainer, ToastOptions } from "react-toastify";
import { useEffect, useState } from 'react';
import { Input, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure, Select, SelectItem, Textarea, RadioGroup, Radio } from '@heroui/react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { Wall, Cate } from '@/types/app/wall';
import { addWallDataAPI, getCateListAPI } from '@/api/wall';
import { Bounce, toast, ToastContainer, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { MdOutlineAdd } from "react-icons/md";
import { MdOutlineAdd } from 'react-icons/md';
const toastConfig: ToastOptions = {
position: "top-right",
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
theme: 'colored',
transition: Bounce,
}
};
export default () => {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
// 获取留言分类列表
const [cateList, setCateList] = useState<Cate[]>([])
const [cateList, setCateList] = useState<Cate[]>([]);
const getCateList = async () => {
const { data } = (await getCateListAPI()) || { data: [] as Cate[] }
setCateList(data?.filter(item => item.id !== 1))
}
const { data } = (await getCateListAPI()) || { data: [] as Cate[] };
setCateList(data?.filter((item) => item.id !== 1));
};
useEffect(() => {
// 页面加载后检查是否有需要显示的消息
const message = localStorage.getItem('toastMessage');
@@ -38,30 +38,35 @@ export default () => {
localStorage.removeItem('toastMessage'); // 显示后删除消息
}
getCateList()
}, [])
getCateList();
}, []);
const [defaultValues, setDefaultValues] = useState<Wall>({} as Wall)
const { handleSubmit, control, formState: { errors }, trigger } = useForm<Wall>({ defaultValues });
const [defaultValues] = useState<Wall>({} as Wall);
const {
handleSubmit,
control,
formState: { errors },
trigger,
} = useForm<Wall>({ defaultValues });
const onSubmit: SubmitHandler<Wall> = async (data, event) => {
event?.preventDefault();
const { code, message } = (await addWallDataAPI({ ...data, createTime: Date.now().toString() })) || { code: 0, message: "" }
const { code, message } = (await addWallDataAPI({ ...data, createTime: Date.now().toString() })) || { code: 0, message: '' };
if (code !== 200) return toast.error(message, toastConfig);
// 提交成功后存储消息
localStorage.setItem('toastMessage', '🎉 提交成功, 请等待审核!');
window.location.reload();
onOpenChange()
}
onOpenChange();
};
// 表单样式
const inputWrapper = "hover:!border-primary group-data-[focus=true]:border-primary rounded-md"
const inputWrapper = 'hover:!border-primary group-data-[focus=true]:border-primary rounded-md';
return (
<>
<div className='fixed top-[15%] right-[5%] flex justify-center items-center w-[70px] h-[70px] rounded-full bg-black-b cursor-pointer z-50' onClick={onOpen}>
<MdOutlineAdd className='text-white text-5xl' />
<div className="fixed top-[15%] right-[5%] flex justify-center items-center w-[70px] h-[70px] rounded-full bg-black-b cursor-pointer z-50" onClick={onOpen}>
<MdOutlineAdd className="text-white text-5xl" />
</div>
<Modal
@@ -70,11 +75,11 @@ export default () => {
isOpen={isOpen}
onOpenChange={onOpenChange}
classNames={{
backdrop: "bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20"
backdrop: 'bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20',
}}
>
<ModalContent>
{(onClose) => (
{() => (
<>
<ModalHeader className="flex flex-col gap-1"></ModalHeader>
@@ -85,16 +90,7 @@ export default () => {
rules={{ required: '请输入留言内容' }}
render={({ field }) => (
<>
<Textarea
{...field}
label="留言内容"
variant="bordered"
placeholder="示例:你好呀!"
isInvalid={!!errors.content?.message}
errorMessage={errors.content?.message}
onBlur={() => trigger('content')}
classNames={{ inputWrapper }}
/>
<Textarea {...field} label="留言内容" variant="bordered" placeholder="示例:你好呀!" isInvalid={!!errors.content?.message} errorMessage={errors.content?.message} onBlur={() => trigger('content')} classNames={{ inputWrapper }} />
</>
)}
/>
@@ -104,17 +100,7 @@ export default () => {
control={control}
render={({ field }) => (
<>
<Input
{...field}
type="text"
label="你的名称(选填)"
variant="bordered"
placeholder="示例:宇阳"
isInvalid={!!errors.name?.message}
errorMessage={errors.name?.message}
onBlur={() => trigger('name')}
classNames={{ inputWrapper }}
/>
<Input {...field} type="text" label="你的名称(选填)" variant="bordered" placeholder="示例:宇阳" isInvalid={!!errors.name?.message} errorMessage={errors.name?.message} onBlur={() => trigger('name')} classNames={{ inputWrapper }} />
</>
)}
/>
@@ -124,17 +110,7 @@ export default () => {
control={control}
render={({ field }) => (
<>
<Input
{...field}
type="text"
label="你的邮箱(选填)"
variant="bordered"
placeholder="示例3311118881@qq.com"
isInvalid={!!errors.email?.message}
errorMessage={errors.email?.message}
onBlur={() => trigger('email')}
classNames={{ inputWrapper }}
/>
<Input {...field} type="text" label="你的邮箱(选填)" variant="bordered" placeholder="示例3311118881@qq.com" isInvalid={!!errors.email?.message} errorMessage={errors.email?.message} onBlur={() => trigger('email')} classNames={{ inputWrapper }} />
</>
)}
/>
@@ -153,10 +129,12 @@ export default () => {
isInvalid={!!errors.cateId?.message}
errorMessage={errors.cateId?.message}
classNames={{
trigger: "hover:!border-primary data-[focus=true]:!border-primary data-[open=true]:!border-primary rounded-md"
trigger: 'hover:!border-primary data-[focus=true]:!border-primary data-[open=true]:!border-primary rounded-md',
}}
>
{cateList?.map(item => <SelectItem key={item.id}>{item.name}</SelectItem>)}
{cateList?.map((item) => (
<SelectItem key={item.id}>{item.name}</SelectItem>
))}
</Select>
</>
)}
@@ -190,7 +168,9 @@ export default () => {
</ModalBody>
<ModalFooter>
<Button color="primary" onPress={() => handleSubmit(onSubmit)()} className="w-full"></Button>
<Button color="primary" onPress={() => handleSubmit(onSubmit)()} className="w-full">
</Button>
</ModalFooter>
</>
)}
@@ -200,4 +180,4 @@ export default () => {
<ToastContainer />
</>
);
}
};

View File

@@ -3,73 +3,83 @@ import { getRandom } from '@/utils';
import { Article } from '@/types/app/article';
import dayjs from 'dayjs';
import { RiFireLine } from "react-icons/ri";
import { IoTimeOutline } from "react-icons/io5";
import { GoTag } from "react-icons/go";
import { RiFireLine } from 'react-icons/ri';
import { IoTimeOutline } from 'react-icons/io5';
import { GoTag } from 'react-icons/go';
import Empty from '@/components/Empty';
import Show from '@/components/Show';
import { getWebConfigDataAPI } from '@/api/config'
import { getWebConfigDataAPI } from '@/api/config';
import { Theme } from '@/types/app/config';
interface CardProps {
data: Paginate<Article[]>;
data: Paginate<Article[]>;
}
const Card = async ({ data }: CardProps) => {
const { data: { value: theme } } = (await getWebConfigDataAPI<{ value: Theme }>("theme")) || { data: { value: {} as Theme } };
const covers = theme.covers || []
const {
data: { value: theme },
} = (await getWebConfigDataAPI<{ value: Theme }>('theme')) || { data: { value: {} as Theme } };
const covers = theme.covers || [];
// 生成文章摘要取前100个字
const genArticleInfo = (data: Article) => {
if (data.description?.trim()?.length) {
return data.description
} else {
return data.content.slice(0, 100)
}
// 生成文章摘要取前100个字
const genArticleInfo = (data: Article) => {
if (data.description?.trim()?.length) {
return data.description;
} else {
return data.content.slice(0, 100);
}
};
return (
<div className="space-y-4">
{data?.result?.map((item, index) => (
<div key={item.id} className="relative overflow-hidden flex h-[190px] md:h-60 lg:h-52 xl:h-60 bg-black-b tw_container">
<div className="relative w-full py-5 px-5 sm:px-10 lg:px-5 xl:px-10 z-20">
<Link href={`/article/${item.id}`} className='flex flex-col justify-between h-full text-center sm:text-start'>
<h3 className='overflow-hidden relative w-full my-2.5 text_shadow text-white hover:text-primary text-center text-lg md:text-xl lg:text-[22px] xl:text-2xl line-clamp-1'>{item.title}</h3>
<p className='text-center text-[#cecece] text-sm sm:text-[15px] leading-7 sm:indent-8 line-clamp-2 xl:line-clamp-3'>{genArticleInfo(item)}</p>
return (
<div className="space-y-4">
{data?.result?.map((item) => (
<div key={item.id} className="relative overflow-hidden flex h-[190px] md:h-60 lg:h-52 xl:h-60 bg-black-b tw_container">
<div className="relative w-full py-5 px-5 sm:px-10 lg:px-5 xl:px-10 z-20">
<Link href={`/article/${item.id}`} className="flex flex-col justify-between h-full text-center sm:text-start">
<h3 className="overflow-hidden relative w-full my-2.5 text_shadow text-white hover:text-primary text-center text-lg md:text-xl lg:text-[22px] xl:text-2xl line-clamp-1">{item.title}</h3>
<p className="text-center text-[#cecece] text-sm sm:text-[15px] leading-7 sm:indent-8 line-clamp-2 xl:line-clamp-3">{genArticleInfo(item)}</p>
<div className={`flex justify-center pt-5 text-end space-x-4 sm:space-x-8`}>
<div className='flex items-center text-xs text-white'>
<span className='pr-1'><IoTimeOutline className='p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#539dfd]' /></span>
<span>{dayjs(+item.createTime!).format('YYYY-MM-DD')}</span>
</div>
<div className='flex items-center text-xs text-white'>
<span className='pr-1'><RiFireLine className='p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#eb373a]' /></span>
<span>{item.view}</span>
</div>
<div className='flex items-center text-xs text-white'>
<span className='pr-1'><GoTag className='p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#f5a630]' /></span>
<span>{item.cateList[0]?.name}</span>
</div>
</div>
</Link>
</div>
<div
className="absolute w-full h-60 bg-cover bg-center"
style={{
filter: 'blur(1.8rem) brightness(0.9)',
backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})`
}}
/>
<div className={`flex justify-center pt-5 text-end space-x-4 sm:space-x-8`}>
<div className="flex items-center text-xs text-white">
<span className="pr-1">
<IoTimeOutline className="p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#539dfd]" />
</span>
<span>{dayjs(+item.createTime!).format('YYYY-MM-DD')}</span>
</div>
))}
<Show is={!data?.total} children={<Empty info="暂无文章" />}></Show>
<div className="flex items-center text-xs text-white">
<span className="pr-1">
<RiFireLine className="p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#eb373a]" />
</span>
<span>{item.view}</span>
</div>
<div className="flex items-center text-xs text-white">
<span className="pr-1">
<GoTag className="p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#f5a630]" />
</span>
<span>{item.cateList[0]?.name}</span>
</div>
</div>
</Link>
</div>
<div
className="absolute w-full h-60 bg-cover bg-center"
style={{
filter: 'blur(1.8rem) brightness(0.9)',
backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})`,
}}
/>
</div>
);
))}
<Show is={!data?.total}>
<Empty info="暂无文章" />
</Show>
</div>
);
};
export default Card;
export default Card;

View File

@@ -3,95 +3,104 @@ import { getRandom } from '@/utils';
import { Article } from '@/types/app/article';
import dayjs from 'dayjs';
import { RiFireLine } from "react-icons/ri";
import { IoTimeOutline } from "react-icons/io5";
import { GoTag } from "react-icons/go";
import { RiFireLine } from 'react-icons/ri';
import { IoTimeOutline } from 'react-icons/io5';
import { GoTag } from 'react-icons/go';
import Empty from '@/components/Empty';
import Show from '@/components/Show';
import { getWebConfigDataAPI } from '@/api/config'
import { getWebConfigDataAPI } from '@/api/config';
import { Theme } from '@/types/app/config';
interface ClassicsProps {
data: Paginate<Article[]>;
data: Paginate<Article[]>;
}
const Classics = async ({ data }: ClassicsProps) => {
const { data: { value: theme } } = (await getWebConfigDataAPI<{ value: Theme }>("theme")) || { data: { value: {} as Theme } };
const {
data: { value: theme },
} = (await getWebConfigDataAPI<{ value: Theme }>('theme')) || { data: { value: {} as Theme } };
const covers = theme.covers || []
const covers = theme.covers || [];
// 生成文章摘要取前100个字
const genArticleInfo = (data: Article) => {
if (data.description?.trim()?.length) {
return data.description
} else {
return data.content.slice(0, 100)
}
// 生成文章摘要取前100个字
const genArticleInfo = (data: Article) => {
if (data.description?.trim()?.length) {
return data.description;
} else {
return data.content.slice(0, 100);
}
};
return (
<div className="space-y-4">
{data?.result?.map((item, index) => (
<div key={item.id} className="relative overflow-hidden flex h-[190px] md:h-60 lg:h-52 xl:h-60 bg-black-b tw_container">
{index % 2 === 0 && (
<div
className="hidden sm:block relative min-w-[45%] bg-cover bg-no-repeat bg-center scale-100 hover:scale-125 z-10 transition-transform"
style={{
clipPath: 'polygon(0 0, 100% 0, 90% 100%, 0 100%)',
backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})`,
}}
/>
)}
return (
<div className="space-y-4">
{data?.result?.map((item, index) => (
<div key={item.id} className="relative overflow-hidden flex h-[190px] md:h-60 lg:h-52 xl:h-60 bg-black-b tw_container">
{index % 2 === 0 && (
<div
className="hidden sm:block relative min-w-[45%] bg-cover bg-no-repeat bg-center scale-100 hover:scale-125 z-10 transition-transform"
style={{
clipPath: 'polygon(0 0, 100% 0, 90% 100%, 0 100%)',
backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})`,
}}
/>
)}
<div className="relative w-full sm:w-[65%] py-5 px-5 sm:px-10 lg:px-5 xl:px-10 z-20">
<Link href={`/article/${item.id}`} className='flex flex-col justify-between h-full text-center sm:text-start'>
<h3 className='overflow-hidden relative w-full my-2.5 text-white hover:text-primary text-lg md:text-xl lg:text-[22px] xl:text-2xl line-clamp-1'>{item.title}</h3>
{/* <p className='text-[#cecece] text-sm sm:text-[15px] leading-7 sm:indent-8 line-clamp-2 xl:line-clamp-3'>{item.description}</p> */}
<p className='text-[#cecece] text-sm sm:text-[15px] leading-7 sm:indent-8 line-clamp-2 xl:line-clamp-3'>{genArticleInfo(item)}</p>
<div className="relative w-full sm:w-[65%] py-5 px-5 sm:px-10 lg:px-5 xl:px-10 z-20">
<Link href={`/article/${item.id}`} className="flex flex-col justify-between h-full text-center sm:text-start">
<h3 className="overflow-hidden relative w-full my-2.5 text-white hover:text-primary text-lg md:text-xl lg:text-[22px] xl:text-2xl line-clamp-1">{item.title}</h3>
<p className="text-[#cecece] text-sm sm:text-[15px] leading-7 sm:indent-8 line-clamp-2 xl:line-clamp-3">{genArticleInfo(item)}</p>
<div className={`flex ${index % 2 === 0 ? 'sm:justify-start' : 'sm:justify-end'} justify-center pt-5 text-end space-x-4 sm:space-x-8`}>
<div className='flex items-center text-xs text-white'>
<span className='pr-1'><IoTimeOutline className='p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#539dfd]' /></span>
<span>{dayjs(+item.createTime!).format('YYYY-MM-DD')}</span>
</div>
<div className='flex items-center text-xs text-white'>
<span className='pr-1'><RiFireLine className='p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#eb373a]' /></span>
<span>{item.view}</span>
</div>
<div className='flex items-center text-xs text-white'>
<span className='pr-1'><GoTag className='p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#f5a630]' /></span>
<span>{item.cateList[0]?.name}</span>
</div>
</div>
</Link>
</div>
<div
className="absolute w-full h-60 bg-cover bg-center"
style={{
filter: 'blur(2.5rem) brightness(0.6)',
backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})`
}}
/>
{index % 2 !== 0 && (
<div
className="relative min-w-[45%] bg-cover bg-no-repeat bg-center scale-100 z-10 hover:scale-125 transition-transform hidden sm:block"
style={{
clipPath: 'polygon(10% 0, 100% 0, 100% 100%, 0 100%)',
backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})`,
}}
/>
)}
<div className={`flex ${index % 2 === 0 ? 'sm:justify-start' : 'sm:justify-end'} justify-center pt-5 text-end space-x-4 sm:space-x-8`}>
<div className="flex items-center text-xs text-white">
<span className="pr-1">
<IoTimeOutline className="p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#539dfd]" />
</span>
<span>{dayjs(+item.createTime!).format('YYYY-MM-DD')}</span>
</div>
))}
<Show is={!data?.total} children={<Empty info="暂无文章" />}></Show>
<div className="flex items-center text-xs text-white">
<span className="pr-1">
<RiFireLine className="p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#eb373a]" />
</span>
<span>{item.view}</span>
</div>
<div className="flex items-center text-xs text-white">
<span className="pr-1">
<GoTag className="p-1 mt-[-2px] mr-[3px] text-[23px] text-white rounded-full align-middle bg-[#f5a630]" />
</span>
<span>{item.cateList[0]?.name}</span>
</div>
</div>
</Link>
</div>
<div
className="absolute w-full h-60 bg-cover bg-center"
style={{
filter: 'blur(2.5rem) brightness(0.6)',
backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})`,
}}
/>
{index % 2 !== 0 && (
<div
className="relative min-w-[45%] bg-cover bg-no-repeat bg-center scale-100 z-10 hover:scale-125 transition-transform hidden sm:block"
style={{
clipPath: 'polygon(10% 0, 100% 0, 100% 100%, 0 100%)',
backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})`,
}}
/>
)}
</div>
);
))}
<Show is={!data?.total}>
<Empty info="暂无文章" />
</Show>
</div>
);
};
export default Classics;
export default Classics;

View File

@@ -1,53 +1,44 @@
"use client"
'use client';
import Link from 'next/link';
import { useConfigStore } from '@/stores'
import { Article } from "@/types/app/article";
import { useConfigStore } from '@/stores';
import { Article } from '@/types/app/article';
import { getRandom } from '@/utils';
import Masonry from 'react-masonry-css'
import Masonry from 'react-masonry-css';
interface WaterfallProps {
data: Paginate<Article[]>;
data: Paginate<Article[]>;
}
const breakpointColumnsObj = {
default: 4,
1024: 3,
700: 2
default: 4,
1024: 3,
700: 2,
};
export default ({ data }: WaterfallProps) => {
const { theme } = useConfigStore()
const covers = theme.covers || []
const { theme } = useConfigStore();
const covers = theme.covers || [];
return (
<>
<Masonry
breakpointCols={breakpointColumnsObj}
className="masonry-grid mb-12"
columnClassName="masonry-grid_column"
>
{
data.result.map(item => (
<div key={item.id} className='group overflow-hidden mt-2.5 rounded-xl bg-white dark:bg-black-b border dark:border-black-b hover:shadow-[0_10px_20px_1px_rgb(83,157,253,.1)] cursor-pointer'>
<Link href={`/article/${item.id}`}>
<div className='overflow-hidden h-32'>
<div
className="relative h-full bg-cover bg-no-repeat bg-center scale-100 hover:scale-125 z-10 transition-transform"
style={{ backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})` }}
/>
</div>
return (
<>
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid mb-12" columnClassName="masonry-grid_column">
{data.result.map((item) => (
<div key={item.id} className="group overflow-hidden mt-2.5 rounded-xl bg-white dark:bg-black-b border dark:border-black-b hover:shadow-[0_10px_20px_1px_rgb(83,157,253,.1)] cursor-pointer">
<Link href={`/article/${item.id}`}>
<div className="overflow-hidden h-32">
<div className="relative h-full bg-cover bg-no-repeat bg-center scale-100 hover:scale-125 z-10 transition-transform" style={{ backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})` }} />
</div>
<div className='py-2 px-4'>
<h1 className='mb-2 text-black dark:text-white group-hover:text-primary line-clamp-2 '>{item.title}</h1>
<div className="py-2 px-4">
<h1 className="mb-2 text-black dark:text-white group-hover:text-primary line-clamp-2 ">{item.title}</h1>
<div className='text-sm text-gray-500 dark:text-[#8c9ab1] line-clamp-4'>{item.description}</div>
</div>
</Link>
</div>
))
}
</Masonry>
</>
)
}
<div className="text-sm text-gray-500 dark:text-[#8c9ab1] line-clamp-4">{item.description}</div>
</div>
</Link>
</div>
))}
</Masonry>
</>
);
};

View File

@@ -1,53 +1,53 @@
"use client"
'use client';
import { useState, useEffect } from "react"
import Image from 'next/image'
import Link from 'next/link'
import dynamic from '../../svg/dynamic.svg'
import { getRecordPagingAPI } from "@/api/record"
import { Record } from "@/types/app/record"
import { extractText } from "@/utils"
import { useState, useEffect } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import dynamic from '../../svg/dynamic.svg';
import { getRecordPagingAPI } from '@/api/record';
import { Record } from '@/types/app/record';
import { extractText } from '@/utils';
export default function Dynamic({ className }: { className?: string }) {
const [list, setList] = useState<Record[]>([])
const [list, setList] = useState<Record[]>([]);
const getRecordList = async () => {
const { data } = await getRecordPagingAPI({ pagination: { page: 1, size: 8 } }) || { data: {} as Paginate<Record[]> }
setList(data?.result || [])
}
const getRecordList = async () => {
const { data } = (await getRecordPagingAPI({ pagination: { page: 1, size: 8 } })) || { data: {} as Paginate<Record[]> };
setList(data?.result || []);
};
useEffect(() => {
getRecordList()
}, [])
useEffect(() => {
getRecordList();
}, []);
// 使用useState来管理当前显示的内容索引
const [currentContentIndex, setCurrentContentIndex] = useState(0);
const [fade, setFade] = useState(true);
// 使用useState来管理当前显示的内容索引
const [currentContentIndex, setCurrentContentIndex] = useState(0);
const [fade, setFade] = useState(true);
// 使用useEffect来设置定时器
useEffect(() => {
const interval = setInterval(() => {
setFade(false); // 开始淡出
setTimeout(() => {
setCurrentContentIndex((prevIndex) => (prevIndex + 1) % list.length);
setFade(true); // 淡入新内容
}, 500); // 500ms的淡出时间
}, 5000); // 每2.5秒切换一次内容包括500ms的过渡时间
// 使用useEffect来设置定时器
useEffect(() => {
const interval = setInterval(() => {
setFade(false); // 开始淡出
setTimeout(() => {
setCurrentContentIndex((prevIndex) => (prevIndex + 1) % list.length);
setFade(true); // 淡入新内容
}, 500); // 500ms的淡出时间
}, 5000); // 每2.5秒切换一次内容包括500ms的过渡时间
// 清除定时器
return () => clearInterval(interval);
}, [list]);
// 清除定时器
return () => clearInterval(interval);
}, [list]);
return (
<div className={`flex justify-between items-center w-full px-4 py-3 border dark:border-transparent rounded-lg bg-white dark:bg-black-b mb-2 ${className}`}>
<div className="flex items-center">
<Image src={dynamic} alt='动态' width={25} height={25} className='mr-2 w-[25px] h-[25px]' />
<span></span>
</div>
return (
<div className={`flex justify-between items-center w-full px-4 py-3 border dark:border-transparent rounded-lg bg-white dark:bg-black-b mb-2 ${className}`}>
<div className="flex items-center">
<Image src={dynamic} alt="动态" width={25} height={25} className="mr-2 w-[25px] h-[25px]" />
<span></span>
</div>
<Link href="/record" className={`flex-1 line-clamp-1 hover:text-primary cursor-pointer ${fade ? 'opacity-100' : 'opacity-0'} transition-opacity`}>
{extractText(list[currentContentIndex]?.content || '')}
</Link>
</div>
)
}
<Link href="/record" className={`flex-1 line-clamp-1 hover:text-primary cursor-pointer ${fade ? 'opacity-100' : 'opacity-0'} transition-opacity`}>
{extractText(list[currentContentIndex]?.content || '')}
</Link>
</div>
);
}

View File

@@ -1,36 +1,38 @@
import Dynamic from './components/Dynamic'
import Swiper from '../Swiper'
import Classics from "./Classics"
import Waterfall from "./Waterfall"
import Card from './Card'
import Pagination from "../Pagination"
import Dynamic from './components/Dynamic';
import Swiper from '../Swiper';
import Classics from './Classics';
import Waterfall from './Waterfall';
import Card from './Card';
import Pagination from '../Pagination';
import { getArticlePagingAPI } from '@/api/article'
import { getWebConfigDataAPI } from '@/api/config'
import { Theme } from '@/types/app/config'
import { Article } from '@/types/app/article'
import { Swiper as SwiperType } from '@/types/app/swiper'
import { getSwiperListAPI } from '@/api/swiper'
import { getArticlePagingAPI } from '@/api/article';
import { getWebConfigDataAPI } from '@/api/config';
import { Theme } from '@/types/app/config';
import { Article } from '@/types/app/article';
import { Swiper as SwiperType } from '@/types/app/swiper';
import { getSwiperListAPI } from '@/api/swiper';
export default async ({ page }: { page: number }) => {
const { data: swiper } = await getSwiperListAPI() || { data: [] as SwiperType[] }
const { data: { value: theme } } = (await getWebConfigDataAPI<{ value: Theme }>("theme")) || { data: { value: {} as Theme } };
const sidebar = theme?.right_sidebar || []
const { data: swiper } = (await getSwiperListAPI()) || { data: [] as SwiperType[] };
const {
data: { value: theme },
} = (await getWebConfigDataAPI<{ value: Theme }>('theme')) || { data: { value: {} as Theme } };
const sidebar = theme?.right_sidebar || [];
// 如果是瀑布流布局就显示28条数据否则显示8条
const { data } = await getArticlePagingAPI({ pagination: { page, size: theme.is_article_layout === "waterfall" ? 28 : 8 } }) || { data: {} as Paginate<Article[]> }
data.result = data?.result?.filter(item => item.config.status !== "no_home")
const { data } = (await getArticlePagingAPI({ pagination: { page, size: theme.is_article_layout === 'waterfall' ? 28 : 8 } })) || { data: {} as Paginate<Article[]> };
data.result = data?.result?.filter((item) => item.config.status !== 'no_home');
return (
<div className={`w-full md:w-[90%] ${sidebar?.length ? 'lg:w-[68%] xl:w-[73%]' : 'w-full'} mx-auto transition-width`}>
{!!swiper?.length && <Swiper data={swiper} />}
<Dynamic className='my-2' />
<Dynamic className="my-2" />
{theme.is_article_layout === "classics" && <Classics data={data} />}
{theme.is_article_layout === "card" && <Card data={data} />}
{theme.is_article_layout === "waterfall" && <Waterfall data={data} />}
{theme.is_article_layout === 'classics' && <Classics data={data} />}
{theme.is_article_layout === 'card' && <Card data={data} />}
{theme.is_article_layout === 'waterfall' && <Waterfall data={data} />}
{data.total && <Pagination total={data?.pages} page={page} className="flex justify-center mt-5" />}
</div>
)
}
);
};

View File

@@ -1,11 +1,12 @@
"use client"
'use client';
import Script from "next/script"
import Script from 'next/script';
export default () => {
return (
<Script dangerouslySetInnerHTML={{
__html: `
return (
<Script
dangerouslySetInnerHTML={{
__html: `
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
@@ -13,7 +14,8 @@ export default () => {
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
`
}} />
)
}
`,
}}
/>
);
};

View File

@@ -1,32 +1,32 @@
"use client"
'use client';
import { useEffect } from "react"
import { useEffect } from 'react';
import confetti from 'canvas-confetti'
import confetti from 'canvas-confetti';
export default () => {
useEffect(() => {
var duration = 15 * 1000;
var animationEnd = Date.now() + duration;
var defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
useEffect(() => {
const duration = 15 * 1000;
const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min;
}
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min;
}
var interval = setInterval(function () {
var timeLeft = animationEnd - Date.now();
const interval = setInterval(function () {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
if (timeLeft <= 0) {
return clearInterval(interval);
}
var particleCount = 50 * (timeLeft / duration);
// since particles fall down, start a bit higher than random
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } });
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } });
}, 250);
}, [])
const particleCount = 50 * (timeLeft / duration);
// since particles fall down, start a bit higher than random
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } });
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } });
}, 250);
}, []);
return null
}
return null;
};

View File

@@ -1,13 +1,11 @@
import "./index.scss"
import './index.scss';
export default ({ children }: { children: React.ReactNode }) => {
return (
<>
<div className="ContainerComponent">
<div className="flex flex-wrap justify-between lg:w-[950px] xl:w-[1200px] p-5 mx-auto">
{children}
</div>
<div className="flex flex-wrap justify-between lg:w-[950px] xl:w-[1200px] p-5 mx-auto">{children}</div>
</div>
</>
)
}
);
};

View File

@@ -1,14 +1,12 @@
import Image from 'next/image'
import EmptySvg from '@/assets/svg/other/empty.svg'
import EmptySvg from '@/assets/svg/other/empty.svg';
const Empty = ({ info }: { info: string }) => {
return (
<div className="w-52 mx-auto py-12 mt-5">
{/* <Image src={EmptySvg} alt="空状态" width={208} height={144} /> */}
<img src={EmptySvg.src} alt="空状态" width={208} height={144} />
<div className="pt-5 text-center text-gray-700 dark:text-white">{info}</div>
</div>
);
return (
<div className="w-52 mx-auto py-12 mt-5">
<img src={EmptySvg.src} alt="空状态" width={208} height={144} />
<div className="pt-5 text-center text-gray-700 dark:text-white">{info}</div>
</div>
);
};
export default Empty;

View File

@@ -1,21 +1,21 @@
"use client"
'use client';
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure, Button, Input } from "@heroui/react";
import { MdEnhancedEncryption } from "react-icons/md";
import { useEffect, useRef, useState } from "react";
import { useRouter, usePathname } from "next/navigation"
import { getArticleDataAPI } from "@/api/article";
import { toast, ToastContainer } from "react-toastify";
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure, Button, Input } from '@heroui/react';
import { MdEnhancedEncryption } from 'react-icons/md';
import { useEffect, useRef, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { getArticleDataAPI } from '@/api/article';
import { toast, ToastContainer } from 'react-toastify';
interface Props {
id: number
id: number;
}
export default function Encrypt({ id }: Props) {
const router = useRouter()
const pathname = usePathname()
const router = useRouter();
const pathname = usePathname();
const [password, setPassword] = useState("")
const [password, setPassword] = useState('');
const { isOpen, onOpen, onOpenChange } = useDisclosure();
@@ -31,24 +31,23 @@ export default function Encrypt({ id }: Props) {
// 验证访问密码
const handleVerifyPassword = async () => {
const res = await getArticleDataAPI(id, password)
res?.code === 200 ? router.push(`${pathname}?password=${password}`) : toast.error("访问密码错误,请重新输入");
const res = await getArticleDataAPI(id, password);
if (res?.code === 200) {
router.push(`${pathname}?password=${password}`);
} else {
toast.error('访问密码错误,请重新输入');
}
};
// 表单样式
const inputWrapper = "hover:!border-primary group-data-[focus=true]:border-primary rounded-md"
const inputWrapper = 'hover:!border-primary group-data-[focus=true]:border-primary rounded-md';
return (
<>
<Modal
isOpen={isOpen}
backdrop="blur"
placement="top-center"
isDismissable={false}
hideCloseButton={true}
onOpenChange={onOpenChange}>
<Modal isOpen={isOpen} backdrop="blur" placement="top-center" isDismissable={false} hideCloseButton={true} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
{() => (
<>
<ModalHeader className="flex flex-col gap-1">🔑 </ModalHeader>
@@ -64,15 +63,15 @@ export default function Encrypt({ id }: Props) {
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => {
if(e.key === "Enter" || e.code === "Enter") {
handleVerifyPassword()
if (e.key === 'Enter' || e.code === 'Enter') {
handleVerifyPassword();
}
}}
/>
</ModalBody>
<ModalFooter>
<Button color="default" onPress={() => router.push("/")}></Button>
<Button color="default" onPress={() => router.push('/')}></Button>
<Button color="primary" onPress={handleVerifyPassword}></Button>
</ModalFooter>
</>
@@ -83,4 +82,4 @@ export default function Encrypt({ id }: Props) {
<ToastContainer />
</>
);
}
}

View File

@@ -1,178 +1,154 @@
'use client'
'use client';
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Button, useDisclosure } from '@heroui/react'
import { BiCog, BiCommand } from "react-icons/bi";
import {
IoSearchOutline,
IoArrowUpOutline,
IoLogoRss
} from 'react-icons/io5'
import { useConfigStore } from '@/stores'
import Search from '../Search'
import Rss from '../Tools/components/Rss'
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Button, useDisclosure } from '@heroui/react';
import { BiCog, BiCommand } from 'react-icons/bi';
import { IoSearchOutline, IoArrowUpOutline, IoLogoRss } from 'react-icons/io5';
import { useConfigStore } from '@/stores';
import Search from '../Search';
import Rss from '../Tools/components/Rss';
import { LuMoonStar } from 'react-icons/lu';
import { FaRegSun } from 'react-icons/fa';
const FloatingBlock = () => {
const [isExpanded, setIsExpanded] = useState(false) // 展开状态的变量
const { isDark, setIsDark, web } = useConfigStore()
const { isOpen: isSearchOpen, onClose: onSearchClose, onOpenChange: onSearchOpenChange } = useDisclosure()
const { isOpen: isRssOpen, onClose: onRssClose, onOpenChange: onRssOpenChange } = useDisclosure()
const [isExpanded, setIsExpanded] = useState(false); // 展开状态的变量
const { isDark, setIsDark, web } = useConfigStore();
const { isOpen: isSearchOpen, onClose: onSearchClose, onOpenChange: onSearchOpenChange } = useDisclosure();
const { isOpen: isRssOpen, onClose: onRssClose, onOpenChange: onRssOpenChange } = useDisclosure();
const toggleExpanded = () => {
setIsExpanded(!isExpanded)
}
const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};
// 返回顶部功能
const onReturnTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
// 返回顶部功能
const onReturnTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// 主题切换功能
const onToggleTheme = () => {
setIsDark(!isDark)
}
// 主题切换功能
const onToggleTheme = () => {
setIsDark(!isDark);
};
const actionItems = [
{
icon: isDark ? FaRegSun : LuMoonStar,
id: 'theme',
label: isDark ? '切换到亮色模式' : '切换到暗色模式',
onClick: onToggleTheme
},
{
icon: IoSearchOutline,
id: 'search',
label: '搜索',
onClick: onSearchOpenChange
},
{
icon: IoLogoRss,
id: 'rss',
label: 'RSS 订阅',
onClick: onRssOpenChange
},
{
icon: IoArrowUpOutline,
id: 'top',
label: '返回顶部',
onClick: onReturnTop
}
]
const actionItems = [
{
icon: isDark ? FaRegSun : LuMoonStar,
id: 'theme',
label: isDark ? '切换到亮色模式' : '切换到暗色模式',
onClick: onToggleTheme,
},
{
icon: IoSearchOutline,
id: 'search',
label: '搜索',
onClick: onSearchOpenChange,
},
{
icon: IoLogoRss,
id: 'rss',
label: 'RSS 订阅',
onClick: onRssOpenChange,
},
{
icon: IoArrowUpOutline,
id: 'top',
label: '返回顶部',
onClick: onReturnTop,
},
];
// 计算每个项目的位置(圆形分布)
const getItemPosition = (index: number, total: number) => {
const angle = (index * 360) / total - 90 // 从顶部开始
const radius = 50 // 半径 [距离按钮的距离]
const x = Math.cos((angle * Math.PI) / 180) * radius
const y = Math.sin((angle * Math.PI) / 180) * radius
return { x, y }
}
// 计算每个项目的位置(圆形分布)
const getItemPosition = (index: number, total: number) => {
const angle = (index * 360) / total - 90; // 从顶部开始
const radius = 50; // 半径 [距离按钮的距离]
const x = Math.cos((angle * Math.PI) / 180) * radius;
const y = Math.sin((angle * Math.PI) / 180) * radius;
return { x, y };
};
return (
<div className={`fixed bottom-[180px] right-[60px] z-50`}>
{/* 围绕的功能项 */}
<AnimatePresence>
{isExpanded && (
<>
{actionItems.map((item, index) => {
const position = getItemPosition(index, actionItems.length)
return (
<motion.div
key={item.id}
initial={{ // 初始状态
opacity: 0,
scale: 0,
x: 0,
y: 0
}}
animate={{ // 动画状态
opacity: 1,
scale: 1,
x: position.x,
y: position.y
}}
exit={{ // 退出状态
opacity: 0,
scale: 0,
x: 0,
y: 0
}}
transition={{ // 过渡动画
duration: 0.4,
delay: index * 0.1,
ease: [0.25, 0.46, 0.45, 0.94]
}}
className="absolute"
style={{
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
}}
>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="relative"
>
<Button
isIconOnly
size="md"
className="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300
return (
<div className={`fixed bottom-[180px] right-[60px] z-50`}>
{/* 围绕的功能项 */}
<AnimatePresence>
{isExpanded && (
<>
{actionItems.map((item, index) => {
const position = getItemPosition(index, actionItems.length);
return (
<motion.div
key={item.id}
initial={{
// 初始状态
opacity: 0,
scale: 0,
x: 0,
y: 0,
}}
animate={{
// 动画状态
opacity: 1,
scale: 1,
x: position.x,
y: position.y,
}}
exit={{
// 退出状态
opacity: 0,
scale: 0,
x: 0,
y: 0,
}}
transition={{
// 过渡动画
duration: 0.4,
delay: index * 0.1,
ease: [0.25, 0.46, 0.45, 0.94],
}}
className="absolute"
style={{
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
}}
>
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} className="relative">
<Button
isIconOnly
size="md"
className="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300
shadow-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50
dark:hover:bg-gray-700 -translate-x-5 -translate-y-5"
onPress={item.onClick}
title={item.label}
aria-label={item.label}
>
<item.icon className="w-5 h-5" />
</Button>
</motion.div>
</motion.div>
)
})}
</>
)}
</AnimatePresence>
{/* 主按钮 */}
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2 }}
className="relative"
>
<Button
isIconOnly
size="lg"
className="bg-blue-500 hover:bg-blue-600 text-white shadow-lg rounded-full"
onPress={toggleExpanded}
aria-label={isExpanded ? "收起功能菜单" : "展开功能菜单"}
title={isExpanded ? "收起功能菜单" : "展开功能菜单"}
>
<motion.div
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.3 }}
onPress={item.onClick}
title={item.label}
aria-label={item.label}
>
{isExpanded ? (
<BiCommand className="w-6 h-6" />
) : (
<BiCog className="w-6 h-6" />
)}
</motion.div>
</Button>
</motion.div>
<item.icon className="w-5 h-5" />
</Button>
</motion.div>
</motion.div>
);
})}
</>
)}
</AnimatePresence>
{/* 搜索组件 */}
<Search disclosure={{ isOpen: isSearchOpen, onClose: onSearchClose, onOpenChange: onSearchOpenChange }} />
{/* 主按钮 */}
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} transition={{ duration: 0.2 }} className="relative">
<Button isIconOnly size="lg" className="bg-blue-500 hover:bg-blue-600 text-white shadow-lg rounded-full" onPress={toggleExpanded} aria-label={isExpanded ? '收起功能菜单' : '展开功能菜单'} title={isExpanded ? '收起功能菜单' : '展开功能菜单'}>
<motion.div animate={{ rotate: isExpanded ? 180 : 0 }} transition={{ duration: 0.3 }}>
{isExpanded ? <BiCommand className="w-6 h-6" /> : <BiCog className="w-6 h-6" />}
</motion.div>
</Button>
</motion.div>
{/* 查看Rss地址 */}
<Rss data={web} disclosure={{ isOpen: isRssOpen, onClose: onRssClose, onOpenChange: onRssOpenChange }} />
</div>
)
}
{/* 搜索组件 */}
<Search disclosure={{ isOpen: isSearchOpen, onClose: onSearchClose, onOpenChange: onSearchOpenChange }} />
export default FloatingBlock
{/* 查看Rss地址 */}
<Rss data={web} disclosure={{ isOpen: isRssOpen, onClose: onRssClose, onOpenChange: onRssOpenChange }} />
</div>
);
};
export default FloatingBlock;

View File

@@ -1,7 +1,11 @@
"use client"
'use client';
import { Tooltip } from "@heroui/react";
import { Tooltip } from '@heroui/react';
export default ({ children, content }: { children: React.ReactNode, content: string }) => {
return <Tooltip showArrow={true} content={content}>{children}</Tooltip>
}
export default ({ children, content }: { children: React.ReactNode; content: string }) => {
return (
<Tooltip showArrow={true} content={content}>
{children}
</Tooltip>
);
};

View File

@@ -10,41 +10,46 @@ import animals from './images/animals.webp';
import ICP from './images/ICP.png';
export default async () => {
const { data: user } = (await getUserDataAPI()) || { data: {} as User }
const { data: { value: web } } = (await getWebConfigDataAPI<{ value: Web }>("web")) || { data: { value: {} as Web } };
const { data: user } = (await getUserDataAPI()) || { data: {} as User };
const {
data: { value: web },
} = (await getWebConfigDataAPI<{ value: Web }>('web')) || { data: { value: {} as Web } };
return (
<>
<div className='sticky bottom-0 z-30 translate-y-[25px] flex justify-center w-full bg-cover bg-center after:content-[""] after:w-full after:h-[60%] after:absolute after:bottom-[25px] after:left-0 after:bg-[linear-gradient(to_top,#fff,transparent)] dark:after:bg-[linear-gradient(to_top,#2c333e,transparent)]'>
<div className='flex justify-center lg:w-[950px] xl:w-[1200px] mx-auto'>
<Image src={animals} alt="动物" width={660.34} height={79.99} className='relative z-40 hidden md:block' />
<div className="flex justify-center lg:w-[950px] xl:w-[1200px] mx-auto">
<Image src={animals} alt="动物" width={660.34} height={79.99} className="relative z-40 hidden md:block" />
</div>
</div>
<div className='bg-white dark:bg-black-b border-t dark:border-black-b px-10 '>
<div className="bg-white dark:bg-black-b border-t dark:border-black-b px-10 ">
<div className="flex justify-center items-center py-4">
<img src={user?.avatar} alt='作者头像' className='w-20 h-20 rounded-full mr-8 avatar-animation shadow-[5px_11px_30px_20px_rgba(255,255,255,0.1)]' />
<img src={user?.avatar} alt="作者头像" className="w-20 h-20 rounded-full mr-8 avatar-animation shadow-[5px_11px_30px_20px_rgba(255,255,255,0.1)]" />
<h2 className="w-[90%] xl:w-3/6 text-sm sm:text-base dark:text-[#8c9ab1] line-clamp-4">{web?.footer}</h2>
</div>
<div className='group flex justify-center space-x-2 pb-4 cursor-pointer'>
<Image src={ICP} alt="ICP" width={20} height={22} className='w-5 h-[22px]' />
<span className='group-hover:text-primary'>{web?.icp}</span>
<div className="group flex justify-center space-x-2 pb-4 cursor-pointer">
<Image src={ICP} alt="ICP" width={20} height={22} className="w-5 h-[22px]" />
<span className="group-hover:text-primary">{web?.icp}</span>
</div>
{/*
为了项目的生态越来越强大,作者在这里恳请大家保留 ThriveX 博客系统版权
在项目 Star 突破 2K 后大家可自由选择删除 or 保留版权
*/}
<div className='py-4 border-t dark:border-black-a '>
<div className="py-4 border-t dark:border-black-a ">
<Tooltip content="一款免费、开源、年轻、高颜值的现代化博客管理系统">
<div className='flex justify-center items-center space-x-3'>
<img src="https://bu.dusays.com/2024/11/17/6739adf188f64.png" width={30} height={30} alt='ThriveX 博客管理系统' />
<Link href="https://github.com/LiuYuYang01/ThriveX-Admin" target='_blank' className='hover:text-primary '> ThriveX </Link>
<div className="flex justify-center items-center space-x-3">
<img src="https://bu.dusays.com/2024/11/17/6739adf188f64.png" width={30} height={30} alt="ThriveX 博客管理系统" />
<Link href="https://github.com/LiuYuYang01/ThriveX-Admin" target="_blank" className="hover:text-primary ">
{' '}
ThriveX
</Link>
</div>
</Tooltip>
</div>
</div>
</>
)
}
);
};

View File

@@ -1,14 +1,14 @@
import Show from "@/components/Show"
import { Cate } from "@/types/app/cate"
import Link from "next/link"
import { IoIosArrowDown } from "react-icons/io"
import { motion, AnimatePresence } from "framer-motion"
import Show from '@/components/Show';
import { Cate } from '@/types/app/cate';
import Link from 'next/link';
import { IoIosArrowDown } from 'react-icons/io';
import { motion, AnimatePresence } from 'framer-motion';
interface Props {
list: Cate[]
open: boolean
onClose: () => void
}
list: Cate[];
open: boolean;
onClose: () => void;
}
export default ({ list, open, onClose }: Props) => {
return (
@@ -16,58 +16,37 @@ export default ({ list, open, onClose }: Props) => {
<AnimatePresence>
{open && (
<div className="flex fixed top-0 left-0 w-full h-full z-[60]">
<motion.div
initial={{ width: 0, opacity: 0 }}
animate={{ width: '100%', opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ type: 'spring', stiffness: 200, damping: 30, opacity: { duration: 0.2 } }}
className="overflow-auto p-5 dark:border-[#2b333e] bg-[rgba(255,255,255,0.9)] dark:bg-[rgba(44,51,62,0.9)] backdrop-blur-[5px] hide_sliding"
>
<motion.div initial={{ width: 0, opacity: 0 }} animate={{ width: '100%', opacity: 1 }} exit={{ width: 0, opacity: 0 }} transition={{ type: 'spring', stiffness: 200, damping: 30, opacity: { duration: 0.2 } }} className="overflow-auto p-5 dark:border-[#2b333e] bg-[rgba(255,255,255,0.9)] dark:bg-[rgba(44,51,62,0.9)] backdrop-blur-[5px] hide_sliding">
<ul className="flex flex-col space-y-2">
{list?.map(one => (
{list?.map((one) => (
<li key={one.id} className="group/one relative hover:bg-[#e0e6ec] dark:hover:bg-[#495362] rounded-md ">
<Link
href={`${one.type === 'cate' ? `/cate/${one.id}?name=${one.name}` : one.url}`}
className={`flex justify-between items-center p-3 px-5 text-[15px] group-hover/one:!text-primary text-[#333] dark:text-white whitespace-nowrap`}
onClick={onClose}
>
<Link href={`${one.type === 'cate' ? `/cate/${one.id}?name=${one.name}` : one.url}`} className={`flex justify-between items-center p-3 px-5 text-[15px] group-hover/one:!text-primary text-[#333] dark:text-white whitespace-nowrap`} onClick={onClose}>
{one.icon} {one.name}
<Show is={!!one.children.length} children={(
<Show is={!!one.children.length}>
<IoIosArrowDown className="ml-2" />
)} />
</Show>
</Link>
<Show is={!!one.children.length} children={(
<Show is={!!one.children.length}>
<ul className="overflow-hidden top-[50px] w-full rounded-md">
{one.children?.map(two => (
<li key={two.id} className='group/two'>
<Link
href={`/cate/${two.id}?name=${two.name}`}
className="inline-block w-full p-2.5 pl-10 text-[15px] box-border text-[#666] dark:text-[#8c9ab1] hover:!text-primary"
onClick={onClose}
>
{one.children?.map((two) => (
<li key={two.id} className="group/two">
<Link href={`/cate/${two.id}?name=${two.name}`} className="inline-block w-full p-2.5 pl-10 text-[15px] box-border text-[#666] dark:text-[#8c9ab1] hover:!text-primary" onClick={onClose}>
{two.name}
</Link>
</li>
))}
</ul>
)} />
</Show>
</li>
))}
</ul>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden h-full bg-[rgba(0,0,0,0.6)] w-full"
onClick={onClose}
/>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} className="overflow-hidden h-full bg-[rgba(0,0,0,0.6)] w-full" onClick={onClose} />
</div>
)}
</AnimatePresence>
</>
)
}
);
};

View File

@@ -1,16 +1,16 @@
"use client"
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import React, { useState, useEffect } from 'react';
import { Switch } from "@heroui/react";
import Show from '@/components/Show'
import { Switch } from '@heroui/react';
import Show from '@/components/Show';
import SidebarNav from './component/SidebarNav';
import { IoIosArrowDown } from 'react-icons/io';
import { FaRegSun } from "react-icons/fa";
import { BsFillMoonStarsFill, BsTextIndentLeft } from "react-icons/bs";
import { FaRegSun } from 'react-icons/fa';
import { BsFillMoonStarsFill, BsTextIndentLeft } from 'react-icons/bs';
import { Cate } from '@/types/app/cate';
import { getCateListAPI } from '@/api/cate';
@@ -20,171 +20,151 @@ import { useConfigStore } from '@/stores';
import { Theme, Web } from '@/types/app/config';
const Header = () => {
// 是否暗黑模式
const { isDark, setIsDark, setWeb, theme, setTheme } = useConfigStore()
// 是否暗黑模式
const { isDark, setIsDark, setWeb, theme, setTheme } = useConfigStore();
// 获取项目配置
const getConfigData = async () => {
const { data: { value: web } } = (await getWebConfigDataAPI<{ value: Web }>("web")) || { data: { value: {} as Web } };
setWeb(web)
// 获取项目配置
const getConfigData = async () => {
const {
data: { value: web },
} = (await getWebConfigDataAPI<{ value: Web }>('web')) || { data: { value: {} as Web } };
setWeb(web);
const { data: { value: theme } } = (await getWebConfigDataAPI<{ value: Theme }>("theme")) || { data: { value: {} as Theme } };
setTheme(theme)
const {
data: { value: theme },
} = (await getWebConfigDataAPI<{ value: Theme }>('theme')) || { data: { value: {} as Theme } };
setTheme(theme);
};
const patchName = usePathname();
// 这些路径段不需要改变导航样式
const isPathSty = ['/my', '/wall', '/record', '/equipment', '/tags', '/resume', '/album', '/fishpond'].some((path) => patchName.includes(path));
// 是否改变导航样式
const [isScrolled, setIsScrolled] = useState(false);
// 获取分类列表
const [cateList, setCateList] = useState<Cate[]>([]);
const getCateList = async () => {
const { data } = (await getCateListAPI()) || { data: [] as Cate[] };
setCateList(data);
};
useEffect(() => {
// 监听系统主题变化
const mediaQuery = matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e: MediaQueryListEvent) => {
setIsDark(e.matches);
});
getConfigData();
getCateList();
const handleScroll = () => {
setIsScrolled(window.scrollY > 100);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// 手动切换主题
const toTheme = () => {
const html = document.querySelector('html');
if (html) {
setIsDark(html.classList.toggle('dark'));
}
};
// 判断当前主题
useEffect(() => {
const html = document.querySelector('html');
html?.classList.toggle('dark', isDark);
}, [isDark]);
const patchName = usePathname();
// 这些路径段不需要改变导航样式
const isPathSty = [
'/my',
'/wall',
'/record',
'/equipment',
'/tags',
'/resume',
'/album',
'/fishpond'
].some(path => patchName.includes(path))
// 是否改变导航样式
const [isScrolled, setIsScrolled] = useState(false);
// 是否打开侧边栏导航
const [isOpenSidebarNav, setIsOpenSidebarNav] = useState(false);
// 获取分类列表
const [cateList, setCateList] = useState<Cate[]>([])
const getCateList = async () => {
const { data } = (await getCateListAPI()) || { data: [] as Cate[] }
setCateList(data)
}
return (
<>
<div className={`header fixed top-0 w-full h-16 backdrop-blur-[5px] z-50 after:content-[''] after:block after:w-full after:h-0 after:bg-[linear-gradient(#fff,transparent_70%)] dark:after:bg-[linear-gradient(#2b333e,transparent_70%)] after: ${isPathSty || isScrolled ? 'bg-[rgba(255,255,255,0.9)] dark:bg-[rgba(44,51,62,0.9)] border-b dark:border-[#2b333e] after:!h-8 after:transition-height]' : 'border-transparent'} transition-border`}>
<div className="relative flex justify-center lg:justify-start w-full lg:w-[1500px] h-16 mx-auto">
<div className={`lg:hidden group absolute top-0 left-0 h-full py-2 px-3 pl-7 ${isPathSty || isScrolled ? 'hover:bg-[#e9edf4] dark:hover:bg-[#455162] rounded-lg' : ''} cursor-pointer `} onClick={() => setIsOpenSidebarNav(true)}>
<BsTextIndentLeft className={`group-hover:text-primary h-full text-[30px] ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'} `} />
</div>
useEffect(() => {
// 监听系统主题变化
const mediaQuery = matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener("change", (e: MediaQueryListEvent) => {
setIsDark(e.matches)
})
{/* logo */}
<Link href="/" className="flex items-center p-5 text-[15px] ">
{isDark ? <img src={theme?.dark_logo} alt="Logo" className="w-32 h-10 pr-5 hover:scale-90 transition-transform" /> : <img src={isPathSty || isScrolled ? theme?.light_logo : theme?.dark_logo} alt="Logo" className="w-32 h-10 pr-5 hover:scale-90 transition-transform" />}
</Link>
getConfigData()
getCateList()
<ul className="hidden lg:flex items-center h-16">
<li className="group/one relative">
<Link href="/" className={`flex items-center p-5 text-[15px] group-hover/one:!text-primary ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
💎
</Link>
</li>
const handleScroll = () => {
setIsScrolled(window.scrollY > 100);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// 手动切换主题
const toTheme = () => {
const html = document.querySelector('html')
setIsDark(html?.classList.toggle('dark')!)
}
// 判断当前主题
useEffect(() => {
const html = document.querySelector('html');
html?.classList.toggle('dark', isDark);
}, [isDark])
// 是否打开侧边栏导航
const [isOpenSidebarNav, setIsOpenSidebarNav] = useState(false)
return (
<>
<div className={`header fixed top-0 w-full h-16 backdrop-blur-[5px] z-50 after:content-[''] after:block after:w-full after:h-0 after:bg-[linear-gradient(#fff,transparent_70%)] dark:after:bg-[linear-gradient(#2b333e,transparent_70%)] after: ${isPathSty || isScrolled ? 'bg-[rgba(255,255,255,0.9)] dark:bg-[rgba(44,51,62,0.9)] border-b dark:border-[#2b333e] after:!h-8 after:transition-height]' : 'border-transparent'} transition-border`}>
<div className="relative flex justify-center lg:justify-start w-full lg:w-[1500px] h-16 mx-auto">
<div className={`lg:hidden group absolute top-0 left-0 h-full py-2 px-3 pl-7 ${isPathSty || isScrolled ? 'hover:bg-[#e9edf4] dark:hover:bg-[#455162] rounded-lg' : ''} cursor-pointer `} onClick={() => setIsOpenSidebarNav(true)}>
<BsTextIndentLeft className={`group-hover:text-primary h-full text-[30px] ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'} `} />
</div>
{/* logo */}
<Link href="/" className="flex items-center p-5 text-[15px] ">
{
isDark
? <img src={theme?.dark_logo} alt="Logo" className='w-32 h-10 pr-5 hover:scale-90 transition-transform' />
: <img src={isPathSty || isScrolled ? theme?.light_logo : theme?.dark_logo} alt="Logo" className='w-32 h-10 pr-5 hover:scale-90 transition-transform' />
}
{/* 文章分类 */}
{cateList?.map(
(one) =>
one.type === 'cate' && (
<li key={one.id} className="group/one relative">
<Link href={`/cate/${one.id}?name=${one.name}`} className={`flex items-center p-5 text-[15px] group-hover/one:!text-primary ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
{one.icon} {one.name}
<Show is={!!one.children.length}>
<IoIosArrowDown className="ml-2" />
</Show>
</Link>
<ul className="hidden lg:flex items-center h-16">
<li className="group/one relative" >
<Link
href="/"
className={`flex items-center p-5 text-[15px] group-hover/one:!text-primary ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}
>💎 </Link>
</li>
{/* 文章分类 */}
{cateList?.map(one => (
one.type === "cate" &&
(
<li key={one.id} className="group/one relative" >
<Link
href={`/cate/${one.id}?name=${one.name}`}
className={`flex items-center p-5 text-[15px] group-hover/one:!text-primary ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}
>
{one.icon} {one.name}
<Show is={!!one.children.length} children={(
<IoIosArrowDown className="ml-2" />
)} />
</Link>
<Show is={!!one.children.length} children={(
<ul className="hidden group-hover/one:block overflow-hidden absolute top-[50px] w-full rounded-md backdrop-blur-[5px] bg-[rgba(255,255,255,0.95)] dark:bg-[rgba(44,51,62,0.95)]" style={{ boxShadow: '0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08)' }}>
{one.children?.map(two => (
<li key={two.id} className='group/two'>
<Link href={`/cate/${two.id}?name=${two.name}`} className="relative inline-block w-full p-2.5 text-[15px] box-border text-[#666] dark:text-white hover:!text-primary transition-all after:content-[''] after:absolute after:left-2.5 after:top-1/2 after:-translate-y-1/2 after:w-0 after:h-[3px] after:bg-primary after:transition-width group-hover/two:bg-[#f2f2f2] dark:group-hover/two:bg-[#323e50] group-hover/two:pl-8 hover:after:w-2.5">
{two.name}
</Link>
</li>
))}
</ul>
)} />
</li>
)
))}
<li className="group/one relative">
<Link href="" className={`flex items-center p-5 px-10 text-[15px] group-hover/one:!text-primary ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
🧩
<Show is={true} children={(
<IoIosArrowDown className="ml-2" />
)} />
<Show is={!!one.children.length}>
<ul className="hidden group-hover/one:block overflow-hidden absolute top-[50px] w-full rounded-md backdrop-blur-[5px] bg-[rgba(255,255,255,0.95)] dark:bg-[rgba(44,51,62,0.95)]" style={{ boxShadow: '0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08)' }}>
{one.children?.map((two) => (
<li key={two.id} className="group/two">
<Link href={`/cate/${two.id}?name=${two.name}`} className="relative inline-block w-full p-2.5 text-[15px] box-border text-[#666] dark:text-white hover:!text-primary transition-all after:content-[''] after:absolute after:left-2.5 after:top-1/2 after:-translate-y-1/2 after:w-0 after:h-[3px] after:bg-primary after:transition-width group-hover/two:bg-[#f2f2f2] dark:group-hover/two:bg-[#323e50] group-hover/two:pl-8 hover:after:w-2.5">
{two.name}
</Link>
</li>
))}
</ul>
</Show>
</li>
)
)}
<Show is={true} children={(
<ul className="hidden group-hover/one:block overflow-hidden absolute top-[50px] w-full rounded-md backdrop-blur-sm bg-[rgba(255,255,255,0.95)] dark:bg-[rgba(44,51,62,0.95)]" style={{ boxShadow: '0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08)' }}>
{cateList?.map(item => (
item.type === "nav" &&
(
<li key={item.id} className="group/two relative" >
<Link
href={`${item.url}`}
className={`relative inline-block w-full p-2.5 pl-5 text-[15px] box-border text-[#666] dark:text-white hover:!text-primary transition-all after:content-[''] after:absolute after:left-2.5 after:top-1/2 after:-translate-y-1/2 after:w-0 after:h-[3px] after:bg-primary after:transition-width group-hover/two:pl-8 hover:after:w-2.5`}
>
{item.icon} {item.name}
</Link>
</li>
)
))}
</ul>
)} />
<li className="group/one relative">
<Link href="" className={`flex items-center p-5 px-10 text-[15px] group-hover/one:!text-primary ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
🧩
<Show is={true}>
<IoIosArrowDown className="ml-2" />
</Show>
</Link>
<Show is={true}>
<ul className="hidden group-hover/one:block overflow-hidden absolute top-[50px] w-full rounded-md backdrop-blur-sm bg-[rgba(255,255,255,0.95)] dark:bg-[rgba(44,51,62,0.95)]" style={{ boxShadow: '0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08)' }}>
{cateList?.map(
(item) =>
item.type === 'nav' && (
<li key={item.id} className="group/two relative">
<Link href={`${item.url}`} className={`relative inline-block w-full p-2.5 pl-5 text-[15px] box-border text-[#666] dark:text-white hover:!text-primary transition-all after:content-[''] after:absolute after:left-2.5 after:top-1/2 after:-translate-y-1/2 after:w-0 after:h-[3px] after:bg-primary after:transition-width group-hover/two:pl-8 hover:after:w-2.5`}>
{item.icon} {item.name}
</Link>
</li>
</ul>
)
)}
</ul>
</Show>
</li>
</ul>
{/* 主题切换开关 */}
<Switch
size="lg"
isSelected={isDark}
onValueChange={toTheme}
thumbIcon={({ isSelected }) => isSelected ? <BsFillMoonStarsFill className="text-gray-500" /> : <FaRegSun className="text-gray-500" />}
className={`absolute top-0 right-7 h-full ${isDark ? '[&>.bg-default-200]:!bg-[#4e5969]' : '[&>.bg-default-200]:!bg-[#e1e1e1]'}`}
/>
</div>
</div >
{/* 主题切换开关 */}
<Switch size="lg" isSelected={isDark} onValueChange={toTheme} thumbIcon={({ isSelected }) => (isSelected ? <BsFillMoonStarsFill className="text-gray-500" /> : <FaRegSun className="text-gray-500" />)} className={`absolute top-0 right-7 h-full ${isDark ? '[&>.bg-default-200]:!bg-[#4e5969]' : '[&>.bg-default-200]:!bg-[#e1e1e1]'}`} />
</div>
</div>
{/* 侧边导航:移动端时候显示 */}
<SidebarNav list={cateList} open={isOpenSidebarNav} onClose={() => setIsOpenSidebarNav(false)} />
</>
);
{/* 侧边导航:移动端时候显示 */}
<SidebarNav list={cateList} open={isOpenSidebarNav} onClose={() => setIsOpenSidebarNav(false)} />
</>
);
};
export default Header;
export default Header;

View File

@@ -1,7 +1,7 @@
"use client"
'use client';
import { HeroUIProvider } from "@heroui/react";
import { HeroUIProvider } from '@heroui/react';
export default ({ children }: { children: React.ReactNode }) => {
return <HeroUIProvider>{children}</HeroUIProvider>
}
return <HeroUIProvider>{children}</HeroUIProvider>;
};

View File

@@ -2,40 +2,36 @@ import React from 'react';
import './index.scss';
interface Props {
data: string[];
};
data: string[];
}
export default ({ data }: Props) => {
// 定义灯笼的位置数组
const positions = [
{ left: '10px', top: '0' },
{ left: '160px', top: '0' },
{ right: '160px', top: '0' },
{ right: '10px', top: '0' }
];
// 定义灯笼的位置数组
const positions = [
{ left: '10px', top: '0' },
{ left: '160px', top: '0' },
{ right: '160px', top: '0' },
{ right: '10px', top: '0' },
];
return (
<div className='w-full hidden md:block'>
{data.map((item, index) => (
<div
key={index}
className="lantern-box z-[999]"
style={positions[index]}
>
<div className="lantern-light">
<div className="lantern-line"></div>
<div className="lantern-circle">
<div className="lantern-rect">
<div className="lantern-text">{item}</div>
</div>
</div>
<div className="lantern-tassel-top">
<div className="lantern-tassel-middle"></div>
<div className="lantern-tassel-bottom"></div>
</div>
</div>
</div>
))}
return (
<div className="w-full hidden md:block">
{data.map((item, index) => (
<div key={index} className="lantern-box z-[999]" style={positions[index]}>
<div className="lantern-light">
<div className="lantern-line"></div>
<div className="lantern-circle">
<div className="lantern-rect">
<div className="lantern-text">{item}</div>
</div>
</div>
<div className="lantern-tassel-top">
<div className="lantern-tassel-middle"></div>
<div className="lantern-tassel-bottom"></div>
</div>
</div>
</div>
);
};
))}
</div>
);
};

View File

@@ -1,34 +1,26 @@
import "./index.scss"
import './index.scss';
export default () => {
return (
<>
<div className='LoadingComponent fixed w-full h-full z-50 bg-[rgb(255,255,255,0.5)] dark:bg-[rgb(36,41,38,0.5)] rounded-lg flex justify-center items-center'>
<svg
className="container"
x="0px"
y="0px"
viewBox="0 0 50 31.25"
height="31.25"
width="50"
preserveAspectRatio='xMidYMid meet'
>
<path
className="track"
strokeWidth="4" // Corrected here
fill="none"
pathLength="100"
d="M0.625 21.5 h10.25 l3.75 -5.875 l7.375 15 l9.75 -30 l7.375 20.875 v0 h10.25"
/>
<path
className="car"
strokeWidth="4" // Corrected here
fill="none"
pathLength="100"
d="M0.625 21.5 h10.25 l3.75 -5.875 l7.375 15 l9.75 -30 l7.375 20.875 v0 h10.25"
/>
</svg>
</div>
</>
)
}
return (
<>
<div className="LoadingComponent fixed w-full h-full z-50 bg-[rgb(255,255,255,0.5)] dark:bg-[rgb(36,41,38,0.5)] rounded-lg flex justify-center items-center">
<svg className="container" x="0px" y="0px" viewBox="0 0 50 31.25" height="31.25" width="50" preserveAspectRatio="xMidYMid meet">
<path
className="track"
strokeWidth="4"
fill="none"
pathLength="100"
d="M0.625 21.5 h10.25 l3.75 -5.875 l7.375 15 l9.75 -30 l7.375 20.875 v0 h10.25"
/>
<path
className="car"
strokeWidth="4"
fill="none"
pathLength="100"
d="M0.625 21.5 h10.25 l3.75 -5.875 l7.375 15 l9.75 -30 l7.375 20.875 v0 h10.25"
/>
</svg>
</div>
</>
);
};

View File

@@ -1,16 +1,11 @@
"use client"
'use client';
import { AppProgressBar as ProgressBar } from 'next-nprogress-bar';
export default () => {
return (
<>
<ProgressBar
height="4px"
color="#539dfd"
options={{ showSpinner: false }}
shallowRouting
/>
</>
)
}
return (
<>
<ProgressBar height="4px" color="#539dfd" options={{ showSpinner: false }} shallowRouting />
</>
);
};

View File

@@ -1,37 +1,31 @@
"use client"
'use client';
import { useRouter } from "next/navigation"
import { Pagination } from "@heroui/react"
import { useRouter } from 'next/navigation';
import { Pagination } from '@heroui/react';
interface Props {
total: number,
page: number,
size?: number,
path?: string,
className?: string
total: number;
page: number;
size?: number;
path?: string;
className?: string;
}
export default ({ total, page, path, className }: Props) => {
const router = useRouter()
const router = useRouter();
const onChange = (page: number) => {
router.push(path ? `${path}&page=${page}` : `?page=${page}`)
const onChange = (page: number) => {
router.push(path ? `${path}&page=${page}` : `?page=${page}`);
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};
return (
<div className={className}>
<Pagination
showControls
total={total}
page={+page}
onChange={onChange}
classNames={{ item: "shadow-none bg-transparent dark:hover:!bg-black-b ", prev: "dark:bg-black-b ", next: "dark:bg-black-b " }}
/>
</div>
)
}
return (
<div className={className}>
<Pagination showControls total={total} page={+page} onChange={onChange} classNames={{ item: 'shadow-none bg-transparent dark:hover:!bg-black-b ', prev: 'dark:bg-black-b ', next: 'dark:bg-black-b ' }} />
</div>
);
};

View File

@@ -5,17 +5,17 @@ import { createAvatar } from '@dicebear/core';
import { pixelArt } from '@dicebear/collection';
export default ({ className }: { className: string }) => {
const avatar = useMemo(() => {
// 生成一个随机种子
const seed = Math.random().toString(36).substring(2, 15);
const avatar = useMemo(() => {
// 生成一个随机种子
const seed = Math.random().toString(36).substring(2, 15);
// 创建头像
return createAvatar(pixelArt, {
seed: seed, // 使用随机种子
size: 128,
// 其他选项
}).toDataUri();
}, []);
// 创建头像
return createAvatar(pixelArt, {
seed: seed, // 使用随机种子
size: 128,
// 其他选项
}).toDataUri();
}, []);
return <img src={avatar} alt="Avatar" className={className}/>
}
return <img src={avatar} alt="Avatar" className={className} />;
};

View File

@@ -1,25 +1,24 @@
import "./index.scss"
import './index.scss';
export default () => {
return (
<>
<div className="rippleComponent">
<div className="ripple">
<svg className="waves" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shapeRendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z"></path>
</defs>
return (
<>
<div className="rippleComponent">
<div className="ripple">
<svg className="waves" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 24 150 28" preserveAspectRatio="none" shapeRendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z"></path>
</defs>
<g className="parallax">
<use xlinkHref="#gentle-wave" x="48" y="0" className='fill-[rgba(249,249,249,0.7)] dark:fill-[rgba(35,41,49,0.9)]'></use>
<use xlinkHref="#gentle-wave" x="48" y="3" className='fill-[rgba(249,249,249,0.5)] dark:fill-[rgba(35,41,49,0.9)]'></use>
<use xlinkHref="#gentle-wave" x="48" y="5" className='fill-[rgba(249,249,249,0.3)] dark:fill-[rgba(35,41,49,0.9)]'></use>
<use xlinkHref="#gentle-wave" x="48" y="7" className='fill-[rgba(249,249,249)] dark:fill-[rgba(35,41,49,0.9)]'></use>
</g>
</svg>
</div>
</div>
</>
)
}
<g className="parallax">
<use xlinkHref="#gentle-wave" x="48" y="0" className="fill-[rgba(249,249,249,0.7)] dark:fill-[rgba(35,41,49,0.9)]"></use>
<use xlinkHref="#gentle-wave" x="48" y="3" className="fill-[rgba(249,249,249,0.5)] dark:fill-[rgba(35,41,49,0.9)]"></use>
<use xlinkHref="#gentle-wave" x="48" y="5" className="fill-[rgba(249,249,249,0.3)] dark:fill-[rgba(35,41,49,0.9)]"></use>
<use xlinkHref="#gentle-wave" x="48" y="7" className="fill-[rgba(249,249,249)] dark:fill-[rgba(35,41,49,0.9)]"></use>
</g>
</svg>
</div>
</div>
</>
);
};

View File

@@ -1,28 +1,25 @@
"use client"
'use client';
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
// 监听路由变化
const RouteChangeHandler: React.FC = () => {
const pathname = usePathname();
const pathname = usePathname();
// 每次切换页面滚动到顶部
useEffect(() => {
// 尊重开源,禁止删除此版权信息!!!
console.log(`%c 博客系统 %c ThriveX `,
'background: #35495e; padding: 4px; border-radius: 3px 0 0 3px; color: #fff',
'background: #539dfd; padding: 4px; border-radius: 0 3px 3px 0; color: #fff',
);
console.log("🚀 欢迎使用 ThriveX 现代化博客管理系统")
console.log("🎉 开源地址https://github.com/LiuYuYang01/ThriveX-Blog")
console.log("🏕 作者主页https://liuyuyang.net")
console.log("🌟 觉得好用的话记得点个 Star 哦 🙏")
// 每次切换页面滚动到顶部
useEffect(() => {
// 尊重开源,禁止删除此版权信息!!!
console.log(`%c 博客系统 %c ThriveX `, 'background: #35495e; padding: 4px; border-radius: 3px 0 0 3px; color: #fff', 'background: #539dfd; padding: 4px; border-radius: 0 3px 3px 0; color: #fff');
console.log('🚀 欢迎使用 ThriveX 现代化博客管理系统');
console.log('🎉 开源地址https://github.com/LiuYuYang01/ThriveX-Blog');
console.log('🏕 作者主页https://liuyuyang.net');
console.log('🌟 觉得好用的话记得点个 Star 哦 🙏');
window.scrollTo(0, 0);
}, [pathname]);
window.scrollTo(0, 0);
}, [pathname]);
return null;
return null;
};
export default RouteChangeHandler;

View File

@@ -1,88 +1,81 @@
"use client"
'use client';
import { useState } from "react"
import Link from "next/link"
import { Modal, ModalContent, ModalHeader, ModalBody, UseDisclosureProps, Input } from "@heroui/react"
import { getArticlePagingAPI } from '@/api/article'
import { Article } from "@/types/app/article"
import useDebounce from "@/hooks/useDebounce"
import Empty from "../Empty"
import { useState } from 'react';
import Link from 'next/link';
import { Modal, ModalContent, ModalHeader, ModalBody, UseDisclosureProps, Input } from '@heroui/react';
import { getArticlePagingAPI } from '@/api/article';
import { Article } from '@/types/app/article';
import useDebounce from '@/hooks/useDebounce';
import Empty from '../Empty';
interface Props {
disclosure: UseDisclosureProps & { onOpenChange: () => void }
disclosure: UseDisclosureProps & { onOpenChange: () => void };
}
export default ({ disclosure }: Props) => {
const { isOpen, onClose, onOpenChange } = disclosure;
const { isOpen, onOpenChange } = disclosure;
const [data, setData] = useState<Paginate<Article[]>>()
const [data, setData] = useState<Paginate<Article[]>>();
// 获取文章数据
const getArticleList = async (key: string) => {
if (key.trim().length === 0) {
setData(undefined)
return
}
const { data } = (await getArticlePagingAPI({
query: { key },
pagination: { page: 1 }
})) || { data: {} as Paginate<Article[]> }
setData(data)
// 获取文章数据
const getArticleList = async (key: string) => {
if (key.trim().length === 0) {
setData(undefined);
return;
}
// 使用自定义防抖函数
const debouncedFetchArticles = useDebounce(getArticleList, 300);
const { data } = (await getArticlePagingAPI({
query: { key },
pagination: { page: 1 },
})) || { data: {} as Paginate<Article[]> };
// 根据关键词搜索文章
const onSearchArticle = (e: React.ChangeEvent<HTMLInputElement>) => {
let key = e.target.value
debouncedFetchArticles(key)
}
setData(data);
};
return (
<>
<Modal
size="lg"
backdrop="opaque"
isOpen={isOpen}
onOpenChange={onOpenChange}
classNames={{
backdrop: "bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20"
}}
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1"></ModalHeader>
// 使用自定义防抖函数
const debouncedFetchArticles = useDebounce(getArticleList, 300);
<ModalBody>
<div className="mb-7">
<Input type="text" placeholder="请输入文章关键词" onChange={onSearchArticle} />
// 根据关键词搜索文章
const onSearchArticle = (e: React.ChangeEvent<HTMLInputElement>) => {
const key = e.target.value;
debouncedFetchArticles(key);
};
<div className="mt-4">
{data?.result ? (
data?.result?.map(item => (
<Link
key={item.id}
href={`/article/${item.id}`}
className="inline-block w-full py-2 px-4 mb-1 text-gray-700 dark:text-[#8c9ab1] hover:!text-primary hover:bg-[#f0f7ff] dark:hover:bg-[#25282d] hover:pl-8 rounded-md transition-[padding]"
onClick={onClose}
>
{item.title}
</Link>
))
) : (
data && <Empty info="暂无文章" />
)}
</div>
</div>
</ModalBody>
</>
)}
</ModalContent>
</Modal>
</>
)
}
return (
<>
<Modal
size="lg"
backdrop="opaque"
isOpen={isOpen}
onOpenChange={onOpenChange}
classNames={{
backdrop: 'bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20',
}}
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1"></ModalHeader>
<ModalBody>
<div className="mb-7">
<Input type="text" placeholder="请输入文章关键词" onChange={onSearchArticle} />
<div className="mt-4">
{data?.result
? data?.result?.map((item) => (
<Link key={item.id} href={`/article/${item.id}`} className="inline-block w-full py-2 px-4 mb-1 text-gray-700 dark:text-[#8c9ab1] hover:!text-primary hover:bg-[#f0f7ff] dark:hover:bg-[#25282d] hover:pl-8 rounded-md transition-[padding]" onClick={onClose}>
{item.title}
</Link>
))
: data && <Empty info="暂无文章" />}
</div>
</div>
</ModalBody>
</>
)}
</ModalContent>
</Modal>
</>
);
};

View File

@@ -1,11 +1,5 @@
import { ReactNode } from "react"
import { ReactNode } from 'react';
export default ({ is, children }: { is: boolean, children: ReactNode }) => {
return (
<>
{
is ? children : null
}
</>
)
}
export default ({ is, children }: { is: boolean; children: ReactNode }) => {
return <>{is ? children : null}</>;
};

View File

@@ -1,70 +1,75 @@
import Image from 'next/image';
import avatarBg from '@/assets/image/avatar_bg.jpg';
import CSDN from '@/assets/svg/socializing/CSDN.svg'
import Douyin from '@/assets/svg/socializing/Douyin.svg'
import GitHub from '@/assets/svg/socializing/GitHub.svg'
import Gitee from '@/assets/svg/socializing/Gitee.svg'
import Juejin from '@/assets/svg/socializing/Juejin.svg'
import QQ from '@/assets/svg/socializing/QQ.svg'
import Weixin from '@/assets/svg/socializing/Weixin.svg'
import CSDN from '@/assets/svg/socializing/CSDN.svg';
import Douyin from '@/assets/svg/socializing/Douyin.svg';
import GitHub from '@/assets/svg/socializing/GitHub.svg';
import Gitee from '@/assets/svg/socializing/Gitee.svg';
import Juejin from '@/assets/svg/socializing/Juejin.svg';
import QQ from '@/assets/svg/socializing/QQ.svg';
import Weixin from '@/assets/svg/socializing/Weixin.svg';
import { getUserDataAPI } from '@/api/user';
import { getWebConfigDataAPI } from '@/api/config'
import { getWebConfigDataAPI } from '@/api/config';
import { User } from '@/types/app/user';
import { Social, Theme } from '@/types/app/config';
const Author = async () => {
const { data: user } = await getUserDataAPI() || { data: {} as User }
const { data: { value: theme } } = (await getWebConfigDataAPI<{ value: Theme }>("theme")) || { data: { value: {} as Theme } };
const { data: user } = (await getUserDataAPI()) || { data: {} as User };
const {
data: { value: theme },
} = (await getWebConfigDataAPI<{ value: Theme }>('theme')) || { data: { value: {} as Theme } };
const socialList = theme?.social || []
const socialList = theme?.social || [];
// 图标列表
const images: { [string: string]: string } = {
"CSDN": CSDN,
"Douyin": Douyin,
"GitHub": GitHub,
"Gitee": Gitee,
"Juejin": Juejin,
"QQ": QQ,
"Weixin": Weixin,
}
// 图标列表
const images: { [string: string]: string } = {
CSDN: CSDN,
Douyin: Douyin,
GitHub: GitHub,
Gitee: Gitee,
Juejin: Juejin,
QQ: QQ,
Weixin: Weixin,
};
const getIcon = (name: string) => images[name];
const getIcon = (name: string) => images[name];
return (
<div className="flex flex-col items-center pt-16 bg-no-repeat bg-white dark:bg-black-b w-full h-[350px] mb-5 tw_container" style={{
backgroundSize: `100% 35%`,
backgroundImage: `url(${avatarBg.src})`
}}>
{/* 作者头像 */}
<div className="avatar flex justify-center items-center w-[90px] h-[90px] rounded-full bg-white shadow-md overflow-hidden">
<img src={user?.avatar} alt="" className="w-[90%] h-[90%] rounded-full transition-transform hover:scale-110" />
</div>
return (
<div
className="flex flex-col items-center pt-16 bg-no-repeat bg-white dark:bg-black-b w-full h-[350px] mb-5 tw_container"
style={{
backgroundSize: `100% 35%`,
backgroundImage: `url(${avatarBg.src})`,
}}
>
{/* 作者头像 */}
<div className="avatar flex justify-center items-center w-[90px] h-[90px] rounded-full bg-white shadow-md overflow-hidden">
<img src={user?.avatar} alt="" className="w-[90%] h-[90%] rounded-full transition-transform hover:scale-110" />
</div>
{/* 作者介绍 */}
<div className="info text-center mt-4">
<h3 className="text-lg text-[#333] dark:text-white">{user?.name}</h3>
<p className="w-[90%] mx-auto mt-2 text-sm text-[#686868] dark:text-[#cecece]">{user?.info}</p>
</div>
{/* 作者介绍 */}
<div className="info text-center mt-4">
<h3 className="text-lg text-[#333] dark:text-white">{user?.name}</h3>
<p className="w-[90%] mx-auto mt-2 text-sm text-[#686868] dark:text-[#cecece]">{user?.info}</p>
</div>
{/* 社交账号 */}
<div className="socializing w-full pt-8">
<div className="title relative w-full h-[1px] bg-[#eee] dark:bg-black-a">
<span className="absolute top-[-10px] left-1/2 transform -translate-x-1/2 w-[110px] bg-white dark:bg-black-b text-center text-sm text-[#666] dark:text-[#979797] "></span>
</div>
<div className="list flex justify-evenly w-[70%] mx-auto pt-6">
{socialList?.map((item: Social, index: number) => (
<a key={index} href={item?.url} target="_blank" rel="noopener noreferrer">
<Image src={getIcon(item?.name)} alt={item?.name} title={item?.name} className="w-[23px] h-[23px]" />
</a>
))}
</div>
</div>
{/* 社交账号 */}
<div className="socializing w-full pt-8">
<div className="title relative w-full h-[1px] bg-[#eee] dark:bg-black-a">
<span className="absolute top-[-10px] left-1/2 transform -translate-x-1/2 w-[110px] bg-white dark:bg-black-b text-center text-sm text-[#666] dark:text-[#979797] "></span>
</div>
);
<div className="list flex justify-evenly w-[70%] mx-auto pt-6">
{socialList?.map((item: Social, index: number) => (
<a key={index} href={item?.url} target="_blank" rel="noopener noreferrer">
<Image src={getIcon(item?.name)} alt={item?.name} title={item?.name} className="w-[23px] h-[23px]" />
</a>
))}
</div>
</div>
</div>
);
};
export default Author;
export default Author;

View File

@@ -1,4 +1,4 @@
"use client"
'use client';
import Image from 'next/image';
import Link from 'next/link';
@@ -6,48 +6,41 @@ import { useEffect, useState } from 'react';
import { getCommentPagingAPI } from '@/api/comment';
import NewComment from '@/assets/svg/other/comments.svg';
import RandomAvatar from '@/components/RandomAvatar';
import { Comment } from '@/types/app/comment'
import { Comment } from '@/types/app/comment';
import dayjs from 'dayjs';
const NewComments = () => {
const [list, setList] = useState<Comment[]>([])
const [list, setList] = useState<Comment[]>([]);
const getCommentPaging = async () => {
const { data } = await getCommentPagingAPI() || { data: {} as Paginate<Comment[]> }
setList(data.result)
}
const getCommentPaging = async () => {
const { data } = (await getCommentPagingAPI()) || { data: {} as Paginate<Comment[]> };
setList(data.result);
};
useEffect(() => {
getCommentPaging()
}, [])
useEffect(() => {
getCommentPaging();
}, []);
return (
<div className="flex flex-col tw_container bg-white dark:bg-black-b p-4 mb-5 tw_title">
<div className="tw_title w-full dark:text-white">
<Image src={NewComment} alt="最新评论" width={33} height={23} />
return (
<div className="flex flex-col tw_container bg-white dark:bg-black-b p-4 mb-5 tw_title">
<div className="tw_title w-full dark:text-white">
<Image src={NewComment} alt="最新评论" width={33} height={23} />
</div>
<div className="mt-2.5">
{list?.map((item) => (
<Link href={`/article/${item.articleId}`} target="_blank" className="group flex items-center py-2.5 border-b dark:border-b-black-b last:border-b-0" key={item.id}>
{item.avatar ? <img src={item.avatar} className="w-11 h-11 rounded-full mr-2.5 transition-transform hover:scale-110" alt="avatar" /> : <RandomAvatar className="w-11 h-11 rounded-full mr-2.5 transition-transform hover:scale-110" />}
<div className="flex flex-col justify-center">
<div className="w-48 text-sm text-gray-600 dark:text-[#8c9ab1] group-hover:text-primary overflow-hidden line-clamp-2">{item.content}</div>
<div className="pt-2.5 text-xs text-gray-400">{dayjs(+item.createTime!).format('YYYY-MM-DD HH:mm')}</div>
</div>
<div className="mt-2.5">
{list?.map((item) => (
<Link href={`/article/${item.articleId}`} target='_blank' className="group flex items-center py-2.5 border-b dark:border-b-black-b last:border-b-0" key={item.id}>
{item.avatar
? <img src={item.avatar} className="w-11 h-11 rounded-full mr-2.5 transition-transform hover:scale-110" alt="avatar" />
: <RandomAvatar className='w-11 h-11 rounded-full mr-2.5 transition-transform hover:scale-110' />
}
<div className="flex flex-col justify-center">
<div className="w-48 text-sm text-gray-600 dark:text-[#8c9ab1] group-hover:text-primary overflow-hidden line-clamp-2">
{item.content}
</div>
<div className="pt-2.5 text-xs text-gray-400">
{dayjs(+item.createTime!).format('YYYY-MM-DD HH:mm')}
</div>
</div>
</Link>
))}
</div>
</div>
);
</Link>
))}
</div>
</div>
);
};
export default NewComments;
export default NewComments;

View File

@@ -1,41 +1,43 @@
import Link from 'next/link';
import Image from 'next/image';
import { getWebConfigDataAPI } from '@/api/config'
import { getWebConfigDataAPI } from '@/api/config';
import { getArticleListAPI } from '@/api/article';
import { IoIosArrowForward } from "react-icons/io";
import { IoIosArrowForward } from 'react-icons/io';
import fire from '@/assets/svg/other/fire.svg';
import { Theme } from '@/types/app/config';
import { Article } from '@/types/app/article';
const RandomArticle = async () => {
const { data: { value: theme } } = (await getWebConfigDataAPI<{ value: Theme }>("theme")) || { data: { value: {} as Theme } };
const { data: article } = await getArticleListAPI() || { data: [] as Article[] }
const {
data: { value: theme },
} = (await getWebConfigDataAPI<{ value: Theme }>('theme')) || { data: { value: {} as Theme } };
const { data: article } = (await getArticleListAPI()) || { data: [] as Article[] };
const ids = theme.reco_article || []
const list = article?.filter((item: Article) => ids.includes(item.id as number))
const ids = theme.reco_article || [];
const list = article?.filter((item: Article) => ids.includes(item.id as number));
return (
<div className='hotArticleComponent'>
<div className="flex flex-col tw_container bg-white dark:bg-black-b p-4 mb-5 tw_title">
<div className="tw_title w-full dark:text-white">
<Image src={fire} alt='作者推荐' width={30} height={20} />
<span> </span>
</div>
{/* 文章列表 */}
<div className='w-full'>
{list?.map((item: Article) => (
<div key={item.id}>
<Link href={`/article/${item.id}`} target='_blank' className='w-full flex items-center py-2 text-gray-600 dark:text-[#8c9ab1] text-sm transition-[padding] hover:!text-primary hover:pl-2'>
<IoIosArrowForward className="text-lg mr-1" />
<span className='w-full line-clamp-1'>{item.title}</span>
</Link>
</div>
))}
</div>
</div>
return (
<div className="hotArticleComponent">
<div className="flex flex-col tw_container bg-white dark:bg-black-b p-4 mb-5 tw_title">
<div className="tw_title w-full dark:text-white">
<Image src={fire} alt="作者推荐" width={30} height={20} />
<span> </span>
</div>
);
{/* 文章列表 */}
<div className="w-full">
{list?.map((item: Article) => (
<div key={item.id}>
<Link href={`/article/${item.id}`} target="_blank" className="w-full flex items-center py-2 text-gray-600 dark:text-[#8c9ab1] text-sm transition-[padding] hover:!text-primary hover:pl-2">
<IoIosArrowForward className="text-lg mr-1" />
<span className="w-full line-clamp-1">{item.title}</span>
</Link>
</div>
))}
</div>
</div>
</div>
);
};
export default RandomArticle;
export default RandomArticle;

View File

@@ -1,55 +1,51 @@
"use client"
'use client';
import Link from 'next/link';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { getRandomArticleListAPI } from '@/api/article';
import { useConfigStore } from '@/stores'
import { useConfigStore } from '@/stores';
import { Article } from '@/types/app/article';
import { getRandom } from '@/utils';
import RandomArticle from '@/assets/svg/other/article.svg'
import "./index.scss"
import RandomArticle from '@/assets/svg/other/article.svg';
import './index.scss';
const HotArticle = () => {
const { theme } = useConfigStore()
const covers = theme.covers || []
const { theme } = useConfigStore();
const covers = theme.covers || [];
const [list, setList] = useState<Article[]>([])
const [list, setList] = useState<Article[]>([]);
const getRandomArticleList = async () => {
const { data } = await getRandomArticleListAPI() || { data: [] as Article[] }
setList(data)
}
const getRandomArticleList = async () => {
const { data } = (await getRandomArticleListAPI()) || { data: [] as Article[] };
setList(data);
};
useEffect(() => {
getRandomArticleList()
}, [])
useEffect(() => {
getRandomArticleList();
}, []);
return (
<div className='RandomArticleComponent'>
<div className="flex flex-col p-4 mb-5 bg-white dark:bg-black-b tw_container tw_title">
<h3 className="w-full tw_title dark:text-white">
<Image src={RandomArticle} alt="随机推荐" />
</h3>
return (
<div className="RandomArticleComponent">
<div className="flex flex-col p-4 mb-5 bg-white dark:bg-black-b tw_container tw_title">
<h3 className="w-full tw_title dark:text-white">
<Image src={RandomArticle} alt="随机推荐" />
</h3>
<div className="w-full pt-2.5 mt-2 min-h-[120px] space-y-4">
{list?.map((item, index) => (
<div
key={index}
className="item relative h-32 bg-no-repeat bg-center rounded-md transition-all after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-full after:h-12 after:transition-opacity after:rounded-md after:bg-[linear-gradient(transparent,#000)]"
style={{ backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})` }}
>
<Link href={`/article/${item.id}`} target='_blank' className='inline-block w-full h-full'>
<h4 className=' absolute bottom-2.5 w-[95%] px-2.5 text-white text-[15px] font-normal line-clamp-1 z-10'>{item.title}</h4>
</Link>
<div className="w-full pt-2.5 mt-2 min-h-[120px] space-y-4">
{list?.map((item, index) => (
<div key={index} className="item relative h-32 bg-no-repeat bg-center rounded-md transition-all after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-full after:h-12 after:transition-opacity after:rounded-md after:bg-[linear-gradient(transparent,#000)]" style={{ backgroundImage: `url(${item.cover || covers[getRandom(0, covers.length - 1)]})` }}>
<Link href={`/article/${item.id}`} target="_blank" className="inline-block w-full h-full">
<h4 className=" absolute bottom-2.5 w-[95%] px-2.5 text-white text-[15px] font-normal line-clamp-1 z-10">{item.title}</h4>
</Link>
<span className='ranking absolute top-2.5 left-[-16px] w-[30px] h-[25px] pl-[7px] text-white rounded-tr-full rounded-br-full font-black box-border after:content-[""] after:absolute after:bottom-[-5px] after:left-0 after:w-0 after:h-0 after:border-[5px] after:border-solid'>{index + 1}</span>
</div>
))}
</div>
<span className='ranking absolute top-2.5 left-[-16px] w-[30px] h-[25px] pl-[7px] text-white rounded-tr-full rounded-br-full font-black box-border after:content-[""] after:absolute after:bottom-[-5px] after:left-0 after:w-0 after:h-0 after:border-[5px] after:border-solid'>{index + 1}</span>
</div>
))}
</div>
);
</div>
</div>
);
};
export default HotArticle;
export default HotArticle;

View File

@@ -1,4 +1,4 @@
"use client"
'use client';
import { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
@@ -6,7 +6,7 @@ import { useConfigStore } from '@/stores';
import { motion, useMotionValue, useTransform, animate, useInView } from 'framer-motion';
import Timer from '@/assets/svg/other/timer.svg';
const AnimatedNumber = ({ value, suffix, onComplete }: { value: number, suffix: string, onComplete?: () => void }) => {
const AnimatedNumber = ({ value, suffix, onComplete }: { value: number; suffix: string; onComplete?: () => void }) => {
const ref = useRef(null);
const isInView = useInView(ref);
const count = useMotionValue(0);
@@ -16,10 +16,10 @@ const AnimatedNumber = ({ value, suffix, onComplete }: { value: number, suffix:
if (isInView) {
const animation = animate(count, value, {
duration: 2,
ease: "easeOut",
ease: 'easeOut',
onComplete: () => {
onComplete?.();
}
},
});
return animation.stop;
}
@@ -40,11 +40,11 @@ export default () => {
const calculateTimeDifference = (startTimestamp: number) => {
const startDate = new Date(+startTimestamp);
const currentDate = new Date();
let years = currentDate.getFullYear() - startDate.getFullYear();
let months = currentDate.getMonth() - startDate.getMonth();
let days = currentDate.getDate() - startDate.getDate();
if (days < 0) {
const lastMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 0);
days += lastMonth.getDate();
@@ -76,17 +76,9 @@ export default () => {
<div className="mt-2.5">
{!showDetailed ? (
<AnimatedNumber
value={timeDiff.totalDays}
suffix="天"
onComplete={handleTotalDaysComplete}
/>
<AnimatedNumber value={timeDiff.totalDays} suffix="天" onComplete={handleTotalDaysComplete} />
) : (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }}>
<AnimatedNumber value={timeDiff.years} suffix="年 " />
<AnimatedNumber value={timeDiff.months} suffix="个月 " />
<AnimatedNumber value={timeDiff.days} suffix="天" />
@@ -94,5 +86,5 @@ export default () => {
)}
</div>
</div>
)
}
);
};

Some files were not shown because too many files have changed in this diff Show More