mirror of
https://github.com/LiuYuYang01/ThriveX-Blog.git
synced 2026-09-03 06:24:24 +08:00
refactor(Popover, CommentForm, Header, SidebarNav): 更新组件样式以增强一致性和交互效果
- 在 Popover 组件中新增 'top-end' 和 'bottom-end' 位置选项,优化内容显示。 - 调整 CommentForm 组件的布局,改善表情按钮的交互体验。 - 更新 Header 组件的高度和样式,确保在不同屏幕尺寸下的响应式表现。 - 修改 SidebarNav 组件的子菜单位置,提升视觉一致性。
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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: '请输入正确的网站链接' } })} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user