feat: 添加实名认证功能,更新路由和组件,优化个人设置页面
This commit is contained in:
BIN
src/assets/images/header.jpg
Normal file
BIN
src/assets/images/header.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -42,10 +42,22 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: "/check_in",
|
||||
component: () => import("@/views/check_in/index.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/invite",
|
||||
component: () => import("@/views/invite/index.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
component: () => import("@/views/settings/index.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/real_name",
|
||||
component: () => import("@/views/real_name/index.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
} */
|
||||
|
||||
.ion-toolbar {
|
||||
--background: var(--ion-color-primary-contrast);
|
||||
--min-height: 50px;
|
||||
--background: var(--ion-color-primary);
|
||||
--min-height: 58px;
|
||||
--color: white;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export const myApps = [
|
||||
name: "实名认证",
|
||||
icon: shieldCheckmarkOutline,
|
||||
color: "#c32120",
|
||||
path: "/real-name",
|
||||
path: "/real_name",
|
||||
},
|
||||
{
|
||||
id: "address",
|
||||
@@ -27,7 +27,7 @@ export const myApps = [
|
||||
name: "在线客服",
|
||||
icon: chatbubblesOutline,
|
||||
color: "#c32120",
|
||||
path: "/customer-service",
|
||||
path: "/customer_service",
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
@@ -48,7 +48,7 @@ export const myApps = [
|
||||
name: "资产明细",
|
||||
icon: listOutline,
|
||||
color: "#c32120",
|
||||
path: "/asset-details",
|
||||
path: "/asset_details",
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
396
src/views/real_name/index.vue
Normal file
396
src/views/real_name/index.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<script lang='ts' setup>
|
||||
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";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const formData = ref({
|
||||
realName: "",
|
||||
idCard: "",
|
||||
idCardFrontImage: "",
|
||||
idCardBackImage: "",
|
||||
});
|
||||
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
// 表单验证 Schema
|
||||
const RealNameSchema = zod.object({
|
||||
realName: zod
|
||||
.string()
|
||||
.min(2, "请输入真实姓名")
|
||||
.max(10, "姓名长度不能超过10个字符"),
|
||||
|
||||
idCard: zod
|
||||
.string()
|
||||
.min(1, "请输入身份证号码")
|
||||
.regex(
|
||||
/(^\d{15}$)|(^\d{18}$)|(^\d{17}([\dX])$)/i,
|
||||
"请输入正确的身份证号码",
|
||||
),
|
||||
|
||||
idCardFrontImage: zod
|
||||
.string()
|
||||
.min(1, "请上传身份证正面照片"),
|
||||
|
||||
idCardBackImage: zod
|
||||
.string()
|
||||
.min(1, "请上传身份证反面照片"),
|
||||
});
|
||||
|
||||
async function showToast(message: string, color: "success" | "danger" | "warning" = "danger") {
|
||||
const toast = await toastController.create({
|
||||
message,
|
||||
duration: 2000,
|
||||
position: "top",
|
||||
color,
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
|
||||
async function takePicture(type: "front" | "back") {
|
||||
try {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 80,
|
||||
allowEditing: false,
|
||||
resultType: CameraResultType.DataUrl,
|
||||
source: CameraSource.Camera,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
});
|
||||
|
||||
if (image.dataUrl) {
|
||||
if (type === "front") {
|
||||
formData.value.idCardFrontImage = image.dataUrl;
|
||||
}
|
||||
else {
|
||||
formData.value.idCardBackImage = image.dataUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error?.message !== "User cancelled photos app") {
|
||||
await showToast("拍照失败,请重试", "danger");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFromGallery(type: "front" | "back") {
|
||||
try {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 80,
|
||||
allowEditing: false,
|
||||
resultType: CameraResultType.DataUrl,
|
||||
source: CameraSource.Photos,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
});
|
||||
|
||||
if (image.dataUrl) {
|
||||
if (type === "front") {
|
||||
formData.value.idCardFrontImage = image.dataUrl;
|
||||
}
|
||||
else {
|
||||
formData.value.idCardBackImage = image.dataUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error?.message !== "User cancelled photos app") {
|
||||
await showToast("选择图片失败,请重试", "danger");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function showImageSourceOptions(type: "front" | "back") {
|
||||
const actionSheet = await actionSheetController.create({
|
||||
header: "选择图片来源",
|
||||
buttons: [
|
||||
{
|
||||
text: "拍照",
|
||||
handler: () => takePicture(type),
|
||||
},
|
||||
{
|
||||
text: "从相册选择",
|
||||
handler: () => selectFromGallery(type),
|
||||
},
|
||||
{
|
||||
text: "取消",
|
||||
role: "cancel",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await actionSheet.present();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const result = RealNameSchema.safeParse(formData.value);
|
||||
if (!result.success) {
|
||||
const first = result.error.issues[0];
|
||||
await showToast(first.message);
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
// TODO: 调用实名认证 API
|
||||
// const { data } = await safeClient(client.api.realname.post(formData.value));
|
||||
|
||||
// 模拟 API 调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
await showToast("实名认证提交成功,等待审核", "success");
|
||||
router.back();
|
||||
}
|
||||
catch (error) {
|
||||
await showToast("提交失败,请重试", "danger");
|
||||
}
|
||||
finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ion-page>
|
||||
<ion-header class="ion-no-border">
|
||||
<ion-toolbar class="ion-toolbar">
|
||||
<ion-buttons slot="start">
|
||||
<back-button />
|
||||
</ion-buttons>
|
||||
<ion-title>实名认证</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="space-y-5">
|
||||
<!-- 提示信息 -->
|
||||
<div class="info-card">
|
||||
<ion-icon :icon="checkmarkCircleOutline" class="text-2xl text-[#c41e3a]" />
|
||||
<div class="text-sm text-[#666] leading-relaxed">
|
||||
为了保障您的账户安全,请完成实名认证。您的个人信息将严格保密。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单卡片 -->
|
||||
<div class="form-card">
|
||||
<div class="space-y-4">
|
||||
<!-- 真实姓名 -->
|
||||
<div class="form-item">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ion-icon :icon="personOutline" class="text-lg text-primary" />
|
||||
<label class="form-label">真实姓名</label>
|
||||
</div>
|
||||
<ion-item lines="none" class="input-item">
|
||||
<ion-input
|
||||
v-model="formData.realName"
|
||||
type="text"
|
||||
placeholder="请输入真实姓名"
|
||||
class="custom-input"
|
||||
/>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<!-- 身份证号码 -->
|
||||
<div class="form-item">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ion-icon :icon="cardOutline" class="text-lg text-primary" />
|
||||
<label class="form-label">身份证号码</label>
|
||||
</div>
|
||||
<ion-item lines="none" class="input-item">
|
||||
<ion-input
|
||||
v-model="formData.idCard"
|
||||
type="text"
|
||||
placeholder="请输入身份证号码"
|
||||
class="custom-input"
|
||||
:maxlength="18"
|
||||
/>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<!-- 身份证正面 -->
|
||||
<div class="form-item">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ion-icon :icon="cardOutline" class="text-lg text-primary" />
|
||||
<label class="form-label">身份证正面</label>
|
||||
</div>
|
||||
<div
|
||||
class="upload-box"
|
||||
@click="showImageSourceOptions('front')"
|
||||
>
|
||||
<div v-if="!formData.idCardFrontImage" class="upload-placeholder">
|
||||
<ion-icon :icon="cameraOutline" class="text-4xl text-[#999]" />
|
||||
<div class="text-sm text-[#999] mt-2">
|
||||
点击拍照或选择照片
|
||||
</div>
|
||||
<div class="text-xs text-[#bbb] mt-1">
|
||||
请上传人像面
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="formData.idCardFrontImage"
|
||||
alt="身份证正面"
|
||||
class="uploaded-image"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 身份证反面 -->
|
||||
<div class="form-item">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<ion-icon :icon="cardOutline" class="text-lg text-primary" />
|
||||
<label class="form-label">身份证反面</label>
|
||||
</div>
|
||||
<div
|
||||
class="upload-box"
|
||||
@click="showImageSourceOptions('back')"
|
||||
>
|
||||
<div v-if="!formData.idCardBackImage" class="upload-placeholder">
|
||||
<ion-icon :icon="cameraOutline" class="text-4xl text-[#999]" />
|
||||
<div class="text-sm text-[#999] mt-2">
|
||||
点击拍照或选择照片
|
||||
</div>
|
||||
<div class="text-xs text-[#bbb] mt-1">
|
||||
请上传国徽面
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="formData.idCardBackImage"
|
||||
alt="身份证反面"
|
||||
class="uploaded-image"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<ion-button
|
||||
expand="block"
|
||||
class="submit-button"
|
||||
:disabled="isSubmitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<ion-spinner v-if="isSubmitting" name="crescent" />
|
||||
<span v-else>提交认证</span>
|
||||
</ion-button>
|
||||
|
||||
<!-- 注意事项 -->
|
||||
<div class="notice-card">
|
||||
<div class="text-sm font-semibold text-[#333] mb-2">
|
||||
注意事项:
|
||||
</div>
|
||||
<ul class="text-xs text-[#666] space-y-1 pl-4">
|
||||
<li>• 请确保身份证照片清晰完整,信息可见</li>
|
||||
<li>• 身份证必须在有效期内</li>
|
||||
<li>• 一个身份证只能认证一个账号</li>
|
||||
<li>• 认证信息提交后将无法修改</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped>
|
||||
.info-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffffff 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #ffe0e0;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-item {
|
||||
--background: #f7f8fa;
|
||||
--border-radius: 12px;
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
--min-height: 48px;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
--placeholder-color: #999;
|
||||
--placeholder-opacity: 1;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/10;
|
||||
background: #f7f8fa;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-box:active {
|
||||
transform: scale(0.98);
|
||||
border-color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.uploaded-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
--background: linear-gradient(135deg, #c41e3a 0%, #8b1a2e 100%);
|
||||
--background-activated: linear-gradient(135deg, #8b1a2e 0%, #c41e3a 100%);
|
||||
--border-radius: 12px;
|
||||
--padding-top: 14px;
|
||||
--padding-bottom: 14px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin-top: 8px;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
background: #f9fafb;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.notice-card ul li {
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
87
src/views/settings/index.vue
Normal file
87
src/views/settings/index.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang='ts' setup>
|
||||
import { callOutline, cardOutline, keyOutline, personOutline } from "ionicons/icons";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { userProfile } = storeToRefs(userStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ion-page>
|
||||
<ion-header class="ion-no-border">
|
||||
<ion-toolbar class="ion-toolbar">
|
||||
<back-button slot="start" />
|
||||
<ion-title>个人设置</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<!-- UID -->
|
||||
<div class="info-item">
|
||||
<div class="flex items-center gap-2">
|
||||
<ion-icon :icon="keyOutline" class="text-xl text-primary" />
|
||||
<span class="label">用户UID</span>
|
||||
</div>
|
||||
<span class="value">{{ userProfile?.uid || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 姓名 -->
|
||||
<div class="info-item">
|
||||
<div class="flex items-center gap-2">
|
||||
<ion-icon :icon="personOutline" class="text-xl text-primary" />
|
||||
<span class="label">真实姓名</span>
|
||||
</div>
|
||||
<span class="value">{{ userProfile?.fullName || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<div class="info-item">
|
||||
<div class="flex items-center gap-2">
|
||||
<ion-icon :icon="callOutline" class="text-xl text-primary" />
|
||||
<span class="label">手机号码</span>
|
||||
</div>
|
||||
<span class="value">{{ userProfile?.user.username || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 身份证号 -->
|
||||
<div class="info-item">
|
||||
<div class="flex items-center gap-2">
|
||||
<ion-icon :icon="cardOutline" class="text-xl text-primary" />
|
||||
<span class="label">身份证号</span>
|
||||
</div>
|
||||
<span class="value">{{ (userProfile as any)?.idCard || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped>
|
||||
.card {
|
||||
background: linear-gradient(180deg, #ffeef1, #ffffff 15%);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user