refactor(components, config): 重构配置管理和组件导入以提升一致性和性能

- 更新多个组件以使用新的 AppConfigProvider,简化配置获取逻辑。
- 移除不再使用的 InjectData 组件,整合配置管理。
- 优化主题和其他配置的获取方式,提升组件性能和可维护性。
- 更新相关组件以适应新的配置结构,确保功能正常。
This commit is contained in:
刘宇阳
2026-07-07 17:21:04 +08:00
parent c4c0f9543d
commit bb96c54f04
31 changed files with 184 additions and 198 deletions

View File

@@ -9,7 +9,7 @@ import HCaptchaType from '@hcaptcha/react-hcaptcha';
import List from './components/List';
import HCaptcha from '@/components/HCaptcha';
import EmojiBag from '@/components/EmojiBag';
import { useConfigStore } from '@/stores';
import { useAppConfig } from '@/components/AppConfigProvider';
import 'react-toastify/dist/ReactToastify.css';
import './index.scss';
@@ -52,9 +52,8 @@ const CommentForm = ({ articleId }: Props) => {
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [captchaError, setCaptchaError] = useState<string>('');
// 获取HCaptcha配置
const config = useConfigStore();
const hasHCaptcha = !!config?.other?.hcaptcha_key;
const { other } = useAppConfig();
const hasHCaptcha = !!other?.hcaptcha_key;
const {
register,

View File

@@ -1,10 +1,10 @@
'use client';
import { useAuthorStore } from '@/stores';
import { useAppConfig } from '@/components/AppConfigProvider';
import { FiShield, FiUser } from 'react-icons/fi';
const Copyright = () => {
const author = useAuthorStore((state) => state.author);
const { author } = useAppConfig();
// 增加一个 fallback防止未加载时出现空隙
const authorName = author?.name || '匿名作者';

View File

@@ -4,13 +4,13 @@ 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 { useAppConfig } from '@/components/AppConfigProvider';
import { Article } from '@/types/app/article';
import { getRandomImage } from '@/utils';
import RandomArticleSvg from '@/assets/svg/other/article.svg';
const RandomArticle = () => {
const { theme } = useConfigStore();
const { theme } = useAppConfig();
const [list, setList] = useState<Article[]>([]);

View File

@@ -17,7 +17,7 @@ import { addWebDataAPI, getWebTypeListAPI } from '@/api/web';
import { Bounce, toast, ToastOptions } from 'react-toastify';
import HCaptchaType from '@hcaptcha/react-hcaptcha';
import HCaptcha from '@/components/HCaptcha';
import { useConfigStore } from '@/stores';
import { useAppConfig } from '@/components/AppConfigProvider';
import 'react-toastify/dist/ReactToastify.css';
const toastConfig: ToastOptions = {
@@ -40,8 +40,8 @@ export default () => {
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [captchaError, setCaptchaError] = useState<string>('');
const config = useConfigStore();
const hasHCaptcha = !!config?.other?.hcaptcha_key;
const { other } = useAppConfig();
const hasHCaptcha = !!other?.hcaptcha_key;
const [typeList, setTypeList] = useState<WebType[]>([]);
const getWebTypeList = async () => {

View File

@@ -5,7 +5,7 @@ import Link from 'next/link';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { Web } from '@/types/app/web';
import { useConfigStore, useAuthorStore } from '@/stores';
import { useAppConfig } from '@/components/AppConfigProvider';
import ApplyForAdd from './components/ApplyForAdd';
// 默认头像
@@ -223,8 +223,7 @@ const FriendCard = ({ item, type, index }: { item: Web; type: string; index: num
};
export default ({ data }: { data: { [string: string]: { order: number; list: Web[] } } }) => {
const web = useConfigStore((state) => state.web);
const author = useAuthorStore((state) => state.author);
const { web, author } = useAppConfig();
return (
<>

View File

@@ -10,8 +10,7 @@ import Tools from '@/components/Tools';
import Confetti from '@/components/Confetti';
import RouteChangeHandler from '@/components/RouteChangeHandler';
import { getWebConfigDataAPI } from '@/api/config';
import { Web } from '@/types/app/config';
import { getAppConfigCacheAPI, getWebConfigCacheAPI } from '@/lib/config';
// 加载样式文件
import '@/styles/tailwind.css';
@@ -19,7 +18,7 @@ import '@/styles/global.scss';
import '@/styles/index.scss';
import BaiduStatis from '@/components/BaiduStatis';
import FloatingBlock from '@/components/FloatingBlock';
import InjectData from '@/components/InjectData';
import AppConfigProvider from '@/components/AppConfigProvider';
// 加载本地字体
const LXGWWenKai = localFont({
@@ -29,8 +28,7 @@ const LXGWWenKai = localFont({
// 生成动态metadata
export async function generateMetadata(): Promise<Metadata> {
const response = await getWebConfigDataAPI<{ value: Web }>('web');
const data = response?.data?.value as Web;
const data = await getWebConfigCacheAPI();
return {
title: {
@@ -93,8 +91,7 @@ export async function generateMetadata(): Promise<Metadata> {
}
export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
const response = await getWebConfigDataAPI<{ value: Web }>('web');
const data = response?.data?.value as Web;
const { web: data, theme, other, author } = await getAppConfigCacheAPI();
return (
<html lang="zh-CN" className={LXGWWenKai.className}>
@@ -108,33 +105,33 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
</>
)}
{/* 百度统计 */}
<BaiduStatis />
<BaiduStatis other={other} />
</head>
{/* 监听路由变化 */}
<RouteChangeHandler />
<body id="root" className={`dark:bg-black-a!`}>
{/* 数据注入 */}
<InjectData />
{/* 🎉 礼花效果 */}
{/* <Confetti /> */}
<AppConfigProvider web={data} theme={theme} other={other} author={author}>
{/* 🎉 礼花效果 */}
{/* <Confetti /> */}
{/* 进度条组件 */}
<NProgress />
{/* 顶部导航组件 */}
<Header />
{/* 进度条组件 */}
<NProgress />
{/* 顶部导航组件 */}
<Header theme={theme} />
{/* 主体内容 */}
<div className="min-h-[calc(100vh-300px)]">{children}</div>
{/* 主体内容 */}
<div className="min-h-[calc(100vh-300px)]">{children}</div>
{/* 底部组件 */}
<Footer />
{/* 右侧工具栏组件 */}
{/* <Tools /> */}
{/* 底部组件 */}
<Footer />
{/* 右侧工具栏组件 */}
{/* <Tools /> */}
{/* 悬浮块 */}
<FloatingBlock />
{/* 悬浮块 */}
<FloatingBlock />
</AppConfigProvider>
</body>
</html>
);

View File

@@ -5,8 +5,7 @@ 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 { getThemeConfigCacheAPI, getThemeCoversCacheAPI } from '@/lib/theme';
interface Props {
searchParams: Promise<{ page: number }>;
@@ -15,18 +14,17 @@ interface Props {
export default async (props: Props) => {
const searchParams = await props.searchParams;
const page = searchParams.page ?? 1;
const response = await getWebConfigDataAPI<{ value: Theme }>('theme');
const data = response?.data?.value as Theme;
const [theme, covers] = await Promise.all([getThemeConfigCacheAPI(), getThemeCoversCacheAPI()]);
return (
<>
{/* <Lantern data={['新', '春', '快', '乐']} /> */}
<Slide src={data?.swiper_image}>
<Slide src={theme?.swiper_image} covers={covers}>
{/* 星空背景组件 */}
<Starry />
{/* 打字机组件 */}
<Typed className="absolute top-[45%] sm:top-[40%] left-[50%] transform -translate-x-1/2 w-[80%] text-center text-white xs:text-xl sm:text-[30px] leading-7 sm:leading-[40px] md:leading-[50px] custom_text_shadow"></Typed>
<Typed swiperText={theme?.swiper_text} className="absolute top-[45%] sm:top-[40%] left-[50%] transform -translate-x-1/2 w-[80%] text-center text-white xs:text-xl sm:text-[30px] leading-7 sm:leading-[40px] md:leading-[50px] custom_text_shadow"></Typed>
</Slide>
<Container>

View File

@@ -12,7 +12,7 @@ import HCaptcha from '@/components/HCaptcha';
import Show from '@/components/Show';
import { addRecordCommentDataAPI, getRecordCommentListAPI } from '@/api/recordComment';
import { RecordComment } from '@/types/app/recordComment';
import { useConfigStore } from '@/stores';
import { useAppConfig } from '@/components/AppConfigProvider';
interface Props {
recordId: number;
@@ -50,8 +50,8 @@ export default function RecordCommentPanel({ recordId, onCountChange }: Props) {
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [captchaError, setCaptchaError] = useState('');
const config = useConfigStore();
const hasHCaptcha = !!config?.other?.hcaptcha_key;
const { other } = useAppConfig();
const hasHCaptcha = !!other?.hcaptcha_key;
const methods = useForm<CommentForm>({});
const { setValue, setFocus, reset, handleSubmit } = methods;

View File

@@ -3,6 +3,7 @@ import Slide from '@/components/Slide';
import Classics from '@/components/ArticleLayout/Classics';
import Pagination from '@/components/Pagination';
import { getTagArticleListAPI } from '@/api/tag';
import { getThemeCoversCacheAPI } from '@/lib/theme';
interface Props {
params: Promise<{ id: number }>;
@@ -16,7 +17,10 @@ export default async (props: Props) => {
const page = searchParams.page ?? 1;
const name = searchParams.name;
const { data } = await getTagArticleListAPI(id, { pageNum: page, pageSize: 8 });
const [{ data }, covers] = await Promise.all([
getTagArticleListAPI(id, { pageNum: page, pageSize: 8 }),
getThemeCoversCacheAPI(),
]);
return (
<>
@@ -24,7 +28,7 @@ export default async (props: Props) => {
<meta name="description" content={name} />
<div>
<Slide isRipple={false}>
<Slide isRipple={false} covers={covers}>
{/* 星空背景组件 */}
<Starry />

View File

@@ -20,7 +20,7 @@ import { addWallDataAPI, getCateListAPI } from '@/api/wall';
import { Bounce, toast, ToastContainer, ToastOptions } from 'react-toastify';
import HCaptchaType from '@hcaptcha/react-hcaptcha';
import HCaptcha from '@/components/HCaptcha';
import { useConfigStore } from '@/stores';
import { useAppConfig } from '@/components/AppConfigProvider';
import 'react-toastify/dist/ReactToastify.css';
const toastConfig: ToastOptions = {
@@ -42,8 +42,8 @@ export default () => {
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [captchaError, setCaptchaError] = useState<string>('');
const config = useConfigStore();
const hasHCaptcha = !!config?.other?.hcaptcha_key;
const { other } = useAppConfig();
const hasHCaptcha = !!other?.hcaptcha_key;
const [cateList, setCateList] = useState<Cate[]>([]);
const getCateList = async () => {

View File

@@ -0,0 +1,31 @@
'use client';
import { createContext, useContext, type ReactNode } from 'react';
import { Other, Theme, Web } from '@/types/app/config';
import { User } from '@/types/app/user';
interface AppConfigValue {
web: Web;
theme: Theme;
other: Other;
author: User;
}
const AppConfigContext = createContext<AppConfigValue | null>(null);
interface Props extends AppConfigValue {
children: ReactNode;
}
// 传递数据给子组件
export default function AppConfigProvider({ web, theme, other, author, children }: Props) {
return <AppConfigContext.Provider value={{ web, theme, other, author }}>{children}</AppConfigContext.Provider>;
}
// 获取数据
export function useAppConfig() {
const value = useContext(AppConfigContext);
if (!value) throw new Error('useAppConfig 必须在 AppConfigProvider 内使用');
return value;
}

View File

@@ -1,6 +1,6 @@
import Link from 'next/link';
import { getRandomImage } from '@/utils';
import { getThemeCovers } from '@/lib/theme';
import { getThemeCoversCacheAPI } from '@/lib/theme';
import { Article } from '@/types/app/article';
import ArticleMeta from '@/components/ArticleLayout/components/ArticleMeta';
import Empty from '@/components/Empty';
@@ -11,7 +11,7 @@ interface CardProps {
}
const Card = async ({ data }: CardProps) => {
const covers = await getThemeCovers();
const covers = await getThemeCoversCacheAPI();
const genArticleInfo = (data: Article) => {
if (data.description?.trim()?.length) {

View File

@@ -1,6 +1,6 @@
import Link from 'next/link';
import { getRandomImage } from '@/utils';
import { getThemeCovers } from '@/lib/theme';
import { getThemeCoversCacheAPI } from '@/lib/theme';
import { Article } from '@/types/app/article';
import ArticleMeta from '@/components/ArticleLayout/components/ArticleMeta';
import Empty from '@/components/Empty';
@@ -11,7 +11,7 @@ interface ClassicsProps {
}
const Classics = async ({ data }: ClassicsProps) => {
const covers = await getThemeCovers();
const covers = await getThemeCoversCacheAPI();
const genArticleInfo = (data: Article) => {
if (data.description?.trim()?.length) {

View File

@@ -1,13 +1,13 @@
'use client';
import Link from 'next/link';
import { useConfigStore } from '@/stores';
import { Article } from '@/types/app/article';
import { getRandomImage } from '@/utils';
import Masonry from 'react-masonry-css';
interface WaterfallProps {
data: Paginate<Article[]>;
covers: string[];
}
const breakpointColumnsObj = {
@@ -16,9 +16,7 @@ const breakpointColumnsObj = {
700: 2,
};
export default ({ data }: WaterfallProps) => {
const { theme } = useConfigStore();
export default ({ data, covers }: WaterfallProps) => {
return (
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid mb-12" columnClassName="masonry-grid_column">
{data.result.map((item) => (
@@ -27,7 +25,7 @@ export default ({ data }: WaterfallProps) => {
<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-[scale] duration-300 ease-out"
style={{ backgroundImage: `url(${getRandomImage(item.cover, theme.covers)})` }}
style={{ backgroundImage: `url(${getRandomImage(item.cover, covers)})` }}
/>
</div>
<div className="py-2 px-4">

View File

@@ -6,12 +6,13 @@ import Card from './Card';
import Pagination from '../Pagination';
import { getArticlePagingAPI } from '@/api/article';
import { getThemeConfig } from '@/lib/theme';
import { getThemeConfigCacheAPI, getThemeCoversCacheAPI } from '@/lib/theme';
import { getSwiperListAPI } from '@/api/swiper';
export default async ({ page }: { page: number }) => {
const { data: swiper } = await getSwiperListAPI();
const theme = await getThemeConfig();
const theme = await getThemeConfigCacheAPI();
const covers = await getThemeCoversCacheAPI();
const sidebar = theme?.right_sidebar ?? [];
// 按order排序轮播图顺序越小越靠前
@@ -32,7 +33,7 @@ export default async ({ page }: { page: number }) => {
{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 === 'waterfall' && <Waterfall data={data} covers={covers} />}
{!!data.total && <Pagination total={data?.pages} page={page} className="flex justify-center mt-5" />}
</div>

View File

@@ -1,11 +1,9 @@
'use client';
import { useEffect } from 'react';
import { useConfigStore } from '@/stores';
export default function BaiduAnalytics() {
const other = useConfigStore((state) => state.other);
import { Other } from '@/types/app/config';
export default function BaiduAnalytics({ other }: { other: Other }) {
useEffect(() => {
if (other?.baidu_token) {
window._hmt = window._hmt || [];

View File

@@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
import { Button, useDisclosure } from '@/ThriveUI';
import { BiCog, BiCommand } from 'react-icons/bi';
import { IoSearchOutline, IoArrowUpOutline, IoLogoRss } from 'react-icons/io5';
import { useAppConfig } from '@/components/AppConfigProvider';
import { useConfigStore } from '@/stores';
import Search from '../Search';
import Rss from '../Tools/components/Rss';
@@ -20,7 +21,8 @@ const FloatingBlock = () => {
const [isExpanded, setIsExpanded] = useState(false); // 展开状态的变量
const [isDragging, setIsDragging] = useState(false); // 拖拽状态
const constraintsRef = useRef(null); // 拖拽约束参考
const { isDark, setIsDark, web } = useConfigStore();
const { web } = useAppConfig();
const { isDark, setIsDark } = useConfigStore();
const { isOpen: isSearchOpen, onOpen: onSearchOpen, onClose: onSearchClose } = useDisclosure();
const { isOpen: isRssOpen, onOpen: onRssOpen, onClose: onRssClose } = useDisclosure();

View File

@@ -1,19 +1,20 @@
import HCaptcha from '@hcaptcha/react-hcaptcha';
import { forwardRef, Ref } from 'react';
import { useAppConfig } from '@/components/AppConfigProvider';
import { useConfigStore } from '@/stores';
export default forwardRef(({ setToken }: { setToken: (token: string) => void }, ref: Ref<HCaptcha>) => {
const config = useConfigStore();
const sitekey = config?.other?.hcaptcha_key;
const { other } = useAppConfig();
const isDark = useConfigStore((state) => state.isDark);
const sitekey = other?.hcaptcha_key;
// 如果没有配置 hcaptcha_key不渲染组件
if (!sitekey) {
return null;
}
return (
<div>
<HCaptcha theme={config.isDark ? 'dark' : 'light'} sitekey={sitekey} onVerify={setToken} ref={ref} />
<HCaptcha theme={isDark ? 'dark' : 'light'} sitekey={sitekey} onVerify={setToken} ref={ref} />
</div>
);
});

View File

@@ -14,6 +14,7 @@ import { LuMenu } from 'react-icons/lu';
import { BsFillMoonStarsFill } from 'react-icons/bs';
import { Cate } from '@/types/app/cate';
import { Theme } from '@/types/app/config';
import { getCateListAPI } from '@/api/cate';
import { getCateNavHref, getCateNavRel, getCateNavTarget } from '@/utils/cateNav';
@@ -25,10 +26,10 @@ const submenuPanelClass =
const submenuItemClass =
'group/item relative flex w-full items-center min-w-0 px-5 py-2.5 text-[15px] text-[#666] dark:text-white transition-colors duration-150 hover:text-primary! hover:bg-[#f2f2f2] dark:hover:bg-[#323e50] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:h-0 before:w-[3px] before:rounded-r-full before:bg-primary before:transition-[height] before:duration-150 hover:before:h-[50%]';
export default () => {
export default ({ theme }: { theme: Theme }) => {
const patchName = usePathname();
const { isDark, setIsDark, theme } = useConfigStore();
const { isDark, setIsDark } = useConfigStore();
// 这些路径段不需要改变导航样式
const isPathSty = ['/my', '/wall', '/record', '/equipment', '/tags', '/resume', '/album', '/fishpond', '/friend'].some((path) => patchName.includes(path));

View File

@@ -1,42 +0,0 @@
'use client';
import { useEffect } from 'react';
import { getWebConfigDataAPI } from '@/api/config';
import { useAuthorStore, useConfigStore } from '@/stores';
import { Web, Theme, Other } from '@/types/app/config';
import { getAuthorDataAPI } from '@/api/user';
export default () => {
const setAuthor = useAuthorStore((state) => state.setAuthor);
// 获取作者信息
const getAuthorData = async () => {
const { data: user } = await getAuthorDataAPI();
setAuthor(user);
};
const { setWeb, setTheme, setOther } = useConfigStore();
// 获取项目配置
const getConfigData = async () => {
const webResponse = await getWebConfigDataAPI<{ value: Web }>('web');
const web = webResponse?.data?.value as Web;
setWeb(web);
const themeResponse = await getWebConfigDataAPI<{ value: Theme }>('theme');
const theme = themeResponse?.data?.value as Theme;
setTheme(theme);
const otherResponse = await getWebConfigDataAPI<{ value: Other }>('other');
const other = otherResponse?.data?.value as Other;
setOther(other);
};
useEffect(() => {
getAuthorData();
getConfigData();
}, []);
return null;
};

View File

@@ -19,7 +19,7 @@ import {
actionPrimaryClass,
actionTextColClass,
} from '@/components/ActionCard/styles';
import { useConfigStore, useAuthorStore } from '@/stores';
import { useAppConfig } from '@/components/AppConfigProvider';
import { generateArticlePoster } from '@/utils/generateArticlePoster';
import dayjs from 'dayjs';
@@ -46,8 +46,7 @@ export default function ArticleSharePoster({ data, minimal = false, className, s
const [posterUrl, setPosterUrl] = useState('');
const [loading, setLoading] = useState(false);
const web = useConfigStore((s) => s.web);
const author = useAuthorStore((s) => s.author);
const { web, author } = useAppConfig();
const buildPoster = useCallback(async () => {
setLoading(true);

View File

@@ -4,7 +4,7 @@ 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 { useAppConfig } from '@/components/AppConfigProvider';
import { Article } from '@/types/app/article';
import { getRandomImage } from '@/utils';
import RandomArticleSvg from '@/assets/svg/other/article.svg';
@@ -19,7 +19,7 @@ const RANKING_COLORS = [
] as const;
const HotArticle = () => {
const { theme } = useConfigStore();
const { theme } = useAppConfig();
const [list, setList] = useState<Article[]>([]);

View File

@@ -2,7 +2,7 @@
import { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { useConfigStore } from '@/stores';
import { useAppConfig } from '@/components/AppConfigProvider';
import { motion, useMotionValue, useTransform, animate, useInView } from 'framer-motion';
import TimerSvg from '@/assets/svg/other/timer.svg';
import SidebarCard from '@/components/Sidebar/SidebarCard';
@@ -188,7 +188,7 @@ const statItems = [
] as const;
export default () => {
const { web } = useConfigStore();
const { web } = useAppConfig();
const [timeDiff, setTimeDiff] = useState(() => calculateTimeDifference(web?.create_time));
useEffect(() => {

View File

@@ -3,19 +3,17 @@
import { ReactNode, useMemo } from 'react';
import Ripple from '@/components/Ripple';
import { getRandomImage } from '@/utils';
import { useConfigStore } from '@/stores';
interface Props {
src?: string;
covers?: string[];
isRipple?: boolean;
fullImage?: boolean;
children?: ReactNode;
}
export default ({ src, isRipple = true, fullImage = false, children }: Props) => {
const theme = useConfigStore((state) => state.theme);
const fallbackImage = useMemo(() => getRandomImage(undefined, theme.covers), [theme.covers]);
export default ({ src, covers = [], isRipple = true, fullImage = false, children }: Props) => {
const fallbackImage = useMemo(() => getRandomImage(undefined, covers), [covers]);
const bgImage = src?.trim() || fallbackImage;
const sty = {

View File

@@ -7,6 +7,7 @@ import moon from './image/moon.svg';
import search from './image/search.svg';
import returnTop from './image/returnTop.svg';
import rss from './image/rss.svg';
import { useAppConfig } from '@/components/AppConfigProvider';
import { useConfigStore } from '@/stores';
import Search from '../Search';
import Rss from './components/Rss';
@@ -14,7 +15,8 @@ import Rss from './components/Rss';
const itemSty = 'p-2 hover:bg-[#edf5ff] dark:hover:bg-[#4e5969] cursor-pointer ';
export default () => {
const { isDark, setIsDark, web } = useConfigStore();
const { web } = useAppConfig();
const { isDark, setIsDark } = useConfigStore();
const { isOpen: isSwiper, onOpen: onSwiperOpen, onClose: onSwiperClose } = useDisclosure();
const { isOpen: isRssOpen, onOpen: onRssOpen, onClose: onRssClose } = useDisclosure();
const onReturnTop = () => {

View File

@@ -1,26 +1,28 @@
'use client';
import { useEffect, useRef } from 'react';
import { useConfigStore } from '@/stores';
import Typed from 'typed.js';
export default ({ className }: { className?: string }) => {
const { theme } = useConfigStore();
interface Props {
className?: string;
swiperText?: string[];
}
export default ({ className, swiperText = [] }: Props) => {
const el = useRef(null);
useEffect(() => {
if (theme.swiper_text) {
const typed = new Typed(el.current, {
strings: theme.swiper_text,
typeSpeed: 100,
backSpeed: 30,
loop: true,
});
if (!swiperText.length) return;
return () => typed.destroy();
}
}, [theme]);
const typed = new Typed(el.current, {
strings: swiperText,
typeSpeed: 100,
backSpeed: 30,
loop: true,
});
return () => typed.destroy();
}, [swiperText]);
return <span ref={el} className={className} />;
};

36
src/lib/config.ts Normal file
View File

@@ -0,0 +1,36 @@
import { cache } from 'react';
import { getWebConfigDataAPI } from '@/api/config';
import { getAuthorDataAPI } from '@/api/user';
import { Other, Theme, Web } from '@/types/app/config';
import { User } from '@/types/app/user';
export const getWebConfigCacheAPI = cache(async () => {
const { data } = await getWebConfigDataAPI<{ value: Web }>('web');
return data?.value as Web;
});
export const getThemeConfigCacheAPI = cache(async () => {
const { data } = await getWebConfigDataAPI<{ value: Theme }>('theme');
return data?.value as Theme;
});
export const getOtherConfigCacheAPI = cache(async () => {
const { data } = await getWebConfigDataAPI<{ value: Other }>('other');
return data?.value as Other;
});
export const getAuthorDataCacheAPI = cache(async () => {
const { data } = await getAuthorDataAPI();
return data as User;
});
export const getAppConfigCacheAPI = cache(async () => {
const [web, theme, other, author] = await Promise.all([
getWebConfigCacheAPI(),
getThemeConfigCacheAPI(),
getOtherConfigCacheAPI(),
getAuthorDataCacheAPI(),
]);
return { web, theme, other, author };
});

View File

@@ -1,12 +1,8 @@
import { cache } from 'react';
import { getWebConfigDataAPI } from '@/api/config';
import { Theme } from '@/types/app/config';
import { getThemeConfigCacheAPI } from '@/lib/config';
import { parseThemeCovers } from '@/utils/cover';
export const getThemeConfig = cache(async () => {
const { data } = await getWebConfigDataAPI<{ value: Theme }>('theme');
return data?.value as Theme;
});
export { getThemeConfigCacheAPI } from '@/lib/config';
export const getThemeCovers = cache(async () => parseThemeCovers((await getThemeConfig())?.covers));
export const getThemeCoversCacheAPI = cache(async () => parseThemeCovers((await getThemeConfigCacheAPI())?.covers));

View File

@@ -1,4 +1,3 @@
import useConfigStore from './modules/config'
import useAuthorStore from './modules/author'
import useConfigStore from './modules/config';
export { useConfigStore, useAuthorStore };
export { useConfigStore };

View File

@@ -1,13 +0,0 @@
import { create } from 'zustand';
import { User } from '@/types/app/user';
interface AuthorState {
// 作者信息
author: User;
setAuthor: (data: User) => void;
}
export default create<AuthorState>((set) => ({
author: {} as User,
setAuthor: (data: User) => set(() => ({ author: data })),
}));

View File

@@ -1,43 +1,23 @@
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { Other, Theme, Web } from '@/types/app/config';
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface ConfigState {
// 是否暗黑模式
interface ThemeState {
isDark: boolean;
setIsDark: (status: boolean) => void;
// 网站配置
web: Web;
setWeb: (data: Web) => void;
// 主题配置
theme: Theme;
setTheme: (data: Theme) => void;
// 其他配置
other: Other;
setOther: (data: Other) => void;
}
type ThemePersist = Pick<ThemeState, 'isDark'>;
export default create(
persist<ConfigState>(
persist<ThemeState, [], [], ThemePersist>(
(set) => ({
isDark: false,
setIsDark: (status: boolean) => set(() => ({ isDark: status })),
web: {} as Web,
setWeb: (data: Web) => set(() => ({ web: data })),
theme: {} as Theme,
setTheme: (data: Theme) => set(() => ({ theme: data })),
other: {} as Other,
setOther: (data: Other) => set(() => ({ other: data }))
setIsDark: (status: boolean) => set({ isDark: status }),
}),
{
name: 'config_storage',
storage: createJSONStorage(() => localStorage)
}
)
)
storage: createJSONStorage<ThemePersist>(() => localStorage),
partialize: (state) => ({ isDark: state.isDark }),
},
),
);