fix(frontend): 修复分组描述换行和下拉框溢出

This commit is contained in:
Cynicismcart
2026-07-27 00:35:15 +08:00
parent 2730c1c43b
commit 005a5d2a37
4 changed files with 115 additions and 3 deletions
@@ -16,7 +16,7 @@
<!-- Row 2: description with top spacing -->
<span
v-if="description"
class="mt-1.5 w-full text-left text-xs leading-relaxed text-gray-500 dark:text-gray-400 line-clamp-2"
class="mt-1.5 w-full whitespace-pre-line [overflow-wrap:anywhere] text-left text-xs leading-relaxed text-gray-500 dark:text-gray-400 line-clamp-3"
>
{{ description }}
</span>
+7 -2
View File
@@ -184,6 +184,7 @@ const dropdownRef = ref<HTMLElement | null>(null)
const optionsListRef = ref<HTMLElement | null>(null)
const dropdownPosition = ref<'bottom' | 'top'>('bottom')
const triggerRect = ref<DOMRect | null>(null)
const dropdownViewportPadding = 8
// i18n placeholders
const placeholderText = computed(() => props.placeholder ?? t('common.selectOption'))
@@ -200,10 +201,14 @@ const dropdownStyle = computed(() => {
if (!triggerRect.value) return {}
const rect = triggerRect.value
const left = Math.max(dropdownViewportPadding, rect.left)
const availableWidth = Math.max(0, window.innerWidth - left - dropdownViewportPadding)
const minWidth = Math.min(rect.width, availableWidth)
const style: Record<string, string> = {
position: 'fixed',
left: `${rect.left}px`,
minWidth: `${rect.width}px`,
left: `${left}px`,
minWidth: `${minWidth}px`,
maxWidth: `${availableWidth}px`,
zIndex: '100000020'
}
@@ -0,0 +1,44 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import GroupOptionItem from '../GroupOptionItem.vue'
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({ t: (key: string) => key }),
}
})
vi.mock('@/stores/app', () => ({
useAppStore: () => ({ cachedPublicSettings: null }),
}))
describe('GroupOptionItem description layout', () => {
it('applies multiline and overflow-safe text styles', () => {
const description = 'First section\nvery-long-unbroken-description-value-that-must-not-overflow'
const wrapper = mount(GroupOptionItem, {
props: {
name: 'Example group',
platform: 'openai',
description,
},
global: {
stubs: {
GroupBadge: true,
},
},
})
const descriptionElement = wrapper
.findAll('span')
.find((element) => element.text() === description)
expect(descriptionElement).toBeDefined()
expect(descriptionElement?.classes()).toContain('whitespace-pre-line')
expect(descriptionElement?.classes()).toContain('[overflow-wrap:anywhere]')
expect(descriptionElement?.classes()).toContain('line-clamp-3')
expect(wrapper.find('[title]').attributes('title')).toBe(description)
})
})
@@ -0,0 +1,63 @@
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import Select from '../Select.vue'
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({ t: (key: string) => key }),
}
})
afterEach(() => {
document.body.innerHTML = ''
vi.restoreAllMocks()
})
describe('Select dropdown viewport constraints', () => {
it('limits the teleported dropdown to the available viewport width', async () => {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
value: 320,
})
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
x: 220,
y: 20,
top: 20,
right: 300,
bottom: 60,
left: 220,
width: 80,
height: 40,
toJSON: () => ({}),
})
const wrapper = mount(Select, {
props: {
modelValue: null,
options: [
{
value: 'example',
label: 'very-long-unbroken-option-value-that-must-not-overflow',
},
],
},
})
await wrapper.get('button').trigger('click')
await nextTick()
const dropdown = document.body.querySelector<HTMLElement>('.select-dropdown-portal')
expect(dropdown).not.toBeNull()
expect(dropdown?.style.left).toBe('220px')
expect(dropdown?.style.minWidth).toBe('80px')
expect(dropdown?.style.maxWidth).toBe('92px')
wrapper.unmount()
})
})