Prechádzať zdrojové kódy

feat:提供实名申请功能

yangyi 4 mesiacov pred
rodič
commit
0e92266a63

+ 20 - 1
src/api/endpoints/auth.ts

@@ -11,10 +11,26 @@ import type {
   ResultLong,
   ResultUserProfileVO,
   ResultVoid,
+  SubmitAuthBody,
 } from "../models";
 import { customAxiosInstance } from "../../util/axios-instance";
 
 export const getAuth = () => {
+  /**
+   * 上传校园卡照片进行实名审核;被拒后可重新提交
+   * @summary 提交实名认证
+   */
+  const submitAuth = (submitAuthBody: SubmitAuthBody) => {
+    const formData = new FormData();
+    formData.append("campusCardImage", submitAuthBody.campusCardImage);
+
+    return customAxiosInstance<ResultVoid>({
+      url: `/auth/submit-auth`,
+      method: "POST",
+      headers: { "Content-Type": "multipart/form-data" },
+      data: formData,
+    });
+  };
   /**
    * 学号+姓名+密码+校园卡照片,提交后进入待审核状态
    * @summary 用户注册
@@ -69,8 +85,11 @@ export const getAuth = () => {
       data: changePasswordRequest,
     });
   };
-  return { register, logout, login, changePassword };
+  return { submitAuth, register, logout, login, changePassword };
 };
+export type SubmitAuthResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getAuth>["submitAuth"]>>
+>;
 export type RegisterResult = NonNullable<
   Awaited<ReturnType<ReturnType<typeof getAuth>["register"]>>
 >;

+ 1 - 0
src/api/models/index.ts

@@ -45,6 +45,7 @@ export * from "./searchParams";
 export * from "./sendMessageRequest";
 export * from "./soldParams";
 export * from "./statusRequest";
+export * from "./submitAuthBody";
 export * from "./user";
 export * from "./userProfileRequest";
 export * from "./userProfileVO";

+ 5 - 0
src/store/user.ts

@@ -43,6 +43,10 @@ export const useUserStore = defineStore('user', () => {
     if (profile.avatarUrl !== undefined) state.avatarUrl = profile.avatarUrl
   }
 
+  function updateAuthStatus(status: string) {
+    state.authStatus = status
+  }
+
   return {
     state,
     isAuthenticated,
@@ -50,5 +54,6 @@ export const useUserStore = defineStore('user', () => {
     login,
     logout,
     updateProfile,
+    updateAuthStatus,
   }
 })

+ 109 - 2
src/view/Profile.vue

@@ -33,6 +33,24 @@
           <el-tag :type="authStatusTagType">
             {{ authStatusLabel }}
           </el-tag>
+          <el-button
+            v-if="userStore.state.authStatus === 'UNAUTHENTICATED'"
+            size="small"
+            type="primary"
+            style="margin-left: 12px"
+            @click="dialogVisible = true"
+          >
+            提交实名认证
+          </el-button>
+          <el-button
+            v-else-if="userStore.state.authStatus && userStore.state.authStatus !== 'PENDING' && userStore.state.authStatus !== 'AUTHENTICATED'"
+            size="small"
+            type="warning"
+            style="margin-left: 12px"
+            @click="dialogVisible = true"
+          >
+            重新提交认证
+          </el-button>
         </el-form-item>
       </el-form>
 
@@ -43,17 +61,44 @@
         <el-button @click="router.push('/profile/password')">修改密码</el-button>
       </div>
     </el-card>
+
+    <el-dialog v-model="dialogVisible" title="实名认证" width="420px" :close-on-click-modal="false">
+      <el-form label-width="100px">
+        <el-form-item label="校园卡照片">
+          <el-upload
+            ref="uploadRef"
+            v-model:file-list="cardFileList"
+            :auto-upload="false"
+            list-type="picture-card"
+            :limit="1"
+            :on-change="onCardChange"
+            :before-upload="beforeCardUpload"
+          >
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+          <div class="upload-hint">上传校园卡照片用于实名审核</div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="handleCancelAuth">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleSubmitAuth">提交</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, computed } from 'vue'
-import { useRouter } from 'vue-router'
+import { ref, computed, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+import type { UploadFile, UploadRawFile, UploadUserFile } from 'element-plus'
 import { useUserStore } from '../store'
 import { getUsers } from '../api/endpoints/users'
+import { getAuth } from '../api/endpoints/auth'
 
 const router = useRouter()
+const route = useRoute()
 const userStore = useUserStore()
 const fileInputRef = ref<HTMLInputElement | null>(null)
 const nickname = ref(userStore.state.nickname)
@@ -61,6 +106,11 @@ const signature = ref('')
 const avatarFile = ref<File | null>(null)
 const saving = ref(false)
 
+const dialogVisible = ref(false)
+const cardFile = ref<File | null>(null)
+const cardFileList = ref<UploadUserFile[]>([])
+const submitting = ref(false)
+
 const authStatusTagType = computed(() => {
   const map: Record<string, string> = {
     AUTHENTICATED: 'success',
@@ -79,6 +129,52 @@ const authStatusLabel = computed(() => {
   return map[userStore.state.authStatus] || userStore.state.authStatus
 })
 
+const onCardChange = (file: UploadFile) => {
+  if (file.raw) cardFile.value = file.raw as UploadRawFile
+}
+
+const beforeCardUpload = (file: UploadRawFile) => {
+  if (!file.type.startsWith('image/')) {
+    ElMessage.error('只能上传图片')
+    return false
+  }
+  if (file.size > 10 * 1024 * 1024) {
+    ElMessage.error('图片大小不能超过 10MB')
+    return false
+  }
+  return true
+}
+
+const handleCancelAuth = () => {
+  dialogVisible.value = false
+  cardFile.value = null
+  cardFileList.value = []
+}
+
+const handleSubmitAuth = async () => {
+  if (!cardFile.value) {
+    ElMessage.warning('请上传校园卡照片')
+    return
+  }
+  submitting.value = true
+  try {
+    const res = await getAuth().submitAuth({ campusCardImage: cardFile.value })
+    if (res.code === 200) {
+      userStore.updateAuthStatus('PENDING')
+      ElMessage.success('提交成功,等待管理员审核')
+      dialogVisible.value = false
+      cardFile.value = null
+      cardFileList.value = []
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  } finally {
+    submitting.value = false
+  }
+}
+
 const triggerUpload = () => {
   fileInputRef.value?.click()
 }
@@ -90,6 +186,12 @@ const handleAvatarChange = (event: Event) => {
   }
 }
 
+onMounted(() => {
+  if (route.query.verify === '1') {
+    dialogVisible.value = true
+  }
+})
+
 const handleSave = async () => {
   saving.value = true
   try {
@@ -139,4 +241,9 @@ const handleSave = async () => {
   gap: 12px;
   justify-content: center;
 }
+.upload-hint {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 8px;
+}
 </style>