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 unref: typeof import('vue').unref
|
||||||
const unrefElement: typeof import('@vueuse/core').unrefElement
|
const unrefElement: typeof import('@vueuse/core').unrefElement
|
||||||
const until: typeof import('@vueuse/core').until
|
const until: typeof import('@vueuse/core').until
|
||||||
|
const uploadToS3: typeof import('./src/utils/aws/s3').uploadToS3
|
||||||
const useActiveElement: typeof import('@vueuse/core').useActiveElement
|
const useActiveElement: typeof import('@vueuse/core').useActiveElement
|
||||||
const useAnimate: typeof import('@vueuse/core').useAnimate
|
const useAnimate: typeof import('@vueuse/core').useAnimate
|
||||||
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
|
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
|
||||||
@@ -330,6 +331,9 @@ declare global {
|
|||||||
export type { Language } from './src/composables/useLanguage'
|
export type { Language } from './src/composables/useLanguage'
|
||||||
import('./src/composables/useLanguage')
|
import('./src/composables/useLanguage')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
export type { UploadFetchOptions } from './src/utils/aws/s3'
|
||||||
|
import('./src/utils/aws/s3')
|
||||||
|
// @ts-ignore
|
||||||
export type { Wallet } from './src/store/wallet'
|
export type { Wallet } from './src/store/wallet'
|
||||||
import('./src/store/wallet')
|
import('./src/store/wallet')
|
||||||
}
|
}
|
||||||
@@ -461,6 +465,7 @@ declare module 'vue' {
|
|||||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
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 useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
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
|
specifier: 8.0.0
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
'@capp/eden':
|
'@capp/eden':
|
||||||
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.10.tgz
|
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.11.tgz
|
||||||
version: 0.0.10
|
version: 0.0.11
|
||||||
'@cloudflare/workers-types':
|
'@cloudflare/workers-types':
|
||||||
specifier: ^4.20260113.0
|
specifier: ^4.20260113.0
|
||||||
version: 4.20260116.0
|
version: 4.20260116.0
|
||||||
@@ -298,7 +298,7 @@ importers:
|
|||||||
version: 8.0.0(@capacitor/core@8.0.0)
|
version: 8.0.0(@capacitor/core@8.0.0)
|
||||||
'@capp/eden':
|
'@capp/eden':
|
||||||
specifier: 'catalog:'
|
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':
|
'@elysiajs/eden':
|
||||||
specifier: 'catalog:'
|
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))
|
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':
|
'@capacitor/synapse@1.0.4':
|
||||||
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
|
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
|
||||||
|
|
||||||
'@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':
|
||||||
resolution: {tarball: 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.11.tgz}
|
||||||
version: 0.0.10
|
version: 0.0.11
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@elysiajs/eden': ^1.4.6
|
'@elysiajs/eden': ^1.4.6
|
||||||
|
|
||||||
@@ -6903,7 +6903,7 @@ snapshots:
|
|||||||
|
|
||||||
'@capacitor/synapse@1.0.4': {}
|
'@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:
|
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))
|
'@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/keyboard': 8.0.0
|
||||||
'@capacitor/share': ^8.0.0
|
'@capacitor/share': ^8.0.0
|
||||||
'@capacitor/status-bar': 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
|
'@cloudflare/workers-types': ^4.20260113.0
|
||||||
'@elysiajs/eden': ^1.4.6
|
'@elysiajs/eden': ^1.4.6
|
||||||
'@faker-js/faker': ^10.2.0
|
'@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();
|
await toast.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是编辑模式,加载地址数据
|
onMounted(async () => {
|
||||||
onMounted(() => {
|
|
||||||
if (isEditMode.value) {
|
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 = {
|
formData.value = {
|
||||||
recipientName: "张三",
|
recipientName: data.value?.recipientName || "",
|
||||||
phoneNumber: "13800138000",
|
phoneNumber: data.value?.phoneNumber || "",
|
||||||
detailAddress: "北京市朝阳区建国路88号SOHO现代城A座1001室",
|
detailAddress: data.value?.detailAddress || "",
|
||||||
isDefault: true,
|
isDefault: data.value?.isDefault || false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -72,9 +71,11 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
try {
|
try {
|
||||||
// TODO: 调用添加/编辑地址 API
|
|
||||||
if (isEditMode.value) {
|
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 {
|
else {
|
||||||
await safeClient(client.api.shipping_address.post({
|
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 { actionSheetController, toastController } from "@ionic/vue";
|
||||||
import { cameraOutline, cardOutline, checkmarkCircleOutline, imageOutline, personOutline } from "ionicons/icons";
|
import { cameraOutline, cardOutline, checkmarkCircleOutline, imageOutline, personOutline } from "ionicons/icons";
|
||||||
import zod from "zod";
|
import zod from "zod";
|
||||||
|
import { client } from "@/api";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
realName: "",
|
kycMethod: "id_card",
|
||||||
idCard: "",
|
documentName: "",
|
||||||
idCardFrontImage: "",
|
documentNumber: "",
|
||||||
idCardBackImage: "",
|
fileId1: "",
|
||||||
|
fileId2: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
|
|
||||||
// 表单验证 Schema
|
// 表单验证 Schema
|
||||||
const RealNameSchema = zod.object({
|
const RealNameSchema = zod.object({
|
||||||
realName: zod
|
documentName: zod
|
||||||
.string()
|
.string()
|
||||||
.min(2, "请输入真实姓名")
|
.min(2, "请输入真实姓名")
|
||||||
.max(10, "姓名长度不能超过10个字符"),
|
.max(10, "姓名长度不能超过10个字符"),
|
||||||
|
|
||||||
idCard: zod
|
documentNumber: zod
|
||||||
.string()
|
.string()
|
||||||
.min(1, "请输入身份证号码")
|
.min(1, "请输入身份证号码")
|
||||||
.regex(
|
.regex(
|
||||||
@@ -30,11 +32,11 @@ const RealNameSchema = zod.object({
|
|||||||
"请输入正确的身份证号码",
|
"请输入正确的身份证号码",
|
||||||
),
|
),
|
||||||
|
|
||||||
idCardFrontImage: zod
|
fileId1: zod
|
||||||
.string()
|
.string()
|
||||||
.min(1, "请上传身份证正面照片"),
|
.min(1, "请上传身份证正面照片"),
|
||||||
|
|
||||||
idCardBackImage: zod
|
fileId2: zod
|
||||||
.string()
|
.string()
|
||||||
.min(1, "请上传身份证反面照片"),
|
.min(1, "请上传身份证反面照片"),
|
||||||
});
|
});
|
||||||
@@ -62,10 +64,10 @@ async function takePicture(type: "front" | "back") {
|
|||||||
|
|
||||||
if (image.dataUrl) {
|
if (image.dataUrl) {
|
||||||
if (type === "front") {
|
if (type === "front") {
|
||||||
formData.value.idCardFrontImage = image.dataUrl;
|
formData.value.fileId1 = image.dataUrl;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
formData.value.idCardBackImage = image.dataUrl;
|
formData.value.fileId2 = image.dataUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,13 +88,25 @@ async function selectFromGallery(type: "front" | "back") {
|
|||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
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 (image.dataUrl) {
|
||||||
if (type === "front") {
|
if (type === "front") {
|
||||||
formData.value.idCardFrontImage = image.dataUrl;
|
formData.value.fileId1 = fileId;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
formData.value.idCardBackImage = image.dataUrl;
|
formData.value.fileId2 = fileId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +199,7 @@ async function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
<ion-item lines="none" class="input-item">
|
<ion-item lines="none" class="input-item">
|
||||||
<ion-input
|
<ion-input
|
||||||
v-model="formData.realName"
|
v-model="formData.documentName"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="请输入真实姓名"
|
placeholder="请输入真实姓名"
|
||||||
class="custom-input"
|
class="custom-input"
|
||||||
@@ -201,7 +215,7 @@ async function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
<ion-item lines="none" class="input-item">
|
<ion-item lines="none" class="input-item">
|
||||||
<ion-input
|
<ion-input
|
||||||
v-model="formData.idCard"
|
v-model="formData.documentNumber"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="请输入身份证号码"
|
placeholder="请输入身份证号码"
|
||||||
class="custom-input"
|
class="custom-input"
|
||||||
@@ -220,7 +234,7 @@ async function handleSubmit() {
|
|||||||
class="upload-box"
|
class="upload-box"
|
||||||
@click="showImageSourceOptions('front')"
|
@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]" />
|
<ion-icon :icon="cameraOutline" class="text-4xl text-[#999]" />
|
||||||
<div class="text-sm text-[#999] mt-2">
|
<div class="text-sm text-[#999] mt-2">
|
||||||
点击拍照或选择照片
|
点击拍照或选择照片
|
||||||
@@ -231,7 +245,7 @@ async function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
:src="formData.idCardFrontImage"
|
:src="formData.fileId1"
|
||||||
alt="身份证正面"
|
alt="身份证正面"
|
||||||
class="uploaded-image"
|
class="uploaded-image"
|
||||||
>
|
>
|
||||||
@@ -248,7 +262,7 @@ async function handleSubmit() {
|
|||||||
class="upload-box"
|
class="upload-box"
|
||||||
@click="showImageSourceOptions('back')"
|
@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]" />
|
<ion-icon :icon="cameraOutline" class="text-4xl text-[#999]" />
|
||||||
<div class="text-sm text-[#999] mt-2">
|
<div class="text-sm text-[#999] mt-2">
|
||||||
点击拍照或选择照片
|
点击拍照或选择照片
|
||||||
@@ -259,7 +273,7 @@ async function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
:src="formData.idCardBackImage"
|
:src="formData.fileId2"
|
||||||
alt="身份证反面"
|
alt="身份证反面"
|
||||||
class="uploaded-image"
|
class="uploaded-image"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user