feat: 添加 S3 文件上传功能,更新 '@capp/eden' 依赖至 0.0.11,优化实名认证表单逻辑
This commit is contained in:
5
auto-imports.d.ts
vendored
5
auto-imports.d.ts
vendored
@@ -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<typeof import('vue')['unref']>
|
||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly uploadToS3: UnwrapRef<typeof import('./src/utils/aws/s3')['uploadToS3']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
77
src/utils/aws/s3.ts
Normal file
77
src/utils/aws/s3.ts
Normal file
@@ -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<typeof client.api.file_storage.upload_url.post>;
|
||||
|
||||
export async function uploadToS3(file: File, options: UploadOptions): Promise<string> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
1
src/utils/index.ts
Normal file
1
src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./aws/s3";
|
||||
@@ -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({
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<ion-item lines="none" class="input-item">
|
||||
<ion-input
|
||||
v-model="formData.realName"
|
||||
v-model="formData.documentName"
|
||||
type="text"
|
||||
placeholder="请输入真实姓名"
|
||||
class="custom-input"
|
||||
@@ -201,7 +215,7 @@ async function handleSubmit() {
|
||||
</div>
|
||||
<ion-item lines="none" class="input-item">
|
||||
<ion-input
|
||||
v-model="formData.idCard"
|
||||
v-model="formData.documentNumber"
|
||||
type="text"
|
||||
placeholder="请输入身份证号码"
|
||||
class="custom-input"
|
||||
@@ -220,7 +234,7 @@ async function handleSubmit() {
|
||||
class="upload-box"
|
||||
@click="showImageSourceOptions('front')"
|
||||
>
|
||||
<div v-if="!formData.idCardFrontImage" class="upload-placeholder">
|
||||
<div v-if="!formData.fileId1" class="upload-placeholder">
|
||||
<ion-icon :icon="cameraOutline" class="text-4xl text-[#999]" />
|
||||
<div class="text-sm text-[#999] mt-2">
|
||||
点击拍照或选择照片
|
||||
@@ -231,7 +245,7 @@ async function handleSubmit() {
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="formData.idCardFrontImage"
|
||||
:src="formData.fileId1"
|
||||
alt="身份证正面"
|
||||
class="uploaded-image"
|
||||
>
|
||||
@@ -248,7 +262,7 @@ async function handleSubmit() {
|
||||
class="upload-box"
|
||||
@click="showImageSourceOptions('back')"
|
||||
>
|
||||
<div v-if="!formData.idCardBackImage" class="upload-placeholder">
|
||||
<div v-if="!formData.fileId2" class="upload-placeholder">
|
||||
<ion-icon :icon="cameraOutline" class="text-4xl text-[#999]" />
|
||||
<div class="text-sm text-[#999] mt-2">
|
||||
点击拍照或选择照片
|
||||
@@ -259,7 +273,7 @@ async function handleSubmit() {
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="formData.idCardBackImage"
|
||||
:src="formData.fileId2"
|
||||
alt="身份证反面"
|
||||
class="uploaded-image"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user