style(appShell): 优化应用外壳及底部导航样式与结构
- 在 tailwind 配置中添加应用外壳相关的 safelist 类名 - 替换 App.tsx 中应用外壳容器的类名,去除多余的内边距容器 - 在现代和遗留样式文件中新增移动端全屏、桌面居中列的样式规则 - 新增底部导航外层和内层的样式及响应式布局支持 - appShell.ts 中将样式类改为统一的 x8- 前缀类名 - BottomNav 组件使用新的样式类名以匹配样式调整 - Home 页面 ListRow 组件支持可选缩略图展示,增加界面展示丰富度
This commit is contained in:
+3
-5
@@ -22,7 +22,7 @@ import SelectPage from "./pages/SelectPage";
|
|||||||
import CommunityPage from "./pages/CommunityPage";
|
import CommunityPage from "./pages/CommunityPage";
|
||||||
import { AIDrawerProvider } from "./contexts/AIDrawerContext";
|
import { AIDrawerProvider } from "./contexts/AIDrawerContext";
|
||||||
import DesktopAIShell from "./components/DesktopAIShell";
|
import DesktopAIShell from "./components/DesktopAIShell";
|
||||||
import { appShellColumnClass, appShellGutterClass } from "./lib/appShell";
|
import { appShellColumnClass, appShellOuterClass } from "./lib/appShell";
|
||||||
|
|
||||||
const EQPage = lazy(() => import("./pages/EQPage"));
|
const EQPage = lazy(() => import("./pages/EQPage"));
|
||||||
const AIPage = lazy(() => import("./pages/AIPage"));
|
const AIPage = lazy(() => import("./pages/AIPage"));
|
||||||
@@ -75,10 +75,8 @@ function App() {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<Router hook={useHashLocation}>
|
<Router hook={useHashLocation}>
|
||||||
<AIDrawerProvider>
|
<AIDrawerProvider>
|
||||||
<div
|
<div className={appShellOuterClass}>
|
||||||
className={`min-h-dvh bg-[#0d1117] flex justify-center ${appShellGutterClass}`}
|
<div className={appShellColumnClass}>
|
||||||
>
|
|
||||||
<div className={`min-h-dvh ${appShellColumnClass}`}>
|
|
||||||
<DesktopAIShell>
|
<DesktopAIShell>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</DesktopAIShell>
|
</DesktopAIShell>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { appShellColumnClass, appShellGutterClass } from "@/lib/appShell";
|
import { bottomNavInnerClass, bottomNavOuterClass } from "@/lib/appShell";
|
||||||
|
|
||||||
type BottomNavLocale = NonNullable<(typeof localeZh)["bottomNav"]>;
|
type BottomNavLocale = NonNullable<(typeof localeZh)["bottomNav"]>;
|
||||||
|
|
||||||
@@ -57,10 +57,10 @@ export default function BottomNav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={`fixed inset-x-0 bottom-0 z-50 flex justify-center pointer-events-none ${appShellGutterClass}`}
|
className={bottomNavOuterClass}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-auto ${appShellColumnClass}`}
|
className={bottomNavInnerClass}
|
||||||
style={{
|
style={{
|
||||||
background: "rgba(10, 10, 12, 0.96)",
|
background: "rgba(10, 10, 12, 0.96)",
|
||||||
backdropFilter: "blur(40px) saturate(200%)",
|
backdropFilter: "blur(40px) saturate(200%)",
|
||||||
|
|||||||
@@ -119,6 +119,8 @@
|
|||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +178,61 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* App shell: mobile full-bleed, desktop centered column (modern + legacy share class names). */
|
||||||
|
.x8-app-shell-outer {
|
||||||
|
min-height: 100dvh;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #0d1117;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x8-app-shell-column {
|
||||||
|
min-height: 100dvh;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x8-bottom-nav-outer {
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
justify-content: center;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x8-bottom-nav-inner {
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.x8-app-shell-outer,
|
||||||
|
.x8-bottom-nav-outer {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x8-app-shell-column,
|
||||||
|
.x8-bottom-nav-inner {
|
||||||
|
max-width: 1180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.container {
|
.container {
|
||||||
padding-left: 1.5rem;
|
padding-left: 1.5rem;
|
||||||
|
|||||||
@@ -6,9 +6,22 @@
|
|||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
}
|
||||||
|
#root {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #000;
|
background: #000;
|
||||||
color: #f2f2f2;
|
color: #f2f2f2;
|
||||||
@@ -44,6 +57,62 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
/* App shell: mobile full-bleed, desktop centered column (plain CSS for Chrome 91). */
|
||||||
|
.x8-app-shell-outer {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #0d1117;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x8-app-shell-column {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x8-bottom-nav-outer {
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
justify-content: center;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x8-bottom-nav-inner {
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.x8-app-shell-outer,
|
||||||
|
.x8-bottom-nav-outer {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x8-app-shell-column,
|
||||||
|
.x8-bottom-nav-inner {
|
||||||
|
max-width: 1180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Match modern .cyan-slider styling (input[type=range]) for legacy build. */
|
/* Match modern .cyan-slider styling (input[type=range]) for legacy build. */
|
||||||
.cyan-slider {
|
.cyan-slider {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/** Centered phone-style shell width (desktop / ultrawide). */
|
/** Centered phone-style shell width (desktop / ultrawide). */
|
||||||
export const APP_SHELL_MAX_PX = 1180;
|
export const APP_SHELL_MAX_PX = 1180;
|
||||||
|
|
||||||
/** Inner column inside horizontal gutters — use with a flex/grid centering parent. */
|
/** Responsive rules live in `.x8-app-shell-*` (index.css + index.legacy.css). */
|
||||||
export const appShellColumnClass =
|
export const appShellOuterClass = "x8-app-shell-outer";
|
||||||
"w-full min-w-0 max-w-[min(1180px,100%)]";
|
export const appShellColumnClass = "x8-app-shell-column";
|
||||||
|
export const bottomNavOuterClass = "x8-bottom-nav-outer";
|
||||||
export const appShellGutterClass = "px-4 sm:px-6";
|
export const bottomNavInnerClass = "x8-bottom-nav-inner";
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: bool
|
|||||||
|
|
||||||
// ── List row ──
|
// ── List row ──
|
||||||
function ListRow({
|
function ListRow({
|
||||||
icon, label, value, onClick, toggle, checked, onToggle,
|
icon, label, value, onClick, toggle, checked, onToggle, thumbnailSrc,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -144,6 +144,8 @@ function ListRow({
|
|||||||
toggle?: boolean;
|
toggle?: boolean;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
onToggle?: (v: boolean) => void;
|
onToggle?: (v: boolean) => void;
|
||||||
|
/** Optional small thumbnail shown between value text and chevron. */
|
||||||
|
thumbnailSrc?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="ios-list-row cursor-pointer" onClick={onClick}>
|
<div className="ios-list-row cursor-pointer" onClick={onClick}>
|
||||||
@@ -157,7 +159,15 @@ function ListRow({
|
|||||||
{toggle ? (
|
{toggle ? (
|
||||||
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
|
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5">
|
||||||
|
{thumbnailSrc && (
|
||||||
|
<img
|
||||||
|
src={thumbnailSrc}
|
||||||
|
alt=""
|
||||||
|
className="w-16 h-12 rounded-[8px] object-cover flex-shrink-0"
|
||||||
|
style={{ border: "1px solid rgba(255,255,255,0.12)" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{value && <span className="ios-row-value">{value}</span>}
|
{value && <span className="ios-row-value">{value}</span>}
|
||||||
<ChevronRight size={16} className="ios-chevron" />
|
<ChevronRight size={16} className="ios-chevron" />
|
||||||
</div>
|
</div>
|
||||||
@@ -873,6 +883,7 @@ export default function Home() {
|
|||||||
icon={<Gauge size={16} />}
|
icon={<Gauge size={16} />}
|
||||||
label={homeText.vu ?? "VU表"}
|
label={homeText.vu ?? "VU表"}
|
||||||
value={vuLabel}
|
value={vuLabel}
|
||||||
|
thumbnailSrc={`${import.meta.env.BASE_URL}vu/vu${(ds.vu ?? 0) + 1}.png`}
|
||||||
onClick={() => setLocation("/vu")}
|
onClick={() => setLocation("/vu")}
|
||||||
/>
|
/>
|
||||||
<ListRow
|
<ListRow
|
||||||
|
|||||||
@@ -29,5 +29,11 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
safelist: [
|
||||||
|
"x8-app-shell-outer",
|
||||||
|
"x8-app-shell-column",
|
||||||
|
"x8-bottom-nav-outer",
|
||||||
|
"x8-bottom-nav-inner",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user