关于 add-camera
add-camera 是面向 Power Apps 移动应用的内部技能,用于生成相机、图片选择器及可选条码/QR 扫描控件,也可提供 Dataverse 图像上传辅助代码。它只写入 `src/native/` 中的 JS 文件,并要求所需原生模块已包含在模板中。
主要功能
- 01生成相机拍摄与图库选择封装
- 02可选生成条码和 QR 扫描控件
- 03可选生成 Dataverse 图像上传辅助代码
- 04仅写入 `src/native/` 中的 JS 文件
适用场景
- 01为应用添加相机拍摄或图库选图流程
- 02构建条码或 QR 扫描门禁流程
- 03为自定义 Dataverse 图像列上传准备辅助代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill add-camera安装后在 Claude Code 中输入/add-camera确认已加载。
详细介绍
共享说明:shared-instructions.md — 请先阅读。
**工作目录:**读取任何项目内容或执行任何命令之前,先执行
native-artifact-compatibility.md Step 0。
继承 /add-native 解析出的绝对 working_dir;将每次 shell 调用和文件工具都绑定到该目录,
即使此辅助工具从其他目录启动也是如此。
参考资料:
- dataverse-reference.md — 文件/图像列上传模式(第 7–8 步)
添加相机
内部辅助工具。 用户应调用 /add-native camera、/add-native image-picker、/add-native barcode-scanner 或 /add-native qr-scanner;/add-native 会在解析功能后路由至此。
生成类型化的相机和图像选择器封装、可选的条码/QR 扫描控件,以及 Dataverse 图像/文件工作流的可选自定义上传指南。
此技能仅在 src/native/ 下写入 JS 文件。不会安装模块,也不会修改 package.json / app.config.js。下方经批准的构件集合所需的模块必须已包含在模板/原生构建中;如果缺少必需模块,请停止。对于仅扫描或仅上传的请求,不要要求无关的采集软件包。
原因:客户二进制文件基于预构建的重打包基础镜像构建,而非客户的 package.json。在此处添加原生模块会导致针对二进制文件实际不包含的模块进行编译,从而在重打包后引发运行时崩溃。相同的硬性规则见 /add-native。
所需模块取决于经批准的构件集合(所有模块都必须已在
package.json 中):
| 请求的构件键 | 必需模块 | 输出 |
|---|---|---|
photo / gallery | expo-image-picker | camera.ts 拍摄/选择器 API |
scanner | expo-camera | barcodeScanner.tsx 实时扫描器 |
upload | expo-file-system | cameraUpload.ts 自定义 Image 辅助工具 |
组合请求使用所需模块的并集。仅扫描请求不需要
expo-image-picker 或 camera.ts;仅上传请求不需要采集/扫描软件包。
**Dataverse 文件/图像边界:**对于常规 Dataverse 文件/图像表单字段,屏幕应使用 @microsoft/power-apps-native-host 中的 FilePicker / ImagePicker(参见 /add-native 文件/图像选择器的职责边界)。/add-native camera 负责自定义相机/图库/扫描工作流,例如专用证据采集屏幕、条码/QR 扫描验证,或经过转换后再保存的图库选取图像。
**笔和签名边界:**签名、签署、墨迹、绘图或笔迹采集应由 /add-native pen-input 负责(其内部会路由至笔输入辅助工具)。相机照片和笔迹签名都可以保存到 Dataverse 图像/文件列,但采集封装彼此独立。
工作流
- 验证项目 → 2. 验证模块已随模板提供 → 2a. 核对请求的构件 → 3. 应用相机决策 → 3b. 应用扫描器决策 → 4. 确认存储决策 → 5. 应用上传决策 → 6. 验证 → 7. 总结
第 1 步 — 验证项目
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
test -f app.config.js && test -f power.config.json && test -f package.json || { echo "BLOCKED: working_dir is not an initialized app" >&2; exit 1; }
如果缺少任何文件,请报告并停止 — 此技能要求使用已初始化的 Power Apps 移动应用。
步骤 2 — 验证模块是否由模板提供
在此检查前,先解析已批准的制品键。意图缺失或含糊时,应交还负责人处理(单独询问);不要假定需要所有制品。在下方替换为确切的 JSON 列表,例如 ["scanner"] 或 ["photo","upload"]:
使用负责人的规范化公开能力解析这些键:
| 公开能力 | 制品键 |
|---|---|
camera | ["photo"] |
image-picker | ["gallery"] |
barcode-scanner | ["scanner"] |
qr-scanner | ["scanner"] |
对于组合的已批准能力,使用并集。只有在明确批准了自定义 Dataverse 图像上传辅助工具时才添加 upload;拍摄或扫描不应自动添加。检查器接受制品键 JSON,不接受原始 $ARGUMENTS 或公开能力字符串。意图缺失或含糊时,仍应交还负责人处理。
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node - '<approved-artifact-keys-json>' <<'NODE'
const p = require('./package.json');
const modules = {
photo: 'expo-image-picker',
gallery: 'expo-image-picker',
scanner: 'expo-camera',
upload: 'expo-file-system',
};
const artifacts = JSON.parse(process.argv[2]);
if (!Array.isArray(artifacts) || artifacts.length === 0
|| artifacts.some(key => typeof key !== 'string' || !Object.hasOwn(modules, key))) {
throw new Error('Expected a nonempty approved artifact list: photo, gallery, scanner, upload');
}
const need = [...new Set(artifacts.map(key => modules[key]))];
const missing = need.filter(name => !p.dependencies?.[name]);
if (missing.length) {
console.error('MISSING from package.json: ' + missing.join(', ') + '. Do not install native modules.');
process.exit(1);
}
console.log('OK: requested modules present');
NODE
如果检查失败,立即停止。逐字打印错误。不要运行 npx expo install。不要编辑 app.config.js。告知用户模板缺少哪些所需模块;他们需要的是模板支持,而不是在应用本地安装原生包。
步骤 2a — 核对所需制品
在任何写入或复用之前,阅读并执行 native-artifact-compatibility.md
中适用于 camera/gallery/scanner 行的步骤 1–3。沿用当前 /add-native 的批准/模式;不要重复有效的范围限定批准。
分别检查 src/native/camera.ts(包括图库 pickImage,不要求有 imagePicker.ts)、所需的 src/native/barcodeScanner.tsx,以及所需的 src/native/cameraUpload.ts。检查所需导出、选项、失败路径、扫描器锁定/重置/叠加层行为,以及所需的 Dataverse 图像上传载荷语义。现在以只读方式确定已批准的存储平台和步骤 4 的要求,然后再进行写入;非 Dataverse 保留不得要求 Dataverse 列或 cameraUpload.ts。
所需存储缺失或存在批准范围之外的不兼容制品时,应向负责人返回 NEEDS_CONTEXT,或单独询问;这不算成功跳过。
仅当当前已批准的请求/范围需要扫描时,才将 SCANNER_NEEDED=yes(例如 barcode、QR、扫描器,或提供的扫描门控要求);不要依据旧计划中的无关匹配项设置。兼容的 camera 文件不能替代缺失的扫描器或上传辅助工具。在部分制品中,保持兼容文件不变。
步骤 3 — 编写 camera 封装
仅在创建/更新所需的 camera 封装时打印:
"→ Writing src/native/camera.ts wrapper (takePhoto + pickImage with discriminated-union results)…"
按照步骤 2a 对 src/native/camera.ts 的决定执行:如果有所需且文件缺失,则创建;仅在兼容时原样复用;或者只进行已批准范围内的更新。仅需要扫描器的请求不要求此文件。保留自定义代码和现有导出;不要附加重新生成注释,也不要用示例替换它。
// src/native/camera.ts
// Camera capture and image picker wrapper for Power Apps mobile apps.
// Uses expo-image-picker for both camera capture and gallery selection.
// Capture/pick functions return discriminated results; permission helpers retain boolean APIs.
import * as ImagePicker from 'expo-image-picker';
// --- Result types ---
export type PhotoResult =
| { ok: true; uri: string; width: number; height: number; mimeType?: string; fileSize?: number }
| { ok: false; reason: 'permission-denied' | 'cancelled' | 'unsupported' | 'error'; message?: string };
// --- Permission ---
type PermissionResult =
| { ok: true }
| { ok: false; reason: 'permission-denied' | 'unsupported' | 'error'; message?: string };
async function requestPermission(
request?: () => Promise<{ status: string }>,
): Promise<PermissionResult> {
if (typeof request !== 'function') {
return { ok: false, reason: 'unsupported', message: 'Permission API is unavailable.' };
}
try {
const { status } = await request();
return status === 'granted'
? { ok: true }
: { ok: false, reason: 'permission-denied' };
} catch (error) {
return {
ok: false,
reason: 'error',
message: error instanceof Error ? error.message : 'Permission request failed.',
};
}
}
async function permissionGranted(
request?: () => Promise<{ status: string }>,
): Promise<boolean> {
const permission = await requestPermission(request);
if (!permission.ok && permission.reason !== 'permission-denied') {
// Preserve the public boolean API without silently hiding a failed native request.
console.warn(`Device permission request failed (${permission.reason}).`);
}
return permission.ok;
}
export async function requestCameraPermission(): Promise<boolean> {
return permissionGranted(ImagePicker.requestCameraPermissionsAsync);
}
export async function requestMediaLibraryPermission(): Promise<boolean> {
return permissionGranted(ImagePicker.requestMediaLibraryPermissionsAsync);
}
// --- Capture ---
/**
* Launch the device camera and capture a photo.
* Missing APIs return 'unsupported'; rejected native calls return 'error'.
*/
export async function takePhoto(options?: {
quality?: number;
allowsEditing?: boolean;
}): Promise<PhotoResult> {
try {
if (typeof ImagePicker.launchCameraAsync !== 'function') {
return { ok: false, reason: 'unsupported', message: 'Camera API is unavailable.' };
}
const permission = await requestPermission(ImagePicker.requestCameraPermissionsAsync);
if (!permission.ok) return permission;
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ['images'],
quality: options?.quality ?? 0.8,
allowsEditing: options?.allowsEditing ?? false,
exif: false,
});
if (result.canceled) return { ok: false, reason: 'cancelled' };
const asset = result.assets[0];
return {
ok: true,
uri: asset.uri,
width: asset.width ?? 0,
height: asset.height ?? 0,
mimeType: asset.mimeType ?? undefined,
fileSize: asset.fileSize ?? undefined,
};
} catch (error) {
return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Camera capture failed.' };
}
}
/**
* Open the device photo gallery and pick an image.
* Works on all platforms including web (uses native file picker).
*/
export async function pickImage(options?: {
quality?: number;
allowsEditing?: boolean;
allowsMultipleSelection?: boolean;
}): Promise<PhotoResult> {
try {
if (typeof ImagePicker.launchImageLibraryAsync !== 'function') {
return { ok: false, reason: 'unsupported', message: 'Image picker API is unavailable.' };
}
const permission = await requestPermission(ImagePicker.requestMediaLibraryPermissionsAsync);
if (!permission.ok) return permission;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
quality: options?.quality ?? 0.8,
allowsEditing: options?.allowsEditing ?? false,
allowsMultipleSelection: options?.allowsMultipleSelection ?? false,
exif: false,
});
if (result.canceled) return { ok: false, reason: 'cancelled' };
const asset = result.assets[0];
return {
ok: true,
uri: asset.uri,
width: asset.width ?? 0,
height: asset.height ?? 0,
mimeType: asset.mimeType ?? undefined,
fileSize: asset.fileSize ?? undefined,
};
} catch (error) {
return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Image selection failed.' };
}
}
步骤 3b — 在需要时编写条形码/QR 扫描器控制组件
除非 SCANNER_NEEDED=yes,否则跳过此步骤。 仅拍照和仅从图库选择的流程不需要实时 CameraView。
开始前打印:
“→ 正在编写 src/native/barcodeScanner.tsx(CameraView 条形码/QR 扫描器控制组件)…”
根据步骤 2a 对 src/native/barcodeScanner.tsx 单独做出决定:在需要且文件不存在时创建;仅在兼容时原样复用;或仅进行已批准范围内的更新。仅凭文件存在,无法确认它是否支持 BarcodeScannerView、onScanned、paused、resetKey 或所需的条形码类型。
// src/native/barcodeScanner.tsx
// Barcode / QR scanner control for Power Apps mobile apps.
// Uses expo-camera CameraView. Never throws; permission state is rendered inline.
import React from 'react';
import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
import type { BarcodeScanningResult, BarcodeType } from 'expo-camera';
export type ScannerResult = {
ok: true;
data: string;
type: string;
raw: BarcodeScanningResult;
};
export type BarcodeScannerViewProps = {
onScanned: (result: ScannerResult) => void;
paused?: boolean;
resetKey?: unknown;
barcodeTypes?: BarcodeType[];
style?: StyleProp<ViewStyle>;
overlay?: React.ReactNode;
children?: React.ReactNode;
};
const DEFAULT_BARCODE_TYPES = [
'aztec',
'qr',
'ean13',
'ean8',
'upc_a',
'upc_e',
'datamatrix',
'code39',
'code93',
'code128',
'pdf417',
'itf14',
'codabar',
] as BarcodeType[];
export function BarcodeScannerView({
onScanned,
paused = false,
resetKey,
barcodeTypes = DEFAULT_BARCODE_TYPES,
style,
overlay,
children,
}: BarcodeScannerViewProps) {
const [permission, requestPermission] = useCameraPermissions();
const [permissionError, setPermissionError] = React.useState<string | null>(null);
const permissionAttemptedRef = React.useRef(false);
const scanLockedRef = React.useRef(false);
React.useEffect(() => {
if (permission && !permission.granted && permission.canAskAgain && !permissionAttemptedRef.current) {
permissionAttemptedRef.current = true;
async function requestScannerPermission() {
try {
await requestPermission();
} catch {
setPermissionError('Camera permission request failed. Check device settings and reopen the scanner.');
}
}
void requestScannerPermission();
}
}, [permission, requestPermission]);
React.useEffect(() => {
if (!paused) {
scanLockedRef.current = false;
}
}, [paused, resetKey]);
const handleBarcodeScanned = React.useCallback((event: BarcodeScanningResult) => {
if (paused || scanLockedRef.current) return;
scanLockedRef.current = true;
onScanned({ ok: true, data: event.data, type: event.type, raw: event });
}, [onScanned, paused]);
if (permissionError) {
return <View style={[styles.fallback, style]}><Text>{permissionError}</Text></View>;
}
if (!permission) {
return <View style={[styles.fallback, style]}><Text>Checking camera permission...</Text></View>;
}
if (!permission.granted) {
return <View style={[styles.fallback, style]}><Text>Camera permission is required to scan codes.</Text></View>;
}
return (
<View style={[styles.container, style]}>
<CameraView
style={StyleSheet.absoluteFill}
facing="back"
active={!paused}
barcodeScannerSettings={{ barcodeTypes }}
onBarcodeScanned={paused ? undefined : handleBarcodeScanned}
/>
{overlay || children ? <View pointerEvents="box-none" style={styles.overlay}>{overlay ?? children}</View> : null}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, overflow: 'hidden', position: 'relative' },
overlay: { ...StyleSheet.absoluteFillObject },
fallback: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 },
});
扫描器渲染规则:不要将覆盖层 UI 作为 CameraView 的子项。React 子项嵌套在原生相机预览中时,Expo Camera 可能无法正确渲染。生成的控制组件会将相机作为一个图层渲染,并将 overlay/children 作为其上方的同级绝对定位图层渲染。
扫描变更规则:生成的控制组件带有内部的一次性扫描锁,可防止快速触发的 onBarcodeScanned 回调重复提交。屏幕仍应在导航或修改数据前设置 paused=true,然后在屏幕重新获得焦点时重置 paused=false 并更改 resetKey。这样可确保成功扫描后返回扫描器时,扫描器仍能可靠工作。
扫描门控业务规则:对于 QR 查询流程,应先根据目标实体解析扫描的代码(例如 Test Item)。如果查询无结果,应显示清晰的行内消息 Item does not exist,并留在当前扫描器流程中。除非已批准的计划明确要求这种回退行为,否则不要自动创建 Unknown 扫描记录。
扫描器加载 UI 规则:如果扫描处理需要时间(查询/创建变更),请通过 overlay 属性在扫描器预览中渲染仅包含微调器的覆盖层。不要在相机预览上方渲染单独的加载卡片/面板。
步骤 4 — 检测 Dataverse 图像/文件列
重用步骤 2a 中关于存储的只读调查结果;只有当这些文件发生变化时才重新检查。搜索 Dataverse 之前,先对获批的存储目标进行分类:
| 获批的存储范围 | 助手操作 |
|---|---|
| 不保留数据 / 仅本地预览 | 跳过 Dataverse 搜索和步骤 5;完成所请求的捕获/扫描器产物。 |
| 非 Dataverse 保留(例如现有 SharePoint 文档库)或获批的本地文件保留 | 跳过 Dataverse 搜索和步骤 5。根据获批目标的契约验证所需的捕获输出/负载,然后将连接器/本地持久化工作交还负责人。此路径中缺少 Dataverse 列并非错误。不要生成 cameraUpload.ts,也不要声称仅靠捕获就已实现持久化。 |
| Dataverse File 保留或普通宿主 File/Image 字段 | 应用共享存储检查,并将 File/宿主控件集成工作交还负责人;不要使用自定义 Image update() 助手。File 目标不受 Image update() 示例支持。 |
| 请求了自定义 Dataverse Image 上传助手 | 检查下方实际获批的 Image 列和生成的服务签名,然后执行步骤 5。 |
| 目标或所需负载契约未知 | 向负责人返回 NEEDS_CONTEXT(单独询问);不要假设目标是 Dataverse,也不要悄悄丢弃保留要求。 |
只有自定义 Dataverse Image 助手分支需要搜索此列:
Grep pattern="ImageColumnName|FileColumnName|UploadColumnName" path="src/generated/"
如果找到匹配项: 验证获批目标、列类型、负载和写入结果契约。只有在所请求的是宿主控件之外的自定义相机/图库 Image 保留时,才继续执行步骤 5。仅仅存在生成的列,并不代表获准使用上传助手。如果匹配的目标是 File,而非获批的 Image 目标,则将不匹配情况交还负责人,不要生成 Image 助手,也不要声称已实现 File 持久化。
File/Image 宿主控件安全须知:此技能不能替代常规 Dataverse 表单控件。对于标准的 Dataverse 表单绑定 Image/File 字段,请保留宿主 ImagePicker / FilePicker。
如果请求的 Dataverse Image 目标缺失/未经验证: 向负责人返回
NEEDS_CONTEXT,或单独询问;不要悄悄地将仅捕获成功作为所请求助手的交付结果。此阻塞条件仅适用于 Dataverse Image 助手分支,不适用于获批的连接器/本地目标。不要生成架构/服务,也不要手动编辑 src/generated/。
步骤 5 — 编写图像上传辅助函数
开始前打印:
"→ 正在编写 src/native/cameraUpload.ts(Dataverse 图像列 base64 补丁辅助函数)…"
按照步骤 2a 中对 src/native/cameraUpload.ts 的独立判断执行:如果用户请求创建且文件不存在,则创建;只有在文件与已批准的 Image 目标和已安装的文件 API 兼容时,才原样复用;否则只进行已批准范围内的更新。现有 Image 辅助函数无法满足 File 保留需求。
对于普通 Dataverse File/Image 表单字段,不要生成此辅助函数。这些字段使用宿主提供的 FilePicker / ImagePicker 控件。只有当当前已批准范围请求自定义相机/相册 Image 保留时,才生成此辅助函数;明确要求仅实现上传辅助函数的情况也包括在内。此类有界操作不要求屏幕计划。
此辅助函数不会改变宿主 ImagePicker / FilePicker 的行为。它仅适用于自定义拍照流程:应用接收相机 URI,然后显式更新 Dataverse Image 列。
此辅助函数用于原生应用中的 Dataverse Image 列。它使用 Expo 文件 API 读取本地照片 URI,将其转换为 base64,然后调用生成的服务的 update(),并传入 { [imageColumnName]: base64 }。
此路径中不要将相机 URI 转换为浏览器风格的 File / Blob 对象。这种模式在 RN/Expo 运行时中不可靠,并会导致类似 arrayBuffer is not a function 的上传失败。
// src/native/cameraUpload.ts
// Bridges camera/gallery photo output to Dataverse Image column updates.
// Reads local image URI as base64 and PATCHes the image column via service.update().
import * as FileSystem from 'expo-file-system';
export type UploadResult =
| { ok: true }
| { ok: false; reason: 'read-failed' | 'update-failed' | 'error'; message?: string };
export type ImageUpdateService = {
update: (id: string, body: any) => Promise<{ success: boolean; error?: { message?: string } }>;
};
/**
* Upload a photo from takePhoto() / pickImage() to a Dataverse Image column.
*
* @param uri - Photo URI from the camera wrapper result
* @param service - Generated Dataverse service with update()
* @param recordId - The Dataverse record GUID to patch
* @param imageColumnName - Dataverse Image column logical name
*/
export async function uploadPhotoToImageColumn(
uri: string,
service: ImageUpdateService,
recordId: string,
imageColumnName: string,
): Promise<UploadResult> {
try {
const base64 = await readUriAsBase64(uri);
if (!base64) {
return { ok: false, reason: 'read-failed', message: 'Could not read the image URI as base64.' };
}
// Dataverse Image columns expect base64 payload value (without data URI prefix).
const result = await service.update(recordId, { [imageColumnName]: base64 });
if (!result.success || result.error) {
return { ok: false, reason: 'update-failed', message: result.error?.message ?? 'Dataverse image update failed.' };
}
return { ok: true };
} catch (error) {
return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Unable to upload image.' };
}
}
/**
* Read a local photo URI to a base64 string using Expo file APIs.
*/
async function readUriAsBase64(uri: string): Promise<string | null> {
try {
const fsAny = FileSystem as any;
if (typeof fsAny.readAsStringAsync !== 'function') {
return null;
}
const normalizedUri = uri.startsWith('file://') ? uri : `file://${uri}`;
const base64 = await fsAny.readAsStringAsync(normalizedUri, {
encoding: fsAny.EncodingType?.Base64 ?? 'base64',
});
if (typeof base64 !== 'string' || base64.length === 0) {
return null;
}
return base64;
} catch {
return null;
}
}
重要: service 参数采用宽松类型(ImageUpdateService),因此可与任何具有 update() 方法的生成服务配合使用。调用方传入具体服务,这样辅助函数就无需导入特定的生成服务。
写入约定: 即使缺少 result.error,只要 result.success 不为真值,辅助函数就必须将其视为失败。生成的服务会返回 IOperationResult;只有在 success 为真值时,原生应用屏幕才能在 Dataverse 写入后显示成功状态或进行导航。
模板/API 提示: 如果你的模板版本无法从 expo-file-system 获取 readAsStringAsync,请从 expo-file-system/legacy 导入,并保持相同的 base64 行为。
步骤 6 — 类型检查
开始前打印:
"→ 正在运行 tsc,以验证请求的原生构件能够通过编译(约 10–20 秒)。"
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
npx --no-install tsc --noEmit
仅修复范围内辅助函数的错误。常见问题:
expo-file-system上找不到readAsStringAsync— 对此辅助函数改为从expo-file-system/legacy导入。- 导入路径不匹配 — 确认屏幕组件能够访问
src/native/。
类型检查后,执行共享兼容性约定的步骤 4。重新检查每项请求的相机/扫描仪/上传构件和存储要求,包括权限失败、重复扫描回调、URI 读取和写入结果为 false 的情况。仅类型检查成功还不够;应返回尚未解决的要求,而不是将部分输出集标记为已完成。不要修改屏幕/生成的文件。
步骤 7 — 总结
返回共享兼容性结果以及实际创建/更新/复用的路径。仅展示适用的用法示例;如果保留问题尚未解决,不得声称上传成功;仅实现辅助函数的工作不得声称功能完整。
Requested native artifacts ready
---
Capabilities : <fulfilled requested capabilities only>
Modules (template-shipped) : <required modules for those capabilities only>
package.json : unchanged ✓
app.config.js : unchanged ✓
Artifacts : <one row per requested path: created / updated / reused>
Type-check : <actual result>
UI integration : <returned to owner / intentionally not performed>
---
对于仅扫描仪或仅上传的结果,不要列出 camera.ts。不要将未请求的配套路径列为已生成,即使这些路径原本就已存在。
仅在拍摄和自定义 Image 上传均已完成时,包含以下用法:
import { takePhoto } from '../native/camera';
import { uploadPhotoToImageColumn } from '../native/cameraUpload';
import { Cr123_inspectionService } from '../generated/services/Cr123_inspectionService';
const result = await takePhoto();
if (result.ok) {
const upload = await uploadPhotoToImageColumn(
result.uri,
Cr123_inspectionService,
recordId,
'cr123_sitephoto' // Dataverse Image column logical name
);
if (upload.ok) {
showToast('Photo attached to record');
}
}
仅在相册选择功能已完成时,包含以下用法:
import { pickImage } from '../native/camera';
const result = await pickImage();
if (result.ok) {
setPreviewUri(result.uri);
}
仅在扫描仪支持已完成时,包含以下用法:
import { BarcodeScannerView } from '../native/barcodeScanner';
import { useFocusEffect } from 'expo-router';
const [paused, setPaused] = useState(false);
const [scanResetKey, setScanResetKey] = useState(0);
useFocusEffect(
React.useCallback(() => {
setPaused(false);
setScanResetKey((value) => value + 1);
return () => setPaused(true);
}, [])
);
<BarcodeScannerView
paused={paused}
resetKey={scanResetKey}
barcodeTypes={['qr', 'ean13', 'code128']}
overlay={
<>
<ScannerFrame />
{isScanning ? <ScannerOverlaySpinner /> : null}
</>
}
onScanned={({ data, type }) => {
setPaused(true);
handleCode(data, type);
}}
/>
不会执行原生重建,也不会更改权限/配置。这些改动仅涉及对模板自带原生模块的 JS 封装/控件。
说明
- 此技能不会修改
src/playerConfig.ts、src/generated/或任何屏幕文件。 - 如果缺少 API,
takePhoto()会返回unsupported;如果原生调用被拒绝,则返回error;这两种情况都不会导致其 Promise 被拒绝。 - 如有需要,此处会通过
src/native/barcodeScanner.tsx处理条形码/QR 码扫描。请将其用于扫描门控和查询流程;不要将其用作 Dataverse 文件/图像宿主控件的替代品。 - 此技能中的
cameraUpload.ts仅适用于自定义 Dataverse 图像列捕获流程。它不会替代标准 Dataverse 表单中的宿主ImagePicker/FilePicker控件。 - 如果之后添加了 Dataverse 表(通过
/add-dataverse),请根据已批准的自定义图像保留要求重新运行/add-native camera。再次核对所有请求的构件,原样复用兼容的包装器,并且仅在此范围内创建/更新cameraUpload.ts。此操作绝不会安装模块。