refactor(Popover, CommentForm, Header, SidebarNav): 更新组件样式以增强一致性和交互效果

- 在 Popover 组件中新增 'top-end' 和 'bottom-end' 位置选项,优化内容显示。
- 调整 CommentForm 组件的布局,改善表情按钮的交互体验。
- 更新 Header 组件的高度和样式,确保在不同屏幕尺寸下的响应式表现。
- 修改 SidebarNav 组件的子菜单位置,提升视觉一致性。
This commit is contained in:
刘宇阳
2026-07-07 15:53:26 +08:00
parent 0494906db9
commit 1cea3e2f03
5 changed files with 61 additions and 49 deletions

View File

@@ -5,7 +5,7 @@ import { useEffect, useRef, useState, type ReactNode } from 'react';
export interface PopoverProps {
isOpen?: boolean;
onOpenChange?: (open: boolean) => void;
placement?: 'top' | 'bottom' | 'left' | 'right';
placement?: 'top' | 'top-end' | 'bottom' | 'bottom-end' | 'left' | 'right';
children: ReactNode;
}
@@ -20,7 +20,9 @@ export interface PopoverContentProps {
const placementCls = {
top: 'bottom-full left-1/2 mb-2 -translate-x-1/2',
'top-end': 'bottom-full right-0 mb-2',
bottom: 'top-full left-1/2 mt-2 -translate-x-1/2',
'bottom-end': 'top-full right-0 mt-2',
left: 'right-full top-1/2 mr-2 -translate-y-1/2',
right: 'left-full top-1/2 ml-2 -translate-y-1/2',
};
@@ -42,20 +44,26 @@ export function Popover({ isOpen, onOpenChange, placement = 'bottom', children }
let trigger: ReactNode = null;
let content: ReactNode = null;
let contentClassName = '';
for (const child of Array.isArray(children) ? children : [children]) {
if (!child || typeof child !== 'object' || !('type' in child)) continue;
const el = child as React.ReactElement<{ children?: ReactNode }>;
const el = child as React.ReactElement<{ children?: ReactNode; className?: string }>;
if (el.type === PopoverTrigger) trigger = el.props.children;
if (el.type === PopoverContent) content = el.props.children;
if (el.type === PopoverContent) {
content = el.props.children;
contentClassName = el.props.className ?? '';
}
}
return (
<div ref={rootRef} className="relative inline-flex">
<span onClick={() => setOpen(!open)}>{trigger}</span>
<span onClick={() => setOpen(!open)} className="inline-flex">
{trigger}
</span>
{open && content ? (
<div
className={`absolute z-50 rounded-xl border border-neutral-200/80 bg-surface p-2 shadow-panel dark:border-neutral-700/60 ${placementCls[placement]}`}
className={`absolute z-50 w-max shrink-0 rounded-xl border border-neutral-200/80 bg-surface p-2 shadow-panel dark:border-neutral-700/60 ${placementCls[placement]} ${contentClassName}`}
>
{content}
</div>

View File

@@ -165,51 +165,55 @@ const CommentForm = ({ articleId }: Props) => {
<div className="mt-[70px]">
<div className="title relative top-0 left-0 w-full h-px mb-10 bg-[#f7f7f7] dark:bg-black-b "></div>
<form className="flex flex-wrap justify-between mt-4 space-y-2 text-xs xs:text-sm" onSubmit={handleSubmit(onSubmit)}>
<div className="w-full relative">
<textarea
{...register('content', { required: '请输入内容' })}
placeholder={placeholder}
className="form-control w-full p-4 min-h-36"
ref={(e) => {
register('content').ref(e);
contentRef.current = e;
}}
/>
<span className="text-red-400 text-sm pl-3">{errors.content?.message}</span>
<form className="mt-4 space-y-2 text-xs xs:text-sm" onSubmit={handleSubmit(onSubmit)}>
<div className="w-full">
<div className="relative w-full">
<textarea
{...register('content', { required: '请输入内容' })}
placeholder={placeholder}
className="form-control w-full p-4 pb-12 min-h-36"
ref={(e) => {
register('content').ref(e);
contentRef.current = e;
}}
/>
{/* 表情按钮与面板 */}
<div className="absolute bottom-5 right-5">
<Popover placement="bottom" isOpen={isEmojiOpen} onOpenChange={setIsEmojiOpen}>
<PopoverTrigger>
<button type="button" className="py-1 px-2 text-2xl rounded-md hover:bg-gray-50 border border-transparent hover:border-gray-200">
😀
</button>
</PopoverTrigger>
<div className="absolute right-3 bottom-3 z-10">
<Popover placement="top-end" isOpen={isEmojiOpen} onOpenChange={setIsEmojiOpen}>
<PopoverTrigger>
<button
type="button"
className="cursor-pointer rounded-md border border-transparent px-2 py-1 text-2xl hover:border-gray-200 hover:bg-gray-50 dark:hover:bg-black-b"
>
😀
</button>
</PopoverTrigger>
<PopoverContent>
<div className="max-w-96 z-50">
<PopoverContent className="p-0">
<EmojiBag onEmojiSelect={handleEmojiSelect} />
</div>
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
</div>
</div>
<span className="text-red-400 text-sm pl-3">{errors.content?.message}</span>
</div>
<div className="flex flex-col w-[32%]">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
<div className="flex flex-col">
<input type="text" className="form-control 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%]">
<div className="flex flex-col">
<input type="text" className="form-control 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%]">
<div className="flex flex-col">
<input type="text" className="form-control 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>
<div className="w-full flex flex-col">
<input type="text" className="form-control w-full h-9 pl-4" placeholder="你的站点(选填)" {...register('url', { pattern: { value: /^https?:\/\//, message: '请输入正确的网站链接' } })} />

View File

@@ -283,7 +283,7 @@ export default ({ onEmojiSelect, className = '' }: Props) => {
};
return (
<div className={`${className}`}>
<div className={`w-80 ${className}`}>
{/* 搜索栏 */}
<div className="p-4 border-b border-gray-100">
<div className="relative">

View File

@@ -36,7 +36,7 @@ export default ({ list, open, onClose }: Props) => {
)}
<Show is={!!one.children.length}>
<ul className="overflow-hidden top-[50px] w-full rounded-md">
<ul className="overflow-hidden top-[60px] w-full rounded-md">
{one.children?.map((two) => (
<li key={two.id} className="group/two">
<Link href={getCateNavHref(two)} target={getCateNavTarget(two.type)} rel={getCateNavRel(two.type)} className="inline-block w-full p-2.5 pl-10 text-[15px] box-border text-[#666] dark:text-[#8c9ab1] hover:text-primary!" onClick={onClose}>
@@ -63,7 +63,7 @@ export default ({ list, open, onClose }: Props) => {
)}
<Show is={!!one.children.length}>
<ul className="overflow-hidden top-[50px] w-full rounded-md">
<ul className="overflow-hidden top-[60px] w-full rounded-md">
{one.children?.map((two) => (
<li key={two.id} className="group/two">
<Link href={getCateNavHref(two)} target={getCateNavTarget(two.type)} rel={getCateNavRel(two.type)} className="inline-block w-full p-2.5 pl-10 text-[15px] box-border text-[#666] dark:text-[#8c9ab1] hover:text-primary!" onClick={onClose}>
@@ -90,7 +90,7 @@ export default ({ list, open, onClose }: Props) => {
)}
<Show is={!!one.children.length}>
<ul className="overflow-hidden top-[50px] w-full rounded-md">
<ul className="overflow-hidden top-[60px] w-full rounded-md">
{one.children?.map((two) => (
<li key={two.id} className="group/two">
<Link href={getCateNavHref(two)} target={getCateNavTarget(two.type)} rel={getCateNavRel(two.type)} className="inline-block w-full p-2.5 pl-10 text-[15px] box-border text-[#666] dark:text-[#8c9ab1] hover:text-primary!" onClick={onClose}>

View File

@@ -85,8 +85,8 @@ export default () => {
return (
<>
<div className={`header fixed inset-x-0 top-0 w-full h-[50px] z-50 overflow-visible 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%)] ${isPathSty || isScrolled ? 'bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(44,51,62,0.7)] backdrop-blur-md border-b dark:border-[#2b333e] after:h-5! after:transition-height]' : 'border-transparent'}`}>
<div className="grid grid-cols-[auto_1fr_auto] grid-rows-[50px] h-[50px] md:flex md:justify-between items-center w-full max-w-[1200px] mx-auto px-4 md:px-6 lg:px-0!">
<div className={`header fixed inset-x-0 top-0 w-full h-[60px] z-50 overflow-visible 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%)] ${isPathSty || isScrolled ? 'bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(44,51,62,0.7)] backdrop-blur-md border-b dark:border-[#2b333e] after:h-5! after:transition-height]' : 'border-transparent'}`}>
<div className="grid grid-cols-[auto_1fr_auto] grid-rows-[60px] h-[60px] md:flex md:justify-between items-center w-full max-w-[1200px] mx-auto px-4 md:px-6 lg:px-0!">
<div
className="md:hidden group flex items-center justify-center size-9 shrink-0 rounded-full transition-colors cursor-pointer hover:bg-[#e9edf4] dark:hover:bg-[#455162]"
onClick={() => setIsOpenSidebarNav(true)}
@@ -94,15 +94,15 @@ export default () => {
<LuMenu className={`text-xl transition-colors ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white group-hover:text-[#333] dark:group-hover:text-white'}`} />
</div>
<div className="flex h-[50px] items-center justify-center md:justify-start min-w-0 md:flex-1 overflow-visible">
<div className="flex h-[60px] items-center justify-center md:justify-start min-w-0 md:flex-1 overflow-visible">
{/* logo */}
<Link href="/" className="flex items-center h-[50px] text-[15px]">
<Link href="/" className="flex items-center h-[60px] text-[15px]">
{isDark ? <img src={theme?.dark_logo} alt="Logo" className="h-10 w-auto object-contain pr-0 md:pr-5 hover:scale-90 transition-[scale]" /> : <img src={isPathSty || isScrolled ? theme?.light_logo : theme?.dark_logo} alt="Logo" className="h-10 w-auto object-contain pr-0 md:pr-5 hover:scale-90 transition-[scale]" />}
</Link>
<ul className="hidden md:flex items-center h-[50px] overflow-visible">
<ul className="hidden md:flex items-center h-[60px] overflow-visible">
<li className="group/one relative">
<Link href="/" className={`flex items-center h-[50px] px-5 text-[15px] group-hover/one:text-primary! ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
<Link href="/" className={`flex items-center h-[60px] px-5 text-[15px] group-hover/one:text-primary! ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
💎
</Link>
</li>
@@ -114,12 +114,12 @@ export default () => {
{one.type === 'cate' && (
<li className="group/one relative">
{one.children.length ? (
<span className={`flex items-center h-[50px] px-5 text-[15px] whitespace-nowrap group-hover/one:text-primary! cursor-default ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
<span className={`flex items-center h-[60px] px-5 text-[15px] whitespace-nowrap group-hover/one:text-primary! cursor-default ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
{one.icon} {one.name}
<IoIosArrowDown className="ml-2 transition-[rotate] duration-200 group-hover/one:rotate-180" />
</span>
) : (
<Link href={getCateNavHref(one)} target={getCateNavTarget(one.type)} rel={getCateNavRel(one.type)} className={`flex items-center h-[50px] px-5 text-[15px] whitespace-nowrap group-hover/one:text-primary! ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
<Link href={getCateNavHref(one)} target={getCateNavTarget(one.type)} rel={getCateNavRel(one.type)} className={`flex items-center h-[60px] px-5 text-[15px] whitespace-nowrap group-hover/one:text-primary! ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
{one.icon} {one.name}
</Link>
)}
@@ -142,12 +142,12 @@ export default () => {
{one.type === 'page' && (
<li className="group/one relative">
{one.children?.length ? (
<span className={`flex items-center h-[50px] px-10 text-[15px] whitespace-nowrap group-hover/one:text-primary! cursor-default ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
<span className={`flex items-center h-[60px] px-10 text-[15px] whitespace-nowrap group-hover/one:text-primary! cursor-default ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
{one.icon} {one.name}
<IoIosArrowDown className="ml-2 transition-[rotate] duration-200 group-hover/one:rotate-180" />
</span>
) : (
<Link href={getCateNavHref(one)} target={getCateNavTarget(one.type)} rel={getCateNavRel(one.type)} className={`flex items-center h-[50px] px-10 text-[15px] whitespace-nowrap group-hover/one:text-primary! ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
<Link href={getCateNavHref(one)} target={getCateNavTarget(one.type)} rel={getCateNavRel(one.type)} className={`flex items-center h-[60px] px-10 text-[15px] whitespace-nowrap group-hover/one:text-primary! ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
{one.icon} {one.name}
</Link>
)}
@@ -171,12 +171,12 @@ export default () => {
{one.type === 'nav' && (
<li className="group/one relative">
{one.children?.length ? (
<span className={`flex items-center h-[50px] px-10 text-[15px] whitespace-nowrap group-hover/one:text-primary! cursor-default ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
<span className={`flex items-center h-[60px] px-10 text-[15px] whitespace-nowrap group-hover/one:text-primary! cursor-default ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
{one.icon} {one.name}
<IoIosArrowDown className="ml-2 transition-[rotate] duration-200 group-hover/one:rotate-180" />
</span>
) : (
<Link href={getCateNavHref(one)} target={getCateNavTarget(one.type)} rel={getCateNavRel(one.type)} className={`flex items-center h-[50px] px-10 text-[15px] whitespace-nowrap group-hover/one:text-primary! ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
<Link href={getCateNavHref(one)} target={getCateNavTarget(one.type)} rel={getCateNavRel(one.type)} className={`flex items-center h-[60px] px-10 text-[15px] whitespace-nowrap group-hover/one:text-primary! ${isPathSty || isScrolled ? 'text-[#333] dark:text-white' : 'text-white'}`}>
{one.icon} {one.name}
</Link>
)}