重构文章页面,删除不再使用的组件和样式文件,优化布局逻辑,添加微信朋友圈风格的自定义颜色和阴影效果,提升整体用户体验和视觉效果。
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 868 B After Width: | Height: | Size: 868 B |
12
src/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className="min-h-[calc(100vh-300px)]">{children}</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,6 @@ import { Metadata } from 'next';
|
||||
|
||||
import HeroUIProvider from '@/components/HeroUIProvider';
|
||||
import NProgress from '@/components/NProgress';
|
||||
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 { getWebConfigDataAPI } from '@/api/config';
|
||||
@@ -117,23 +111,11 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
|
||||
<body id="root" className={`dark:!bg-black-a`}>
|
||||
{/* 数据注入 */}
|
||||
<InjectData />
|
||||
{/* 🎉 礼花效果 */}
|
||||
{/* <Confetti /> */}
|
||||
|
||||
{/* 进度条组件 */}
|
||||
<NProgress />
|
||||
{/* 顶部导航组件 */}
|
||||
<Header />
|
||||
|
||||
{/* 主体内容 */}
|
||||
<HeroUIProvider>
|
||||
<div className="min-h-[calc(100vh-300px)]">{children}</div>
|
||||
</HeroUIProvider>
|
||||
|
||||
{/* 底部组件 */}
|
||||
<Footer />
|
||||
{/* 右侧工具栏组件 */}
|
||||
{/* <Tools /> */}
|
||||
{/* 主体内容:各路由组/页面通过自己的 layout 决定是否包含 Header/Footer */}
|
||||
<HeroUIProvider>{children}</HeroUIProvider>
|
||||
|
||||
{/* 悬浮块 */}
|
||||
<FloatingBlock />
|
||||
|
||||
@@ -8,21 +8,27 @@ interface Props {
|
||||
}
|
||||
|
||||
export default ({ list }: Props) => {
|
||||
if (!list?.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!list?.length && (
|
||||
<div className={`flex justify-center 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 ? 'grid-cols-1 justify-center' : 'grid-cols-2 md:grid-cols-3'}`}>
|
||||
{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>
|
||||
))}
|
||||
<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)')}>
|
||||
{list.length === 1 ? (
|
||||
<div className="max-w-[70%]">
|
||||
<PhotoView src={list[0]}>
|
||||
<img src={list[0]} alt="闪念图片" className="w-full h-auto rounded-sm object-cover cursor-pointer active:opacity-90" />
|
||||
</PhotoView>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-1 max-w-[90%]">
|
||||
{list.map((url, index) => (
|
||||
<div key={index} className="aspect-square bg-wx-gray dark:bg-black-a overflow-hidden cursor-pointer active:opacity-90">
|
||||
<PhotoView src={url}>
|
||||
<img src={url} alt="闪念图片" className="w-full h-full object-cover" />
|
||||
</PhotoView>
|
||||
</div>
|
||||
</PhotoProvider>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</PhotoProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,30 +15,33 @@ export default function RecordCard({ id, content, images, createTime, user }: Re
|
||||
const imageList: string[] = Array.isArray(images) ? images : JSON.parse((images as string) || '[]');
|
||||
|
||||
return (
|
||||
<div key={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" />
|
||||
|
||||
<div className="flex sm:hidden">
|
||||
<img src={user?.avatar} alt="作者头像" width={44} height={44} className="rounded-lg border dark:border-black-b h-11 mr-2" />
|
||||
|
||||
<div className="flex sm:hidden items-center my-1.5 ml-2 space-x-4">
|
||||
<h3>{user?.name}</h3>
|
||||
<span className="text-xs">{dayFormat(createTime as any)}</span>
|
||||
</div>
|
||||
<article key={id} className="flex space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
src={user?.avatar}
|
||||
alt="作者头像"
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-10 h-10 rounded-lg object-cover cursor-pointer active:bg-wx-gray"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 sm:mt-0 w-full">
|
||||
<div className="hidden sm:flex items-center my-1.5 ml-4 space-x-4">
|
||||
<h3>{user?.name}</h3>
|
||||
<span className="text-xs">{dayFormat(createTime as any)}</span>
|
||||
<div className="flex-grow min-w-0">
|
||||
<h3 className="text-wx-blue font-semibold text-[15px] cursor-pointer w-fit mb-1">{user?.name}</h3>
|
||||
<div className="text-[15px] leading-6 mb-2 text-wx-text break-words">
|
||||
<Editor value={content} />
|
||||
</div>
|
||||
|
||||
<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={content} />
|
||||
|
||||
<div className="mb-3">
|
||||
<ImageList list={imageList} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center text-xs text-wx-light">
|
||||
<span>{dayFormat(createTime as number)}</span>
|
||||
<div className="bg-wx-gray dark:bg-black-a px-2 py-1 rounded text-wx-blue font-bold cursor-pointer active:opacity-80">
|
||||
<span className="tracking-widest text-lg leading-3">··</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ export default () => {
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const currentPageRef = useRef(1);
|
||||
|
||||
// 获取记录列表
|
||||
const fetchRecordList = useCallback(async (page: number, append: boolean = false) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -51,11 +50,9 @@ export default () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始加载:获取用户信息、主题配置和第一页记录
|
||||
useEffect(() => {
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
// 并行获取用户信息、主题配置和第一页记录
|
||||
const [userResponse, themeResponse] = await Promise.all([getAuthorDataAPI(), getWebConfigDataAPI<{ value: Theme }>('theme')]);
|
||||
|
||||
if (userResponse?.data) {
|
||||
@@ -65,7 +62,6 @@ export default () => {
|
||||
setTheme(themeResponse.data.value);
|
||||
}
|
||||
|
||||
// 获取第一页记录
|
||||
setRecords([]);
|
||||
setHasMore(true);
|
||||
setInitialLoading(true);
|
||||
@@ -79,13 +75,10 @@ export default () => {
|
||||
fetchInitialData();
|
||||
}, [fetchRecordList]);
|
||||
|
||||
// 滚动监听
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
// 如果正在加载或没有更多数据,则不处理
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
// 检查是否滚动到底部(距离底部100px时触发)
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
@@ -98,7 +91,6 @@ export default () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 使用防抖优化滚动事件
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const debouncedHandleScroll = () => {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -112,53 +104,79 @@ export default () => {
|
||||
};
|
||||
}, [hasMore, loading, totalPages, fetchRecordList]);
|
||||
|
||||
const coverImage = (theme as { record_cover?: string })?.record_cover || theme?.covers?.split?.(',')?.[0] || 'https://images.unsplash.com/photo-1470770841072-f978cf4d019e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80';
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>🏕️ 闪念</title>
|
||||
<meta name="description" content="🏕️ 闪念" />
|
||||
|
||||
<div className="bg-[linear-gradient(to_right,#fff1eb_0%,#d0edfb_100%)] dark:bg-[linear-gradient(to_right,#232931_0%,#232931_100%)]">
|
||||
<div className="w-full lg:w-[800px] px-6 lg:px-0 mx-auto pt-24 pb-10">
|
||||
<div className="flex items-center flex-col p-4 mb-10 border dark:border-black-b rounded-lg bg-white dark:bg-black-b bg-[url('https://bu.dusays.com/2025/12/04/6930fe4e06985.jpg')] bg-no-repeat bg-center bg-cover ">
|
||||
<img src={user?.avatar} alt="作者头像" width={80} height={80} className="w-20 h-20 rounded-full avatar-animation shadow-[5px_11px_30px_20px_rgba(255,255,255,0.3)]" />
|
||||
<h2 className="my-2 text-white">{theme?.record_name}</h2>
|
||||
<h4 className="text-xs text-gray-300">{theme?.record_info}</h4>
|
||||
</div>
|
||||
|
||||
{initialLoading ? (
|
||||
<div className="flex justify-center items-center py-20">
|
||||
<Loading />
|
||||
<div className="bg-gray-100 min-h-screen flex justify-center text-wx-text selection:bg-wx-blue selection:text-white dark:bg-black-a pt-24">
|
||||
<main className="w-full max-w-[430px] bg-white min-h-screen relative shadow-2xl flex flex-col overflow-y-auto dark:bg-black-b">
|
||||
{/* 封面图区域 (Hero) */}
|
||||
<section className="relative mb-16">
|
||||
<div
|
||||
className="h-80 w-full bg-cover bg-center cursor-pointer"
|
||||
style={{ backgroundImage: `url('${coverImage}')` }}
|
||||
/>
|
||||
<div className="absolute -bottom-10 right-4 flex items-end space-x-3">
|
||||
<div className="text-white font-bold text-lg mb-4 drop-shadow-md select-none">
|
||||
{theme?.record_name || '闪念'} - {user?.name || ''}
|
||||
</div>
|
||||
<div className="w-20 h-20 rounded-xl overflow-hidden border-2 border-white shadow-sm cursor-pointer active:opacity-80 transition-opacity">
|
||||
<img src={user?.avatar} alt="头像" className="w-full h-full object-cover" width={80} height={80} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-12">
|
||||
{!!records?.length && records.map((item) => <RecordCard key={item.id} id={item.id as any} content={item.content as any} images={item.images as any} createTime={item.createTime as any} user={user as any} />)}
|
||||
</section>
|
||||
|
||||
{/* 内容列表 */}
|
||||
<div className="px-4 pb-10 space-y-8">
|
||||
{initialLoading ? (
|
||||
<div className="flex justify-center items-center py-20">
|
||||
<Loading />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!!records?.length &&
|
||||
records.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
<RecordCard
|
||||
id={item.id as never}
|
||||
content={item.content as string}
|
||||
images={item.images as string[]}
|
||||
createTime={item.createTime as number}
|
||||
user={user as User}
|
||||
/>
|
||||
{index < records.length - 1 && <div className="border-b border-gray-100 dark:border-wx-border mt-8" />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Show is={!records?.length}>
|
||||
<Empty info="内容为空~" />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* 懒加载指示器 */}
|
||||
{loading && records.length > 0 && (
|
||||
<div className="flex justify-center items-center py-8 mt-5 gap-2">
|
||||
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400 text-sm">
|
||||
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>正在加载...</span>
|
||||
{loading && records.length > 0 && (
|
||||
<div className="flex justify-center items-center py-8 gap-2">
|
||||
<div className="flex items-center gap-2 text-wx-light text-sm">
|
||||
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
<span>正在加载...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && records.length > 0 && (
|
||||
<div className="flex justify-center items-center py-8 mt-5">
|
||||
<div className="text-gray-500 dark:text-gray-400 text-sm">没有更多内容了</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasMore && records.length > 0 && (
|
||||
<div className="text-center py-6">
|
||||
<div className="h-px bg-gray-200 dark:bg-wx-border w-full mb-3" />
|
||||
<span className="text-xs text-wx-light">已显示全部朋友圈</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { getCateListAPI } from '@/api/cate';
|
||||
|
||||
import { useConfigStore } from '@/stores';
|
||||
|
||||
const Header = () => {
|
||||
export default () => {
|
||||
const patchName = usePathname();
|
||||
|
||||
const { isDark, setIsDark, theme } = useConfigStore();
|
||||
@@ -153,5 +153,3 @@ const Header = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
@@ -16,7 +16,19 @@ const config: Config = {
|
||||
colors: {
|
||||
primary: '#539dfd', // 添加自定义颜色
|
||||
'black-a': '#232931',
|
||||
'black-b': '#2c333e'
|
||||
'black-b': '#2c333e',
|
||||
// 微信朋友圈风格
|
||||
wx: {
|
||||
bg: '#ededed',
|
||||
blue: '#576b95',
|
||||
text: '#111111',
|
||||
gray: '#f7f7f7',
|
||||
light: '#b2b2b2',
|
||||
border: '#e5e5e5',
|
||||
},
|
||||
},
|
||||
boxShadow: {
|
||||
'wx-menu': '0 0 8px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
transitionDuration: {
|
||||
'DEFAULT': '300ms', // 添加默认过渡时间为0.3秒
|
||||
|
||||