feat(mobile): track mobile app scaffold

This commit is contained in:
caoxiaozhu
2026-05-22 12:41:45 +08:00
parent 222ba0bfdc
commit 1f15699013
79 changed files with 16854 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
# Platform Layer
平台能力统一放在本目录,页面不直接绑定具体 Expo/原生库。
- `camera`:相机、相册、票据采集和本地预处理。
- `voice`:录音、语音转写、麦克风权限。
- `upload`:附件上传、进度、重试、临时附件引用。
- `permissions`Android / iOS 权限文案和降级策略。
相机与语音都先产生用户可确认的中间结果,不直接触发草稿持久化或提交审批。

View 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');
}

View 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,
};
}