fix(frontend): 统一原生表单控件样式
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import FilePicker from './FilePicker.vue'
|
||||
|
||||
describe('FilePicker', () => {
|
||||
it('keeps the native file input accessible and reports the selected file', async () => {
|
||||
const wrapper = mount(FilePicker, {
|
||||
props: { file: null, label: '选择文件', emptyLabel: '尚未选择文件', accept: '.json' },
|
||||
})
|
||||
const input = wrapper.get('input[type="file"]')
|
||||
const file = new File(['{}'], 'rules.json', { type: 'application/json' })
|
||||
Object.defineProperty(input.element, 'files', { value: [file], configurable: true })
|
||||
|
||||
await input.trigger('change')
|
||||
|
||||
expect(wrapper.emitted('select')).toEqual([[file]])
|
||||
expect(wrapper.get('label').attributes('for')).toBe(input.attributes('id'))
|
||||
expect(wrapper.text()).toContain('尚未选择文件')
|
||||
|
||||
await wrapper.setProps({ file })
|
||||
expect(wrapper.text()).toContain('rules.json')
|
||||
})
|
||||
|
||||
it('emits null when the native selection is cleared', async () => {
|
||||
const wrapper = mount(FilePicker, {
|
||||
props: { file: null, label: '选择文件', emptyLabel: '尚未选择文件' },
|
||||
})
|
||||
const input = wrapper.get('input[type="file"]')
|
||||
Object.defineProperty(input.element, 'files', { value: [], configurable: true })
|
||||
|
||||
await input.trigger('change')
|
||||
|
||||
expect(wrapper.emitted('select')).toEqual([[null]])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { useId } from 'vue'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
|
||||
defineProps<{
|
||||
file: File | null
|
||||
label: string
|
||||
emptyLabel: string
|
||||
accept?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ select: [file: File | null] }>()
|
||||
const inputId = useId()
|
||||
|
||||
function selectFile(event: Event) {
|
||||
emit('select', (event.target as HTMLInputElement).files?.[0] ?? null)
|
||||
}
|
||||
|
||||
function allowReselect(event: MouseEvent) {
|
||||
;(event.currentTarget as HTMLInputElement).value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="file-picker" :class="{ disabled }">
|
||||
<input
|
||||
:id="inputId"
|
||||
class="file-picker-input"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
:disabled="disabled"
|
||||
@click="allowReselect"
|
||||
@change="selectFile"
|
||||
/>
|
||||
<label class="file-picker-trigger" :for="inputId">
|
||||
<Upload aria-hidden="true" />
|
||||
<span>{{ label }}</span>
|
||||
</label>
|
||||
<span class="file-picker-name" :class="{ empty: !file }" :title="file?.name || emptyLabel">
|
||||
{{ file?.name || emptyLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-picker { display: flex; min-width: 0; align-items: center; gap: var(--space-sm); }
|
||||
.file-picker-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||
.file-picker-trigger { display: inline-flex; min-height: 36px; flex: 0 0 auto; align-items: center; gap: var(--space-sm); padding: 0 var(--space-md); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-primary); font-weight: 600; cursor: pointer; transition: border-color var(--motion-fast), background-color var(--motion-fast), color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast); }
|
||||
.file-picker-trigger svg { width: 16px; height: 16px; }
|
||||
.file-picker-trigger:hover { border-color: var(--color-accent-secondary); background: var(--color-background-hover); color: var(--color-accent-primary); transform: translateY(-1px); }
|
||||
.file-picker-input:focus-visible + .file-picker-trigger { outline: 2px solid var(--color-border-focus); outline-offset: 2px; }
|
||||
.file-picker-name { min-width: 0; overflow: hidden; color: var(--color-text-secondary); text-overflow: ellipsis; white-space: nowrap; user-select: text; }
|
||||
.file-picker-name.empty { color: var(--color-text-tertiary); }
|
||||
.disabled { opacity: .55; }
|
||||
.disabled .file-picker-trigger { cursor: not-allowed; transform: none; }
|
||||
@media (max-width: 560px) { .file-picker { align-items: stretch; flex-direction: column; } .file-picker-trigger { justify-content: center; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user