fix: restore chat layout and file tree
This commit is contained in:
parent
a6da8280ff
commit
bfa7b540cb
1093
static/src/App.vue
Normal file
1093
static/src/App.vue
Normal file
File diff suppressed because it is too large
Load Diff
2139
static/src/app.ts
Normal file
2139
static/src/app.ts
Normal file
File diff suppressed because it is too large
Load Diff
203
static/src/stores/file.ts
Normal file
203
static/src/stores/file.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
interface FileNode {
|
||||
type: 'folder' | 'file';
|
||||
name: string;
|
||||
path: string;
|
||||
annotation?: string;
|
||||
children?: FileNode[];
|
||||
}
|
||||
|
||||
interface TodoTask {
|
||||
index: number;
|
||||
title: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface TodoList {
|
||||
instruction?: string;
|
||||
tasks?: TodoTask[];
|
||||
}
|
||||
|
||||
interface ContextMenuState {
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
node: FileNode | null;
|
||||
}
|
||||
|
||||
interface FileState {
|
||||
fileTree: FileNode[];
|
||||
expandedFolders: Record<string, boolean>;
|
||||
todoList: TodoList | null;
|
||||
contextMenu: ContextMenuState;
|
||||
}
|
||||
|
||||
function buildNodes(treeMap: Record<string, any> | undefined): FileNode[] {
|
||||
if (!treeMap) {
|
||||
return [];
|
||||
}
|
||||
const entries = Object.keys(treeMap).map(name => {
|
||||
const node = treeMap[name] || {};
|
||||
if (node.type === 'folder') {
|
||||
return {
|
||||
type: 'folder' as const,
|
||||
name,
|
||||
path: node.path || name,
|
||||
children: buildNodes(node.children)
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'file' as const,
|
||||
name,
|
||||
path: node.path || name,
|
||||
annotation: node.annotation || ''
|
||||
};
|
||||
});
|
||||
entries.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === 'folder' ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name, 'zh-CN');
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
export const useFileStore = defineStore('file', {
|
||||
state: (): FileState => ({
|
||||
fileTree: [],
|
||||
expandedFolders: {},
|
||||
todoList: null,
|
||||
contextMenu: {
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
node: null
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
async fetchFileTree() {
|
||||
try {
|
||||
const response = await fetch('/api/files');
|
||||
const data = await response.json();
|
||||
console.log('[FileTree] fetch result', data);
|
||||
this.setFileTreeFromResponse(data);
|
||||
} catch (error) {
|
||||
console.error('获取文件树失败:', error);
|
||||
}
|
||||
},
|
||||
setFileTreeFromResponse(payload: any) {
|
||||
if (!payload) {
|
||||
console.warn('[FileTree] 空 payload');
|
||||
return;
|
||||
}
|
||||
const structure = payload.structure || payload;
|
||||
if (!structure || !structure.tree) {
|
||||
console.warn('[FileTree] 缺少 structure.tree', structure);
|
||||
return;
|
||||
}
|
||||
const nodes = buildNodes(structure.tree);
|
||||
const expanded = { ...this.expandedFolders };
|
||||
const validFolderPaths = new Set<string>();
|
||||
|
||||
const ensureExpansion = (list: FileNode[]) => {
|
||||
list.forEach(item => {
|
||||
if (item.type === 'folder') {
|
||||
validFolderPaths.add(item.path);
|
||||
if (expanded[item.path] === undefined) {
|
||||
expanded[item.path] = false;
|
||||
}
|
||||
ensureExpansion(item.children || []);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ensureExpansion(nodes);
|
||||
Object.keys(expanded).forEach(path => {
|
||||
if (!validFolderPaths.has(path)) {
|
||||
delete expanded[path];
|
||||
}
|
||||
});
|
||||
|
||||
this.expandedFolders = expanded;
|
||||
this.fileTree = nodes;
|
||||
},
|
||||
toggleFolder(path: string) {
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
const current = !!this.expandedFolders[path];
|
||||
this.expandedFolders = {
|
||||
...this.expandedFolders,
|
||||
[path]: !current
|
||||
};
|
||||
},
|
||||
async fetchTodoList() {
|
||||
try {
|
||||
const response = await fetch('/api/todo-list');
|
||||
const data = await response.json();
|
||||
if (data && data.success) {
|
||||
this.todoList = data.data || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取待办列表失败:', error);
|
||||
}
|
||||
},
|
||||
setTodoList(payload: TodoList | null) {
|
||||
this.todoList = payload;
|
||||
},
|
||||
showContextMenu(payload: { node: FileNode; event: MouseEvent }) {
|
||||
if (!payload || !payload.node) {
|
||||
return;
|
||||
}
|
||||
const { node, event } = payload;
|
||||
if (!node.path && node.path !== '') {
|
||||
this.hideContextMenu();
|
||||
return;
|
||||
}
|
||||
if (node.type !== 'file' && node.type !== 'folder') {
|
||||
this.hideContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event && typeof event.preventDefault === 'function') {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (event && typeof event.stopPropagation === 'function') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const menuWidth = 200;
|
||||
const menuHeight = 50;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
let x = (event && event.clientX) || 0;
|
||||
let y = (event && event.clientY) || 0;
|
||||
|
||||
if (x + menuWidth > viewportWidth) {
|
||||
x = viewportWidth - menuWidth - 8;
|
||||
}
|
||||
if (y + menuHeight > viewportHeight) {
|
||||
y = viewportHeight - menuHeight - 8;
|
||||
}
|
||||
|
||||
this.contextMenu = {
|
||||
visible: true,
|
||||
x: Math.max(8, x),
|
||||
y: Math.max(8, y),
|
||||
node
|
||||
};
|
||||
},
|
||||
hideContextMenu() {
|
||||
if (!this.contextMenu.visible) {
|
||||
return;
|
||||
}
|
||||
this.contextMenu = {
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
node: null
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
369
static/src/styles/components/chat/_chat-area.scss
Normal file
369
static/src/styles/components/chat/_chat-area.scss
Normal file
@ -0,0 +1,369 @@
|
||||
/* 聊天容器整体布局,保证聊天区可见并支持上下滚动 */
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
/* 核心聊天区样式,确保对话内容在主面板中可见 */
|
||||
.messages-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
padding-bottom: calc(220px + var(--app-bottom-inset, 0px));
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.messages-area::-webkit-scrollbar,
|
||||
.sidebar::-webkit-scrollbar,
|
||||
.conversation-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.messages-area::-webkit-scrollbar-track,
|
||||
.sidebar::-webkit-scrollbar-track,
|
||||
.conversation-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.messages-area::-webkit-scrollbar-thumb,
|
||||
.sidebar::-webkit-scrollbar-thumb,
|
||||
.conversation-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(121, 109, 94, 0.4);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.scroll-lock-toggle {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
bottom: 200px;
|
||||
z-index: 25;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.scroll-lock-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--claude-border);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 6px 16px rgba(61, 57, 41, 0.08);
|
||||
}
|
||||
|
||||
.scroll-lock-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 9px 20px rgba(61, 57, 41, 0.12);
|
||||
}
|
||||
|
||||
.scroll-lock-toggle.locked .scroll-lock-btn {
|
||||
border-color: rgba(218, 119, 86, 0.32);
|
||||
box-shadow: 0 0 10px rgba(218, 119, 86, 0.28);
|
||||
}
|
||||
|
||||
.scroll-lock-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke: var(--claude-text);
|
||||
stroke-width: 1.8;
|
||||
fill: none;
|
||||
transition: stroke 0.2s ease;
|
||||
}
|
||||
|
||||
.scroll-lock-toggle.locked .scroll-lock-btn svg {
|
||||
stroke: var(--claude-accent);
|
||||
}
|
||||
|
||||
.message-block {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.user-message .message-header,
|
||||
.assistant-message .message-header {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--claude-text);
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.user-message .message-text,
|
||||
.assistant-message .message-text {
|
||||
padding: 16px 20px;
|
||||
border-radius: 18px;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
box-shadow: 0 12px 28px rgba(61, 57, 41, 0.08);
|
||||
color: var(--claude-text);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.user-message .message-text {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.assistant-message .message-text {
|
||||
background: rgba(218, 119, 86, 0.12);
|
||||
border-left: 4px solid var(--claude-accent);
|
||||
}
|
||||
|
||||
.thinking-content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--claude-text-secondary);
|
||||
}
|
||||
|
||||
.collapsible-block {
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--claude-border);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 12px 28px rgba(61, 57, 41, 0.08);
|
||||
}
|
||||
|
||||
.collapsible-header {
|
||||
padding: 14px 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
user-select: none;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
transition: background-color 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.collapsible-header:hover {
|
||||
background: rgba(218, 119, 86, 0.07);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--claude-text-secondary);
|
||||
}
|
||||
|
||||
.arrow::before {
|
||||
content: '›';
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.collapsible-block.expanded .arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.captured-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--claude-text);
|
||||
}
|
||||
|
||||
.collapsible-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.collapsible-block.expanded .collapsible-content {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding: 20px 20px 20px 56px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--claude-text-secondary);
|
||||
}
|
||||
|
||||
.action-item {
|
||||
animation: slideInFade 0.6s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
|
||||
.action-item.streaming-content,
|
||||
.action-item.immediate-show {
|
||||
animation: quickFadeIn 0.2s ease-out both;
|
||||
}
|
||||
|
||||
.action-item.completed-tool {
|
||||
animation: slideInFade 0.4s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
animation-delay: 100ms;
|
||||
}
|
||||
|
||||
@keyframes slideInFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes quickFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.text-output {
|
||||
margin: 16px 0;
|
||||
color: var(--claude-text);
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.text-output .text-content {
|
||||
padding: 0 20px 0 15px;
|
||||
}
|
||||
|
||||
.system-action {
|
||||
margin: 12px 0;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.08), rgba(59, 130, 246, 0.08));
|
||||
border-left: 4px solid rgba(79, 70, 229, 0.6);
|
||||
color: var(--claude-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.system-action-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.append-block,
|
||||
.append-placeholder {
|
||||
margin: 12px 0;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border-left: 4px solid rgba(218, 119, 86, 0.32);
|
||||
box-shadow: inset 0 0 0 1px rgba(218, 119, 86, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--claude-text);
|
||||
}
|
||||
|
||||
.append-block.append-error,
|
||||
.append-placeholder.append-error {
|
||||
background: rgba(255, 244, 242, 0.85);
|
||||
border-left-color: rgba(216, 90, 66, 0.38);
|
||||
}
|
||||
|
||||
.append-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.code-block-wrapper {
|
||||
border: 2px solid rgba(118, 103, 84, 0.25);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin: 16px 0;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
background: rgba(218, 119, 86, 0.08);
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(118, 103, 84, 0.25);
|
||||
}
|
||||
|
||||
.code-language {
|
||||
color: var(--claude-text-secondary);
|
||||
font-size: 13px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
background: transparent;
|
||||
color: var(--claude-text-secondary);
|
||||
border: 1px solid rgba(121, 109, 94, 0.35);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.copy-code-btn:hover {
|
||||
background: rgba(218, 119, 86, 0.12);
|
||||
color: var(--claude-accent-strong);
|
||||
}
|
||||
|
||||
.copy-code-btn.copied {
|
||||
background: var(--claude-success);
|
||||
border-color: var(--claude-success);
|
||||
color: #f6fff8;
|
||||
}
|
||||
|
||||
.code-block-wrapper pre {
|
||||
background: #ffffff !important;
|
||||
padding: 16px !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.code-block-wrapper pre code {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.streaming-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cursor-blink {
|
||||
animation: blink 1s steps(1) infinite;
|
||||
color: var(--claude-accent);
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
15
static/src/styles/layout/_app-shell.scss
Normal file
15
static/src/styles/layout/_app-shell.scss
Normal file
@ -0,0 +1,15 @@
|
||||
/* 主体容器,让三栏布局占满视口 */
|
||||
.main-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: var(--app-viewport, 100vh);
|
||||
background: var(--claude-bg);
|
||||
color: var(--claude-text);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: var(--app-viewport, 100vh);
|
||||
background: var(--claude-bg);
|
||||
}
|
||||
3495
static/style.css
3495
static/style.css
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user