mirror of
https://github.com/LiuYuYang01/ThriveX-Blog.git
synced 2026-09-03 06:24:24 +08:00
refactor(wall): 优化留言墙API和组件样式
- 修改 getCateWallListAPI 函数,移除不必要的参数以简化调用。 - 更新 WallMasonry 组件,重构样式和背景处理,提升视觉效果和用户体验。 - 替换 AddWallInfo 组件中的按钮样式,增强可访问性和交互性。 - 移除不再使用的样式文件,简化项目结构。
This commit is contained in:
@@ -12,6 +12,6 @@ export const getCateListAPI = async () => {
|
||||
}
|
||||
|
||||
// 获取当前分类中所有留言
|
||||
export const getCateWallListAPI = async (cateId: number, params?: Page) => {
|
||||
return await Request<Paginate<Wall[]>>('GET', `/wall/cate/${cateId}`, { params: params ?? {} });
|
||||
export const getCateWallListAPI = async (cateId: number) => {
|
||||
return await Request<Paginate<Wall[]>>('GET', `/wall/cate/${cateId}`);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const Copyright = () => {
|
||||
>
|
||||
<div className="absolute left-0 top-0 bottom-0 w-1.5 bg-primary dark:bg-blue-500/80 rounded-l-xl" />
|
||||
|
||||
<FiShield className="absolute right-2.5 top-5 text-7xl text-blue-500/[0.04] dark:text-gray-700 pointer-events-none rotate-12" />
|
||||
<FiShield className="absolute right-2.5 top-5 text-7xl text-blue-500/4 dark:text-gray-700 pointer-events-none rotate-12" />
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-3.5">
|
||||
<div className="flex items-center gap-3 text-sm sm:text-base text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -7,7 +7,7 @@ import RecordCommentPanel from './Comment';
|
||||
import useDebouncedLike from '@/hooks/useDebouncedLike';
|
||||
import { likeRecordAPI } from '@/api/record';
|
||||
import { getRecordCommentListAPI } from '@/api/recordComment';
|
||||
import dayjs from 'dayjs';
|
||||
import { getRelativeTimeLabel } from '@/utils';
|
||||
import { User } from '@/types/app/user';
|
||||
|
||||
interface RecordItemProps {
|
||||
@@ -43,30 +43,6 @@ function CommentIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
function getRelativeTimeLabel(ts: string | number | Date | undefined): string {
|
||||
if (ts == null) return '';
|
||||
const now = dayjs();
|
||||
const then = dayjs(+ts);
|
||||
const diffDays = now.startOf('day').diff(then.startOf('day'), 'day');
|
||||
const diffMonths = now.diff(then, 'month', true);
|
||||
const diffYears = now.diff(then, 'year', true);
|
||||
|
||||
if (diffDays === 0) return `今天 ${then.format('HH:mm')}`;
|
||||
if (diffDays === 1) return `昨天 ${then.format('HH:mm')}`;
|
||||
if (diffDays >= 2 && diffDays <= 6) return `${diffDays}天前`;
|
||||
if (diffDays >= 7 && diffDays <= 13) return '一周前';
|
||||
if (diffDays >= 14 && diffDays <= 20) return '两周前';
|
||||
if (diffDays >= 21 && diffDays <= 27) return '三周前';
|
||||
if (diffMonths >= 1 && diffMonths < 2) return '一月前';
|
||||
if (diffMonths >= 2 && diffMonths < 3) return '两月前';
|
||||
if (diffMonths >= 3 && diffMonths < 6) return '三月前';
|
||||
if (diffMonths >= 6 && diffMonths < 12) return '半年前';
|
||||
if (diffYears >= 1 && diffYears < 2) return '一年前';
|
||||
if (diffYears >= 2 && diffYears < 3) return '两年前';
|
||||
if (diffYears >= 3) return `${Math.floor(diffYears)}年前`;
|
||||
return '';
|
||||
}
|
||||
|
||||
export default function RecordCard({ id, content, images, likeCount, mood, location, createTime, user }: RecordItemProps) {
|
||||
const [showComments, setShowComments] = useState(false);
|
||||
const [commentCount, setCommentCount] = useState(0);
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ZCOOL_KuaiLe } from 'next/font/google';
|
||||
import AddWallInfo from '../components/AddWallInfo';
|
||||
import WallMasonry from '../components/WallMasonry';
|
||||
import Loading from '@/components/Loading';
|
||||
import { getCateListAPI, getCateWallListAPI } from '@/api/wall';
|
||||
import { Cate, Wall } from '@/types/app/wall';
|
||||
import '../wall.css';
|
||||
|
||||
const zcoolKuaiLe = ZCOOL_KuaiLe({ weight: '400', subsets: ['latin'] });
|
||||
|
||||
const floatDots = [
|
||||
{ size: 'w-2 h-2', color: 'bg-rose-400/70', delay: '0s' },
|
||||
{ size: 'w-1.5 h-1.5', color: 'bg-amber-400/70', delay: '0.5s' },
|
||||
{ size: 'w-2.5 h-2.5', color: 'bg-violet-400/70', delay: '1s' },
|
||||
{ size: 'w-1.5 h-1.5', color: 'bg-cyan-400/70', delay: '1.5s' },
|
||||
{ size: 'w-2 h-2', color: 'bg-emerald-400/70', delay: '2s' },
|
||||
];
|
||||
|
||||
export default () => {
|
||||
const params = useParams();
|
||||
@@ -15,13 +27,8 @@ export default () => {
|
||||
|
||||
const [cateList, setCateList] = useState<Cate[]>([]);
|
||||
const [walls, setWalls] = useState<Wall[]>([]);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const currentPageRef = useRef(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 获取分类列表
|
||||
useEffect(() => {
|
||||
const fetchCateList = async () => {
|
||||
const { data } = await getCateListAPI();
|
||||
@@ -31,157 +38,107 @@ export default () => {
|
||||
fetchCateList();
|
||||
}, []);
|
||||
|
||||
// 获取留言列表
|
||||
const getWallList = useCallback(
|
||||
async (params: Page, append: boolean = false) => {
|
||||
const id = cateList.find((item) => item.mark === cate)?.id ?? 0;
|
||||
if (!id) return;
|
||||
const getWallList = useCallback(async () => {
|
||||
const id = cateList.find((item) => item.mark === cate)?.id ?? 0;
|
||||
if (!id) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: tallList } = await getCateWallListAPI(id, params);
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await getCateWallListAPI(id);
|
||||
setWalls(data?.result ?? []);
|
||||
} catch (error) {
|
||||
console.error('获取留言列表失败:', error);
|
||||
setWalls([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [cate, cateList]);
|
||||
|
||||
if (tallList?.result && tallList?.result?.length > 0) {
|
||||
if (append) {
|
||||
setWalls((prev) => [...prev, ...tallList.result]);
|
||||
} else {
|
||||
setWalls(tallList?.result);
|
||||
}
|
||||
setTotalPages(tallList.pages || 1);
|
||||
setHasMore((params.pageNum ?? 1) < (tallList.pages ?? 1));
|
||||
currentPageRef.current = params.pageSize ?? 1;
|
||||
} else {
|
||||
setHasMore(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取留言列表失败:', error);
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setInitialLoading(false);
|
||||
}
|
||||
},
|
||||
[cate, cateList]
|
||||
);
|
||||
|
||||
// 初始加载和分类切换时重新加载
|
||||
useEffect(() => {
|
||||
if (cateList.length > 0 && cate) {
|
||||
setWalls([]);
|
||||
setHasMore(true);
|
||||
setInitialLoading(true);
|
||||
currentPageRef.current = 1;
|
||||
getWallList({ pageNum: 1, pageSize: 8 }, false);
|
||||
getWallList();
|
||||
}
|
||||
}, [cate, cateList, getWallList]);
|
||||
|
||||
// 滚动监听
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
// 如果正在加载或没有更多数据,则不处理
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
// 检查是否滚动到底部(距离底部100px时触发)
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
if (scrollTop + windowHeight >= documentHeight - 100) {
|
||||
const nextPage = currentPageRef.current + 1;
|
||||
if (nextPage <= totalPages) {
|
||||
getWallList({ pageNum: nextPage, pageSize: 8 }, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 使用防抖优化滚动事件
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const debouncedHandleScroll = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(handleScroll, 200);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', debouncedHandleScroll);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', debouncedHandleScroll);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [hasMore, loading, totalPages, getWallList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative min-h-screen overflow-x-hidden bg-[#f4f4f8] text-gray-900 antialiased dark:bg-[#08080d] dark:text-white">
|
||||
<title>💌 留言墙</title>
|
||||
<meta name="description" content="💌 留言墙" />
|
||||
|
||||
{/* 背景装饰 */}
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(148,163,184,0.06)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.06)_1px,transparent_1px)] bg-size-[64px_64px]" />
|
||||
<div className="absolute -top-1/2 left-1/2 -translate-x-1/2 w-[800px] h-[800px] rounded-full bg-primary/6 blur-[120px]" />
|
||||
<div className="absolute top-1/4 right-0 w-96 h-96 rounded-full bg-violet-400/8 blur-[80px]" />
|
||||
<div className="absolute bottom-1/4 left-0 w-80 h-80 rounded-full bg-cyan-400/8 blur-[80px]" />
|
||||
{/* 颗粒纹理 */}
|
||||
<div className="wall-grain-overlay pointer-events-none fixed inset-0 z-50 opacity-[0.025] dark:opacity-[0.03]" aria-hidden="true" />
|
||||
|
||||
{/* 背景渐变光斑 */}
|
||||
<div className="fixed inset-0 pointer-events-none" aria-hidden="true">
|
||||
<div className="absolute -top-24 left-[8%] w-[550px] h-[550px] rounded-full bg-violet-500/8 blur-[140px] dark:bg-violet-600/6" />
|
||||
<div className="absolute top-[28%] right-[4%] w-[460px] h-[460px] rounded-full bg-rose-400/8 blur-[120px] dark:bg-rose-500/5" />
|
||||
<div className="absolute bottom-[5%] left-[30%] w-[400px] h-[400px] rounded-full bg-cyan-400/6 blur-[100px] dark:bg-cyan-500/4" />
|
||||
<div className="absolute top-[58%] left-[2%] w-[320px] h-[320px] rounded-full bg-amber-400/6 blur-[90px] dark:bg-amber-500/4" />
|
||||
</div>
|
||||
|
||||
<div className="py-16 border-b dark:border-[#4e5969]">
|
||||
<div className="relative z-10">
|
||||
{/* 头部区域 */}
|
||||
<div className="flex flex-col items-center px-4 pt-12 md:pt-16 pb-8">
|
||||
<div className="relative text-center mb-10">
|
||||
<h2 className="text-5xl mb-3">留言墙</h2>
|
||||
<p className="text-sm text-gray-600 mb-4">有什么想对我说的,来吧</p>
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-0 w-24 h-1 bg-linear-to-r from-transparent via-blue-400 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<AddWallInfo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分类标签 */}
|
||||
<div className="flex flex-wrap justify-center gap-2 md:gap-3 px-4 mb-8">
|
||||
{cateList?.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/wall/${item.mark}`}
|
||||
className={`
|
||||
relative px-5 py-2.5 text-sm font-medium rounded-full
|
||||
transition-transform
|
||||
${item.mark === cate ? 'text-white bg-primary scale-105' : 'text-gray-700 dark:text-gray-300 bg-white/80 dark:bg-gray-700/80 backdrop-blur-sm border dark:border-gray-700/50 hover:text-primary hover:scale-105'}
|
||||
`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 留言卡片瀑布流 */}
|
||||
{initialLoading ? (
|
||||
<div className="flex justify-center items-center py-20">
|
||||
<Loading />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-[90%] xl:w-[1200px] mx-auto mt-8 pb-12">
|
||||
{walls && walls.length > 0 ? (
|
||||
<>
|
||||
<WallMasonry walls={walls} />
|
||||
{/* 加载更多指示器 */}
|
||||
{loading && (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="text-gray-500 dark:text-gray-400 text-sm">加载中...</div>
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && walls.length > 0 && (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="text-gray-500 dark:text-gray-400 text-sm">没有更多留言了</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">暂无留言</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 头部 */}
|
||||
<header className="relative z-10 pt-20 pb-4 mt-10 text-center">
|
||||
<div className="flex justify-center gap-3 mb-6">
|
||||
{floatDots.map((dot, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`animate-[wall-float-dot_3s_ease-in-out_infinite] rounded-full ${dot.size} ${dot.color}`}
|
||||
style={{ animationDelay: dot.delay }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h1 className={`text-6xl sm:text-7xl font-bold tracking-tight mb-5 ${zcoolKuaiLe.className}`}>
|
||||
<span className="bg-size-[200%_auto] bg-linear-to-r from-amber-500 via-rose-500 to-violet-500 bg-clip-text text-transparent animate-[wall-shimmer_5s_ease_infinite] dark:from-amber-300 dark:via-rose-300 dark:to-violet-400">
|
||||
留言墙
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-neutral-500 dark:text-neutral-400 text-base sm:text-lg tracking-[0.2em] font-light">每一个想法,都值得被看见</p>
|
||||
|
||||
<div className="mt-8 flex items-center justify-center gap-3">
|
||||
<div className="w-20 h-px bg-linear-to-r from-transparent to-neutral-300 dark:to-neutral-700" />
|
||||
<div className="w-2 h-2 rounded-full bg-amber-400/80 shadow-lg shadow-amber-400/20" />
|
||||
<div className="w-20 h-px bg-linear-to-l from-transparent to-neutral-300 dark:to-neutral-700" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 分类标签 */}
|
||||
<div className="relative z-10 flex flex-wrap justify-center gap-2 md:gap-3 px-4 mb-8">
|
||||
{cateList?.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/wall/${item.mark}`}
|
||||
className={`
|
||||
relative px-5 py-2.5 text-sm font-medium rounded-full
|
||||
transition-transform cursor-pointer
|
||||
${item.mark === cate ? 'text-white bg-primary scale-105' : 'text-gray-700 dark:text-gray-300 bg-white/80 dark:bg-gray-700/80 backdrop-blur-sm border dark:border-gray-700/50 hover:text-primary hover:scale-105'}
|
||||
`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* 留言卡片瀑布流 */}
|
||||
<main className="relative z-10 max-w-7xl mx-auto px-6 sm:px-10 pb-24">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-20">
|
||||
<Loading />
|
||||
</div>
|
||||
) : walls.length > 0 ? (
|
||||
<div className="py-6">
|
||||
<WallMasonry walls={walls} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-neutral-500 dark:text-neutral-400">暂无留言</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<AddWallInfo />
|
||||
|
||||
<footer className="relative z-10 text-center pb-16 pt-4">
|
||||
<p className="text-neutral-400 dark:text-neutral-600 text-sm tracking-[0.3em] font-light">— 每一条留言,都是一颗发光的心 —</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -96,9 +96,14 @@ export default () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button color="primary" onPress={onOpen}>
|
||||
点击留言
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
aria-label="写留言"
|
||||
className="fixed bottom-8 right-8 z-50 w-14 h-14 rounded-full bg-linear-to-br from-amber-400 to-rose-500 text-white shadow-xl shadow-rose-500/25 flex items-center justify-center text-3xl font-light transition-all duration-300 hover:scale-110 hover:shadow-2xl hover:shadow-rose-500/40 cursor-pointer leading-none"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
|
||||
<Modal
|
||||
open={isOpen}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// 留言墙瀑布流样式优化
|
||||
.wall-masonry-grid {
|
||||
margin-left: -20px; // 调整左侧间距
|
||||
|
||||
@media (min-width: 640px) {
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
margin-left: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
.wall-masonry-grid .masonry-grid_column {
|
||||
padding-left: 12px; // 列间距
|
||||
}
|
||||
@@ -1,21 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import Masonry from 'react-masonry-css';
|
||||
import dayjs from 'dayjs';
|
||||
import { Wall } from '@/types/app/wall';
|
||||
import { getRelativeTimeLabel } from '@/utils';
|
||||
import '@/components/ArticleLayout/Waterfall/index.scss';
|
||||
import './index.scss';
|
||||
|
||||
// 颜色映射表,将颜色值映射到对应的 Tailwind 类名
|
||||
const colorMap: Record<string, string> = {
|
||||
'#fcafa24d': 'bg-[#fcafa24d]',
|
||||
'#a8ed8a4d': 'bg-[#a8ed8a4d]',
|
||||
'#caa7f74d': 'bg-[#caa7f74d]',
|
||||
'#ffe3944d': 'bg-[#ffe3944d]',
|
||||
'#92e6f54d': 'bg-[#92e6f54d]',
|
||||
const colorThemes: Record<string, { bg: string; pushpin: string; badge: string }> = {
|
||||
'#fcafa24d': {
|
||||
bg: 'linear-gradient(145deg,#FFE4E6,#FECDD3)',
|
||||
pushpin: 'linear-gradient(135deg,#fb7185,#e11d48)',
|
||||
badge: 'bg-rose-400/25 text-rose-800',
|
||||
},
|
||||
'#a8ed8a4d': {
|
||||
bg: 'linear-gradient(145deg,#D1FAE5,#A7F3D0)',
|
||||
pushpin: 'linear-gradient(135deg,#34d399,#059669)',
|
||||
badge: 'bg-emerald-400/25 text-emerald-800',
|
||||
},
|
||||
'#caa7f74d': {
|
||||
bg: 'linear-gradient(145deg,#EDE9FE,#DDD6FE)',
|
||||
pushpin: 'linear-gradient(135deg,#a78bfa,#7c3aed)',
|
||||
badge: 'bg-violet-400/25 text-violet-800',
|
||||
},
|
||||
'#ffe3944d': {
|
||||
bg: 'linear-gradient(145deg,#FEF3C7,#FDE68A)',
|
||||
pushpin: 'linear-gradient(135deg,#fbbf24,#d97706)',
|
||||
badge: 'bg-amber-400/25 text-amber-800',
|
||||
},
|
||||
'#92e6f54d': {
|
||||
bg: 'linear-gradient(145deg,#DBEAFE,#BFDBFE)',
|
||||
pushpin: 'linear-gradient(135deg,#60a5fa,#2563eb)',
|
||||
badge: 'bg-blue-400/25 text-blue-800',
|
||||
},
|
||||
};
|
||||
|
||||
// 瀑布流断点配置
|
||||
const defaultTheme = colorThemes['#ffe3944d'];
|
||||
const rotations = [-3, 2.5, -1.8, 4, -2.8, 1.5, -3.5, 3, -2, 1.8, -3.2, 2.5];
|
||||
|
||||
const breakpointColumnsObj = {
|
||||
default: 4,
|
||||
1024: 3,
|
||||
@@ -23,53 +43,51 @@ const breakpointColumnsObj = {
|
||||
640: 1,
|
||||
};
|
||||
|
||||
const msgCardClass =
|
||||
'wall-paper-texture relative mb-14 cursor-default rounded-2xl p-6 ' +
|
||||
'shadow-[0_2px_8px_-2px_rgba(0,0,0,0.06)] dark:shadow-[0_2px_8px_-2px_rgba(0,0,0,0.18)] ' +
|
||||
'transition-[rotate,scale,translate,box-shadow,opacity] duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)] ' +
|
||||
'animate-[wall-card-enter_0.65s_ease_forwards] ' +
|
||||
'hover:rotate-0! hover:scale-[1.07] hover:-translate-y-2.5 hover:z-30 ' +
|
||||
'hover:shadow-[0_10px_24px_-6px_rgba(0,0,0,0.12)] dark:hover:shadow-[0_10px_24px_-6px_rgba(0,0,0,0.28)]';
|
||||
|
||||
const pushpinClass =
|
||||
'absolute -top-2 left-1/2 z-[5] size-4 -translate-x-1/2 rounded-full ' +
|
||||
'shadow-[0_2px_6px_rgba(0,0,0,0.35),inset_0_-3px_6px_rgba(0,0,0,0.15),inset_0_3px_6px_rgba(255,255,255,0.4)] ' +
|
||||
"after:absolute after:top-[3px] after:left-1 after:size-[5px] after:rounded-full after:bg-white/45 after:content-['']";
|
||||
|
||||
interface WallMasonryProps {
|
||||
walls: Wall[];
|
||||
}
|
||||
|
||||
export default ({ walls }: WallMasonryProps) => {
|
||||
return (
|
||||
<Masonry breakpointCols={breakpointColumnsObj} className="masonry-grid wall-masonry-grid" columnClassName="masonry-grid_column">
|
||||
<Masonry
|
||||
breakpointCols={breakpointColumnsObj}
|
||||
className="masonry-grid -ml-5 sm:ml-[-15px] md:-ml-5 lg:-ml-6"
|
||||
columnClassName="masonry-grid_column pl-3"
|
||||
>
|
||||
{walls.map((item, index) => {
|
||||
const bgColor = colorMap[item.color] || 'bg-[#ffe3944d]';
|
||||
const theme = colorThemes[item.color] || defaultTheme;
|
||||
const rotate = rotations[index % rotations.length];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`
|
||||
group relative flex flex-col p-5 rounded-2xl
|
||||
${bgColor}
|
||||
backdrop-blur-sm
|
||||
border border-white/30 dark:border-gray-700/30
|
||||
cursor-pointer overflow-hidden mb-3 animate-fade-in-up
|
||||
hover:scale-105 hover:-translate-y-1 transition-transform
|
||||
transform-gpu
|
||||
`}
|
||||
className={msgCardClass}
|
||||
style={{
|
||||
animationDelay: `${index * 50}ms`,
|
||||
rotate: `${rotate}deg`,
|
||||
background: theme.bg,
|
||||
animationDelay: `${index * 90 + 300}ms`,
|
||||
}}
|
||||
>
|
||||
{/* 卡片装饰边框 */}
|
||||
<div className="absolute inset-0 rounded-2xl border-2 border-white/20 dark:border-gray-600/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
|
||||
{/* 顶部信息 */}
|
||||
<div className="flex justify-between items-center mb-3 pb-3 border-b border-white/20 dark:border-gray-700/30">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 font-medium">{dayjs(+item.createTime!).format('YYYY-MM-DD HH:mm')}</span>
|
||||
<span className="px-2.5 py-1 text-xs font-semibold backdrop-blur-sm text-gray-700 dark:text-white bg-white/60 dark:bg-gray-800/60 rounded-full transition-transform group-hover:scale-105">{item.cate.name}</span>
|
||||
<div className={pushpinClass} style={{ background: theme.pushpin }} />
|
||||
<span className={`mb-3 inline-block rounded-full px-2.5 py-0.5 text-xs font-medium ${theme.badge}`}>{item.cate.name}</span>
|
||||
<p className="hide_sliding mb-5 min-h-16 overflow-auto text-sm leading-relaxed text-stone-800">{item.content}</p>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-medium text-stone-600 dark:text-black">{item.name ? item.name : '匿名'}</span>
|
||||
<span className="text-stone-400 dark:text-stone-600">{getRelativeTimeLabel(item.createTime)}</span>
|
||||
</div>
|
||||
|
||||
{/* 留言内容 */}
|
||||
<div className="flex-1 hide_sliding overflow-auto min-h-20 text-sm md:text-base leading-relaxed text-gray-800 dark:text-gray-200 my-3 px-1">{item.content}</div>
|
||||
|
||||
{/* 底部署名 */}
|
||||
<div className="flex justify-end items-center mt-4 pt-3 border-t border-white/20 dark:border-gray-700/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">—</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 transition-transform group-hover:translate-x-[-2px]">{item.name ? item.name : '匿名'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover 时的光效 */}
|
||||
<div className="absolute inset-0 bg-linear-to-br from-white/0 via-white/0 to-white/20 dark:from-transparent dark:via-transparent dark:to-white/5 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
46
src/app/wall/wall.css
Normal file
46
src/app/wall/wall.css
Normal file
@@ -0,0 +1,46 @@
|
||||
@keyframes wall-shimmer {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wall-float-dot {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-14px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wall-card-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
translate: 0 55px;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wall-grain-overlay {
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.wall-paper-texture::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
opacity: 0.035;
|
||||
pointer-events: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23p)' opacity='0.5'/%3E%3C/svg%3E");
|
||||
}
|
||||
@@ -26,7 +26,7 @@ const toneStyles: Record<ActionCardTone, { card: string; divider: string }> = {
|
||||
card: cn(
|
||||
'border-rose-100/90 bg-linear-to-br from-rose-50/80 via-white to-orange-50/40',
|
||||
'shadow-[0_4px_20px_-4px_rgba(244,63,94,0.15)]',
|
||||
'dark:border-rose-500/15 dark:from-rose-500/8 dark:via-white/[0.02] dark:to-orange-500/5 dark:shadow-none',
|
||||
'dark:border-rose-500/15 dark:from-rose-500/8 dark:via-white/2 dark:to-orange-500/5 dark:shadow-none',
|
||||
),
|
||||
divider: 'via-rose-200/80 dark:via-rose-500/25',
|
||||
},
|
||||
@@ -35,7 +35,7 @@ const toneStyles: Record<ActionCardTone, { card: string; divider: string }> = {
|
||||
'border-slate-200/90 bg-linear-to-br from-slate-50/90 via-white to-blue-50/40',
|
||||
'shadow-[0_4px_20px_-4px_rgba(83,157,253,0.12)]',
|
||||
'hover:border-primary/30 hover:shadow-[0_4px_24px_-4px_rgba(83,157,253,0.22)]',
|
||||
'dark:border-white/10 dark:from-white/[0.03] dark:via-transparent dark:to-primary/5 dark:shadow-none',
|
||||
'dark:border-white/10 dark:from-white/3 dark:via-transparent dark:to-primary/5 dark:shadow-none',
|
||||
),
|
||||
divider: 'via-blue-200/70 dark:via-primary/25',
|
||||
},
|
||||
|
||||
@@ -39,4 +39,28 @@ export function dayFormat(timestamp: number | string) {
|
||||
} else {
|
||||
return target.fromNow();
|
||||
}
|
||||
}
|
||||
|
||||
export function getRelativeTimeLabel(ts: string | number | Date | undefined): string {
|
||||
if (ts == null) return '';
|
||||
const now = dayjs();
|
||||
const then = dayjs(+ts);
|
||||
const diffDays = now.startOf('day').diff(then.startOf('day'), 'day');
|
||||
const diffMonths = now.diff(then, 'month', true);
|
||||
const diffYears = now.diff(then, 'year', true);
|
||||
|
||||
if (diffDays === 0) return `今天 ${then.format('HH:mm')}`;
|
||||
if (diffDays === 1) return `昨天 ${then.format('HH:mm')}`;
|
||||
if (diffDays >= 2 && diffDays <= 6) return `${diffDays}天前`;
|
||||
if (diffDays >= 7 && diffDays <= 13) return '一周前';
|
||||
if (diffDays >= 14 && diffDays <= 20) return '两周前';
|
||||
if (diffDays >= 21 && diffDays <= 27) return '三周前';
|
||||
if (diffMonths >= 1 && diffMonths < 2) return '一月前';
|
||||
if (diffMonths >= 2 && diffMonths < 3) return '两月前';
|
||||
if (diffMonths >= 3 && diffMonths < 6) return '三月前';
|
||||
if (diffMonths >= 6 && diffMonths < 12) return '半年前';
|
||||
if (diffYears >= 1 && diffYears < 2) return '一年前';
|
||||
if (diffYears >= 2 && diffYears < 3) return '两年前';
|
||||
if (diffYears >= 3) return `${Math.floor(diffYears)}年前`;
|
||||
return '';
|
||||
}
|
||||
Reference in New Issue
Block a user