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
125 lines
3.3 KiB
Vue
125 lines
3.3 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useFilesStore } from '@/stores/hermes/files'
|
|
import type { TreeNodeData } from '@arco-design/web-vue'
|
|
|
|
const { t } = useI18n()
|
|
const filesStore = useFilesStore()
|
|
const props = defineProps<{
|
|
profile?: string | null
|
|
workspaceKey?: string | null
|
|
}>()
|
|
|
|
const effectiveProfile = computed(() => props.profile === undefined ? filesStore.currentProfile : props.profile)
|
|
|
|
const treeData = ref<TreeNodeData[]>([])
|
|
const selectedKeys = ref<string[]>([])
|
|
const treeInstanceKey = ref(0)
|
|
let rootLoadSeq = 0
|
|
|
|
async function loadChildren(path: string): Promise<TreeNodeData[]> {
|
|
try {
|
|
const result = filesStore.currentWorkspaceSessionId
|
|
? await filesStore.listEntries(path)
|
|
: await filesStore.fetchDirectory(path, { profile: effectiveProfile.value })
|
|
return result.entries
|
|
.filter(e => e.isDir)
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
.map(e => ({
|
|
key: e.path,
|
|
title: e.name,
|
|
isLeaf: false,
|
|
}))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
async function handleLoad(node: TreeNodeData): Promise<void> {
|
|
const seq = rootLoadSeq
|
|
const children = await loadChildren(String(node.key ?? ''))
|
|
if (seq === rootLoadSeq) node.children = children
|
|
}
|
|
|
|
function handleSelect(keys: (string | number)[]) {
|
|
if (keys.length > 0) {
|
|
selectedKeys.value = keys.map(String)
|
|
filesStore.navigateTo(String(keys[0]), { profile: effectiveProfile.value })
|
|
}
|
|
}
|
|
|
|
function handleRootClick() {
|
|
selectedKeys.value = []
|
|
filesStore.navigateTo('', { profile: effectiveProfile.value })
|
|
}
|
|
|
|
watch([effectiveProfile, () => filesStore.currentWorkspaceSessionId, () => props.workspaceKey], async () => {
|
|
const seq = ++rootLoadSeq
|
|
selectedKeys.value = []
|
|
treeInstanceKey.value += 1
|
|
const nextTreeData = await loadChildren('')
|
|
if (seq === rootLoadSeq) treeData.value = nextTreeData
|
|
}, { immediate: true })
|
|
</script>
|
|
|
|
<template>
|
|
<div class="file-tree">
|
|
<div class="tree-header" @click="handleRootClick">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
<polyline points="9 22 9 12 15 12 15 22" />
|
|
</svg>
|
|
<span>{{ t('files.breadcrumbRoot') }}</span>
|
|
</div>
|
|
<a-tree
|
|
:key="treeInstanceKey"
|
|
:data="treeData"
|
|
:selected-keys="selectedKeys"
|
|
: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>
|
|
|
|
<style scoped lang="scss">
|
|
@use '@/styles/variables' as *;
|
|
|
|
.file-tree {
|
|
padding: 8px;
|
|
}
|
|
|
|
.tree-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
border-radius: $radius-sm;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: $text-primary;
|
|
|
|
&:hover {
|
|
background-color: rgba(var(--accent-primary-rgb), 0.06);
|
|
}
|
|
}
|
|
|
|
:deep(.tree-node-label) {
|
|
display: inline-block;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
vertical-align: bottom;
|
|
}
|
|
</style>
|