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秒后自动收起品牌名称
useEffect(() => {
brandCollapseTimer.current = setTimeout(() => {

View File

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

View File

@@ -41,6 +41,7 @@ export function ProviderNav() {
const [activeProvider, setActiveProvider] = useState<ProviderId | null>(null);
const contentScrollerRef = useRef<HTMLElement | null>(null);
const navListRef = useRef<HTMLDivElement | null>(null);
const navContainerRef = useRef<HTMLDivElement | null>(null);
const itemRefs = useRef<Record<ProviderId, HTMLButtonElement | null>>({
gemini: null,
codex: null,
@@ -170,6 +171,31 @@ export function ProviderNav() {
updateIndicator(activeProvider);
}, [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 container = getScrollContainer();
const element = document.getElementById(`provider-${providerId}`);
@@ -204,7 +230,7 @@ export function ProviderNav() {
}, [activeProvider, shouldShow, updateIndicator]);
const navContent = (
<div className={styles.navContainer}>
<div className={styles.navContainer} ref={navContainerRef}>
<div className={styles.navList} ref={navListRef}>
<div
className={[

View File

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

View File

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

View File

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