feat(mobile): track mobile app scaffold
This commit is contained in:
10
mobile/app/src/platform/README.md
Normal file
10
mobile/app/src/platform/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Platform Layer
|
||||
|
||||
平台能力统一放在本目录,页面不直接绑定具体 Expo/原生库。
|
||||
|
||||
- `camera`:相机、相册、票据采集和本地预处理。
|
||||
- `voice`:录音、语音转写、麦克风权限。
|
||||
- `upload`:附件上传、进度、重试、临时附件引用。
|
||||
- `permissions`:Android / iOS 权限文案和降级策略。
|
||||
|
||||
相机与语音都先产生用户可确认的中间结果,不直接触发草稿持久化或提交审批。
|
||||
55
mobile/app/src/platform/camera/receiptCapture.ts
Normal file
55
mobile/app/src/platform/camera/receiptCapture.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
|
||||
export type CapturedReceipt = {
|
||||
uri: string;
|
||||
fileName: string;
|
||||
mimeType?: string;
|
||||
source: 'camera' | 'library';
|
||||
};
|
||||
|
||||
function toReceipt(asset: ImagePicker.ImagePickerAsset, source: CapturedReceipt['source']): CapturedReceipt {
|
||||
return {
|
||||
uri: asset.uri,
|
||||
fileName: asset.fileName || `receipt-${Date.now()}.jpg`,
|
||||
mimeType: asset.mimeType,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
export async function captureReceiptFromCamera(): Promise<CapturedReceipt | null> {
|
||||
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
||||
if (!permission.granted) {
|
||||
throw new Error('需要相机权限才能拍摄票据。');
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchCameraAsync({
|
||||
allowsEditing: false,
|
||||
quality: 0.84,
|
||||
mediaTypes: ['images'],
|
||||
});
|
||||
|
||||
if (result.canceled || !result.assets[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toReceipt(result.assets[0], 'camera');
|
||||
}
|
||||
|
||||
export async function pickReceiptFromLibrary(): Promise<CapturedReceipt | null> {
|
||||
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!permission.granted) {
|
||||
throw new Error('需要相册权限才能选择票据图片。');
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
allowsEditing: false,
|
||||
quality: 0.88,
|
||||
mediaTypes: ['images'],
|
||||
});
|
||||
|
||||
if (result.canceled || !result.assets[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toReceipt(result.assets[0], 'library');
|
||||
}
|
||||
25
mobile/app/src/platform/voice/voiceInput.ts
Normal file
25
mobile/app/src/platform/voice/voiceInput.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useAudioRecorder, RecordingPresets } from 'expo-audio';
|
||||
|
||||
export function useVoiceRecorder() {
|
||||
const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
|
||||
|
||||
return {
|
||||
recorder,
|
||||
async start() {
|
||||
await recorder.prepareToRecordAsync();
|
||||
recorder.record();
|
||||
},
|
||||
async stop() {
|
||||
await recorder.stop();
|
||||
return recorder.uri;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function transcribeVoice(uri: string) {
|
||||
// 后续接入 /api/v1/mobile/voice/transcribe;初始化阶段先返回可见占位结果。
|
||||
return {
|
||||
text: `已收到语音文件:${uri.split('/').pop() || 'voice-recording'}`,
|
||||
confidence: 0,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user