From 40ad132ce7cda0b62c6e383424acdcb59bd1bf13 Mon Sep 17 00:00:00 2001 From: Seven Date: Sun, 18 Jan 2026 22:52:44 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20S3=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20'@capp/eden'=20=E4=BE=9D=E8=B5=96=E8=87=B3=200.0.11?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AE=9E=E5=90=8D=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto-imports.d.ts | 5 +++ pnpm-lock.yaml | 14 +++---- pnpm-workspace.yaml | 2 +- src/utils/aws/s3.ts | 77 +++++++++++++++++++++++++++++++++++ src/utils/index.ts | 1 + src/views/address/add.vue | 21 +++++----- src/views/real_name/index.vue | 50 +++++++++++++++-------- 7 files changed, 134 insertions(+), 36 deletions(-) create mode 100644 src/utils/aws/s3.ts create mode 100644 src/utils/index.ts diff --git a/auto-imports.d.ts b/auto-imports.d.ts index c215e2f..07d3e21 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -129,6 +129,7 @@ declare global { const unref: typeof import('vue').unref const unrefElement: typeof import('@vueuse/core').unrefElement const until: typeof import('@vueuse/core').until + const uploadToS3: typeof import('./src/utils/aws/s3').uploadToS3 const useActiveElement: typeof import('@vueuse/core').useActiveElement const useAnimate: typeof import('@vueuse/core').useAnimate const useArrayDifference: typeof import('@vueuse/core').useArrayDifference @@ -330,6 +331,9 @@ declare global { export type { Language } from './src/composables/useLanguage' import('./src/composables/useLanguage') // @ts-ignore + export type { UploadFetchOptions } from './src/utils/aws/s3' + import('./src/utils/aws/s3') + // @ts-ignore export type { Wallet } from './src/store/wallet' import('./src/store/wallet') } @@ -461,6 +465,7 @@ declare module 'vue' { readonly unref: UnwrapRef readonly unrefElement: UnwrapRef readonly until: UnwrapRef + readonly uploadToS3: UnwrapRef readonly useActiveElement: UnwrapRef readonly useAnimate: UnwrapRef readonly useArrayDifference: UnwrapRef diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7de0b0b..28c2e5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,8 +52,8 @@ catalogs: specifier: 8.0.0 version: 8.0.0 '@capp/eden': - specifier: http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz - version: 0.0.10 + specifier: http://192.168.1.2:9538/api/capp-eden-0.0.11.tgz + version: 0.0.11 '@cloudflare/workers-types': specifier: ^4.20260113.0 version: 4.20260116.0 @@ -298,7 +298,7 @@ importers: version: 8.0.0(@capacitor/core@8.0.0) '@capp/eden': specifier: 'catalog:' - version: http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))) + version: http://192.168.1.2:9538/api/capp-eden-0.0.11.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))) '@elysiajs/eden': specifier: 'catalog:' version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) @@ -1182,9 +1182,9 @@ packages: '@capacitor/synapse@1.0.4': resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==} - '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz': - resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz} - version: 0.0.10 + '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.11.tgz': + resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.11.tgz} + version: 0.0.11 peerDependencies: '@elysiajs/eden': ^1.4.6 @@ -6903,7 +6903,7 @@ snapshots: '@capacitor/synapse@1.0.4': {} - '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))': + '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.11.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))': dependencies: '@elysiajs/eden': 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c14b450..1b878b3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,7 +18,7 @@ catalog: '@capacitor/keyboard': 8.0.0 '@capacitor/share': ^8.0.0 '@capacitor/status-bar': 8.0.0 - '@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz + '@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.11.tgz '@cloudflare/workers-types': ^4.20260113.0 '@elysiajs/eden': ^1.4.6 '@faker-js/faker': ^10.2.0 diff --git a/src/utils/aws/s3.ts b/src/utils/aws/s3.ts new file mode 100644 index 0000000..140a05c --- /dev/null +++ b/src/utils/aws/s3.ts @@ -0,0 +1,77 @@ +import type { TreatyBody } from "@/api/types"; +import { client, safeClient } from "@/api"; + +interface UploadOptions { + fetchOptions: UploadFetchOptions; + onProgress?: (progress: number) => void; + signal?: AbortSignal; +} + +export type UploadFetchOptions = TreatyBody; + +export async function uploadToS3(file: File, options: UploadOptions): Promise { + const { onProgress, signal, fetchOptions } = options; + + // 1. 获取预签名 URL + const { data, error } = await safeClient(client.api.file_storage.upload_url.post({ + ...fetchOptions, + })); + + if (error.value || !data.value) { + throw new Error("获取上传 URL 失败"); + } + const { fileId, uploadUrl, method, headers } = toRefs(data.value); + + // 2. 上传文件到 S3 + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + // 监听上传进度 + xhr.upload.addEventListener("progress", (e: ProgressEvent) => { + if (e.lengthComputable && onProgress) { + const progress = Math.round((e.loaded / e.total) * 100); + onProgress(progress); + } + }); + + // 上传成功 + xhr.addEventListener("load", () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(fileId.value); + } + else { + reject(new Error(`上传失败: ${xhr.status} ${xhr.statusText}`)); + } + }); + + // 网络错误 + xhr.addEventListener("error", () => { + reject(new Error("网络错误,上传失败")); + }); + + // 上传取消 + xhr.addEventListener("abort", () => { + reject(new Error("上传已取消")); + }); + + // 超时 + xhr.addEventListener("timeout", () => { + reject(new Error("上传超时")); + }); + + // 支持外部取消 + if (signal) { + signal.addEventListener("abort", () => { + xhr.abort(); + }); + } + + // 开始上传 + xhr.open(method.value, uploadUrl.value, true); + for (const [key, value] of Object.entries(headers.value)) { + xhr.setRequestHeader(key, value); + } + xhr.timeout = 300000; // 5分钟超时 + xhr.send(file); + }); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..9cbc2a6 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./aws/s3"; diff --git a/src/views/address/add.vue b/src/views/address/add.vue index e19ee7b..d40135a 100644 --- a/src/views/address/add.vue +++ b/src/views/address/add.vue @@ -48,16 +48,15 @@ async function showToast(message: string, color: "success" | "danger" | "warning await toast.present(); } -// 如果是编辑模式,加载地址数据 -onMounted(() => { +onMounted(async () => { if (isEditMode.value) { - // TODO: 根据 route.query.id 加载地址数据 - // 模拟数据 + const id = route.query.id as string; + const { data } = await safeClient(client.api.shipping_address({ id }).get()); formData.value = { - recipientName: "张三", - phoneNumber: "13800138000", - detailAddress: "北京市朝阳区建国路88号SOHO现代城A座1001室", - isDefault: true, + recipientName: data.value?.recipientName || "", + phoneNumber: data.value?.phoneNumber || "", + detailAddress: data.value?.detailAddress || "", + isDefault: data.value?.isDefault || false, }; } }); @@ -72,9 +71,11 @@ async function handleSubmit() { isSubmitting.value = true; try { - // TODO: 调用添加/编辑地址 API if (isEditMode.value) { - // const { data } = await safeClient(); + const id = route.query.id as string; + await safeClient(client.api.shipping_address({ id }).patch({ + ...formData.value, + })); } else { await safeClient(client.api.shipping_address.post({ diff --git a/src/views/real_name/index.vue b/src/views/real_name/index.vue index 928b5ad..3ca2a96 100644 --- a/src/views/real_name/index.vue +++ b/src/views/real_name/index.vue @@ -3,26 +3,28 @@ import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; import { actionSheetController, toastController } from "@ionic/vue"; import { cameraOutline, cardOutline, checkmarkCircleOutline, imageOutline, personOutline } from "ionicons/icons"; import zod from "zod"; +import { client } from "@/api"; const router = useRouter(); const formData = ref({ - realName: "", - idCard: "", - idCardFrontImage: "", - idCardBackImage: "", + kycMethod: "id_card", + documentName: "", + documentNumber: "", + fileId1: "", + fileId2: "", }); const isSubmitting = ref(false); // 表单验证 Schema const RealNameSchema = zod.object({ - realName: zod + documentName: zod .string() .min(2, "请输入真实姓名") .max(10, "姓名长度不能超过10个字符"), - idCard: zod + documentNumber: zod .string() .min(1, "请输入身份证号码") .regex( @@ -30,11 +32,11 @@ const RealNameSchema = zod.object({ "请输入正确的身份证号码", ), - idCardFrontImage: zod + fileId1: zod .string() .min(1, "请上传身份证正面照片"), - idCardBackImage: zod + fileId2: zod .string() .min(1, "请上传身份证反面照片"), }); @@ -62,10 +64,10 @@ async function takePicture(type: "front" | "back") { if (image.dataUrl) { if (type === "front") { - formData.value.idCardFrontImage = image.dataUrl; + formData.value.fileId1 = image.dataUrl; } else { - formData.value.idCardBackImage = image.dataUrl; + formData.value.fileId2 = image.dataUrl; } } } @@ -86,13 +88,25 @@ async function selectFromGallery(type: "front" | "back") { width: 1200, height: 800, }); + const file = new File( + [image.dataUrl ? await (await fetch(image.dataUrl)).blob() : ""], + "upload.jpeg", + { type: "image/jpeg" }, + ); + const fileId = await uploadToS3(file, { + fetchOptions: { + fileName: `idCard_${Date.now()}.jpeg`, + fileSize: image.dataUrl?.length || 0, + businessType: "kyc_document", + }, + }); if (image.dataUrl) { if (type === "front") { - formData.value.idCardFrontImage = image.dataUrl; + formData.value.fileId1 = fileId; } else { - formData.value.idCardBackImage = image.dataUrl; + formData.value.fileId2 = fileId; } } } @@ -185,7 +199,7 @@ async function handleSubmit() { -
+
点击拍照或选择照片 @@ -231,7 +245,7 @@ async function handleSubmit() {
身份证正面 @@ -248,7 +262,7 @@ async function handleSubmit() { class="upload-box" @click="showImageSourceOptions('back')" > -
+
点击拍照或选择照片 @@ -259,7 +273,7 @@ async function handleSubmit() {
身份证反面