Phase 007: migrate every remaining view from naive-ui to Arco Design Vue

The project now has exactly one UI library. Every naive-ui component in
packages/client/src is replaced by its Arco equivalent across 24 views and
roughly 80 components, and naive-ui is dropped from package.json along with
its transitive vueuc / @css-render / vdirs / seemly dependencies.

The migration is rendering-layer only. No store logic, no API contract, and
no /chat-run payload was touched.

Three classes of leftover were invisible to `grep naive-ui` and are now
covered by tests/client/no-naive-ui.test.ts, which asserts all three:
  - dead .n-* class names in style blocks
  - dead --n-* custom properties
  - naive-ui in package.json

Notable behavioural traps handled along the way, recorded in
docs/arco-migration-rules.md:
  - a-input-password defaults to masked; passing default-visibility="false"
    would have exposed API keys in clear text
  - a-upload fires a real XHR unless auto-upload is false
  - nested Trigger components fight over the first element child, so the
    group-chat settings dropdown was flattened
  - Arco renders tooltip default-slot text inline, which was silently
    collapsing the chat and group-chat toolbars
This commit is contained in:
2026-10-18 22:57:25 +08:00
parent c3eeea4187
commit 836d0228d2
115 changed files with 4067 additions and 3891 deletions
@@ -1,9 +1,8 @@
<script setup lang="ts">
import { computed, ref, watch, h } from 'vue'
import { NTree } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFilesStore } from '@/stores/hermes/files'
import type { TreeOption } from 'naive-ui'
import type { TreeNodeData } from '@arco-design/web-vue'
const { t } = useI18n()
const filesStore = useFilesStore()
@@ -14,12 +13,12 @@ const props = defineProps<{
const effectiveProfile = computed(() => props.profile === undefined ? filesStore.currentProfile : props.profile)
const treeData = ref<TreeOption[]>([])
const treeData = ref<TreeNodeData[]>([])
const selectedKeys = ref<string[]>([])
const treeInstanceKey = ref(0)
let rootLoadSeq = 0
async function loadChildren(path: string): Promise<TreeOption[]> {
async function loadChildren(path: string): Promise<TreeNodeData[]> {
try {
const result = filesStore.currentWorkspaceSessionId
? await filesStore.listEntries(path)
@@ -29,7 +28,7 @@ async function loadChildren(path: string): Promise<TreeOption[]> {
.sort((a, b) => a.name.localeCompare(b.name))
.map(e => ({
key: e.path,
label: e.name,
title: e.name,
isLeaf: false,
}))
} catch {
@@ -37,16 +36,16 @@ async function loadChildren(path: string): Promise<TreeOption[]> {
}
}
async function handleLoad(node: TreeOption): Promise<void> {
async function handleLoad(node: TreeNodeData): Promise<void> {
const seq = rootLoadSeq
const children = await loadChildren(node.key as string)
const children = await loadChildren(String(node.key ?? ''))
if (seq === rootLoadSeq) node.children = children
}
function handleSelect(keys: string[]) {
function handleSelect(keys: (string | number)[]) {
if (keys.length > 0) {
selectedKeys.value = keys
filesStore.navigateTo(keys[0], { profile: effectiveProfile.value })
selectedKeys.value = keys.map(String)
filesStore.navigateTo(String(keys[0]), { profile: effectiveProfile.value })
}
}
@@ -55,11 +54,6 @@ function handleRootClick() {
filesStore.navigateTo('', { profile: effectiveProfile.value })
}
function renderLabel({ option }: { option: TreeOption }) {
const label = String(option.label || '')
return h('span', { class: 'tree-node-label', title: label }, label)
}
watch([effectiveProfile, () => filesStore.currentWorkspaceSessionId, () => props.workspaceKey], async () => {
const seq = ++rootLoadSeq
selectedKeys.value = []
@@ -78,16 +72,21 @@ watch([effectiveProfile, () => filesStore.currentWorkspaceSessionId, () => props
</svg>
<span>{{ t('files.breadcrumbRoot') }}</span>
</div>
<NTree
<a-tree
:key="treeInstanceKey"
:data="treeData"
:selected-keys="selectedKeys"
:on-load="handleLoad"
:render-label="renderLabel"
expand-on-click
block-line
@update:selected-keys="handleSelect"
/>
:load-more="handleLoad"
:default-expand-all="false"
action-on-node-click="expand"
block-node
show-line
@select="handleSelect"
>
<template #title="node">
<span class="tree-node-label" :title="node.title">{{ node.title }}</span>
</template>
</a-tree>
</div>
</template>