mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 18:50:49 +08:00
fix(ui): center Config Panel action bar and move ProviderNav to bottom
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user