接入反馈凭证原生图片导入

反馈页上传凭证在原生壳内优先调用 HostBridge 图片导入

宿主图片结果转换为 File 后复用现有凭证预览和提交校验

补充反馈凭证原生导入测试和宿主壳文档边界
This commit is contained in:
2026-06-18 06:24:44 +08:00
parent a7d713c806
commit 01349e7882
5 changed files with 88 additions and 3 deletions
@@ -3,6 +3,7 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, expect, test, vi } from 'vitest';
import * as hostBridgeServices from '../../services/host-bridge/hostBridge';
import { PlatformFeedbackView } from './PlatformFeedbackView';
class MockFileReader {
@@ -138,6 +139,56 @@ test('PlatformFeedbackView previews image data urls and submits evidence items',
});
});
test('PlatformFeedbackView imports evidence image through native HostBridge', async () => {
const inputClickSpy = vi
.spyOn(HTMLInputElement.prototype, 'click')
.mockImplementation(() => undefined);
vi.spyOn(hostBridgeServices, 'canUseNativeHostCapability').mockImplementation(
(capability) => capability === 'file.importImage',
);
vi.spyOn(hostBridgeServices, 'importHostImageFile').mockResolvedValue({
action: 'selected',
fileName: 'feedback.png',
base64Data: 'ZmVlZGJhY2s=',
mimeType: 'image/png',
bytes: 8,
});
const onSubmit = vi.fn();
try {
render(<PlatformFeedbackView onBack={vi.fn()} onSubmit={onSubmit} />);
fireEvent.click(screen.getByText('上传凭证'));
const preview = await screen.findByAltText('反馈凭证预览');
expect(preview.getAttribute('src')).toBe(
'data:image/png;base64,ZmVlZGJhY2s=',
);
expect(inputClickSpy).not.toHaveBeenCalled();
fireEvent.change(screen.getByLabelText('问题描述'), {
target: { value: '原生壳导入截图后应该可以提交' },
});
fireEvent.click(screen.getByRole('button', { name: '提交' }));
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
expect(onSubmit).toHaveBeenCalledWith({
description: '原生壳导入截图后应该可以提交',
contactPhone: null,
evidenceItems: [
{
fileName: 'feedback.png',
contentType: 'image/png',
sizeBytes: 8,
dataUrl: 'data:image/png;base64,ZmVlZGJhY2s=',
},
],
});
} finally {
inputClickSpy.mockRestore();
}
});
test('PlatformFeedbackView calls back from header home button', () => {
const onBack = vi.fn();
render(<PlatformFeedbackView onBack={onBack} />);
@@ -2,6 +2,11 @@ import { ArrowLeft, CheckCircle2, Send } from 'lucide-react';
import { useRef, useState } from 'react';
import type { ProfileFeedbackEvidenceItemInput } from '../../../packages/shared/src/contracts/runtime';
import {
canUseNativeHostCapability,
type HostFileImportImageResult,
importHostImageFile,
} from '../../services/host-bridge/hostBridge';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
@@ -55,6 +60,16 @@ function readFileAsDataUrl(file: File) {
});
}
function hostEvidenceImageResultToFile(result: HostFileImportImageResult) {
const binary = atob(result.base64Data);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return new File([bytes], result.fileName, { type: result.mimeType });
}
export function PlatformFeedbackView({
onBack,
onSubmit,
@@ -90,10 +105,22 @@ export function PlatformFeedbackView({
};
const openEvidencePicker = () => {
if (canUseNativeHostCapability('file.importImage')) {
void (async () => {
const importedImageFile = await importHostImageFile();
if (!importedImageFile) {
return;
}
addEvidenceFiles([hostEvidenceImageResultToFile(importedImageFile)]);
})();
return;
}
evidenceInputRef.current?.click();
};
const addEvidenceFiles = (files: FileList | null) => {
const addEvidenceFiles = (files: FileList | File[] | null) => {
const selectedFiles = files ? Array.from(files) : [];
if (evidenceInputRef.current) {
evidenceInputRef.current.value = '';