Просмотр исходного кода

feat: realname auth submit (user) + review page (admin) (Step 26)

yangyi 3 дней назад
Родитель
Сommit
f720a592fd
3 измененных файлов с 179 добавлено и 24 удалено
  1. 8 0
      src/router/index.ts
  2. 76 24
      src/view/EditProfileView.vue
  3. 95 0
      src/view/RealnameReviewView.vue

+ 8 - 0
src/router/index.ts

@@ -14,6 +14,7 @@ import AdminLayout from "../layout/AdminLayout.vue"
 import UserView from "../view/UserView.vue"
 import SiteSettingsView from "../view/SiteSettingsView.vue"
 import PostManageView from "../view/PostManageView.vue"
+import RealnameReviewView from "../view/RealnameReviewView.vue"
 import {
   HomeFilled,
   User,
@@ -21,6 +22,7 @@ import {
   Bell,
   Setting,
   Document,
+  Edit,
 } from '@element-plus/icons-vue'
 import { useLoginUserStore } from "../store"
 import { ElMessage } from "element-plus"
@@ -101,6 +103,12 @@ export const adminRoutes: RouteRecordRaw[] = [
         component: PostManageView,
         meta: { title: '帖子管理', icon: Document, requiresAdmin: true }
       },
+      {
+        path: '/admin/realname',
+        name: 'adminRealname',
+        component: RealnameReviewView,
+        meta: { title: '实名审核', icon: Edit, requiresAdmin: true }
+      },
       {
         path: '/admin/settings',
         name: 'settings',

+ 76 - 24
src/view/EditProfileView.vue

@@ -2,12 +2,7 @@
   <div class="edit-profile-page">
     <el-form ref="formRef" :model="form" label-width="100px" class="edit-form">
       <el-form-item label="头像">
-        <el-upload
-          class="avatar-uploader"
-          :show-file-list="false"
-          accept="image/*"
-          :before-upload="beforeUpload"
-        >
+        <el-upload class="avatar-uploader" :show-file-list="false" accept="image/*">
           <el-avatar :size="64" :src="form.avatar || undefined">
             {{ userStore.loginUser.user?.name?.[0] || 'U' }}
           </el-avatar>
@@ -24,7 +19,7 @@
 
       <el-form-item label="实名认证">
         <el-tag v-if="userStore.userInfo.isRealname" type="success">已认证</el-tag>
-        <el-button v-else size="small" type="primary" plain>去认证</el-button>
+        <el-button v-else size="small" type="primary" plain @click="showRealname = true">去认证</el-button>
       </el-form-item>
 
       <el-divider />
@@ -45,6 +40,33 @@
         <el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
       </el-form-item>
     </el-form>
+
+    <el-dialog v-model="showRealname" title="实名认证" width="90%" max-width="500px">
+      <el-form :model="realnameForm" label-width="100px">
+        <el-form-item label="真实姓名">
+          <el-input v-model="realnameForm.realName" placeholder="请输入真实姓名" />
+        </el-form-item>
+        <el-form-item label="身份证号">
+          <el-input v-model="realnameForm.idCard" placeholder="请输入身份证号" maxlength="18" />
+        </el-form-item>
+        <el-form-item label="身份证正面">
+          <el-upload class="upload-card" accept="image/*" :before-upload="(file: File) => uploadIdCard(file, 'front')">
+            <el-button size="small">上传正面</el-button>
+          </el-upload>
+          <span v-if="realnameForm.idCardFront" class="uploaded">已上传</span>
+        </el-form-item>
+        <el-form-item label="身份证反面">
+          <el-upload class="upload-card" accept="image/*" :before-upload="(file: File) => uploadIdCard(file, 'back')">
+            <el-button size="small">上传反面</el-button>
+          </el-upload>
+          <span v-if="realnameForm.idCardBack" class="uploaded">已上传</span>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showRealname = false">取消</el-button>
+        <el-button type="primary" :loading="realnameSubmitting" @click="submitRealname">提交认证</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -52,37 +74,62 @@
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import { useLoginUserStore } from '../store'
+import { getRealnameAuthController } from '../api/realname-auth-controller'
 
 const userStore = useLoginUserStore()
 const formRef = ref()
 const saving = ref(false)
 
-const form = reactive({
-  username: '',
-  avatar: '',
-  mobile: ''
-})
+const form = reactive({ username: '', avatar: '', mobile: '' })
+const passwordForm = reactive({ oldPassword: '', newPassword: '', confirmPassword: '' })
 
-const passwordForm = reactive({
-  oldPassword: '',
-  newPassword: '',
-  confirmPassword: ''
-})
-
-function beforeUpload(_file: File): boolean {
-  return true
-}
+const showRealname = ref(false)
+const realnameSubmitting = ref(false)
+const realnameForm = reactive({ realName: '', idCard: '', idCardFront: '', idCardBack: '' })
 
-function handleSave() {
+async function handleSave() {
   saving.value = true
-  setTimeout(() => {
+  try {
+    const { getUserController } = await import('../api/user-controller')
+    await getUserController().edit({
+      id: userStore.userInfo.id,
+      username: form.username,
+      account: userStore.userInfo.username,
+      role: userStore.userInfo.role,
+      password: undefined,
+    } as any)
     userStore.updateProfile({ username: form.username, avatar: form.avatar, mobile: form.mobile })
     if (userStore.loginUser.user) {
       userStore.loginUser.user.name = form.username
     }
     ElMessage.success('保存成功')
+  } catch {
+    ElMessage.error('保存失败')
+  } finally {
     saving.value = false
-  }, 500)
+  }
+}
+
+function uploadIdCard(_file: File, _side: string): boolean {
+  ElMessage.info('图片上传功能对接 OSS 后实现')
+  return false
+}
+
+async function submitRealname() {
+  if (!realnameForm.realName || !realnameForm.idCard) {
+    ElMessage.warning('请填写姓名和身份证号')
+    return
+  }
+  realnameSubmitting.value = true
+  try {
+    await getRealnameAuthController().submit(realnameForm)
+    ElMessage.success('认证资料已提交,等待管理员审核')
+    showRealname.value = false
+  } catch {
+    ElMessage.error('提交失败')
+  } finally {
+    realnameSubmitting.value = false
+  }
 }
 
 onMounted(() => {
@@ -104,4 +151,9 @@ onMounted(() => {
   font-weight: 600;
   margin-bottom: 12px;
 }
+.uploaded {
+  margin-left: 8px;
+  color: #67c23a;
+  font-size: 12px;
+}
 </style>

+ 95 - 0
src/view/RealnameReviewView.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="realname-review">
+    <el-card>
+      <template #header>
+        <div class="card-header">
+          <span>实名认证审核</span>
+          <el-button size="small" @click="fetchData">刷新</el-button>
+        </div>
+      </template>
+
+      <el-table :data="tableData" v-loading="loading" stripe style="width: 100%">
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column prop="userId" label="用户ID" width="80" />
+        <el-table-column prop="realName" label="真实姓名" width="120" />
+        <el-table-column prop="idCard" label="身份证号" width="180" />
+        <el-table-column label="身份证图片" width="160">
+          <template #default="{ row }">
+            <el-button v-if="row.idCardFront" link size="small" @click="previewImg(row.idCardFront)">正面</el-button>
+            <el-button v-if="row.idCardBack" link size="small" @click="previewImg(row.idCardBack)">反面</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="提交时间" width="160" />
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="{ row }">
+            <el-button size="small" type="success" @click="handleReview(row, true)">通过</el-button>
+            <el-button size="small" type="danger" @click="handleReview(row, false)">驳回</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <EmptyState v-if="!loading && tableData.length === 0" description="暂无待审核认证" />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getRealnameAuthController } from '../api/realname-auth-controller'
+import type { RealnameAuthVo } from '../api/models'
+import EmptyState from '../components/EmptyState.vue'
+
+const api = getRealnameAuthController()
+const tableData = ref<RealnameAuthVo[]>([])
+const loading = ref(false)
+
+async function fetchData() {
+  loading.value = true
+  try {
+    const res = await api.listPending()
+    if (res.code === 200 && res.data) {
+      tableData.value = res.data
+    }
+  } catch {
+    tableData.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+function previewImg(url: string) {
+  window.open(url, '_blank')
+}
+
+function handleReview(row: RealnameAuthVo, approved: boolean) {
+  const action = approved ? '通过' : '驳回'
+  ElMessageBox.prompt(
+    `确定${action}用户「${row.realName}」的实名认证?${approved ? '' : '请输入驳回原因'}`,
+    action === '通过' ? '确认通过' : '驳回原因',
+    { inputType: 'textarea', inputPlaceholder: approved ? '' : '请输入驳回原因(可选)' },
+  ).then(async ({ value }) => {
+    try {
+      await api.review(Number(row.id), { approved, rejectReason: value || '' })
+      ElMessage.success(`${action}成功`)
+      await fetchData()
+    } catch {
+      ElMessage.error(`${action}失败`)
+    }
+  }).catch(() => {})
+}
+
+onMounted(() => fetchData())
+</script>
+
+<style scoped>
+.realname-review {
+  padding: 16px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 16px;
+  font-weight: 600;
+}
+</style>