fix(ui): center Config Panel action bar and move ProviderNav to bottom

This commit is contained in:
LTbinglingfeng
2026-02-06 03:13:13 +08:00
parent adcf0b6582
commit f53d333198
6 changed files with 104 additions and 58 deletions

View File

@@ -241,6 +241,37 @@ export function MainLayout() {
}; };
}, []); }, []);
// 将主内容区的中心点写入 CSS 变量,供底部浮层(如配置面板操作栏)对齐到内容区而非整窗
useLayoutEffect(() => {
const updateContentCenter = () => {
const el = contentRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
document.documentElement.style.setProperty('--content-center-x', `${centerX}px`);
};
updateContentCenter();
const resizeObserver =
typeof ResizeObserver !== 'undefined' && contentRef.current
? new ResizeObserver(updateContentCenter)
: null;
if (resizeObserver && contentRef.current) {
resizeObserver.observe(contentRef.current);
}
window.addEventListener('resize', updateContentCenter);
return () => {
if (resizeObserver) {
resizeObserver.disconnect();
}
window.removeEventListener('resize', updateContentCenter);
};
}, []);
// 5秒后自动收起品牌名称 // 5秒后自动收起品牌名称
useEffect(() => { useEffect(() => {
brandCollapseTimer.current = setTimeout(() => { brandCollapseTimer.current = setTimeout(() => {

View File

@@ -2,25 +2,34 @@
.navContainer { .navContainer {
position: fixed; position: fixed;
right: 24px; left: var(--content-center-x, 50%);
top: 50%; bottom: calc(12px + env(safe-area-inset-bottom));
transform: translateY(-50%); transform: translateX(-50%);
z-index: 50; z-index: 50;
pointer-events: auto; pointer-events: auto;
width: fit-content;
max-width: calc(100vw - 24px);
} }
.navList { .navList {
position: relative; position: relative;
display: flex; display: inline-flex;
flex-direction: column; flex-direction: row;
gap: 8px; gap: 6px;
padding: 12px 8px; padding: 10px 12px;
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 16px; border-radius: 999px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
overflow-x: auto;
scrollbar-width: none;
max-width: inherit;
&::-webkit-scrollbar {
display: none;
}
} }
.indicator { .indicator {
@@ -29,7 +38,7 @@
left: 0; left: 0;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
border-radius: 10px; border-radius: 999px;
background: rgba(59, 130, 246, 0.15); background: rgba(59, 130, 246, 0.15);
box-shadow: inset 0 0 0 2px var(--primary-color); box-shadow: inset 0 0 0 2px var(--primary-color);
transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1), transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1),
@@ -58,9 +67,10 @@
padding: 0; padding: 0;
border: none; border: none;
background: transparent; background: transparent;
border-radius: 10px; border-radius: 999px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease, transform 0.15s ease; transition: background-color 0.2s ease, transform 0.15s ease;
flex: 0 0 auto;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.06); background: rgba(0, 0, 0, 0.06);
@@ -80,8 +90,8 @@
} }
.icon { .icon {
width: 28px; width: 24px;
height: 28px; height: 24px;
object-fit: contain; object-fit: contain;
} }
@@ -110,42 +120,20 @@
} }
} }
// 小屏幕改为底部横向浮层 // 小屏幕进一步收紧尺寸
@media (max-width: 1200px) { @media (max-width: 1200px) {
.navContainer { .navContainer {
top: auto; max-width: calc(100vw - 16px);
right: auto;
left: 50%;
bottom: calc(12px + env(safe-area-inset-bottom));
transform: translateX(-50%);
width: fit-content;
max-width: calc(100vw - 24px);
} }
.navList { .navList {
display: inline-flex;
flex-direction: row;
gap: 6px; gap: 6px;
padding: 8px 10px; padding: 8px 10px;
border-radius: 999px;
overflow-x: auto;
scrollbar-width: none;
max-width: inherit;
&::-webkit-scrollbar {
display: none;
}
}
.indicator {
border-radius: 999px;
} }
.navItem { .navItem {
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: 999px;
flex: 0 0 auto;
} }
.icon { .icon {

View File

@@ -41,6 +41,7 @@ export function ProviderNav() {
const [activeProvider, setActiveProvider] = useState<ProviderId | null>(null); const [activeProvider, setActiveProvider] = useState<ProviderId | null>(null);
const contentScrollerRef = useRef<HTMLElement | null>(null); const contentScrollerRef = useRef<HTMLElement | null>(null);
const navListRef = useRef<HTMLDivElement | null>(null); const navListRef = useRef<HTMLDivElement | null>(null);
const navContainerRef = useRef<HTMLDivElement | null>(null);
const itemRefs = useRef<Record<ProviderId, HTMLButtonElement | null>>({ const itemRefs = useRef<Record<ProviderId, HTMLButtonElement | null>>({
gemini: null, gemini: null,
codex: null, codex: null,
@@ -170,6 +171,31 @@ export function ProviderNav() {
updateIndicator(activeProvider); updateIndicator(activeProvider);
}, [activeProvider, shouldShow, updateIndicator]); }, [activeProvider, shouldShow, updateIndicator]);
// Expose overlay height to the page, so it can reserve bottom padding and avoid being covered.
useLayoutEffect(() => {
if (!shouldShow) return;
const el = navContainerRef.current;
if (!el) return;
const updateHeight = () => {
const height = el.getBoundingClientRect().height;
document.documentElement.style.setProperty('--provider-nav-height', `${height}px`);
};
updateHeight();
window.addEventListener('resize', updateHeight);
const ro = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(updateHeight);
ro?.observe(el);
return () => {
ro?.disconnect();
window.removeEventListener('resize', updateHeight);
document.documentElement.style.removeProperty('--provider-nav-height');
};
}, [shouldShow]);
const scrollToProvider = (providerId: ProviderId) => { const scrollToProvider = (providerId: ProviderId) => {
const container = getScrollContainer(); const container = getScrollContainer();
const element = document.getElementById(`provider-${providerId}`); const element = document.getElementById(`provider-${providerId}`);
@@ -204,7 +230,7 @@ export function ProviderNav() {
}, [activeProvider, shouldShow, updateIndicator]); }, [activeProvider, shouldShow, updateIndicator]);
const navContent = ( const navContent = (
<div className={styles.navContainer}> <div className={styles.navContainer} ref={navContainerRef}>
<div className={styles.navList} ref={navListRef}> <div className={styles.navList} ref={navListRef}>
<div <div
className={[ className={[

View File

@@ -27,10 +27,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $spacing-xl; gap: $spacing-xl;
padding-bottom: calc(
@include mobile { var(--provider-nav-height, 60px) + 12px + env(safe-area-inset-bottom) + #{$spacing-md}
padding-bottom: calc(72px + env(safe-area-inset-bottom)); );
}
} }
.section { .section {

View File

@@ -6,7 +6,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
padding-bottom: calc(var(--config-action-bar-height, 0px) + #{$spacing-lg}); padding-bottom: calc(
var(--config-action-bar-height, 0px) + 16px + env(safe-area-inset-bottom) + #{$spacing-md}
);
} }
.pageTitle { .pageTitle {
@@ -315,7 +317,7 @@
.floatingActionContainer { .floatingActionContainer {
position: fixed; position: fixed;
left: 50%; left: var(--content-center-x, 50%);
bottom: calc(16px + env(safe-area-inset-bottom)); bottom: calc(16px + env(safe-area-inset-bottom));
transform: translateX(-50%); transform: translateX(-50%);
z-index: 50; z-index: 50;
@@ -327,8 +329,8 @@
.floatingActionList { .floatingActionList {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
padding: 10px 12px; padding: 8px 10px;
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
@@ -345,13 +347,13 @@
} }
.floatingStatus { .floatingStatus {
font-size: 12px; font-size: 11px;
font-weight: 600; font-weight: 600;
padding: 6px 10px; padding: 5px 8px;
border-radius: 999px; border-radius: 999px;
background: rgba(0, 0, 0, 0.06); background: rgba(0, 0, 0, 0.06);
text-align: center; text-align: center;
max-width: min(360px, 52vw); max-width: min(280px, 46vw);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -363,8 +365,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 44px; width: 40px;
height: 44px; height: 40px;
border-radius: 999px; border-radius: 999px;
cursor: pointer; cursor: pointer;
color: var(--text-primary); color: var(--text-primary);
@@ -388,10 +390,10 @@
.dirtyDot { .dirtyDot {
position: absolute; position: absolute;
top: 9px; top: 8px;
right: 9px; right: 8px;
width: 8px; width: 7px;
height: 8px; height: 7px;
border-radius: 999px; border-radius: 999px;
background: #f59e0b; background: #f59e0b;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.25); box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.25);
@@ -431,8 +433,8 @@
} }
.floatingActionButton { .floatingActionButton {
width: 40px; width: 38px;
height: 40px; height: 38px;
flex: 0 0 auto; flex: 0 0 auto;
} }
} }

View File

@@ -300,7 +300,7 @@ export function ConfigPage() {
title={t('config_management.reload')} title={t('config_management.reload')}
aria-label={t('config_management.reload')} aria-label={t('config_management.reload')}
> >
<IconRefreshCw size={18} /> <IconRefreshCw size={16} />
</button> </button>
<button <button
type="button" type="button"
@@ -310,7 +310,7 @@ export function ConfigPage() {
title={t('config_management.save')} title={t('config_management.save')}
aria-label={t('config_management.save')} aria-label={t('config_management.save')}
> >
<IconCheck size={18} /> <IconCheck size={16} />
{isDirty && <span className={styles.dirtyDot} aria-hidden="true" />} {isDirty && <span className={styles.dirtyDot} aria-hidden="true" />}
</button> </button>
</div> </div>