diff --git a/frontend/src/components/common/FilePicker.spec.ts b/frontend/src/components/common/FilePicker.spec.ts new file mode 100644 index 0000000..eab9b3f --- /dev/null +++ b/frontend/src/components/common/FilePicker.spec.ts @@ -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]]) + }) +}) diff --git a/frontend/src/components/common/FilePicker.vue b/frontend/src/components/common/FilePicker.vue new file mode 100644 index 0000000..d741d11 --- /dev/null +++ b/frontend/src/components/common/FilePicker.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/frontend/src/features/media/MediaView.vue b/frontend/src/features/media/MediaView.vue index 93c67ed..a5846f0 100644 --- a/frontend/src/features/media/MediaView.vue +++ b/frontend/src/features/media/MediaView.vue @@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import { useRoute } from 'vue-router' import { mediaService, createMediaSubmission, type MediaJob } from '@/services/mediaService' import { localeTag, t } from '@/i18n' +import FilePicker from '@/components/common/FilePicker.vue' const route = useRoute() const submission = createMediaSubmission() @@ -108,14 +109,14 @@ onUnmounted(() => { stopped = true; clearTimeout(timer) })

{{ t('音视频转写', 'Media Transcription') }}

{{ t('上传音频或视频音轨,转写、校对后保存到知识库。单个文件最多 25 MiB。', 'Upload audio or a video soundtrack, transcribe and correct it, then save it to the knowledge base. Maximum file size: 25 MiB.') }}

{{ notice }}

- - - + +
+

{{ localOnly ? t('本次任务不调用远程模型 API,模型需预先下载。', 'This job will not call a remote model API; models must already be downloaded.') : t('若配置了转写 API,将上传所选附件;API 失败后回退到本地模型。', 'When a transcription API is configured, the selected file is uploaded; failures fall back to the local model.') }}

-
{{ t('术语校对', 'Terminology corrections') }}

{{ t('在识别完成后替换文本,原始识别结果会保留。', 'Replace text after recognition while retaining the original result.') }}