MainLayout.vue
10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<template>
<div class="app-container">
<!-- 主内容区 -->
<div class="main-content">
<!-- 左侧导航栏 -->
<Sidebar />
<!-- 中间工作区 -->
<div class="workspace" :class="{ minimized: isPanelMinimized }">
<!-- 1. 核心常驻页面:用 visibility 替代 v-show(display:none),
避免 iframe 因 DOM 重排出现闪烁 -->
<WelcomePage
v-if="wasVisited('Welcome')"
class="page-layer"
:class="{ 'page-active': route.name === 'Welcome' }"
/>
<WorkspacePage
v-if="wasVisited('Workspace')"
class="page-layer"
:class="{ 'page-active': route.name === 'Workspace' }"
/>
<KnowledgeBasePage
v-if="wasVisited('KnowledgeBase')"
class="page-layer"
:class="{ 'page-active': route.name === 'KnowledgeBase' }"
/>
<!-- 2. 其他非常规页面(如设置、个人中心等),依然走正常的路由销毁流程 -->
<router-view v-if="isOtherRoute" />
</div>
<!-- 注意:IntelligencePanel 已移到 Workspace 内部,这里只保留 IntelligencePanelAgent -->
<IntelligencePanelAgent
v-if="!hideIntelligencePanelAgent"
ref="intelligencePanelAgentRef"
:minimized="isPanelMinimized"
@minimize-changed="handleMinimizeChange"
@new-chat="handleNewChat"
@show-history="handleShowHistory"
/>
</div>
<!-- 新用户引导 (el-tour) -->
<el-tour
v-model="isTourOpen"
:mask="true"
:z-index="3000"
@finish="handleTourFinish"
@close="handleTourFinish"
>
<el-tour-step
target="#tour-logo"
:title="t('tour.step1.title')"
:description="t('tour.step1.desc')"
>
<template #next-button>
<el-button size="small" type="primary">{{ t('tour.next') }}</el-button>
</template>
</el-tour-step>
<el-tour-step
target="#tour-quick-ask"
:title="t('tour.step2.title')"
:description="t('tour.step2.desc')"
>
<template #prev-button>
<el-button size="small">{{ t('tour.prev') }}</el-button>
</template>
<template #next-button>
<el-button size="small" type="primary">{{ t('tour.next') }}</el-button>
</template>
</el-tour-step>
<el-tour-step
target="#tour-deep-search"
:title="t('tour.step3.title')"
:description="t('tour.step3.desc')"
>
<template #prev-button>
<el-button size="small">{{ t('tour.prev') }}</el-button>
</template>
<template #next-button>
<el-button size="small" type="primary">{{ t('tour.next') }}</el-button>
</template>
</el-tour-step>
<el-tour-step
target="#tour-workspace"
:title="t('tour.step4.title')"
:description="t('tour.step4.desc')"
>
<template #prev-button>
<el-button size="small">{{ t('tour.prev') }}</el-button>
</template>
<template #next-button>
<el-button size="small" type="primary">{{ t('tour.next') }}</el-button>
</template>
</el-tour-step>
<el-tour-step
target="#tour-knowledge"
:title="t('tour.step5.title')"
:description="t('tour.step5.desc')"
>
<template #prev-button>
<el-button size="small">{{ t('tour.prev') }}</el-button>
</template>
<template #next-button>
<el-button size="small" type="primary" @click="handleTourFinish">
{{ t('tour.finish') }}
</el-button>
</template>
</el-tour-step>
</el-tour>
</div>
</template>
<script setup lang="ts">
import {
ref,
computed,
onMounted,
onBeforeUnmount,
defineAsyncComponent,
reactive,
watch,
} from "vue";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import Sidebar from "@/layout/Sidebar.vue";
import { useAppStore } from "@/stores/app";
// 注意:IntelligencePanel 已移到 Workspace 内部
// import IntelligencePanel from "@/layout/IntelligencePanel.vue";
const IntelligencePanelAgent = defineAsyncComponent(() =>
import("@/layout/IntelligencePanelAgent.vue")
);
// 常驻页面异步组件
const WelcomePage = defineAsyncComponent(() => import("@/pages/Welcome.vue"));
const WorkspacePage = defineAsyncComponent(() => import("@/pages/Workspace.vue"));
const KnowledgeBasePage = defineAsyncComponent(() =>
import("@/pages/KnowledgeBase.vue")
);
const route = useRoute();
const { t } = useI18n();
const appStore = useAppStore();
// 状态定义
const isPanelMinimized = ref(false);
const visitedMap = reactive<Record<string, boolean>>({
Welcome: false,
Workspace: false,
KnowledgeBase: false,
});
// 新用户引导状态
const isTourOpen = ref(false);
const TOUR_STORAGE_KEY = "linkmed_onboarding_completed";
const checkAndStartTour = () => {
const isCompleted = localStorage.getItem(TOUR_STORAGE_KEY);
// 引导包含欢迎页特有元素,建议仅在欢迎页启动
if (!isCompleted && !appStore.globalModalVisible && route.name === "Welcome") {
// 为了确保 DOM 已渲染 (特别是 Sidebar 里的 ID 和异步加载的 WelcomePage)
setTimeout(() => {
// 再次检查确认没有弹窗且还没完成,且依然在欢迎页
if (
!localStorage.getItem(TOUR_STORAGE_KEY) &&
!appStore.globalModalVisible &&
route.name === "Welcome"
) {
isTourOpen.value = true;
}
}, 1000);
}
};
// 监听全局弹窗状态,当弹窗消失且引导未完成时,触发引导
watch(
[() => appStore.globalModalVisible, () => route.name],
([modalVisible, routeName]) => {
if (!modalVisible && routeName === "Welcome") {
checkAndStartTour();
} else if (isTourOpen.value && (modalVisible || routeName !== "Welcome")) {
// 如果引导正在进行却突然出现了全局弹窗,或者离开了欢迎页,先关闭引导以防重合或目标缺失
isTourOpen.value = false;
}
}
);
const handleTourFinish = () => {
localStorage.setItem(TOUR_STORAGE_KEY, "true");
isTourOpen.value = false;
// 新手引导结束后,自动打开新手任务卡片
window.dispatchEvent(new CustomEvent("open-newbie-task"));
};
// 检查并记录页面是否被访问过
const wasVisited = (name: string) => {
if (route.name === name) {
visitedMap[name] = true;
}
return visitedMap[name];
};
// 判断是否为非常驻页面
const isOtherRoute = computed(() => {
return !["Welcome", "Workspace", "KnowledgeBase"].includes(
route.name as string
);
});
// 计算属性
// 注意:hideIntelligencePanel 已移除,因为 IntelligencePanel 已移到 Workspace 内部
const hideIntelligencePanelAgent = computed(() => {
// console.log("不显示agent智能体:", route.meta.hideIntelligencePanel || !route.query.agent)
return route.meta.hideIntelligencePanel || !route.query.agent;
});
// 方法
// 注意:IntelligencePanel 的 resize 相关方法已移除,因为 IntelligencePanel 已移到 Workspace 内部
// 这些方法现在由 Workspace 组件管理
const handleMinimizeChange = (minimized: boolean) => {
isPanelMinimized.value = minimized;
};
const handleNewChat = () => {
console.log("MainLayout: 新建对话");
// TODO: 实现新建对话功能
};
const handleShowHistory = () => {
console.log("MainLayout: 显示历史对话");
// TODO: 实现显示历史对话功能
};
// 生命周期
// 注意:IntelligencePanel 的 resize 事件监听已移除,因为 IntelligencePanel 已移到 Workspace 内部
onMounted(() => {
// IntelligencePanel 的 resize 逻辑现在由 Workspace 组件管理
checkAndStartTour();
});
onBeforeUnmount(() => {
// IntelligencePanel 的 resize 逻辑现在由 Workspace 组件管理
});
</script>
<style scoped>
.app-container {
display: flex;
height: 100vh;
width: 100vw;
background: var(--color-bg, #141518);
color: var(--color-text, #eee);
overflow: hidden;
}
.main-content {
flex: 1;
display: flex;
overflow: hidden;
background: var(--color-bg, #18181a);
gap: 0;
position: relative;
}
.workspace {
flex: 1;
display: flex;
overflow: hidden;
min-width: 320px;
position: relative;
transition: flex 0.3s ease;
}
/* 常驻页面层:用 opacity 切换,保持 iframe 在布局流中,避免 display:none 重排闪烁。
不用 visibility:子元素显式 visibility:visible 会穿透父元素 visibility:hidden。
opacity:0 无此问题,子元素无法超过父元素的 opacity。 */
.page-layer {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
&.page-active {
opacity: 1;
pointer-events: auto;
}
}
/* 当AI面板最小化时,workspace自动扩展 */
.workspace.minimized {
flex: 1;
}
/* 当AI面板最小化时,隐藏其容器 */
:deep(.intelligence-panel.minimized) {
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
flex-shrink: 0 !important;
overflow: visible !important;
}
/* 可拉伸分隔线样式 */
.resize-handle {
width: 2px;
flex-shrink: 0;
height: 100%;
align-self: stretch;
background: transparent;
cursor: col-resize;
transition: background-color 0.15s ease;
position: relative;
z-index: 10;
}
.resize-handle:hover {
background: var(--color-primary);
}
.resize-handle:active {
background: var(--color-primary);
}
/* 扩大可点击区域,但保持视觉宽度 */
.resize-handle::before {
content: "";
position: absolute;
left: -2px;
right: -2px;
top: 0;
bottom: 0;
}
/* 拖拽时的智能面板样式 */
:deep(.intelligence-panel.resizing) {
transition: none !important;
}
/* 平板端适配 (768px - 1024px) */
@media (min-width: 768px) and (max-width: 1024px) {
.main-content {
gap: 0;
}
.workspace {
flex: 1.5;
min-width: 0;
}
}
/* 小屏平板端适配 (600px - 768px) */
@media (min-width: 600px) and (max-width: 767px) {
.main-content {
gap: 0;
padding: 6px;
}
.workspace {
flex: 1.2;
min-width: 0;
}
}
/* 手机端适配 (小于600px) - 配合同比例缩放逻辑 */
@media (max-width: 599px) {
.app-container {
width: 1024px; /* 强制容器宽度与缩放基准一致 */
overflow-x: hidden;
}
.main-content {
flex-direction: row;
gap: 0;
padding: 0;
}
.workspace {
flex: 1;
min-width: 0;
}
}
</style>