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,11 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
NTabs,
NTabPane,
NSpin,
} from "naive-ui";
import { useI18n } from "vue-i18n";
import { useSettingsStore } from "@/stores/hermes/settings";
import DisplaySettings from "@/components/hermes/settings/DisplaySettings.vue";
@@ -50,7 +45,7 @@ function normalizeTab(value: unknown): string {
return validTabs.value.has(tab) ? tab : "account";
}
function handleTabUpdate(tab: string) {
function handleTabUpdate(tab: string | number) {
activeTab.value = normalizeTab(tab);
router.replace({
query: {
@@ -83,48 +78,47 @@ onMounted(() => {
</header>
<div class="settings-content">
<NSpin
:show="settingsStore.loading || settingsStore.saving"
size="large"
:description="t('common.loading')"
<a-spin
:loading="settingsStore.loading || settingsStore.saving"
:tip="t('common.loading')"
>
<NTabs v-model:value="activeTab" type="line" animated @update:value="handleTabUpdate">
<NTabPane name="account" :tab="t('settings.tabs.account')">
<a-tabs v-model:active-key="activeTab" type="line" @change="handleTabUpdate">
<a-tab-pane key="account" :title="t('settings.tabs.account')">
<AccountSettings />
</NTabPane>
<NTabPane v-if="canManageUsers" name="users" :tab="t('settings.tabs.users')">
</a-tab-pane>
<a-tab-pane v-if="canManageUsers" key="users" :title="t('settings.tabs.users')">
<UserManagementSettings />
</NTabPane>
<NTabPane name="display" :tab="t('settings.tabs.display')">
</a-tab-pane>
<a-tab-pane key="display" :title="t('settings.tabs.display')">
<DisplaySettings />
</NTabPane>
<NTabPane name="proxy" :tab="t('settings.tabs.proxy')">
</a-tab-pane>
<a-tab-pane key="proxy" :title="t('settings.tabs.proxy')">
<ProxySettings />
</NTabPane>
<NTabPane name="agent" :tab="t('settings.tabs.agent')">
</a-tab-pane>
<a-tab-pane key="agent" :title="t('settings.tabs.agent')">
<AgentSettings />
<GatewayAutoStartSettings />
</NTabPane>
<NTabPane name="memory" :tab="t('settings.tabs.memory')">
</a-tab-pane>
<a-tab-pane key="memory" :title="t('settings.tabs.memory')">
<MemorySettings />
</NTabPane>
<NTabPane name="compression" :tab="t('settings.tabs.compression')">
</a-tab-pane>
<a-tab-pane key="compression" :title="t('settings.tabs.compression')">
<CompressionSettings />
</NTabPane>
<NTabPane name="session" :tab="t('settings.tabs.session')">
</a-tab-pane>
<a-tab-pane key="session" :title="t('settings.tabs.session')">
<SessionSettings />
</NTabPane>
<NTabPane name="privacy" :tab="t('settings.tabs.privacy')">
</a-tab-pane>
<a-tab-pane key="privacy" :title="t('settings.tabs.privacy')">
<PrivacySettings />
</NTabPane>
<NTabPane name="models" :tab="t('settings.tabs.models')">
</a-tab-pane>
<a-tab-pane key="models" :title="t('settings.tabs.models')">
<ModelSettings />
</NTabPane>
<NTabPane name="voice" :tab="t('settings.tabs.voice')">
</a-tab-pane>
<a-tab-pane key="voice" :title="t('settings.tabs.voice')">
<VoiceSettings />
</NTabPane>
</NTabs>
</NSpin>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
</div>
</template>