接入反馈凭证原生拍摄

反馈页在移动壳声明拍摄能力时展示拍摄凭证入口

拍摄图片复用现有凭证校验、预览和提交链路

同步原生壳测试、文档和共享记忆
This commit is contained in:
2026-06-19 11:20:32 +08:00
parent 48cd76918b
commit 7c50fa489d
6 changed files with 96 additions and 10 deletions
@@ -37,6 +37,7 @@ test('PlatformFeedbackView renders reference feedback fields', () => {
expect(screen.getByText('0/200')).toBeTruthy();
expect(screen.getByText('上传凭证(提供问题截图)')).toBeTruthy();
expect(screen.getByText('上传凭证')).toBeTruthy();
expect(screen.queryByText('拍摄凭证')).toBeNull();
const contactPhoneField = screen.getByLabelText('联系电话');
expect(contactPhoneField).toBeTruthy();
expect(contactPhoneField.tagName).toBe('INPUT');
@@ -189,6 +190,57 @@ test('PlatformFeedbackView imports evidence image through native HostBridge', as
}
});
test('PlatformFeedbackView captures evidence image through native HostBridge', async () => {
const inputClickSpy = vi
.spyOn(HTMLInputElement.prototype, 'click')
.mockImplementation(() => undefined);
vi.spyOn(hostBridgeServices, 'canUseNativeHostCapability').mockImplementation(
(capability) => capability === 'file.captureImage',
);
vi.spyOn(hostBridgeServices, 'captureHostImageFile').mockResolvedValue({
action: 'captured',
fileName: 'feedback-camera.png',
base64Data: 'Y2FtZXJhLWZlZWRiYWNr',
mimeType: 'image/png',
bytes: 15,
});
const onSubmit = vi.fn();
try {
render(<PlatformFeedbackView onBack={vi.fn()} onSubmit={onSubmit} />);
expect(screen.getByText('拍摄凭证')).toBeTruthy();
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-camera.png',
contentType: 'image/png',
sizeBytes: 15,
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} />);
@@ -4,6 +4,7 @@ import { useRef, useState } from 'react';
import type { ProfileFeedbackEvidenceItemInput } from '../../../packages/shared/src/contracts/runtime';
import {
canUseNativeHostCapability,
captureHostImageFile,
type HostFileImportImageResult,
importHostImageFile,
} from '../../services/host-bridge/hostBridge';
@@ -86,6 +87,8 @@ export function PlatformFeedbackView({
const [submitted, setSubmitted] = useState(false);
const descriptionLength = description.length;
const canCaptureHostEvidenceImage =
canUseNativeHostCapability('file.captureImage');
const showTemporaryNotice = (message: string) => {
setNotice(message);
@@ -104,6 +107,10 @@ export function PlatformFeedbackView({
setSubmitted(false);
};
const addHostEvidenceImage = (imageFile: HostFileImportImageResult) => {
addEvidenceFiles([hostEvidenceImageResultToFile(imageFile)]);
};
const openEvidencePicker = () => {
if (canUseNativeHostCapability('file.importImage')) {
void (async () => {
@@ -112,7 +119,7 @@ export function PlatformFeedbackView({
return;
}
addEvidenceFiles([hostEvidenceImageResultToFile(importedImageFile)]);
addHostEvidenceImage(importedImageFile);
})();
return;
}
@@ -120,6 +127,21 @@ export function PlatformFeedbackView({
evidenceInputRef.current?.click();
};
const captureEvidenceImage = () => {
if (!canCaptureHostEvidenceImage) {
return;
}
void (async () => {
const capturedImageFile = await captureHostImageFile();
if (!capturedImageFile) {
return;
}
addHostEvidenceImage(capturedImageFile);
})();
};
const addEvidenceFiles = (files: FileList | File[] | null) => {
const selectedFiles = files ? Array.from(files) : [];
if (evidenceInputRef.current) {
@@ -312,11 +334,20 @@ export function PlatformFeedbackView({
/>
))}
{evidencePreviews.length < MAX_FEEDBACK_EVIDENCE_COUNT ? (
<PlatformUploadTile
onClick={openEvidencePicker}
label="上传凭证"
hint="(最多四张)"
/>
<>
<PlatformUploadTile
onClick={openEvidencePicker}
label="上传凭证"
hint="(最多四张)"
/>
{canCaptureHostEvidenceImage ? (
<PlatformUploadTile
onClick={captureEvidenceImage}
label="拍摄凭证"
hint="(最多四张)"
/>
) : null}
</>
) : null}
</div>
<input