460 lines
14 KiB
Markdown
460 lines
14 KiB
Markdown
## 1. 架构设计
|
||||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TB
|
|||
|
|
subgraph Frontend["前端 (React + Vite)"]
|
|||
|
|
UI["shadcn/ui 组件层"]
|
|||
|
|
Pages["页面层"]
|
|||
|
|
Hooks["Hooks 层"]
|
|||
|
|
Store["Zustand 状态管理"]
|
|||
|
|
SDK["array-sdk 通信层"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph Backend["后端 (FastAPI + SQLite)"]
|
|||
|
|
API["REST API"]
|
|||
|
|
WS["WebSocket"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
UI --> Pages
|
|||
|
|
Pages --> Hooks
|
|||
|
|
Hooks --> Store
|
|||
|
|
Hooks --> SDK
|
|||
|
|
Store --> SDK
|
|||
|
|
SDK --> API
|
|||
|
|
SDK --> WS
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 2. 技术说明
|
|||
|
|
|
|||
|
|
- **前端框架**:React 18 + TypeScript + Vite 6
|
|||
|
|
- **UI 组件库**:shadcn/ui(Radix UI + Tailwind CSS)
|
|||
|
|
- **状态管理**:Zustand(轻量、无 boilerplate、支持中间件)
|
|||
|
|
- **样式方案**:Tailwind CSS 4 + CSS 变量主题系统
|
|||
|
|
- **图标**:Lucide React
|
|||
|
|
- **动画**:Framer Motion
|
|||
|
|
- **Markdown 渲染**:react-markdown + remark-gfm + rehype-highlight
|
|||
|
|
- **HTTP/WebSocket**:array-sdk(从旧项目迁移,纯 JS 无框架依赖)
|
|||
|
|
- **路由**:React Router DOM v7(Hash 模式,兼容静态部署)
|
|||
|
|
- **国际化**:自定义 i18n hook(中/英双语)
|
|||
|
|
- **PWA**:vite-plugin-pwa
|
|||
|
|
- **项目初始化**:vite-init react-ts 模板
|
|||
|
|
|
|||
|
|
## 3. 路由定义
|
|||
|
|
|
|||
|
|
| 路由 | 用途 |
|
|||
|
|
|------|------|
|
|||
|
|
| `/` | 登录页(未登录时)或主工作台(已登录时) |
|
|||
|
|
| `/task/:taskId` | 主工作台,自动选中指定任务 |
|
|||
|
|
|
|||
|
|
## 4. API 定义
|
|||
|
|
|
|||
|
|
本项目为纯前端,所有 API 调用通过 array-sdk 与后端通信。SDK 封装层提供以下接口:
|
|||
|
|
|
|||
|
|
### 4.1 认证
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface AuthAPI {
|
|||
|
|
login(serverUrl: string, apiKey: string): Promise<Worker>
|
|||
|
|
loginByPassword(serverUrl: string, username: string, password: string): Promise<Worker>
|
|||
|
|
register(serverUrl: string, name: string, username: string, password: string, profile?: RegisterProfile): Promise<Worker>
|
|||
|
|
autoLogin(): Promise<Worker | null>
|
|||
|
|
logout(): void
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 Worker
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface WorkerAPI {
|
|||
|
|
getWorkers(params?: { orgId?: string }): Promise<Worker[]>
|
|||
|
|
getWorkerProfile(workerId: string): Promise<Worker>
|
|||
|
|
updateProfile(data: Partial<WorkerProfile>): Promise<Worker>
|
|||
|
|
getWorkerWorkload(workerId: string): Promise<Workload>
|
|||
|
|
discoverWorkers(options: DiscoverOptions): Promise<DiscoverResult>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 Task
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface TaskAPI {
|
|||
|
|
getTasks(params?: { orgId?: string; categoryId?: string }): Promise<Task[]>
|
|||
|
|
getTask(taskId: string): Promise<Task>
|
|||
|
|
createTask(title: string, participantIds: string[], priority?: string, extra?: CreateTaskExtra): Promise<Task>
|
|||
|
|
updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>
|
|||
|
|
updateTaskTitle(taskId: string, title: string): Promise<Task>
|
|||
|
|
addParticipant(taskId: string, workerId: string): Promise<Participant[]>
|
|||
|
|
removeParticipant(taskId: string, workerId: string): Promise<void>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.4 Message
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface MessageAPI {
|
|||
|
|
getMessages(taskId: string, scope?: string, includePrivate?: boolean, limit?: number, page?: number): Promise<Message[]>
|
|||
|
|
sendMessage(taskId: string, content: string, options?: SendMessageOptions): Promise<Message>
|
|||
|
|
sendMessageWithAttachments(taskId: string, content: string, fileIds: string[], options?: SendMessageOptions): Promise<Message>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.5 Attachment
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface AttachmentAPI {
|
|||
|
|
uploadAttachment(taskId: string, file: File): Promise<Attachment>
|
|||
|
|
listAttachments(taskId: string): Promise<{ attachments: Attachment[]; total: number }>
|
|||
|
|
deleteAttachment(taskId: string, attachmentId: string): Promise<void>
|
|||
|
|
getAttachmentUrl(taskId: string, attachmentId: string): string
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.6 Organization
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface OrganizationAPI {
|
|||
|
|
listMyOrganizations(): Promise<Organization[]>
|
|||
|
|
getOrganization(orgId: string): Promise<Organization>
|
|||
|
|
createOrganization(name: string, description?: string): Promise<Organization>
|
|||
|
|
updateOrganization(orgId: string, data: Partial<Organization>): Promise<Organization>
|
|||
|
|
createInvite(orgId: string, options?: InviteOptions): Promise<OrgInvite>
|
|||
|
|
listInvites(orgId: string): Promise<OrgInvite[]>
|
|||
|
|
joinOrganization(inviteCode: string): Promise<JoinResult>
|
|||
|
|
listOrgMembers(orgId: string): Promise<OrgMember[]>
|
|||
|
|
removeOrgMember(orgId: string, workerId: string): Promise<void>
|
|||
|
|
updateMemberRole(orgId: string, workerId: string, role: 'admin' | 'member'): Promise<OrgMember>
|
|||
|
|
renameOrgMember(orgId: string, workerId: string, name: string): Promise<OrgMember>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.7 Category
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface CategoryAPI {
|
|||
|
|
createCategory(name: string, orgId?: string): Promise<Category>
|
|||
|
|
listCategories(orgId?: string): Promise<Category[]>
|
|||
|
|
updateCategory(categoryId: string, opts: Partial<Category>): Promise<Category>
|
|||
|
|
deleteCategory(categoryId: string): Promise<boolean>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.8 Review
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface ReviewAPI {
|
|||
|
|
createReview(workerId: string, params: CreateReviewParams): Promise<Review>
|
|||
|
|
getWorkerReviews(workerId: string, options?: PaginationOptions): Promise<ReviewListResult>
|
|||
|
|
getReviewSummary(workerId: string): Promise<ReviewSummary>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.9 Notification
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface NotificationAPI {
|
|||
|
|
getNotifications(isRead?: boolean): Promise<Notification[]>
|
|||
|
|
markNotificationRead(notificationId: string): Promise<void>
|
|||
|
|
markAllNotificationsRead(): Promise<void>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.10 WebSocket
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface WebSocketAPI {
|
|||
|
|
startWebSocket(callbacks: WSCallbacks): () => void
|
|||
|
|
stopWebSocket(): void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface WSCallbacks {
|
|||
|
|
onEvent: (event: WSEvent) => void
|
|||
|
|
onConnected: () => void
|
|||
|
|
onDisconnected: () => void
|
|||
|
|
onError: (err: Error) => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type WSEventType = 'task.message' | 'participant.joined' | 'task.updated' | 'worker_status_changed' | 'tasks.pending_recovery'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 数据模型
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
erDiagram
|
|||
|
|
Organization ||--o{ Category : "has"
|
|||
|
|
Organization ||--o{ OrgMember : "has"
|
|||
|
|
Organization ||--o{ OrgInvite : "has"
|
|||
|
|
Worker ||--o{ Task : "creates"
|
|||
|
|
Worker ||--o{ Review : "receives"
|
|||
|
|
Task ||--o{ Participant : "has"
|
|||
|
|
Task ||--o{ Message : "has"
|
|||
|
|
Task ||--o{ Attachment : "has"
|
|||
|
|
Task }o--|| Category : "belongs_to"
|
|||
|
|
Message }o--o| Message : "reply_to"
|
|||
|
|
Message }o--o| Worker : "private_to"
|
|||
|
|
|
|||
|
|
Worker {
|
|||
|
|
string id PK
|
|||
|
|
string name
|
|||
|
|
string type
|
|||
|
|
string status
|
|||
|
|
string organization
|
|||
|
|
string location
|
|||
|
|
string description
|
|||
|
|
string[] tags
|
|||
|
|
object capabilities
|
|||
|
|
number maxConcurrentTasks
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Task {
|
|||
|
|
string id PK
|
|||
|
|
string title
|
|||
|
|
string description
|
|||
|
|
string status
|
|||
|
|
string priority
|
|||
|
|
string creator_id FK
|
|||
|
|
string org_id FK
|
|||
|
|
string category_id FK
|
|||
|
|
datetime created_at
|
|||
|
|
datetime last_message_at
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message {
|
|||
|
|
string id PK
|
|||
|
|
string task_id FK
|
|||
|
|
string sender_id FK
|
|||
|
|
string content
|
|||
|
|
string type
|
|||
|
|
string reply_to_id FK
|
|||
|
|
string private_to_id FK
|
|||
|
|
datetime created_at
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Attachment {
|
|||
|
|
string id PK
|
|||
|
|
string task_id FK
|
|||
|
|
string filename
|
|||
|
|
string content_type
|
|||
|
|
number size
|
|||
|
|
string url
|
|||
|
|
string thumbnail_url
|
|||
|
|
number width
|
|||
|
|
number height
|
|||
|
|
number duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Review {
|
|||
|
|
string id PK
|
|||
|
|
string worker_id FK
|
|||
|
|
string task_id FK
|
|||
|
|
string reviewer_id FK
|
|||
|
|
number rating
|
|||
|
|
string[] capability_tags
|
|||
|
|
string comment
|
|||
|
|
string suggestion
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 6. 项目目录结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
human-adapter-react/
|
|||
|
|
├── src/
|
|||
|
|
│ ├── components/ # UI 组件
|
|||
|
|
│ │ ├── ui/ # shadcn/ui 基础组件
|
|||
|
|
│ │ ├── layout/ # 布局组件
|
|||
|
|
│ │ │ ├── TopBar.tsx
|
|||
|
|
│ │ │ ├── WorkersPanel.tsx
|
|||
|
|
│ │ │ ├── TaskList.tsx
|
|||
|
|
│ │ │ └── Resizer.tsx
|
|||
|
|
│ │ ├── chat/ # 聊天相关组件
|
|||
|
|
│ │ │ ├── ChatDetail.tsx
|
|||
|
|
│ │ │ ├── MessageList.tsx
|
|||
|
|
│ │ │ ├── MessageBubble.tsx
|
|||
|
|
│ │ │ ├── MessageInput.tsx
|
|||
|
|
│ │ │ ├── MentionPopup.tsx
|
|||
|
|
│ │ │ ├── ReplyPreview.tsx
|
|||
|
|
│ │ │ ├── AttachmentRenderer.tsx
|
|||
|
|
│ │ │ ├── DMTab.tsx
|
|||
|
|
│ │ │ └── ScrollToBottom.tsx
|
|||
|
|
│ │ ├── task/ # 任务相关组件
|
|||
|
|
│ │ │ ├── TaskCard.tsx
|
|||
|
|
│ │ │ ├── TaskComposer.tsx
|
|||
|
|
│ │ │ ├── TaskDrawer.tsx
|
|||
|
|
│ │ │ ├── StatusSelector.tsx
|
|||
|
|
│ │ │ └── ParticipantManager.tsx
|
|||
|
|
│ │ ├── org/ # 组织相关组件
|
|||
|
|
│ │ │ ├── OrgSwitcher.tsx
|
|||
|
|
│ │ │ ├── OrgDetailSheet.tsx
|
|||
|
|
│ │ │ └── InviteManager.tsx
|
|||
|
|
│ │ ├── profile/ # 个人中心组件
|
|||
|
|
│ │ │ ├── ProfileDialog.tsx
|
|||
|
|
│ │ │ └── ShortcutSettings.tsx
|
|||
|
|
│ │ ├── review/ # 评价组件
|
|||
|
|
│ │ │ ├── ReviewDialog.tsx
|
|||
|
|
│ │ │ └── ReviewSummary.tsx
|
|||
|
|
│ │ ├── auth/ # 登录组件
|
|||
|
|
│ │ │ └── LoginPage.tsx
|
|||
|
|
│ │ └── shared/ # 共享组件
|
|||
|
|
│ │ ├── LedLogo.tsx
|
|||
|
|
│ │ ├── LedMatrix.tsx
|
|||
|
|
│ │ ├── RobotIcon.tsx
|
|||
|
|
│ │ ├── WorkerAvatar.tsx
|
|||
|
|
│ │ ├── Toast.tsx
|
|||
|
|
│ │ └── PwaInstallBanner.tsx
|
|||
|
|
│ ├── hooks/ # 自定义 Hooks
|
|||
|
|
│ │ ├── useAuth.ts
|
|||
|
|
│ │ ├── useMessages.ts
|
|||
|
|
│ │ ├── useWorkers.ts
|
|||
|
|
│ │ ├── useTasks.ts
|
|||
|
|
│ │ ├── useOrganization.ts
|
|||
|
|
│ │ ├── useNotifications.ts
|
|||
|
|
│ │ ├── useReviews.ts
|
|||
|
|
│ │ ├── useProfile.ts
|
|||
|
|
│ │ ├── useWebSocket.ts
|
|||
|
|
│ │ ├── useI18n.ts
|
|||
|
|
│ │ ├── useTheme.ts
|
|||
|
|
│ │ ├── useAutoScroll.ts
|
|||
|
|
│ │ └── useDrafts.ts
|
|||
|
|
│ ├── stores/ # Zustand Store
|
|||
|
|
│ │ ├── authStore.ts
|
|||
|
|
│ │ ├── appStore.ts
|
|||
|
|
│ │ └── uiStore.ts
|
|||
|
|
│ ├── lib/ # 工具库
|
|||
|
|
│ │ ├── sdk.ts # array-sdk 封装
|
|||
|
|
│ │ ├── api.ts # API 薄封装层
|
|||
|
|
│ │ ├── i18n.ts # 国际化字典
|
|||
|
|
│ │ ├── utils.ts # 通用工具函数
|
|||
|
|
│ │ └── constants.ts # 常量定义
|
|||
|
|
│ ├── pages/ # 页面
|
|||
|
|
│ │ ├── LoginPage.tsx
|
|||
|
|
│ │ └── WorkbenchPage.tsx
|
|||
|
|
│ ├── App.tsx
|
|||
|
|
│ ├── main.tsx
|
|||
|
|
│ └── index.css # Tailwind 入口 + CSS 变量
|
|||
|
|
├── public/
|
|||
|
|
├── sdk-js/ # array-sdk 源码(从旧项目迁移)
|
|||
|
|
│ ├── src/
|
|||
|
|
│ │ ├── client.ts
|
|||
|
|
│ │ ├── adapter.ts
|
|||
|
|
│ │ └── index.ts
|
|||
|
|
│ └── package.json
|
|||
|
|
├── .trae/documents/
|
|||
|
|
├── package.json
|
|||
|
|
├── tsconfig.json
|
|||
|
|
├── vite.config.ts
|
|||
|
|
├── tailwind.config.js
|
|||
|
|
├── components.json # shadcn/ui 配置
|
|||
|
|
└── postcss.config.js
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 关键架构决策
|
|||
|
|
|
|||
|
|
### 7.1 状态管理:Zustand vs Context
|
|||
|
|
|
|||
|
|
选择 Zustand 的理由:
|
|||
|
|
- 无 Provider 包裹,任何组件直接 `useStore()` 即可
|
|||
|
|
- 支持 `subscribeWithSelector` 中间件,精确控制重渲染
|
|||
|
|
- 支持 `persist` 中间件,自动持久化到 localStorage
|
|||
|
|
- 比 Redux 少 90% boilerplate,比 Jotai 更适合复杂状态
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// appStore.ts 示例
|
|||
|
|
import { create } from 'zustand'
|
|||
|
|
import { persist } from 'zustand/middleware'
|
|||
|
|
|
|||
|
|
interface AppState {
|
|||
|
|
workers: Worker[]
|
|||
|
|
tasks: Task[]
|
|||
|
|
selectedTaskId: string | null
|
|||
|
|
wsConnected: boolean
|
|||
|
|
setWorkers: (workers: Worker[]) => void
|
|||
|
|
setTasks: (tasks: Task[]) => void
|
|||
|
|
selectTask: (id: string | null) => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const useAppStore = create<AppState>()(
|
|||
|
|
persist(
|
|||
|
|
(set) => ({
|
|||
|
|
workers: [],
|
|||
|
|
tasks: [],
|
|||
|
|
selectedTaskId: null,
|
|||
|
|
wsConnected: false,
|
|||
|
|
setWorkers: (workers) => set({ workers }),
|
|||
|
|
setTasks: (tasks) => set({ tasks }),
|
|||
|
|
selectTask: (id) => set({ selectedTaskId: id }),
|
|||
|
|
}),
|
|||
|
|
{ name: 'array-app-store', partialize: (s) => ({ selectedTaskId: s.selectedTaskId }) }
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 消息系统:自定义 Hook
|
|||
|
|
|
|||
|
|
不使用 Vercel AI SDK 的 `useChat()`,因为:
|
|||
|
|
- 本项目的消息模型更复杂(私密消息、@提及、引用回复、附件)
|
|||
|
|
- WebSocket 事件格式是后端自定义的
|
|||
|
|
- 需要精确控制乐观消息的生命周期
|
|||
|
|
|
|||
|
|
自定义 `useMessages` Hook 借鉴 AI SDK 的状态机思想:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
type MessageStatus = 'idle' | 'sending' | 'streaming' | 'error'
|
|||
|
|
|
|||
|
|
interface UseMessagesReturn {
|
|||
|
|
messages: Message[]
|
|||
|
|
pendingMessages: PendingMessage[]
|
|||
|
|
isLoading: boolean
|
|||
|
|
isAtBottom: boolean
|
|||
|
|
sendMessage: (content: string, options?: SendOptions) => Promise<void>
|
|||
|
|
retryMessage: (pendingId: string) => Promise<void>
|
|||
|
|
cancelMessage: (pendingId: string) => void
|
|||
|
|
loadMore: () => Promise<void>
|
|||
|
|
hasMore: boolean
|
|||
|
|
scrollToBottom: () => void
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.3 主题系统:Tailwind CSS 变量
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
:root {
|
|||
|
|
--background: 0 0% 100%;
|
|||
|
|
--foreground: 222.2 84% 4.9%;
|
|||
|
|
--primary: 221.2 83.2% 53.3%;
|
|||
|
|
--accent: 221.2 83.2% 53.3%;
|
|||
|
|
/* ... shadcn 默认变量 */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark {
|
|||
|
|
--background: 222.2 84% 4.9%;
|
|||
|
|
--foreground: 210 40% 98%;
|
|||
|
|
--primary: 217.2 91.2% 59.8%;
|
|||
|
|
--accent: 217.2 91.2% 59.8%;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
切换主题只需在 `<html>` 上添加/移除 `dark` 类,所有组件自动响应。
|
|||
|
|
|
|||
|
|
### 7.4 SDK 迁移策略
|
|||
|
|
|
|||
|
|
array-sdk 是纯 JavaScript,无框架依赖,直接迁移:
|
|||
|
|
- `sdk-js/` 目录整体复制到新项目
|
|||
|
|
- `package.json` 中 `"array-sdk": "file:./sdk-js"` 保持不变
|
|||
|
|
- API 封装层 `lib/api.ts` 从旧项目 `api/index.js` 翻译为 TypeScript
|
|||
|
|
- 所有方法签名添加 TypeScript 类型
|
|||
|
|
|
|||
|
|
### 7.5 组件拆分原则
|
|||
|
|
|
|||
|
|
旧项目 TaskDetail.vue 72KB → 拆分为 8+ 个子组件:
|
|||
|
|
|
|||
|
|
| 旧代码位置 | 新组件 | 职责 |
|
|||
|
|
|------------|--------|------|
|
|||
|
|
| 消息流模板 | `MessageList.tsx` | 消息列表渲染 + 时间分割线 |
|
|||
|
|
| 单条消息 | `MessageBubble.tsx` | 消息气泡 + 头像 + 引用 + 附件 |
|
|||
|
|
| 回复输入栏 | `MessageInput.tsx` | 输入框 + 发送按钮 + 快捷键 |
|
|||
|
|
| @提及弹窗 | `MentionPopup.tsx` | 搜索 + 选择参与者 |
|
|||
|
|
| 私密消息 | `DMTab.tsx` | 私聊通道列表 + 消息 |
|
|||
|
|
| 附件渲染 | `AttachmentRenderer.tsx` | 图片/视频/音频/文件智能渲染 |
|
|||
|
|
| 任务抽屉 | `TaskDrawer.tsx` | 参与者 + 详情 + 评价 |
|
|||
|
|
| 状态选择器 | `StatusSelector.tsx` | 状态下拉切换 |
|