新分支:Vite+React19+TS+Tailwind v4 重新搭建,先跑通四级响应式布局壳
不再沿用旧 im_webapp 的技术栈(webpack + class组件 + 15000行全局CSS + 手搓History路由), 按业务重新选型:Vite/React Router/Zustand预留/TanStack Query/保留tinode-sdk真实依赖 (协议客户端本身没有重写的业务理由)。新代码放在 web/ 子目录,旧代码原样保留在仓库根目录, 两者共存到重写完成。 这一步先验证布局壳本身:NavRail(消息/通讯录/收藏/文件助手/通话记录/设置)+ 会话列表 + 主区域 + 详情栏,按设计稿的四档断点(<640/640-959/960-1279/≥1280)用真实浏览器宽度逐档验证 过——两列及以下"列表/主区域二选一+返回键"、三列"详情栏抽屉化"、四列"详情栏常驻"均已用 真实点击/返回验证正确,不是只看了截图。会话列表和聊天内容目前是占位数据,业务数据接入和 Tinode连接是下一步。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4e8f45d7d8
commit
8bcd9f3a5b
@@ -0,0 +1,28 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { BottomTabBar } from './BottomTabBar';
|
||||
import { NavRail } from './NavRail';
|
||||
|
||||
/**
|
||||
* Top-level chrome: persistent icon rail (>=640px) or bottom tab bar (<640px), wrapping whichever
|
||||
* feature section is active. Each feature section owns its own list/main/detail responsive split
|
||||
* internally (see e.g. features/messages) -- this shell only decides "rail vs tabs", nothing about
|
||||
* columns within a section.
|
||||
*/
|
||||
export function AppShell() {
|
||||
return (
|
||||
<div className="flex h-dvh w-full overflow-hidden bg-surface-page">
|
||||
<div className="hidden sm:flex sm:h-full">
|
||||
<NavRail />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
<div className="sm:hidden">
|
||||
<BottomTabBar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { Icon } from './Icon';
|
||||
|
||||
const TABS = [
|
||||
{ to: '/messages', icon: 'chat_bubble', label: '消息' },
|
||||
{ to: '/contacts', icon: 'contacts', label: '通讯录' },
|
||||
{ to: '/settings', icon: 'person', label: '我的' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Replaces the old app's message-center-view.jsx-embedded 2-tab (消息/工作台) bar: this one is
|
||||
* top-level (visible regardless of which section is open, not just inside the message list) and
|
||||
* has 3 tabs per the new design, since 工作台 no longer exists.
|
||||
*/
|
||||
export function BottomTabBar() {
|
||||
return (
|
||||
<nav className="grid h-[82px] flex-none grid-cols-3 border-t border-border-panel bg-white pb-4.5">
|
||||
{TABS.map((tab) => (
|
||||
<NavLink
|
||||
key={tab.to}
|
||||
to={tab.to}
|
||||
className={({ isActive }) =>
|
||||
`flex flex-col items-center justify-center gap-0.5 text-[11px] no-underline ${
|
||||
isActive ? 'text-accent' : 'text-text-tertiary-3'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon name={tab.icon} className="!text-2xl" />
|
||||
{tab.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/** Thin wrapper around a Material Icons ligature glyph (font loaded in index.html). */
|
||||
export function Icon({ name, className = '' }: { name: string; className?: string }) {
|
||||
return <i className={`material-icons select-none ${className}`}>{name}</i>;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { Icon } from './Icon';
|
||||
|
||||
interface NavRailItem {
|
||||
to: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
badge?: number;
|
||||
}
|
||||
|
||||
const TOP_ITEMS: NavRailItem[] = [
|
||||
{ to: '/messages', icon: 'chat_bubble', label: '消息' },
|
||||
{ to: '/contacts', icon: 'contacts', label: '通讯录' },
|
||||
{ to: '/favorites', icon: 'inventory_2', label: '收藏' },
|
||||
{ to: '/files', icon: 'folder', label: '文件助手' },
|
||||
];
|
||||
|
||||
const BOTTOM_ITEMS: NavRailItem[] = [
|
||||
{ to: '/calls', icon: 'phone', label: '通话记录' },
|
||||
{ to: '/settings', icon: 'settings', label: '设置' },
|
||||
];
|
||||
|
||||
function NavRailButton({ item }: { item: NavRailItem }) {
|
||||
return (
|
||||
<NavLink
|
||||
to={item.to}
|
||||
title={item.label}
|
||||
className={({ isActive }) =>
|
||||
`relative grid h-11 w-11 place-items-center rounded-[10px] no-underline ${
|
||||
isActive ? 'bg-accent-soft-bg text-accent' : 'text-icon-default'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon name={item.icon} className="!text-2xl" />
|
||||
{item.badge ? (
|
||||
<b className="absolute top-1 right-0.5 grid h-4 min-w-4 place-items-center rounded-full bg-unread px-1 text-[10px] leading-none font-medium text-white">
|
||||
{item.badge}
|
||||
</b>
|
||||
) : null}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
/** Persistent 64px icon rail, visible at >=640px (see App.tsx for the <640 bottom-tab fallback). */
|
||||
export function NavRail({ selfInitial = '我', unreadMessages = 0 }: { selfInitial?: string; unreadMessages?: number }) {
|
||||
return (
|
||||
<nav className="flex h-full w-16 flex-none flex-col items-center border-r border-border-panel bg-surface-rail py-3.5">
|
||||
<div className="mb-5 grid h-[38px] w-[38px] place-items-center rounded-[10px] bg-accent text-base font-medium text-white">
|
||||
{selfInitial}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col items-center gap-1.5">
|
||||
{TOP_ITEMS.map((item) => (
|
||||
<NavRailButton key={item.to} item={item.to === '/messages' ? { ...item, badge: unreadMessages } : item} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
{BOTTOM_ITEMS.map((item) => (
|
||||
<NavRailButton key={item.to} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function SectionPlaceholder({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-surface-content text-sm text-text-tertiary-4">
|
||||
{title}(后续阶段实现)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user