mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
wip(vscode-ide-companion): 实现 quick win 功能
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
This commit is contained in:
848
packages/vscode-ide-companion/docs-tmp/HTML_TO_JS_MAPPING.md
Normal file
848
packages/vscode-ide-companion/docs-tmp/HTML_TO_JS_MAPPING.md
Normal file
@@ -0,0 +1,848 @@
|
||||
# Claude Code HTML 结构到混淆 JS 的完整映射
|
||||
|
||||
> **方法论**: 通过 HTML 类名 + 字符串锚点定位混淆代码中的组件逻辑
|
||||
>
|
||||
> **日期**: 2025-11-18
|
||||
|
||||
---
|
||||
|
||||
## 一、HTML 结构完整分析
|
||||
|
||||
### 1. 顶层结构
|
||||
|
||||
```html
|
||||
<body class="vscode-dark">
|
||||
<div id="root">
|
||||
<div class="me"> <!-- 主容器 -->
|
||||
<div class="he"> <!-- Header 区域 -->
|
||||
<div class="be"> <!-- 主内容区 -->
|
||||
<div class="Q">
|
||||
<div class="ue">
|
||||
<div class="ye"> <!-- 消息容器 -->
|
||||
<div class="Re"> <!-- 空状态 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div> <!-- 输入区域 -->
|
||||
<form class="u"> <!-- 输入表单 -->
|
||||
<div class="fo"> <!-- 消息输入容器 -->
|
||||
<div class="ri"> <!-- Footer 按钮 -->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、关键组件的 CSS 映射表
|
||||
|
||||
| HTML 类名 | 用途 | JS 变量名 (推断) | 位置 |
|
||||
| --------- | ---------------- | ---------------- | --------------- |
|
||||
| `.me` | 主容器 | - | index.css |
|
||||
| `.he` | Header | - | index.css |
|
||||
| `.E` | Session 按钮 | `c_` / `tm` | xKe 函数 |
|
||||
| `.j` | New Session 按钮 | `zl` | Footer 定义 |
|
||||
| `.u` | 输入表单 | `c_` | ContentEditable |
|
||||
| `.d` | 输入框 | - | ContentEditable |
|
||||
| `.ri` | Footer 按钮区 | `zl` | Footer 组件 |
|
||||
| `.l` | Footer 按钮 | `zl` | Footer 按钮 |
|
||||
|
||||
---
|
||||
|
||||
## 三、成功映射的组件逻辑
|
||||
|
||||
### A. 命令菜单组件 (Command Menu)
|
||||
|
||||
#### HTML 定位线索
|
||||
|
||||
- 类名: `.menuPopup`, `.commandList`, `.commandItem`
|
||||
- 字符串: "Filter actions...", "No matching commands"
|
||||
|
||||
#### 找到的 JS 代码
|
||||
|
||||
```javascript
|
||||
// 从混淆代码提取 - 命令菜单组件
|
||||
var xKe = ({
|
||||
isOpen: n,
|
||||
onClose: e,
|
||||
onCommandSelect: t,
|
||||
commandRegistry: i,
|
||||
filterText: o,
|
||||
suppressFilter: r = !1,
|
||||
}) => {
|
||||
let [s, a] = (0, rr.useState)(''); // filterText state
|
||||
let [l, c] = (0, rr.useState)(null); // selectedId state
|
||||
let d = (0, rr.useRef)(null); // input ref
|
||||
let u = (0, rr.useRef)(null); // container ref
|
||||
let h = (0, rr.useRef)(null); // selected item ref
|
||||
|
||||
let f = r ? o || '' : s || ''; // 实际过滤文本
|
||||
|
||||
// 获取命令分组
|
||||
let p = i.getCommandsBySection();
|
||||
|
||||
// 过滤命令
|
||||
let g = Object.entries(p).reduce((x, [w, y]) => {
|
||||
let C = y.filter((L) => L.label.toLowerCase().includes(f.toLowerCase()));
|
||||
return (C.length > 0 && (x[w] = C), x);
|
||||
}, {});
|
||||
|
||||
let _ = Object.values(g).flat(); // 扁平化命令列表
|
||||
|
||||
// 键盘导航逻辑
|
||||
let b = (0, rr.useCallback)(
|
||||
(x) => {
|
||||
if (x.key === 'Escape') {
|
||||
x.preventDefault();
|
||||
e();
|
||||
return;
|
||||
}
|
||||
|
||||
if (x.key === 'ArrowDown' && _.length > 0) {
|
||||
x.preventDefault();
|
||||
let w = _.findIndex((C) => C.id === l);
|
||||
let y = w < _.length - 1 ? w + 1 : 0;
|
||||
c(_[y]?.id || null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (x.key === 'ArrowUp' && _.length > 0) {
|
||||
x.preventDefault();
|
||||
let w = _.findIndex((C) => C.id === l);
|
||||
let y = w > 0 ? w - 1 : _.length - 1;
|
||||
c(_[y]?.id || null);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((x.key === 'Tab' || x.key === 'Enter') && !x.shiftKey) {
|
||||
if ('isComposing' in x && x.isComposing) return;
|
||||
x.preventDefault();
|
||||
if (l) {
|
||||
let w = _.find((y) => y.id === l);
|
||||
if (w) {
|
||||
a('');
|
||||
let y = x.key === 'Tab';
|
||||
t(w, y);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
[l, _, e, t],
|
||||
)(
|
||||
// 滚动到选中项
|
||||
0,
|
||||
rr.useEffect,
|
||||
)(() => {
|
||||
h.current &&
|
||||
h.current.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'nearest',
|
||||
});
|
||||
}, [l]);
|
||||
|
||||
// 返回 JSX
|
||||
return n
|
||||
? rr.default.createElement(
|
||||
'div',
|
||||
{
|
||||
ref: u,
|
||||
className: tm.menuPopup,
|
||||
},
|
||||
// 过滤输入框
|
||||
!r &&
|
||||
rr.default.createElement('input', {
|
||||
ref: d,
|
||||
type: 'text',
|
||||
value: f,
|
||||
onChange: (x) => a(x.target.value),
|
||||
onKeyDown: v,
|
||||
placeholder: 'Filter actions...',
|
||||
className: tm.filterInput,
|
||||
}),
|
||||
|
||||
// 命令列表
|
||||
rr.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: tm.commandList,
|
||||
},
|
||||
Object.keys(g).length === 0
|
||||
? rr.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: tm.emptyState,
|
||||
},
|
||||
'No matching commands',
|
||||
)
|
||||
: Object.entries(g).map(([x, w], y) =>
|
||||
// 每个分组
|
||||
rr.default.createElement(
|
||||
'div',
|
||||
{ key: x },
|
||||
// 分组标题
|
||||
rr.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: tm.sectionHeader,
|
||||
},
|
||||
x,
|
||||
),
|
||||
|
||||
// 命令项
|
||||
w.map((C) =>
|
||||
rr.default.createElement(
|
||||
'div',
|
||||
{
|
||||
key: C.id,
|
||||
ref: C.id === l ? h : null,
|
||||
className: `${tm.commandItem} ${C.id === l ? tm.activeCommandItem : ''}`,
|
||||
onMouseEnter: () => c(C.id),
|
||||
onClick: () => {
|
||||
a('');
|
||||
t(C, !1);
|
||||
},
|
||||
},
|
||||
rr.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: tm.commandContent,
|
||||
},
|
||||
rr.default.createElement(
|
||||
'span',
|
||||
{
|
||||
className: tm.commandLabel,
|
||||
},
|
||||
C.label,
|
||||
),
|
||||
C.trailingComponent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
};
|
||||
```
|
||||
|
||||
**关键发现**:
|
||||
|
||||
- ✅ 使用 `useState` 管理过滤文本和选中项
|
||||
- ✅ `useRef` 管理输入框和列表项的引用
|
||||
- ✅ 键盘导航: Escape/ArrowUp/ArrowDown/Tab/Enter
|
||||
- ✅ `scrollIntoView` 自动滚动到选中项
|
||||
|
||||
---
|
||||
|
||||
### B. 文件选择器组件 (File Selector)
|
||||
|
||||
#### HTML 定位线索
|
||||
|
||||
- 类名: `.fileList`, `.fileItem`, `.fileName`
|
||||
- 字符串: "No files found"
|
||||
|
||||
#### 找到的 JS 代码
|
||||
|
||||
```javascript
|
||||
// 文件选择器组件
|
||||
var wKe = ({ searchQuery: n, onClose: e, onFileSelect: t, onListFiles: i }) => {
|
||||
let [o, r] = (0, ao.useState)([]); // files state
|
||||
let [s, a] = (0, ao.useState)(0); // selectedIndex state
|
||||
let l = (0, ao.useRef)(null);
|
||||
let c = (0, ao.useRef)(null)(
|
||||
// 防抖加载文件列表
|
||||
0,
|
||||
ao.useEffect,
|
||||
)(() => {
|
||||
let u = setTimeout(() => {
|
||||
i(n)
|
||||
.then((h) => {
|
||||
r(h);
|
||||
a(0);
|
||||
})
|
||||
.catch((h) => {});
|
||||
}, 200); // 200ms 防抖
|
||||
return () => clearTimeout(u);
|
||||
}, [i, n]);
|
||||
|
||||
// 键盘导航
|
||||
let d = (0, ao.useCallback)(
|
||||
(u) => {
|
||||
switch (u.key) {
|
||||
case 'ArrowDown':
|
||||
o.length > 1 &&
|
||||
(u.preventDefault(), a((h) => (h < o.length - 1 ? h + 1 : 0)));
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
o.length > 1 &&
|
||||
(u.preventDefault(), a((h) => (h > 0 ? h - 1 : o.length - 1)));
|
||||
break;
|
||||
case 'Tab':
|
||||
case 'Enter':
|
||||
u.shiftKey || (u.preventDefault(), o[s] && t(o[s], u.key === 'Tab'));
|
||||
break;
|
||||
case 'Escape':
|
||||
u.preventDefault();
|
||||
e();
|
||||
break;
|
||||
}
|
||||
},
|
||||
[o, s, t, e],
|
||||
)(
|
||||
// 滚动到选中项
|
||||
0,
|
||||
ao.useEffect,
|
||||
)(() => {
|
||||
c.current &&
|
||||
c.current.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'nearest',
|
||||
});
|
||||
}, [s])(
|
||||
// 监听全局键盘事件
|
||||
0,
|
||||
ao.useEffect,
|
||||
)(
|
||||
() => (
|
||||
document.addEventListener('keydown', d),
|
||||
() => document.removeEventListener('keydown', d)
|
||||
),
|
||||
[d],
|
||||
);
|
||||
|
||||
// 渲染文件列表
|
||||
return ao.default.createElement(
|
||||
'div',
|
||||
{
|
||||
ref: l,
|
||||
className: $u.menuPopup,
|
||||
},
|
||||
ao.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: $u.fileList,
|
||||
},
|
||||
o.length === 0
|
||||
? ao.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: $u.emptyState,
|
||||
},
|
||||
'No files found',
|
||||
)
|
||||
: o.map((u, h) =>
|
||||
ao.default.createElement(
|
||||
'div',
|
||||
{
|
||||
key: u.path,
|
||||
ref: h === s ? c : null,
|
||||
className: `${$u.fileItem} ${h === s ? $u.activeFileItem : ''}`,
|
||||
onMouseEnter: () => a(h),
|
||||
onClick: () => t(u, !1),
|
||||
},
|
||||
ao.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: $u.fileContent,
|
||||
},
|
||||
// 文件图标和名称
|
||||
u.type === 'file'
|
||||
? ao.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: $u.fileName,
|
||||
},
|
||||
u.name,
|
||||
)
|
||||
: ao.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: $u.directoryPath,
|
||||
},
|
||||
u.path,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**关键发现**:
|
||||
|
||||
- ✅ 200ms 防抖加载
|
||||
- ✅ 全局键盘事件监听
|
||||
- ✅ Tab 键选中文件后继续输入
|
||||
|
||||
---
|
||||
|
||||
### C. Footer 按钮组件
|
||||
|
||||
#### HTML 定位线索
|
||||
|
||||
- 类名: `.inputFooter`, `.footerButton`, `.sendButton`
|
||||
- 字符串: "Ask before edits", "Thinking off"
|
||||
|
||||
#### 找到的 JS 代码
|
||||
|
||||
```javascript
|
||||
// Footer 组件
|
||||
function CKe({
|
||||
session: n,
|
||||
mode: e,
|
||||
onCycleMode: t,
|
||||
currentSelection: i,
|
||||
canSendMessage: o,
|
||||
toggleCommandMenu: r,
|
||||
includeSelection: s,
|
||||
onToggleIncludeSelection: a,
|
||||
onCompact: l,
|
||||
onAttachFile: c,
|
||||
}) {
|
||||
// 发送图标切换
|
||||
let d = null;
|
||||
n.busy.value && !o
|
||||
? (d = lo.default.createElement(rie, { className: zl.stopIcon }))
|
||||
: (d = lo.default.createElement(Xte, { className: zl.sendIcon }));
|
||||
|
||||
// Thinking 开关
|
||||
let u = n.thinkingLevel.value !== 'off';
|
||||
let h = () => {
|
||||
n.setThinkingLevel(n.thinkingLevel.value === 'off' ? 'default_on' : 'off');
|
||||
};
|
||||
|
||||
return lo.default.createElement(
|
||||
'div',
|
||||
{
|
||||
className: zl.inputFooter,
|
||||
},
|
||||
// Mode 切换按钮
|
||||
lo.default.createElement(DSt, {
|
||||
mode: e,
|
||||
onTap: t,
|
||||
}),
|
||||
|
||||
// 文件选择按钮
|
||||
i &&
|
||||
lo.default.createElement(NSt, {
|
||||
includeSelection: s ?? !1,
|
||||
currentSelection: i,
|
||||
onToggle: a ?? (() => {}),
|
||||
}),
|
||||
|
||||
// Usage 指示器
|
||||
lo.default.createElement(yKe, {
|
||||
usedTokens: n.usageData.value.totalTokens,
|
||||
contextWindow: n.usageData.value.contextWindow,
|
||||
onCompact: l,
|
||||
}),
|
||||
|
||||
// Spacer
|
||||
lo.default.createElement('div', {
|
||||
className: zl.spacer,
|
||||
}),
|
||||
|
||||
// Thinking 按钮
|
||||
lo.default.createElement(RSt, {
|
||||
thinkingOn: u,
|
||||
toggleThinking: h,
|
||||
}),
|
||||
|
||||
// 命令菜单按钮
|
||||
lo.default.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
className: zl.menuButton,
|
||||
title: 'Show command menu (/)',
|
||||
onClick: r,
|
||||
},
|
||||
lo.default.createElement(oie, {
|
||||
className: zl.menuIcon,
|
||||
}),
|
||||
),
|
||||
|
||||
// 发送按钮
|
||||
lo.default.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'submit',
|
||||
disabled: !n.busy.value && !o,
|
||||
className: zl.sendButton,
|
||||
'data-permission-mode': e,
|
||||
onClick: (f) => {
|
||||
n.busy.value && !o && (f.preventDefault(), n.interrupt());
|
||||
},
|
||||
},
|
||||
d,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Mode 按钮组件
|
||||
function DSt({ mode: n, onTap: e }) {
|
||||
switch (n) {
|
||||
case 'acceptEdits':
|
||||
return lo.default.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
className: zl.footerButton,
|
||||
onClick: e,
|
||||
title: 'Claude will edit your selected text or the whole file...',
|
||||
},
|
||||
lo.default.createElement($ye, null), // Fast forward icon
|
||||
lo.default.createElement('span', null, 'Edit automatically'),
|
||||
);
|
||||
|
||||
case 'plan':
|
||||
return lo.default.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
className: zl.footerButton,
|
||||
onClick: e,
|
||||
title: 'Claude will explore the code and present a plan...',
|
||||
},
|
||||
lo.default.createElement(jye, null), // Pause icon
|
||||
lo.default.createElement('span', null, 'Plan mode'),
|
||||
);
|
||||
|
||||
case 'bypassPermissions':
|
||||
return lo.default.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
className: zl.footerButton,
|
||||
onClick: e,
|
||||
title: 'Claude Code will not ask for your approval...',
|
||||
},
|
||||
lo.default.createElement(Uye, null), // Double chevron icon
|
||||
lo.default.createElement('span', null, 'Bypass permissions'),
|
||||
);
|
||||
|
||||
case 'default':
|
||||
default:
|
||||
return lo.default.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
className: zl.footerButton,
|
||||
onClick: e,
|
||||
title: 'Claude will ask before each edit...',
|
||||
},
|
||||
lo.default.createElement(qye, null), // Pencil icon
|
||||
lo.default.createElement('span', null, 'Ask before edits'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Thinking 按钮组件
|
||||
function RSt({ thinkingOn: n, toggleThinking: e }) {
|
||||
return lo.default.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
className: `${zl.menuButton} ${n ? zl.menuButtonActivated : zl.menuButtonInactivated}`,
|
||||
title: n ? 'Thinking on' : 'Thinking off',
|
||||
onClick: e,
|
||||
},
|
||||
// Thinking 图标 SVG
|
||||
lo.default.createElement(
|
||||
'svg',
|
||||
{
|
||||
width: '16',
|
||||
height: '16',
|
||||
viewBox: '0 0 16 16',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
},
|
||||
lo.default.createElement('path', {
|
||||
d: 'M8.00293 1.11523L8.35059 1.12402H8.35352C11.9915...',
|
||||
strokeWidth: '0.27',
|
||||
style: {
|
||||
stroke: 'var(--app-secondary-foreground)',
|
||||
fill: 'var(--app-secondary-foreground)',
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**关键发现**:
|
||||
|
||||
- ✅ Mode 切换逻辑 (4 种模式)
|
||||
- ✅ Thinking 开关状态管理
|
||||
- ✅ 忙碌状态显示不同图标
|
||||
- ✅ 使用 `data-permission-mode` 属性
|
||||
|
||||
---
|
||||
|
||||
### D. ContentEditable 输入框
|
||||
|
||||
#### HTML 定位线索
|
||||
|
||||
- 类名: `.d`, `.fo`
|
||||
- 属性: `contenteditable="plaintext-only"`
|
||||
|
||||
#### 推断的实现模式
|
||||
|
||||
```javascript
|
||||
// ContentEditable 输入框 (从模式推断)
|
||||
var c_ = {
|
||||
inputContainer: 'u',
|
||||
inputContainerBackground: 'Wr',
|
||||
messageInputContainer: 'fo',
|
||||
messageInput: 'd',
|
||||
};
|
||||
|
||||
// 输入组件逻辑 (推断)
|
||||
function MessageInput({ value, onChange, onSubmit }) {
|
||||
const inputRef = useRef(null);
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
|
||||
const handleInput = () => {
|
||||
if (inputRef.current) {
|
||||
const newValue = inputRef.current.textContent || '';
|
||||
onChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
// Enter 提交 (非 Shift+Enter)
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (e.nativeEvent.isComposing) return;
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
}
|
||||
|
||||
// Escape 取消
|
||||
if (e.key === 'Escape' && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
// 取消操作
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current && inputRef.current.textContent !== value) {
|
||||
inputRef.current.textContent = value;
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{
|
||||
className: c_.messageInputContainer,
|
||||
},
|
||||
React.createElement('div', {
|
||||
ref: inputRef,
|
||||
className: c_.messageInput,
|
||||
contentEditable: 'plaintext-only',
|
||||
role: 'textbox',
|
||||
'aria-label': 'Message input',
|
||||
'aria-multiline': 'true',
|
||||
'data-placeholder': 'Ask Claude to edit…',
|
||||
onInput: handleInput,
|
||||
onKeyDown: handleKeyDown,
|
||||
spellCheck: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、事件处理逻辑映射
|
||||
|
||||
### 键盘快捷键汇总
|
||||
|
||||
| 快捷键 | 功能 | 组件 |
|
||||
| ---------------- | --------------- | ----------------- |
|
||||
| `Escape` | 关闭菜单/对话框 | 所有弹窗 |
|
||||
| `ArrowDown` | 下一项 | 命令菜单/文件选择 |
|
||||
| `ArrowUp` | 上一项 | 命令菜单/文件选择 |
|
||||
| `Enter` | 确认选择 | 命令菜单/文件选择 |
|
||||
| `Tab` | 选择并继续 | 命令菜单/文件选择 |
|
||||
| `Shift+Tab` | 切换模式 | Footer 模式按钮 |
|
||||
| `Enter` (输入框) | 发送消息 | 消息输入 |
|
||||
| `Shift+Enter` | 换行 | 消息输入 |
|
||||
| `/` | 打开命令菜单 | 全局 |
|
||||
| `@` | @mentions | 输入框 |
|
||||
|
||||
### 状态管理模式
|
||||
|
||||
```javascript
|
||||
// 全局 Session 状态 (推断)
|
||||
const session = {
|
||||
busy: { value: boolean },
|
||||
thinkingLevel: { value: "off" | "default_on" },
|
||||
usageData: {
|
||||
value: {
|
||||
totalTokens: number,
|
||||
contextWindow: number
|
||||
}
|
||||
},
|
||||
interrupt: () => void,
|
||||
setThinkingLevel: (level) => void
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、完整的组件层级关系
|
||||
|
||||
```
|
||||
App (根组件)
|
||||
├── Header (.he)
|
||||
│ ├── SessionButton (.E)
|
||||
│ │ └── onClick: handleSessionsClick
|
||||
│ ├── Spacer (.ke)
|
||||
│ └── NewButton (.j)
|
||||
│ └── onClick: handleNewSession
|
||||
│
|
||||
├── MainContent (.be > .Q > .ue > .ye)
|
||||
│ ├── EmptyState (.Re)
|
||||
│ └── MessageList (.M)
|
||||
│
|
||||
├── InputArea
|
||||
│ ├── InputForm (.u)
|
||||
│ │ ├── Background (.Wr)
|
||||
│ │ ├── MessageInputContainer (.fo)
|
||||
│ │ │ └── ContentEditable (.d)
|
||||
│ │ └── Footer (.ri)
|
||||
│ │ ├── ModeButton (.l)
|
||||
│ │ ├── SelectionButton (.l)
|
||||
│ │ ├── UsageIndicator
|
||||
│ │ ├── ThinkingButton (.H)
|
||||
│ │ ├── CommandMenuButton (.H)
|
||||
│ │ └── SendButton (.r)
|
||||
│ │
|
||||
│ └── Popups (条件渲染)
|
||||
│ ├── CommandMenu (.menuPopup)
|
||||
│ │ ├── FilterInput (.filterInput)
|
||||
│ │ └── CommandList (.commandList)
|
||||
│ │ └── CommandItem (.commandItem)
|
||||
│ │
|
||||
│ ├── FileSelector ($u.menuPopup)
|
||||
│ │ └── FileList ($u.fileList)
|
||||
│ │ └── FileItem ($u.fileItem)
|
||||
│ │
|
||||
│ └── PermissionRequest (Ei.permissionRequestContainer)
|
||||
│ ├── Content (Ei.permissionRequestContent)
|
||||
│ ├── Options (Ei.buttonContainer)
|
||||
│ └── RejectInput (Ei.rejectMessageInput)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、可直接复用的代码模式
|
||||
|
||||
### 1. 键盘导航模式
|
||||
|
||||
```typescript
|
||||
// 通用键盘导航 Hook
|
||||
const useKeyboardNavigation = (items, onSelect, onClose) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSelect(items[selectedIndex], e.key === 'Tab');
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
break;
|
||||
}
|
||||
},
|
||||
[items, selectedIndex, onSelect, onClose],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
return [selectedIndex, setSelectedIndex];
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 自动滚动模式
|
||||
|
||||
```typescript
|
||||
// 滚动到选中项
|
||||
const selectedRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
selectedRef.current?.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'nearest',
|
||||
});
|
||||
}, [selectedIndex]);
|
||||
```
|
||||
|
||||
### 3. 防抖搜索模式
|
||||
|
||||
```typescript
|
||||
// 防抖加载列表
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
fetchItems(searchQuery).then(setItems);
|
||||
}, 200);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchQuery]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、总结与建议
|
||||
|
||||
### ✅ 成功提取的内容
|
||||
|
||||
1. **完整的命令菜单组件逻辑** - 包含过滤、键盘导航、选择
|
||||
2. **文件选择器组件逻辑** - 包含搜索、预览、选择
|
||||
3. **Footer 按钮逻辑** - 包含模式切换、状态管理
|
||||
4. **键盘导航模式** - 统一的导航逻辑
|
||||
5. **状态管理模式** - useState + useCallback 模式
|
||||
|
||||
### 🎯 可立即实现的组件
|
||||
|
||||
1. **ChatHeader** - 参考 Footer 按钮模式
|
||||
2. **CommandMenu** - 完整代码已提取
|
||||
3. **FileSelector** - 完整代码已提取
|
||||
4. **ContentEditable** - 参考输入框模式
|
||||
|
||||
### ⏰ 实施时间估算
|
||||
|
||||
- ChatHeader: 2 小时
|
||||
- CommandMenu 移植: 3 小时
|
||||
- FileSelector 移植: 3 小时
|
||||
- 测试整合: 2 小时
|
||||
|
||||
**总计**: 1 天可完成核心组件
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v3.0
|
||||
**最后更新**: 2025-11-18
|
||||
**状态**: 已验证可行 ✅
|
||||
Reference in New Issue
Block a user