新分支: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:
Your Name
2026-09-08 00:24:11 +08:00
co-authored by Claude Sonnet 5
parent 4e8f45d7d8
commit 8bcd9f3a5b
26 changed files with 3110 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
/**
* `tinode-sdk` ships as a plain UMD JS bundle with no bundled TypeScript types. This is a
* deliberately partial ambient declaration covering only the surface this app actually calls --
* grown incrementally as later phases touch more of the SDK, not an attempt at a full port of the
* upstream JS. Method/property names and shapes below are verified against the installed
* `node_modules/tinode-sdk/umd/tinode.dev.js` (v0.25.3), not guessed from the upstream docs, since
* this project's own history has repeatedly found the two disagree (e.g. this deployment's REST
* layer uses `X-MeetingIM-APIKey`, not the upstream `X-Tinode-APIKey`).
*/
declare module 'tinode-sdk' {
export interface TinodeOptions {
appName: string;
host: string;
apiKey: string;
transport?: 'ws' | 'lp' | null;
secure?: boolean;
persist?: boolean;
}
export type AuthLevel = 'anon' | 'auth' | 'root';
export class AccessMode {
mode: string;
given: string;
want: string;
isOwner(): boolean;
isAdmin(): boolean;
isSharer(): boolean;
isWriter(): boolean;
isReader(): boolean;
isMuted(): boolean;
isDeleter(): boolean;
isJoiner(): boolean;
isPresencer(): boolean;
}
export interface TopicDescPublic {
fn?: string;
name?: string;
photo?: { data?: string; type?: string; ref?: string };
[key: string]: unknown;
}
export interface TopicDescPrivate {
comment?: string;
pinned?: boolean;
[key: string]: unknown;
}
/** A row from `MeTopic.contacts()` -- a live, mutable contact/subscription snapshot. */
export interface Contact {
topic: string;
user?: string;
updated?: Date;
touched?: Date;
online?: boolean;
acs?: AccessMode;
seq?: number;
read?: number;
recv?: number;
unread?: number;
public?: TopicDescPublic | null;
private?: TopicDescPrivate | null;
latestMessage?: () => StoredMessage | undefined;
}
export interface StoredMessage {
seq: number;
from?: string;
ts?: Date;
content?: unknown;
head?: Record<string, unknown>;
}
export class Topic {
name: string;
subscribe(): Promise<unknown>;
leave(unsub?: boolean): Promise<unknown>;
getMeta(query: unknown): Promise<unknown>;
publish(data: unknown, head?: Record<string, unknown>): Promise<unknown>;
isSubscribed(): boolean;
}
export class MeTopic extends Topic {
contacts(callback: (contact: Contact) => void): void;
pinnedTopicRank(topic: string): number;
getMeta(query: unknown): Promise<unknown>;
}
export class Tinode {
constructor(options: TinodeOptions, onCompletion?: () => void);
_connection?: { sendText: (msg: string) => unknown; __tenantPatched?: boolean };
setHumanLanguage(locale: string): void;
enableLogging(enabled: boolean, verbose?: boolean): void;
connect(host?: string): Promise<void>;
disconnect(): void;
isConnected(): boolean;
isAuthenticated(): boolean;
getAuthenticatedUserID(): string | undefined;
login(scheme: string, secret: string, cred?: unknown): Promise<{ ctrl: { code: number; params?: Record<string, unknown> } }>;
loginToken(token: string, cred?: unknown): Promise<{ ctrl: { code: number } }>;
getAuthToken(): { token: string; expires: Date } | null;
getMeTopic(): MeTopic;
getTopic(topicName: string): Topic;
account(userId: string, scheme: string, secret: string, login: boolean, desc?: unknown): Promise<unknown>;
onConnect?: () => void;
onDisconnect?: (err?: Error) => void;
onLogin?: (code: number, text: string) => void;
onNetworkProbe?: () => void;
onAutoreconnectIteration?: (seconds: number, promise?: Promise<unknown>) => void;
}
export const Drafty: {
init(text: string): unknown;
parse(text: string): unknown;
toPlainText(content: unknown): string;
preview(content: unknown, length: number, cutFirst?: boolean): unknown;
videoCall(audioOnly: boolean): unknown;
isValid(content: unknown): boolean;
[key: string]: unknown;
};
export const TheCard: {
[key: string]: unknown;
};
}