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, onUnmounted } from 'vue'
import { NModal, NButton, NSpin } from 'naive-ui'
import { useMessage } from '@/composables/useMessage'
import { useI18n } from 'vue-i18n'
import { startNousLogin, pollNousLogin } from '@/api/hermes/nous-auth'
@@ -117,18 +116,17 @@ startLogin()
</script>
<template>
<NModal
v-model:show="showModal"
preset="card"
<a-modal
v-model:visible="showModal"
:title="t('models.nousLoginTitle')"
:style="{ width: 'min(440px, calc(100vw - 32px))' }"
width="min(440px, calc(100vw - 32px))"
:mask-closable="status !== 'waiting'"
@after-leave="emit('close')"
@close="emit('close')"
>
<div class="nous-login">
<!-- Idle / Loading -->
<div v-if="status === 'idle' || status === 'loading'" class="nous-login__state">
<NSpin size="small" />
<a-spin />
</div>
<!-- Waiting for authorization -->
@@ -138,12 +136,12 @@ startLogin()
<span class="nous-login__code-text">{{ userCode }}</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</div>
<NButton type="primary" block @click="openLink">
<a-button type="primary" long @click="openLink">
<template #icon>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
</template>
{{ t('models.nousOpenLink') }}
</NButton>
</a-button>
</div>
<!-- Approved -->
@@ -155,22 +153,22 @@ startLogin()
<!-- Expired -->
<div v-else-if="status === 'expired'" class="nous-login__state">
<p class="nous-login__error">{{ t('models.nousExpired') }}</p>
<NButton size="small" @click="retry">{{ t('common.retry') }}</NButton>
<a-button size="small" @click="retry">{{ t('common.retry') }}</a-button>
</div>
<!-- Error -->
<div v-else-if="status === 'error'" class="nous-login__state">
<p class="nous-login__error">{{ errorMessage }}</p>
<NButton size="small" @click="retry">{{ t('common.retry') }}</NButton>
<a-button size="small" @click="retry">{{ t('common.retry') }}</a-button>
</div>
</div>
<template #footer>
<div class="modal-footer">
<NButton :disabled="status === 'waiting'" @click="handleClose">{{ t('common.cancel') }}</NButton>
<a-button :disabled="status === 'waiting'" @click="handleClose">{{ t('common.cancel') }}</a-button>
</div>
</template>
</NModal>
</a-modal>
</template>
<style scoped lang="scss">
@@ -193,7 +191,7 @@ startLogin()
.nous-login__hint {
font-size: 14px;
color: var(--n-text-color, inherit);
color: var(--color-text-2, inherit);
text-align: center;
line-height: 1.6;
}
@@ -203,14 +201,14 @@ startLogin()
align-items: center;
gap: 12px;
padding: 12px 20px;
border: 1px solid var(--n-border-color, #e0e0e6);
border: 1px solid var(--color-border-2, #e0e0e6);
border-radius: 8px;
cursor: pointer;
transition: border-color 0.2s;
background: var(--n-color, #fafafa);
background: var(--color-fill-2, #fafafa);
&:hover {
border-color: var(--n-primary-color, #18a058);
border-color: var(--arcoblue-6, #165dff);
}
}
@@ -219,7 +217,7 @@ startLogin()
font-weight: 700;
font-family: monospace;
letter-spacing: 4px;
color: var(--n-text-color, inherit);
color: var(--color-text-1, inherit);
}
.nous-login__state--success {