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,6 +1,5 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { NButton, NCheckbox, NCheckboxGroup, NModal, NInput, NSelect } from 'naive-ui'
import { useMessage } from '@/composables/useMessage'
import { useDialog } from '@/composables/useDialog'
import type { AvailableModelGroup } from '@/api/hermes/system'
@@ -260,16 +259,16 @@ async function handleDelete() {
</div>
<div class="default-model-row">
<span class="info-label">{{ t('models.defaultModel') }}</span>
<NSelect
<a-select
class="default-model-select"
size="tiny"
filterable
:value="defaultModelSelectValue"
size="mini"
allow-search
:model-value="defaultModelSelectValue ?? undefined"
:options="defaultModelOptions"
:placeholder="t('models.selectModel')"
:disabled="provider.models.length === 0"
:loading="defaultingModel !== null"
@update:value="handleSetDefaultModel"
@change="handleSetDefaultModel($event as string | null)"
/>
</div>
<div class="models-list">
@@ -293,25 +292,25 @@ async function handleDelete() {
</div>
<div class="card-actions">
<NButton
size="tiny"
quaternary
<a-button
size="mini"
type="text"
:disabled="isDefaultProvider"
:loading="defaultingProvider"
@click="handleSetDefaultProvider"
>
{{ isDefaultProvider ? t('models.currentDefault') : t('models.setDefaultProvider') }}
</NButton>
<NButton size="tiny" quaternary @click="showAliasListModal = true">{{ t('models.aliasManage') }}</NButton>
<NButton size="tiny" quaternary @click="openVisibilityModal">{{ t('models.manageVisibleModels') }}</NButton>
<NButton size="tiny" quaternary type="error" :loading="deleting" @click="handleDelete">{{ destructiveActionLabel }}</NButton>
</a-button>
<a-button size="mini" type="text" @click="showAliasListModal = true">{{ t('models.aliasManage') }}</a-button>
<a-button size="mini" type="text" @click="openVisibilityModal">{{ t('models.manageVisibleModels') }}</a-button>
<a-button size="mini" type="text" status="danger" :loading="deleting" @click="handleDelete">{{ destructiveActionLabel }}</a-button>
</div>
<NModal
v-model:show="showAliasListModal"
preset="card"
<a-modal
v-model:visible="showAliasListModal"
:footer="false"
:title="t('models.aliasManageFor', { provider: displayName })"
:style="{ width: 'min(560px, calc(100vw - 32px))' }"
width="min(560px, calc(100vw - 32px))"
:mask-closable="true"
>
<div class="alias-list-hint">{{ t('models.aliasHint') }}</div>
@@ -322,22 +321,21 @@ async function handleDelete() {
<span v-if="isDefaultModel(model)" class="alias-row-default">{{ t('models.defaultShort') }}</span>
<code class="alias-row-id">{{ model }}</code>
</div>
<NButton size="tiny" quaternary @click="openAliasEditor(model)">{{ t('models.aliasEdit') }}</NButton>
<a-button size="mini" type="text" @click="openAliasEditor(model)">{{ t('models.aliasEdit') }}</a-button>
</div>
</div>
</NModal>
</a-modal>
<NModal
v-model:show="showAliasModal"
preset="card"
<a-modal
v-model:visible="showAliasModal"
:title="aliasModel ? t('models.aliasTitleFor', { model: aliasModel }) : t('models.aliasTitle')"
:style="{ width: 'min(420px, calc(100vw - 32px))' }"
width="min(420px, calc(100vw - 32px))"
:mask-closable="true"
>
<NInput
v-model:value="aliasInput"
<a-input
v-model="aliasInput"
:placeholder="t('models.aliasPlaceholder')"
clearable
allow-clear
@keydown.enter="saveAlias"
/>
<div v-if="aliasModel" class="model-alias-canonical">
@@ -346,21 +344,21 @@ async function handleDelete() {
<div class="model-alias-hint">{{ t('models.aliasHint') }}</div>
<template #footer>
<div class="model-alias-actions">
<NButton quaternary :disabled="!appStore.getModelAlias(aliasModel, aliasProvider)" @click="clearAlias">
<a-button type="text" :disabled="!appStore.getModelAlias(aliasModel, aliasProvider)" @click="clearAlias">
{{ t('models.aliasUseOriginal') }}
</NButton>
</a-button>
<div class="model-alias-spacer" />
<NButton @click="showAliasModal = false">{{ t('common.cancel') }}</NButton>
<NButton type="primary" @click="saveAlias">{{ t('common.save') }}</NButton>
<a-button @click="showAliasModal = false">{{ t('common.cancel') }}</a-button>
<a-button type="primary" @click="saveAlias">{{ t('common.save') }}</a-button>
</div>
</template>
</NModal>
</a-modal>
<NModal
v-model:show="showVisibilityModal"
preset="card"
<a-modal
v-model:visible="showVisibilityModal"
:footer="false"
:title="t('models.manageVisibleModelsFor', { name: displayName })"
:style="{ width: 'min(560px, calc(100vw - 32px))' }"
width="min(560px, calc(100vw - 32px))"
:mask-closable="!visibilitySaving"
>
<p class="visibility-hint">{{ t('models.visibilityHint') }}</p>
@@ -368,8 +366,8 @@ async function handleDelete() {
{{ selectedVisibleModels.length }}/{{ allModels.length }} {{ t('models.count') }}
</div>
<div class="visibility-list">
<NCheckboxGroup v-model:value="selectedVisibleModels">
<NCheckbox
<a-checkbox-group v-model="selectedVisibleModels">
<a-checkbox
v-for="model in allModels"
:key="model"
:value="model"
@@ -377,25 +375,25 @@ async function handleDelete() {
>
<code>{{ modelDisplayName(model) }}</code>
<code v-if="modelAlias(model)" class="visibility-model-id">{{ model }}</code>
</NCheckbox>
</NCheckboxGroup>
</a-checkbox>
</a-checkbox-group>
</div>
<div class="visibility-actions">
<NButton size="small" quaternary :disabled="visibilitySaving" @click="resetVisibility">
<a-button size="small" type="text" :disabled="visibilitySaving" @click="resetVisibility">
{{ t('models.showAllModels') }}
</NButton>
<NButton size="small" quaternary :disabled="visibilitySaving" @click="clearVisibility">
</a-button>
<a-button size="small" type="text" :disabled="visibilitySaving" @click="clearVisibility">
{{ t('models.clearVisibleModels') }}
</NButton>
</a-button>
<div class="visibility-action-spacer" />
<NButton size="small" :disabled="visibilitySaving" @click="showVisibilityModal = false">
<a-button size="small" :disabled="visibilitySaving" @click="showVisibilityModal = false">
{{ t('common.cancel') }}
</NButton>
<NButton size="small" type="primary" :loading="visibilitySaving" @click="handleVisibilitySave">
</a-button>
<a-button size="small" type="primary" :loading="visibilitySaving" @click="handleVisibilitySave">
{{ t('common.save') }}
</NButton>
</a-button>
</div>
</NModal>
</a-modal>
</div>
</template>