Phase 006: migrate the chat page from naive-ui to Arco Design Vue

Swaps the rendering layer only. Chat store logic, keyboard handling, and the
/chat-run Socket.IO contract are untouched, so a mixed UI cannot hide a chat
regression behind a library swap.

Arco's dropdowns take their items from a #content slot of a-doption children
rather than a droplist prop, and Arco Triggers cannot be nested, so the two
toolbar menus lost their a-tooltip in favour of a native title. Both rules are
recorded in docs/arco-mapping.md, and tests/e2e/chat-input-dropdown.spec.ts plus
tests/e2e/chat-rendering-smoke.spec.ts keep the menus, the streaming markdown
rendering, and the no-Arco-inside-the-bubble red line from regressing silently.
This commit is contained in:
2026-10-17 22:17:32 +08:00
parent 4baccd5d88
commit effc29085c
20 changed files with 1356 additions and 819 deletions
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { NDrawer, NDrawerContent, NSpin } from 'naive-ui'
import { useMessage } from '@/composables/useMessage'
import type MarkdownIt from 'markdown-it'
import MarkdownItConstructor from 'markdown-it'
@@ -488,37 +487,29 @@ async function previewTextFile(path: string, fileName: string): Promise<void> {
textPreviewLoading.value = false
}
}
function closeTextPreview(): void {
textPreviewVisible.value = false
}
</script>
<template>
<div ref="markdownBody" class="markdown-body" v-html="renderedHtml" @click="handleMarkdownClick"></div>
<!-- File preview area -->
<NDrawer
v-model:show="textPreviewVisible"
<!-- File preview area. Lazily mounted: keeps any Arco component out of the
message bubble render tree while streaming (Phase 006 §1 perf red line). -->
<a-drawer
v-if="textPreviewVisible"
v-model:visible="textPreviewVisible"
:width="PREVIEW_AREA_WIDTH"
placement="right"
:show-mask="false"
:trap-focus="false"
:mask="false"
unmount-on-close
class="markdown-text-preview-drawer"
>
<NDrawerContent
:title="t('download.contentDisplay')"
closable
:body-content-style="{ padding: 0 }"
@close="closeTextPreview"
>
<NSpin :show="textPreviewLoading">
<div v-if="textPreviewContent !== null && textPreviewIsMarkdown" class="text-preview-markdown">
<MarkdownRenderer :content="textPreviewContent" />
</div>
<pre v-else-if="textPreviewContent !== null" class="text-preview-body">{{ textPreviewContent }}</pre>
</NSpin>
</NDrawerContent>
</NDrawer>
<template #title>{{ t('download.contentDisplay') }}</template>
<a-spin :loading="textPreviewLoading" class="text-preview-spin">
<div v-if="textPreviewContent !== null && textPreviewIsMarkdown" class="text-preview-markdown">
<MarkdownRenderer :content="textPreviewContent" />
</div>
<pre v-else-if="textPreviewContent !== null" class="text-preview-body">{{ textPreviewContent }}</pre>
</a-spin>
</a-drawer>
<Teleport to="body">
<div v-if="previewUrl" class="image-preview-overlay" @click.self="previewUrl = null">
<img :src="previewUrl" class="image-preview-img" @click="previewUrl = null" />
@@ -827,18 +818,24 @@ function closeTextPreview(): void {
.markdown-text-preview-drawer {
max-width: 100vw;
.n-drawer-content,
.n-drawer-body-content-wrapper {
.arco-drawer {
max-width: 100vw;
}
}
.text-preview-spin {
display: block;
:deep(.arco-spin-children) {
display: block;
}
}
@media (max-width: $breakpoint-mobile) {
.markdown-text-preview-drawer {
max-width: 100vw;
.n-drawer-content,
.n-drawer-body-content-wrapper {
.arco-drawer {
max-width: 100vw;
}
}