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, onMounted } from 'vue'
import { NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NButton, NCheckbox } from 'naive-ui'
import { useMessage } from '@/composables/useMessage'
import { useI18n } from 'vue-i18n'
import { useKanbanStore } from '@/stores/hermes/kanban'
@@ -35,6 +34,23 @@ const saving = ref(false)
const skillsLoading = ref(false)
const skillOptions = ref<Array<{ label: string; value: string }>>([])
// a-form 的 model 是必填 prop;这里的字段本来就各自是独立的 ref,
// 收集成一个对象只是为了满足 Arco 的类型要求,不改变任何提交逻辑。
const formModel = computed(() => ({
title: title.value,
body: body.value,
assignee: assignee.value,
priority: priority.value,
tenant: tenant.value,
workspaceKind: workspaceKind.value,
workspacePath: workspacePath.value,
branch: branch.value,
skills: skills.value,
maxRuntime: maxRuntime.value,
maxRetries: maxRetries.value,
goalMaxTurns: goalMaxTurns.value,
}))
const priorityOptions = computed(() => [
{ label: t('kanban.card.priority.low'), value: 1 },
{ label: t('kanban.card.priority.medium'), value: 2 },
@@ -128,78 +144,101 @@ async function handleSubmit() {
</script>
<template>
<NModal :show="true" preset="dialog" :title="t('kanban.createTask')" style="width: 480px;" @close="emit('close')">
<NForm label-placement="top">
<NFormItem :label="t('kanban.form.title')">
<NInput v-model:value="title" :placeholder="t('kanban.form.titlePlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.body')">
<NInput v-model:value="body" type="textarea" :rows="3" :placeholder="t('kanban.form.bodyPlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.assignee')">
<NSelect v-model:value="assignee" :options="assigneeOptions" :placeholder="t('kanban.form.selectAssignee')" clearable />
</NFormItem>
<NFormItem :label="t('kanban.form.priority')">
<NSelect v-model:value="priority" :options="priorityOptions" :placeholder="t('kanban.form.selectPriority')" clearable />
</NFormItem>
<NFormItem :label="t('kanban.form.tenant')">
<NInput v-model:value="tenant" :placeholder="t('kanban.form.tenantPlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.workspace')">
<NSelect v-model:value="workspaceKind" :options="workspaceOptions" />
</NFormItem>
<NFormItem v-if="workspaceKind !== 'scratch'" :label="t('kanban.form.workspacePath')">
<NInput v-model:value="workspacePath" :placeholder="t('kanban.form.workspacePathPlaceholder')" />
</NFormItem>
<NFormItem v-if="workspaceKind === 'worktree'" :label="t('kanban.form.branch')">
<NInput v-model:value="branch" :placeholder="t('kanban.form.branchPlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.skills')">
<NSelect
v-model:value="skills"
<a-modal
:visible="true"
:title="t('kanban.createTask')"
:style="{ width: '480px' }"
:mask-closable="false"
:esc-to-close="false"
@cancel="emit('close')"
>
<a-form :model="formModel" layout="vertical">
<a-form-item :label="t('kanban.form.title')">
<a-input v-model="title" :placeholder="t('kanban.form.titlePlaceholder')" />
</a-form-item>
<a-form-item :label="t('kanban.form.body')">
<a-textarea v-model="body" :textarea-attrs="{ rows: 3 }" :placeholder="t('kanban.form.bodyPlaceholder')" />
</a-form-item>
<a-form-item :label="t('kanban.form.assignee')">
<a-select
:model-value="assignee ?? undefined"
:options="assigneeOptions"
:placeholder="t('kanban.form.selectAssignee')"
allow-clear
@change="value => assignee = (value as string) ?? null"
/>
</a-form-item>
<a-form-item :label="t('kanban.form.priority')">
<a-select
:model-value="priority ?? undefined"
:options="priorityOptions"
:placeholder="t('kanban.form.selectPriority')"
allow-clear
@change="value => priority = (value as number) ?? null"
/>
</a-form-item>
<a-form-item :label="t('kanban.form.tenant')">
<a-input v-model="tenant" :placeholder="t('kanban.form.tenantPlaceholder')" />
</a-form-item>
<a-form-item :label="t('kanban.form.workspace')">
<a-select v-model="workspaceKind" :options="workspaceOptions" />
</a-form-item>
<a-form-item v-if="workspaceKind !== 'scratch'" :label="t('kanban.form.workspacePath')">
<a-input v-model="workspacePath" :placeholder="t('kanban.form.workspacePathPlaceholder')" />
</a-form-item>
<a-form-item v-if="workspaceKind === 'worktree'" :label="t('kanban.form.branch')">
<a-input v-model="branch" :placeholder="t('kanban.form.branchPlaceholder')" />
</a-form-item>
<a-form-item :label="t('kanban.form.skills')">
<a-select
v-model="skills"
multiple
filterable
clearable
allow-search
allow-clear
:loading="skillsLoading"
:options="skillOptions"
:placeholder="t('kanban.form.skillsPlaceholder')"
/>
</NFormItem>
</a-form-item>
<div class="advanced-row">
<NCheckbox v-model:checked="triage">{{ t('kanban.form.triage') }}</NCheckbox>
<NCheckbox v-model:checked="goalMode">{{ t('kanban.form.goalMode') }}</NCheckbox>
<a-checkbox v-model="triage">{{ t('kanban.form.triage') }}</a-checkbox>
<a-checkbox v-model="goalMode">{{ t('kanban.form.goalMode') }}</a-checkbox>
</div>
<div class="advanced-grid">
<NFormItem :label="t('kanban.form.maxRuntime')">
<NInput v-model:value="maxRuntime" :placeholder="t('kanban.form.maxRuntimePlaceholder')" />
</NFormItem>
<NFormItem :label="t('kanban.form.maxRetries')">
<NInputNumber
v-model:value="maxRetries"
<a-form-item :label="t('kanban.form.maxRuntime')">
<a-input v-model="maxRuntime" :placeholder="t('kanban.form.maxRuntimePlaceholder')" />
</a-form-item>
<a-form-item :label="t('kanban.form.maxRetries')">
<a-input-number
:model-value="maxRetries ?? undefined"
:min="1"
:max="100"
:precision="0"
:placeholder="t('kanban.form.maxRetriesPlaceholder')"
clearable
allow-clear
style="width: 100%"
@change="value => maxRetries = value ?? null"
/>
</NFormItem>
<NFormItem v-if="goalMode" :label="t('kanban.form.goalMaxTurns')">
<NInputNumber
v-model:value="goalMaxTurns"
</a-form-item>
<a-form-item v-if="goalMode" :label="t('kanban.form.goalMaxTurns')">
<a-input-number
:model-value="goalMaxTurns ?? undefined"
:min="1"
:max="100"
:precision="0"
:placeholder="t('kanban.form.goalMaxTurnsPlaceholder')"
clearable
allow-clear
style="width: 100%"
@change="value => goalMaxTurns = value ?? null"
/>
</NFormItem>
</a-form-item>
</div>
</NForm>
<template #action>
<NButton @click="emit('close')">{{ t('common.cancel') }}</NButton>
<NButton type="primary" :loading="saving" @click="handleSubmit">{{ t('common.create') }}</NButton>
</a-form>
<template #footer>
<a-button @click="emit('close')">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :loading="saving" @click="handleSubmit">{{ t('common.create') }}</a-button>
</template>
</NModal>
</a-modal>
</template>
<style scoped lang="scss">