diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 1597ea4..2efe437 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -463,6 +463,12 @@ const AdminDistributionPaymentChannelsPage = lazy(() => const AdminLayout = lazy(() => import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout })) ) +const ProfilePage = lazy(() => + import('@/features/profile/ProfilePage').then(module => ({ default: module.ProfilePage })) +) +const RealnamePage = lazy(() => + import('@/features/realname/RealnamePage').then(module => ({ default: module.RealnamePage })) +) const AdminSectionPlaceholder = lazy(() => import('@/layouts/AdminLayout').then(module => ({ default: module.AdminSectionPlaceholder })) ) @@ -490,6 +496,8 @@ export function AppRouter() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/features/profile/ProfilePage.tsx b/frontend/src/features/profile/ProfilePage.tsx new file mode 100644 index 0000000..42cefa6 --- /dev/null +++ b/frontend/src/features/profile/ProfilePage.tsx @@ -0,0 +1,192 @@ +import { useQuery } from '@tanstack/react-query' +import { App, Avatar, Button, Card, Col, Descriptions, Row, Space, Tag, Typography } from 'antd' +import { + ArrowRightOutlined, + BellOutlined, + IdcardOutlined, + MessageOutlined, + ProfileOutlined, + ShopOutlined, + TeamOutlined, + WalletOutlined, +} from '@ant-design/icons' +import { useNavigate } from 'react-router-dom' + +import { BrandLogo } from '@/components/BrandLogo' +import { fetchWalletBalance } from '@/features/wallet/api' +import { useSessionStore } from '@/stores/session' +import { formatMoney } from '@/shared/utils/format' + +const realnameMeta: Record = { + verified: { label: '已实名', color: 'green' }, + pending: { label: '审核中', color: 'gold' }, + rejected: { label: '未通过', color: 'volcano' }, + unverified: { label: '未实名', color: 'default' }, +} + +const quickEntries = [ + { key: 'orders', label: '我的订单', hint: '查看租号进度与交接记录', icon: , to: '/orders' }, + { key: 'messages', label: '消息中心', hint: '账号群聊与客服会话', icon: , to: '/messages' }, + { key: 'wallet', label: '我的钱包', hint: '余额、流水与提现', icon: , to: '/wallet' }, + { key: 'seller', label: '卖家中心', hint: '发布账号与号主待办', icon: , to: '/seller/listings' }, + { key: 'invitations', label: '邀请好友', hint: '邀请码与返利记录', icon: , to: '/invitations' }, + { key: 'notifications', label: '我的通知', hint: '系统提醒与公告推送', icon: , to: '/notifications' }, +] + +export function ProfilePage() { + const navigate = useNavigate() + const { modal, message } = App.useApp() + const user = useSessionStore(state => state.user) + const token = useSessionStore(state => state.token) + const hydrated = useSessionStore(state => state.hydrated) + const logout = useSessionStore(state => state.logout) + + const wallet = useQuery({ + queryKey: ['wallet-balance', 'profile'], + queryFn: fetchWalletBalance, + enabled: Boolean(token), + }) + + if (!hydrated) { + return ( +
+ +
+ ) + } + + if (!token || !user) { + return ( +
+ + + 登录后查看个人中心 + + 账号资料、实名状态、订单与钱包只对当前账号开放。 + + + +
+ ) + } + + const status = user.realname_status || 'unverified' + const meta = realnameMeta[status] ?? realnameMeta.unverified + const initial = (user.nickname || user.phone || '客').slice(0, 1) + + const confirmLogout = () => { + modal.confirm({ + title: '确认退出登录', + content: '退出后需要重新登录才能查看订单与钱包。', + okText: '退出登录', + cancelText: '再想想', + okButtonProps: { danger: true }, + onOk: async () => { + await logout() + message.success('已退出登录') + navigate('/') + }, + }) + } + + return ( +
+
+
+ MY ACCOUNT + + 个人中心 + + + 账号资料、实名认证、订单与资金入口都集中在这里。 + +
+ +
+ + + + + + + {initial} + +
+ + {user.nickname || '我的商行'} + + {user.phone} +
+
+ + {meta.label} + {user.credit_score != null ? 信用分 {user.credit_score} : null} + {user.risk_status && user.risk_status !== 'normal' ? ( + 风险提示 + ) : null} + + +
+ 可用余额 + {wallet.data ? formatMoney(wallet.data.available_balance_cent) : '—'} + + 冻结 {wallet.data ? formatMoney(wallet.data.frozen_balance_cent) : '—'} + +
+ +
+ + + + +
+ {quickEntries.map(item => ( + + ))} +
+
+ + + + {user.id} + {user.phone} + {user.nickname || '未设置'} + + + {meta.label} + {status !== 'verified' ? ( + + ) : null} + + + + + + + + + +
+
+ ) +} diff --git a/frontend/src/features/realname/RealnamePage.tsx b/frontend/src/features/realname/RealnamePage.tsx new file mode 100644 index 0000000..d8abd69 --- /dev/null +++ b/frontend/src/features/realname/RealnamePage.tsx @@ -0,0 +1,209 @@ +import { useState } from 'react' +import { + Alert, + App, + Button, + Card, + Descriptions, + Form, + Input, + Space, + Tag, + Typography, +} from 'antd' +import { CheckCircleOutlined, IdcardOutlined, ReloadOutlined } from '@ant-design/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 = { + unverified: { label: '未实名', color: 'default', hint: '完成实名后才能创建租赁订单。' }, + pending: { label: '审核中', color: 'gold', hint: '资料已提交,正在等待实名校验结果。' }, + verified: { label: '已实名', color: 'green', hint: '实名信息已通过校验,可以正常下单。' }, + rejected: { label: '未通过', color: 'volcano', hint: '实名信息未通过校验,请核对后重新提交。' }, +} + +export function RealnamePage() { + const navigate = useNavigate() + const { message, modal } = App.useApp() + 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 [form] = Form.useForm() + + if (!hydrated) { + return ( +
+ +
+ ) + } + + if (!token) { + return ( +
+ + + 登录后完成实名 + + 实名信息只对当前账号开放,请先登录。 + + + +
+ ) + } + + const status = query.data?.status ?? 'unverified' + const meta = statusMeta[status] ?? statusMeta.unverified + + const submit = async () => { + const values = await form.validateFields() + modal.confirm({ + title: '确认提交实名认证', + content: '姓名与身份证号仅用于实名校验,请确认填写无误。', + okText: '确认提交', + cancelText: '再检查一下', + onOk: async () => { + setSaving(true) + try { + const result = await startRealname(values.name.trim(), values.idNo.trim()) + message.success(result.status === 'verified' ? '实名认证已通过' : '实名资料已提交') + form.resetFields() + await query.refetch() + await loadMe().catch(() => undefined) + } finally { + setSaving(false) + } + }, + }) + } + + return ( +
+
+
+ IDENTITY + + 实名认证 + + + 用于确认租赁双方身份,降低账号交易风险。 + +
+ }> + {meta.label} + +
+ + {query.isError ? ( + + void query.refetch()}> + 重新加载 + + } + /> + + ) : ( + } + loading={query.isFetching} + onClick={() => void query.refetch()} + > + 刷新 + + } + > + + + + {meta.label} + + {query.data?.masked_name || '—'} + + {query.data?.masked_id_no || '—'} + + + {formatDateTime(query.data?.verified_at)} + + {query.data?.fail_reason ? ( + + {query.data.fail_reason} + + ) : null} + + + {status === 'verified' ? ( + } + message="已完成实名认证,无需重复提交。" + /> + ) : null} + + {status === 'unverified' || status === 'rejected' ? ( +
+ 填写实名信息 + + 姓名与身份证号必须与证件一致。 + +
+ + + + + + + + + + +
+ + 证件信息由后端加密保存,页面仅展示脱敏结果;如实名服务暂时不可用,可稍后重试或联系客服。 + +
+ ) : null} +
+ )} +
+ ) +} diff --git a/frontend/src/layouts/StorefrontLayout.tsx b/frontend/src/layouts/StorefrontLayout.tsx index 735ba0b..f8c15d2 100644 --- a/frontend/src/layouts/StorefrontLayout.tsx +++ b/frontend/src/layouts/StorefrontLayout.tsx @@ -44,6 +44,16 @@ export function StorefrontLayout() { menu={{ items: [ { key: 'phone', label: `已登录:${user.phone}`, disabled: true }, + { + key: 'profile', + label: '个人中心', + onClick: () => navigate('/profile'), + }, + { + key: 'realname', + label: '实名认证', + onClick: () => navigate('/realname'), + }, { key: 'orders', label: '我的订单', diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 270ffd3..a40e1e0 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -18,6 +18,7 @@ @import './mobile-messages.css'; @import './mobile-chat.css'; @import './pc-messages.css'; +@import './pc-account.css'; @import './seller.css'; @import './mobile-seller.css'; @import './components.css'; diff --git a/frontend/src/styles/pc-account.css b/frontend/src/styles/pc-account.css new file mode 100644 index 0000000..4325cff --- /dev/null +++ b/frontend/src/styles/pc-account.css @@ -0,0 +1,139 @@ +/* PC 个人中心与实名认证 */ +.pc-account-page { + max-width: 1180px; + margin: 0 auto; + padding: 32px 24px 56px; +} + +.pc-account-hero { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + padding: 28px 32px; + margin-bottom: 20px; + border: 1px solid var(--line); + border-radius: 18px; + background: linear-gradient(135deg, rgba(30, 77, 66, 0.06), rgba(176, 138, 74, 0.08)); +} + +.pc-account-hero__eyebrow { + display: inline-block; + font-size: 12px; + letter-spacing: 0.24em; + color: var(--ink-muted); +} + +.pc-account-hero h2 { + margin: 6px 0 4px; +} + +.pc-account-card { + height: 100%; + border-radius: 16px; + border-color: var(--line); + background: var(--paper-warm); +} + +.pc-account-avatar { + background: var(--brand-ink); + color: #fff; + font-size: 26px; +} + +.pc-account-balance { + display: flex; + flex-direction: column; + gap: 2px; + margin: 20px 0 14px; + padding: 14px 16px; + border-radius: 12px; + background: rgba(30, 77, 66, 0.05); +} + +.pc-account-balance span { + font-size: 12px; + color: var(--ink-muted); +} + +.pc-account-balance strong { + font-size: 26px; + color: var(--brand-ink-deep); + letter-spacing: 0.02em; +} + +.pc-account-balance__frozen { + color: var(--ink-secondary) !important; +} + +.pc-account-quick { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px; +} + +.pc-account-quick__item { + position: relative; + display: flex; + flex-direction: column; + gap: 4px; + padding: 16px 18px; + text-align: left; + border: 1px solid var(--line); + border-radius: 14px; + background: #fff; + cursor: pointer; + transition: + border-color 0.18s ease, + transform 0.18s ease, + box-shadow 0.18s ease; +} + +.pc-account-quick__item:hover { + border-color: var(--brand-brass); + transform: translateY(-2px); + box-shadow: 0 10px 24px rgba(30, 77, 66, 0.08); +} + +.pc-account-quick__icon { + font-size: 20px; + color: var(--brand-ink); +} + +.pc-account-quick__hint { + font-size: 12px; + color: var(--ink-muted); +} + +.pc-account-quick__arrow { + position: absolute; + top: 18px; + right: 16px; + color: var(--ink-muted); +} + +.pc-account-login { + max-width: 460px; + margin: 64px auto; + text-align: center; + border-radius: 18px; + border-color: var(--line); + background: var(--paper-warm); +} + +.pc-account-form { + margin-top: 20px; + padding-top: 16px; + border-top: 1px dashed var(--line); +} + +@media (max-width: 640px) { + .pc-account-page { + padding: 20px 16px 40px; + } + + .pc-account-hero { + flex-direction: column; + padding: 22px; + } +}