feat(mobile): 接入实名认证与商品详情客服入口

This commit is contained in:
yml2213
2026-10-30 14:11:15 +08:00
parent f2552fe523
commit 395f056d64
8 changed files with 475 additions and 8 deletions
@@ -5,6 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom'
import { BrandLogo } from '@/components/BrandLogo'
import { useListingDetail } from '@/features/listings/api'
import { ensureSupportConversation } from '@/features/messages/api'
import {
createOrder,
useOrderAgreements,
@@ -41,6 +42,24 @@ export function MobileListingDetailPage() {
const [confirmOpen, setConfirmOpen] = useState(false)
const [agreed, setAgreed] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [openingSupport, setOpeningSupport] = useState(false)
const openSupport = async () => {
if (!token) {
navigate(`/m/login?redirect=/m/listings/${id}`)
return
}
if (openingSupport) return
setOpeningSupport(true)
try {
const conversation = await ensureSupportConversation()
navigate(`/m/messages/${conversation.id}`)
} catch {
// 全局响应拦截已提示,保留按钮可重试。
} finally {
setOpeningSupport(false)
}
}
const showAgreement = (title: string, content?: string) => {
Dialog.alert({
@@ -66,8 +85,9 @@ export function MobileListingDetailPage() {
if (user?.realname_status !== 'verified') {
Dialog.alert({
title: '需要先完成实名认证',
content: '当前账号未完成实名认证,无法创建租赁订单。请先联系客服或在实名入口完成认证后再下单。',
confirmText: '知道了',
content: '当前账号未完成实名认证,无法创建租赁订单。完成实名后即可继续下单。',
confirmText: '去实名',
onConfirm: () => navigate('/m/realname'),
})
return
}
@@ -204,9 +224,8 @@ export function MobileListingDetailPage() {
<div className="mobile-detail__actions">
<Button
block
onClick={() => {
Toast.show({ content: '客服接入将在下一批完成' })
}}
loading={openingSupport}
onClick={() => void openSupport()}
>
<PhoneFill /> 联系客服
</Button>
@@ -1,13 +1,22 @@
import { Button, List } from 'antd-mobile'
import { Button, List, Tag } from 'antd-mobile'
import { useNavigate } from 'react-router-dom'
import { BrandLogo } from '@/components/BrandLogo'
import { useSessionStore } from '@/stores/session'
const realnameLabel: Record<string, string> = {
verified: '已实名',
pending: '审核中',
rejected: '未通过',
}
export function MobileProfilePage() {
const navigate = useNavigate()
const user = useSessionStore(state => state.user)
const logout = useSessionStore(state => state.logout)
const realnameStatus = user?.realname_status || 'unverified'
const realnameTone =
realnameStatus === 'verified' ? 'success' : realnameStatus === 'rejected' ? 'danger' : 'default'
return (
<div className="mobile-profile">
@@ -21,7 +30,11 @@ export function MobileProfilePage() {
<List className="mobile-profile__list">
<List.Item extra={user ? '已登录' : '未登录'}>账号状态</List.Item>
<List.Item extra={user?.realname_status === 'verified' ? '已实名' : '未实名'}>
<List.Item
clickable
onClick={() => navigate(user ? '/m/realname' : '/m/login?redirect=/m/realname')}
extra={<Tag color={realnameTone}>{realnameLabel[realnameStatus] ?? '未实名'}</Tag>}
>
实名认证
</List.Item>
<List.Item onClick={() => navigate('/m/orders')}>我的订单</List.Item>
@@ -0,0 +1,223 @@
import { useState } from 'react'
import {
Button,
Dialog,
ErrorBlock,
Form,
Input,
List,
Skeleton,
Tag,
Toast,
} from 'antd-mobile'
import { CheckShieldOutline, LeftOutline } from 'antd-mobile-icons'
import { useNavigate } from 'react-router-dom'
import { BrandLogo } from '@/components/BrandLogo'
import { startRealname, useRealnameStatus } from '@/features/realname/api'
import { useSessionStore } from '@/stores/session'
import { formatDateTime } from '@/shared/utils/format'
interface RealnameFormValues {
name: string
idNo: string
}
const statusMeta: Record<
string,
{
label: string
color: 'default' | 'primary' | 'success' | 'warning' | 'danger'
hint: string
}
> = {
unverified: {
label: '未实名',
color: 'default',
hint: '完成实名后才能创建租赁订单。',
},
pending: {
label: '审核中',
color: 'warning',
hint: '资料已提交,正在等待实名校验结果。',
},
verified: {
label: '已实名',
color: 'success',
hint: '实名信息已通过校验,可以正常下单。',
},
rejected: {
label: '未通过',
color: 'danger',
hint: '实名信息未通过校验,请核对后重新提交。',
},
}
export function MobileRealnamePage() {
const navigate = useNavigate()
const token = useSessionStore(state => state.token)
const hydrated = useSessionStore(state => state.hydrated)
const loadMe = useSessionStore(state => state.loadMe)
const query = useRealnameStatus(Boolean(token))
const [saving, setSaving] = useState(false)
const status = query.data?.status ?? 'unverified'
const meta = statusMeta[status] ?? statusMeta.unverified
const submit = async (values: RealnameFormValues) => {
if (saving) return
const confirmed = await Dialog.confirm({
title: '确认提交实名认证',
content: '姓名与身份证号仅用于实名校验,请确认填写无误。',
confirmText: '确认提交',
cancelText: '再检查一下',
})
if (!confirmed) return
setSaving(true)
try {
const result = await startRealname(values.name.trim(), values.idNo.trim())
Toast.show({
icon: 'success',
content: result.status === 'verified' ? '实名认证已通过' : '实名资料已提交',
})
await query.refetch()
await loadMe().catch(() => undefined)
} catch {
// 全局响应拦截已提示具体原因。
} finally {
setSaving(false)
}
}
if (!hydrated) {
return (
<div className="mobile-realname">
<Skeleton.Title animated />
<Skeleton.Paragraph lineCount={6} animated />
</div>
)
}
if (!token) {
return (
<div className="mobile-realname">
<div className="mobile-realname__back">
<button type="button" onClick={() => navigate('/m/profile')}>
<LeftOutline /> 返回个人中心
</button>
</div>
<div className="mobile-orders__login">
<BrandLogo height={40} variant="seal" />
<h2>登录后完成实名</h2>
<p>实名信息只对当前账号开放,请先登录。</p>
<Button block color="primary" onClick={() => navigate('/m/login?redirect=/m/realname')}>
去登录
</Button>
</div>
</div>
)
}
return (
<div className="mobile-realname">
<div className="mobile-realname__back">
<button type="button" onClick={() => navigate('/m/profile')}>
<LeftOutline /> 返回个人中心
</button>
</div>
<section className="mobile-realname__hero">
<span className="mobile-realname__icon">
<CheckShieldOutline />
</span>
<div>
<span className="mobile-realname__eyebrow">IDENTITY</span>
<h1>实名认证</h1>
<p>用于确认租赁双方身份,降低账号交易风险。</p>
</div>
<Tag color={meta.color}>{meta.label}</Tag>
</section>
{query.isPending ? (
<Skeleton.Paragraph lineCount={5} animated />
) : query.isError ? (
<ErrorBlock status="disconnected" title="实名状态没有加载出来">
<Button size="small" onClick={() => void query.refetch()}>
重新加载
</Button>
</ErrorBlock>
) : (
<>
<List className="mobile-realname__info">
<List.Item extra={<Tag color={meta.color}>{meta.label}</Tag>}>当前状态</List.Item>
<List.Item>{meta.hint}</List.Item>
{query.data?.masked_name ? (
<List.Item extra={query.data.masked_name}>姓名</List.Item>
) : null}
{query.data?.masked_id_no ? (
<List.Item extra={query.data.masked_id_no}>身份证号</List.Item>
) : null}
{query.data?.verified_at ? (
<List.Item extra={formatDateTime(query.data.verified_at)}>认证时间</List.Item>
) : null}
{query.data?.fail_reason ? (
<List.Item extra={query.data.fail_reason}>失败原因</List.Item>
) : null}
</List>
{status === 'pending' ? (
<Button
block
fill="outline"
loading={query.isFetching}
onClick={() => void query.refetch()}
>
刷新审核状态
</Button>
) : null}
{status === 'unverified' || status === 'rejected' ? (
<section className="mobile-realname__form">
<div className="mobile-realname__form-head">
<strong>填写实名信息</strong>
<span>姓名与身份证号必须与证件一致。</span>
</div>
<Form
layout="horizontal"
onFinish={values => void submit(values as RealnameFormValues)}
footer={
<Button block color="primary" type="submit" loading={saving}>
提交实名认证
</Button>
}
>
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请填写真实姓名' }]}
>
<Input placeholder="请输入证件上的姓名" maxLength={30} />
</Form.Item>
<Form.Item
name="idNo"
label="身份证号"
rules={[
{ required: true, message: '请填写身份证号' },
{
pattern: /^\d{17}[\dXx]$/,
message: '请填写 18 位居民身份证号',
},
]}
>
<Input placeholder="请输入 18 位身份证号" maxLength={18} />
</Form.Item>
</Form>
<p className="mobile-realname__privacy">
证件信息由后端加密保存,页面仅展示脱敏结果;如实名服务暂时不可用,可稍后重试或联系客服。
</p>
</section>
) : null}
</>
)}
</div>
)
}