feat: 添加实名认证功能,更新路由和组件,优化个人设置页面

This commit is contained in:
2026-01-18 02:57:39 +07:00
parent 967b87fc83
commit c2f6af8625
7 changed files with 505 additions and 5 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -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 },
},
];

View File

@@ -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;
}

View File

@@ -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;

View 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>

View 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>