feat(models): add external catalog proxy selection

This commit is contained in:
elky
2026-07-31 09:32:25 +08:00
parent 12057db476
commit beb003b7ad
34 changed files with 2286 additions and 176 deletions
@@ -2,10 +2,13 @@
<div class="space-y-1.5">
<Select
:model-value="modelValue"
:disabled="proxyNodesStore.loading || nodeOptions.length === 0"
:disabled="disabled || proxyNodesStore.loading || nodeOptions.length === 0"
@update:model-value="(v: string) => $emit('update:modelValue', v)"
>
<SelectTrigger :class="triggerClass">
<SelectTrigger
:class="triggerClass"
:aria-label="triggerAriaLabel"
>
<SelectValue
:placeholder="proxyNodesStore.loading
? legacyT('加载节点列表中...')
@@ -40,10 +43,16 @@ import { useProxyNodesStore } from '@/stores/proxy-nodes'
import { useI18n } from '@/i18n'
import { formatRegion } from '@/utils/region'
const props = defineProps<{
const props = withDefaults(defineProps<{
modelValue: string
triggerClass?: string
}>()
triggerAriaLabel?: string
disabled?: boolean
}>(), {
triggerClass: '',
triggerAriaLabel: undefined,
disabled: false,
})
defineEmits<{
'update:modelValue': [value: string]
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h } from 'vue'
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
@@ -19,34 +19,81 @@ vi.mock('@/components/ui', async () => {
const { defineComponent, h } = await import('vue')
const passthrough = (name: string) => defineComponent({
name,
setup(_, { slots }) {
return () => h('div', slots.default?.())
inheritAttrs: false,
setup(_, { attrs, slots }) {
return () => h('div', attrs, slots.default?.())
},
})
return {
Select: passthrough('SelectStub'),
Select: defineComponent({
name: 'SelectStub',
props: {
disabled: { type: Boolean, default: false },
},
setup(props, { slots }) {
return () => h('div', {
'data-testid': 'proxy-select',
'data-disabled': String(props.disabled),
}, slots.default?.())
},
}),
SelectTrigger: passthrough('SelectTriggerStub'),
SelectValue: passthrough('SelectValueStub'),
SelectContent: passthrough('SelectContentStub'),
SelectItem: passthrough('SelectItemStub'),
SelectItem: defineComponent({
name: 'SelectItemStub',
props: {
value: { type: String, required: true },
},
setup(props, { slots }) {
return () => h('div', { 'data-select-value': props.value }, slots.default?.())
},
}),
}
})
function mountSelect(props: Record<string, unknown> = {}) {
const root = document.createElement('div')
const app = createApp(defineComponent({
setup() {
return () => h(ProxyNodeSelect, { modelValue: '', ...props })
},
}))
app.use(createI18n())
app.mount(root)
return { app, root }
}
beforeEach(() => {
proxyNodesStore.ensureLoaded.mockClear()
})
describe('ProxyNodeSelect', () => {
it('loads proxy nodes when mounted', () => {
const root = document.createElement('div')
const app = createApp(defineComponent({
setup() {
return () => h(ProxyNodeSelect, { modelValue: '' })
},
}))
app.use(createI18n())
app.mount(root)
const { app } = mountSelect()
expect(proxyNodesStore.ensureLoaded).toHaveBeenCalledTimes(1)
app.unmount()
})
})
it('stays disabled when there are no proxy nodes', () => {
const { app, root } = mountSelect()
expect(root.querySelector('[data-testid="proxy-select"]')
?.getAttribute('data-disabled')).toBe('true')
app.unmount()
})
it('applies an accessible label to the select trigger', () => {
const { app, root } = mountSelect({
triggerAriaLabel: 'External model catalog access',
})
expect(root.querySelector('[aria-label="External model catalog access"]')).not.toBeNull()
app.unmount()
})
})