feat:PC 端搜索框样式

This commit is contained in:
hzm
2026-02-14 19:58:45 +08:00
parent 49d820d1f1
commit 7e9c3e4394
2 changed files with 240 additions and 99 deletions

View File

@@ -101,6 +101,7 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 16px;
} }
.brand { .brand {
@@ -108,6 +109,92 @@ body {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-weight: 600; font-weight: 600;
flex: 1 1 auto;
min-width: 0;
transition: flex 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.brand.search-focused-sibling {
flex: 0 0 auto;
}
.navbar-add-fund {
flex: 0 1 auto;
width: 100%;
min-width: 260px;
max-width: 280px;
padding: 0;
background: transparent;
border: none;
box-shadow: none;
backdrop-filter: none;
transition: max-width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.navbar-add-fund.search-focused {
max-width: 800px;
flex: 1;
}
.navbar-add-fund .form {
width: 100%;
}
.navbar-add-fund .search-input-wrapper {
position: relative;
flex: 1;
}
.navbar-add-fund .input {
width: 100%;
border-radius: 999px;
background: rgba(11, 18, 32, 0.9);
}
.navbar-search-icon {
position: absolute;
left: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--muted);
pointer-events: none;
z-index: 2;
}
.navbar-input-shell {
display: flex;
align-items: center;
gap: 8px;
padding-left: 40px;
padding-right: 12px;
padding-top: 6px !important;
padding-bottom: 6px !important;
min-height: 44px;
flex-wrap: wrap;
position: relative;
z-index: 1;
height: auto;
}
.navbar-input-field {
flex: 1;
min-width: 120px;
height: 24px;
border: none;
outline: none;
background: transparent;
color: var(--text);
font-size: 14px;
margin-left: 20px;
}
.navbar-input-field::placeholder {
color: var(--muted);
}
.navbar-add-fund .button {
display: none;
} }
.content { .content {
@@ -116,7 +203,7 @@ body {
@media (max-width: 640px) { @media (max-width: 640px) {
.content { .content {
padding-top: 90px; padding-top: 140px;
} }
.navbar { .navbar {
@@ -127,10 +214,20 @@ body {
border-top: none; border-top: none;
border-left: none; border-left: none;
border-right: none; border-right: none;
flex-wrap: wrap;
gap: 12px;
} }
.add-fund-section { .navbar-add-fund {
margin-top: 60px; order: 3;
width: 100%;
min-width: 0;
max-width: none;
margin-top: 8px;
}
.navbar-add-fund .button {
display: inline-flex;
} }
} }
@@ -142,7 +239,8 @@ body {
.input { .input {
flex: 1; flex: 1;
height: 44px; min-height: 44px;
height: auto;
padding: 0 14px; padding: 0 14px;
border-radius: 12px; border-radius: 12px;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -394,6 +492,14 @@ input[type="number"] {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
flex: 1 1 auto;
justify-content: flex-end;
min-width: 0;
transition: flex 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.actions.search-focused-sibling {
flex: 0 0 auto;
} }
.icon-button { .icon-button {
@@ -659,6 +765,14 @@ input[type="number"] {
.filter-bar { .filter-bar {
transition: all 0.3s ease; transition: all 0.3s ease;
position: sticky;
top: 90px;
z-index: 40;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(12px);
padding: 8px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -972,6 +1086,7 @@ input[type="number"] {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
margin-left: 24px;
} }
.fund-chip { .fund-chip {

View File

@@ -1938,6 +1938,7 @@ export default function HomePage() {
// 搜索相关状态 // 搜索相关状态
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [searchResults, setSearchResults] = useState([]); const [searchResults, setSearchResults] = useState([]);
const [selectedFunds, setSelectedFunds] = useState([]); const [selectedFunds, setSelectedFunds] = useState([]);
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
@@ -1946,6 +1947,22 @@ export default function HomePage() {
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const [addResultOpen, setAddResultOpen] = useState(false); const [addResultOpen, setAddResultOpen] = useState(false);
const [addFailures, setAddFailures] = useState([]); const [addFailures, setAddFailures] = useState([]);
// 动态计算 Navbar 高度
const navbarRef = useRef(null);
const [navbarHeight, setNavbarHeight] = useState(0);
useEffect(() => {
const updateNavbarHeight = () => {
if (navbarRef.current) {
setNavbarHeight(navbarRef.current.offsetHeight);
}
};
updateNavbarHeight();
window.addEventListener('resize', updateNavbarHeight);
return () => window.removeEventListener('resize', updateNavbarHeight);
}, []);
const [holdingModal, setHoldingModal] = useState({ open: false, fund: null }); const [holdingModal, setHoldingModal] = useState({ open: false, fund: null });
const [actionModal, setActionModal] = useState({ open: false, fund: null }); const [actionModal, setActionModal] = useState({ open: false, fund: null });
const [tradeModal, setTradeModal] = useState({ open: false, fund: null, type: 'buy' }); // type: 'buy' | 'sell' const [tradeModal, setTradeModal] = useState({ open: false, fund: null, type: 'buy' }); // type: 'buy' | 'sell'
@@ -3698,9 +3715,9 @@ export default function HomePage() {
return ( return (
<div className="container content"> <div className="container content">
<Announcement /> <Announcement />
<div className="navbar glass"> <div className="navbar glass" ref={navbarRef}>
{refreshing && <div className="loading-bar"></div>} {refreshing && <div className="loading-bar"></div>}
<div className="brand"> <div className={`brand ${(isSearchFocused || selectedFunds.length > 0) ? 'search-focused-sibling' : ''}`}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="var(--accent)" strokeWidth="2" /> <circle cx="12" cy="12" r="10" stroke="var(--accent)" strokeWidth="2" />
<path d="M5 14c2-4 7-6 14-5" stroke="var(--primary)" strokeWidth="2" /> <path d="M5 14c2-4 7-6 14-5" stroke="var(--primary)" strokeWidth="2" />
@@ -3736,7 +3753,107 @@ export default function HomePage() {
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
<div className="actions"> <div className={`glass add-fund-section navbar-add-fund ${(isSearchFocused || selectedFunds.length > 0) ? 'search-focused' : ''}`} role="region" aria-label="添加基金">
<div className="search-container" ref={dropdownRef}>
<form className="form" onSubmit={addFund}>
<div className="search-input-wrapper" style={{ flex: 1, gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
<span className="navbar-search-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2" />
<path d="M16.5 16.5L21 21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</span>
<div className="input navbar-input-shell">
{selectedFunds.length > 0 && (
<div className="selected-inline-chips">
{selectedFunds.map(fund => (
<div key={fund.CODE} className="fund-chip">
<span>{fund.NAME}</span>
<button onClick={() => toggleSelectFund(fund)} className="remove-chip">
<CloseIcon width="14" height="14" />
</button>
</div>
))}
</div>
)}
<input
className="navbar-input-field"
placeholder="搜索基金名称或代码..."
value={searchTerm}
onChange={handleSearchInput}
onFocus={() => {
setShowDropdown(true);
setIsSearchFocused(true);
}}
onBlur={() => setIsSearchFocused(false)}
/>
</div>
{isSearching && <div className="search-spinner" />}
</div>
<button
className="button"
type="submit"
disabled={loading || refreshing}
onMouseDown={(e) => e.preventDefault()}
style={{
pointerEvents: refreshing ? 'none' : 'auto',
opacity: refreshing ? 0.6 : 1,
display: (isSearchFocused || selectedFunds.length > 0) ? 'inline-flex' : undefined,
alignItems: 'center',
justifyContent: 'center'
}}
>
{loading ? '添加中…' : '添加'}
</button>
</form>
<AnimatePresence>
{showDropdown && (searchTerm.trim() || searchResults.length > 0) && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="search-dropdown glass"
>
{searchResults.length > 0 ? (
<div className="search-results">
{searchResults.map((fund) => {
const isSelected = selectedFunds.some(f => f.CODE === fund.CODE);
const isAlreadyAdded = funds.some(f => f.code === fund.CODE);
return (
<div
key={fund.CODE}
className={`search-item ${isSelected ? 'selected' : ''} ${isAlreadyAdded ? 'added' : ''}`}
onClick={() => {
if (isAlreadyAdded) return;
toggleSelectFund(fund);
}}
>
<div className="fund-info">
<span className="fund-name">{fund.NAME}</span>
<span className="fund-code muted">#{fund.CODE} | {fund.TYPE}</span>
</div>
{isAlreadyAdded ? (
<span className="added-label">已添加</span>
) : (
<div className="checkbox">
{isSelected && <div className="checked-mark" />}
</div>
)}
</div>
);
})}
</div>
) : searchTerm.trim() && !isSearching ? (
<div className="no-results muted">未找到相关基金</div>
) : null}
</motion.div>
)}
</AnimatePresence>
</div>
{error && <div className="muted" style={{ marginTop: 8, color: 'var(--danger)' }}>{error}</div>}
</div>
<div className={`actions ${(isSearchFocused || selectedFunds.length > 0) ? 'search-focused-sibling' : ''}`}>
{hasUpdate && ( {hasUpdate && (
<div <div
className="badge" className="badge"
@@ -3876,99 +3993,8 @@ export default function HomePage() {
</div> </div>
<div className="grid"> <div className="grid">
<div className="col-12 glass card add-fund-section" role="region" aria-label="添加基金">
<div className="title" style={{ marginBottom: 12 }}>
<PlusIcon width="20" height="20" />
<span>添加基金</span>
<span className="muted">搜索并选择基金支持名称或代码</span>
</div>
<div className="search-container" ref={dropdownRef}>
<form className="form" onSubmit={addFund}>
<div className="search-input-wrapper" style={{ flex: 1, gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
{selectedFunds.length > 0 && (
<div className="selected-inline-chips">
{selectedFunds.map(fund => (
<div key={fund.CODE} className="fund-chip">
<span>{fund.NAME}</span>
<button onClick={() => toggleSelectFund(fund)} className="remove-chip">
<CloseIcon width="14" height="14" />
</button>
</div>
))}
</div>
)}
<input
className="input"
placeholder="搜索基金名称或代码..."
value={searchTerm}
onChange={handleSearchInput}
onFocus={() => setShowDropdown(true)}
/>
{isSearching && <div className="search-spinner" />}
</div>
<button
className="button"
type="submit"
disabled={loading || refreshing}
style={{pointerEvents: refreshing ? 'none' : 'auto', opacity: refreshing ? 0.6 : 1}}
>
{loading ? '添加中…' : '添加'}
</button>
</form>
<AnimatePresence>
{showDropdown && (searchTerm.trim() || searchResults.length > 0) && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="search-dropdown glass"
>
{searchResults.length > 0 ? (
<div className="search-results">
{searchResults.map((fund) => {
const isSelected = selectedFunds.some(f => f.CODE === fund.CODE);
const isAlreadyAdded = funds.some(f => f.code === fund.CODE);
return (
<div
key={fund.CODE}
className={`search-item ${isSelected ? 'selected' : ''} ${isAlreadyAdded ? 'added' : ''}`}
onClick={() => {
if (isAlreadyAdded) return;
toggleSelectFund(fund);
}}
>
<div className="fund-info">
<span className="fund-name">{fund.NAME}</span>
<span className="fund-code muted">#{fund.CODE} | {fund.TYPE}</span>
</div>
{isAlreadyAdded ? (
<span className="added-label">已添加</span>
) : (
<div className="checkbox">
{isSelected && <div className="checked-mark" />}
</div>
)}
</div>
);
})}
</div>
) : searchTerm.trim() && !isSearching ? (
<div className="no-results muted">未找到相关基金</div>
) : null}
</motion.div>
)}
</AnimatePresence>
</div>
{error && <div className="muted" style={{ marginTop: 8, color: 'var(--danger)' }}>{error}</div>}
</div>
<div className="col-12"> <div className="col-12">
<div className="filter-bar" style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}> <div className="filter-bar" style={{ top: navbarHeight + 16, marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
<div className="tabs-container"> <div className="tabs-container">
<div <div
className="tabs-scroll-area" className="tabs-scroll-area"