From 55e914edd14e7d54b472e46761dd7e2b66cc759e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 30 Oct 2026 13:52:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(storefront):=20=E6=96=B0=E5=A2=9E=20PC=20?= =?UTF-8?q?=E5=95=86=E5=93=81=E8=AF=A6=E6=83=85=E4=B8=8E=E4=B8=8B=E5=8D=95?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Phase1-验收报告.md | 4 +- docs/Phase2-PC商品详情与下单验收.md | 71 +++ docs/项目计划.md | 2 +- frontend/src/app/AppRouter.tsx | 6 + .../features/listings/ListingDetailPage.tsx | 426 ++++++++++++++++++ .../src/features/listings/ListingListPage.tsx | 55 +-- frontend/src/styles/global.css | 285 ++++++++++++ 7 files changed, 820 insertions(+), 29 deletions(-) create mode 100644 docs/Phase2-PC商品详情与下单验收.md create mode 100644 frontend/src/features/listings/ListingDetailPage.tsx diff --git a/docs/Phase1-验收报告.md b/docs/Phase1-验收报告.md index c8d507b..bd804a3 100644 --- a/docs/Phase1-验收报告.md +++ b/docs/Phase1-验收报告.md @@ -193,7 +193,7 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB ## 5. 已知限制 -1. 移动端商品详情、下单确认、订单列表、详情、支付、交接、确认收号、结账、还价与申诉已接入真实接口;移动端实名认证入口与 PC 完整链路仍待迁移。 +1. 移动端商品详情、下单确认、订单列表、详情、支付、交接、确认收号、结账、还价与申诉已接入真实接口;PC 商品详情与下单已接通,移动端实名认证入口、PC 订单中心与 PC 订单操作仍待迁移。 2. 后台导航已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、资金出款、运营开支、公告管理、审计日志、系统配置、分销站点,长尾能力仍待迁移。 3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。 4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`(资金出款列表)与 `finance:manual_disbursement` / `finance:operating_expense`,公告 `announcement:view` / `announcement:manage`,审计日志 `audit_log:view`,系统配置 `system_config:view` / `system_config:update`,分销站点 `distribution:site_view` / `distribution:site_manage`(列表也接受 `qrcode:view`);当前菜单权限码已全部对照后端。 @@ -205,7 +205,7 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB ## 6. 建议下一批次 -1. 移动端实名认证入口、证据上传与订单群聊入口。 +1. PC 订单中心与订单操作页、移动端实名认证入口、证据上传与订单群聊入口。 2. 后台列表页原型:DataTable、FilterBar、详情 Drawer、批量操作。 3. 先迁移后台订单列表与商品审核列表。 4. 分销后台主题轻改。 diff --git a/docs/Phase2-PC商品详情与下单验收.md b/docs/Phase2-PC商品详情与下单验收.md new file mode 100644 index 0000000..3a8ae37 --- /dev/null +++ b/docs/Phase2-PC商品详情与下单验收.md @@ -0,0 +1,71 @@ +# Phase 2 验收报告(PC 商品详情与下单) + +> 日期:2026-10-30 +> 项目:解忧商行(hfb_jysh) +> 范围:`/listings/:id` +> 结论:**本切片通过,PC 商品详情、协议查看、下单确认与订单创建已接通** + +--- + +## 1. 页面与路由 + +- 路由:`/listings/:id` +- 页面:`frontend/src/features/listings/ListingDetailPage.tsx` +- 首页商品卡片改为可点击,进入对应详情 +- 详情页沿用 StorefrontLayout 顶部导航与页脚 + +## 2. 页面结构 + +- 面包屑与返回按钮 +- 左侧图片画廊:封面 + 截图缩略图,点击放大预览 +- 右侧下单卡: + - 商品编号、状态、标题、区服、登录方式、段位 + - 租金、押金、哈夫币、号主 + - 风险提示 + - 「确认租赁」按钮 + - 协议入口 +- 账号说明、资产摘要、交易流程 +- 右侧信任说明:实名审核、全程留痕、账单透明 +- 移动端断点下取消右侧吸顶,改为单列 + +## 3. 下单确认 + +- 未登录跳转 `/login?redirect=/listings/:id` +- 自己发布的商品禁止租用 +- 租赁中的商品禁止租用 +- 未实名用户禁止下单并提示先完成实名 +- 确认弹窗展示商品、租金、押金、哈夫币、号主 +- 必须勾选协议后才能提交 +- 协议内容来自: + - `GET /api/order-agreements` + - `GET /api/post-rental-notice` +- 调用 `POST /api/orders` 创建订单 +- 创建成功提示「去支付」,跳转 `/m/orders/:id` 继续支付 + +## 4. 验证结果 + +```text +npx tsc --noEmit 通过 +npm run build 通过(入口 499.23 kB,gzip 161.80 kB) +GET /listings/1 200 +GET /api/listings/1 ok +GET /api/order-agreements ok +GET /api/post-rental-notice ok +``` + +当前开发库商品 1 的号主是联调用户,页面会正确禁止自租;未执行真实下单写操作。 + +## 5. 已知限制 + +1. PC 端还没有独立订单中心与 PC 订单操作页,创建订单后暂跳移动订单详情继续支付。 +2. PC 下单确认页未做成长等级、免押额度与最终优惠试算,最终金额以订单详情为准。 +3. 客服入口、实名认证入口仍未迁移。 +4. 图片画廊没有视频或 3D 资产展示。 +5. 商品详情未接撞车商城独立样式。 + +## 6. 下一步建议 + +1. PC 订单中心与 PC 订单操作页。 +2. PC 实名认证与客服入口。 +3. PC 发布商品与卖家工作台。 +4. 订单群聊、证据上传与聊天 SSE。 diff --git a/docs/项目计划.md b/docs/项目计划.md index efe477d..1714d27 100644 --- a/docs/项目计划.md +++ b/docs/项目计划.md @@ -381,7 +381,7 @@ hfb_jysh/ ### Phase 3:C 端 PC + 移动端(3 周) -> 状态:**进行中(2026-10-30)**。移动端已完成首页、商品列表/详情、下单确认、登录、个人中心、订单列表/详情、消息中心与聊天,并接入支付、交接、确认收号、结账、还价与申诉;移动端实名认证入口、PC 端完整链路待迁移。 +> 状态:**进行中(2026-10-30)**。移动端已完成首页、商品列表/详情、下单确认、登录、个人中心、订单列表/详情、消息中心与聊天,并接入支付、交接、确认收号、结账、还价与申诉;PC 商品详情与下单已接通,移动端实名认证入口、PC 订单中心与操作页待迁移。 - 首页、商品列表、商品详情、撞车商品。 - 登录、注册、找回密码、实名认证。 diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 28b7049..64bf580 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -8,6 +8,11 @@ import { StorefrontLayout } from '@/layouts/StorefrontLayout' const ListingListPage = lazy(() => import('@/features/listings/ListingListPage').then(module => ({ default: module.ListingListPage })) ) +const ListingDetailPage = lazy(() => + import('@/features/listings/ListingDetailPage').then(module => ({ + default: module.ListingDetailPage, + })) +) const LoginPage = lazy(() => import('@/features/auth/LoginPage').then(module => ({ default: module.LoginPage })) ) @@ -143,6 +148,7 @@ export function AppRouter() { }> } /> + } /> } /> diff --git a/frontend/src/features/listings/ListingDetailPage.tsx b/frontend/src/features/listings/ListingDetailPage.tsx new file mode 100644 index 0000000..59c131e --- /dev/null +++ b/frontend/src/features/listings/ListingDetailPage.tsx @@ -0,0 +1,426 @@ +import { useState } from 'react' +import { + Alert, + App, + Breadcrumb, + Button, + Card, + Checkbox, + Col, + Descriptions, + Divider, + Image, + Modal, + Row, + Space, + Tag, + Typography, +} from 'antd' +import { + ArrowLeftOutlined, + SafetyCertificateOutlined, + ShoppingOutlined, + WalletOutlined, +} from '@ant-design/icons' +import { Link, useNavigate, useParams } from 'react-router-dom' + +import { BrandLogo } from '@/components/BrandLogo' +import { useListingDetail } from '@/features/listings/api' +import { + createOrder, + useOrderAgreements, + usePostRentalNotice, +} from '@/features/orders/api' +import { useSessionStore } from '@/stores/session' +import type { Listing } from '@/shared/api/types' + +function money(cents: number) { + return `¥${(cents / 100).toFixed(2)}` +} + +function assetValue(value: unknown) { + if (value && typeof value === 'object') { + const record = value as Record + if (typeof record.start === 'string' && typeof record.end === 'string') { + return `${record.start} – ${record.end}` + } + return Object.entries(record) + .map(([key, item]) => `${key}:${String(item)}`) + .join(',') + } + if (Array.isArray(value)) return value.map(String).join('、') + return value == null || value === '' ? '未填写' : String(value) +} + +function galleryImages(item: Listing) { + const images = [item.cover_url, ...(item.screenshot_urls ?? [])].filter(Boolean) + return images.length > 0 ? Array.from(new Set(images)) : [] +} + +export function ListingDetailPage() { + const { id } = useParams() + const navigate = useNavigate() + const { modal } = App.useApp() + const { data, isPending, isError, refetch } = useListingDetail(id) + const user = useSessionStore(state => state.user) + const token = useSessionStore(state => state.token) + const agreements = useOrderAgreements() + const postRentalNotice = usePostRentalNotice() + const [confirmOpen, setConfirmOpen] = useState(false) + const [agreed, setAgreed] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [agreementOpen, setAgreementOpen] = useState(false) + const [agreementTitle, setAgreementTitle] = useState('') + const [agreementContent, setAgreementContent] = useState('') + + const showAgreement = (title: string, content?: string) => { + setAgreementTitle(title) + setAgreementContent(content || '暂无内容') + setAgreementOpen(true) + } + + const openConfirm = (item: Listing) => { + if (!token) { + navigate(`/login?redirect=/listings/${item.id}`) + return + } + if (user?.id === item.owner_id) { + modal.warning({ title: '不能租用自己的商品', content: '请选择其他号主的账号。' }) + return + } + if (item.in_transaction) { + modal.warning({ title: '商品正在租赁中', content: '可以稍后再来看看,或先浏览其他柜选。' }) + return + } + if (user?.realname_status !== 'verified') { + modal.warning({ + title: '需要先完成实名认证', + content: '当前账号未完成实名认证,无法创建租赁订单。请先完成实名认证后再下单。', + }) + return + } + setAgreed(false) + setConfirmOpen(true) + } + + const submitOrder = async () => { + if (!data) return + if (!agreed) { + modal.warning({ title: '请先阅读并同意协议', content: '勾选协议后才能提交订单。' }) + return + } + setSubmitting(true) + try { + const order = await createOrder(data.id) + setConfirmOpen(false) + modal.success({ + title: '订单已创建', + content: '订单已生成,接下来请完成支付;支付成功后才进入交接流程。', + okText: '去支付', + onOk: () => navigate(`/m/orders/${order.id}`), + }) + } catch (error) { + modal.error({ + title: '下单失败', + content: error instanceof Error ? error.message : '请稍后再试。', + }) + } finally { + setSubmitting(false) + } + } + + if (isPending) { + return ( +
+ +
+ ) + } + + if (isError || !data) { + return ( +
+ void refetch()}> + 重试 + + } + /> +
+ ) + } + + const images = galleryImages(data) + const assetEntries = Object.entries(data.asset_summary ?? {}).filter( + ([key]) => key !== 'online_time' + ) + const onlineTime = data.asset_summary?.online_time + const isOwner = user?.id === data.owner_id + + return ( +
+
+ 首页 }, + { title: 柜选 }, + { title: data.listing_no }, + ]} + /> + +
+ + + +
+ {images.length > 0 ? ( + +
+ {data.title} +
+ {images.length > 1 ? ( +
+ {images.slice(0, 6).map(url => ( + {data.title} + ))} +
+ ) : null} +
+ ) : ( +
+ + 暂无商品图片 +
+ )} +
+ + + + +
+ {data.listing_no} + + {data.in_transaction ? '租赁中' : '可租'} + +
+ + {data.title} + + + {data.server_region || '区服待定'} + {data.login_platform || '登录方式待定'} + {data.rank_level ? {data.rank_level} : null} + + +
+ 租金 + {money(data.price_cent)} +
+
+
+ 押金 + {money(data.deposit_amount_cent)} +
+
+ 哈夫币 + {data.haf_coin_amount.toLocaleString()} M +
+
+ 号主 + {data.owner_nickname || `#${data.owner_id}`} +
+
+ + + + +
+ + + +
+
+ +
+ + + + + + {data.description || '号主暂未填写补充说明。'} + + + + + + {assetEntries.map(([key, value]) => ( + + {assetValue(value)} + + ))} + {assetValue(onlineTime)} + + + + +
+ {[ + ['01', '确认下单', '核对租金、押金与账号信息,提交订单。'], + ['02', '完成支付', '支付成功后订单进入待交接。'], + ['03', '号主交接', '号主提交账号与登录说明,租客确认收号。'], + ['04', '使用与归还', '租期内按规则使用,结束时发起结账。'], + ['05', '确认结算', '双方确认或客服仲裁,完成押金与租金结算。'], + ].map(([no, title, desc]) => ( +
+ {no} +
+ {title} +

{desc}

+
+
+ ))} +
+
+ + + + +
+ +
+ 实名与审核 + 发布、下单前均需身份核验。 +
+
+ +
+ +
+ 全程留痕 + 交接、归还、结账都有记录。 +
+
+ +
+ +
+ 账单透明 + 租金、押金、消耗逐项列明。 +
+
+
+ +
+ + { + if (!submitting) setConfirmOpen(false) + }} + maskClosable={!submitting} + footer={ + + + + + } + > + + + {data.title} + + {money(data.price_cent)} + {money(data.deposit_amount_cent)} + + {data.haf_coin_amount.toLocaleString()} M + + + {data.owner_nickname || `#${data.owner_id}`} + + + + setAgreed(event.target.checked)}> + 我已阅读并同意《虚拟资产购买协议》《租客协议》和《租后须知》 + + + + setAgreementOpen(false)}> + 知道了 + + } + onCancel={() => setAgreementOpen(false)} + > +
{agreementContent}
+
+
+ ) +} diff --git a/frontend/src/features/listings/ListingListPage.tsx b/frontend/src/features/listings/ListingListPage.tsx index aa9cec6..4f1e286 100644 --- a/frontend/src/features/listings/ListingListPage.tsx +++ b/frontend/src/features/listings/ListingListPage.tsx @@ -23,6 +23,7 @@ import { import { BrandLogo } from '@/components/BrandLogo' import { useListings } from '@/features/listings/api' import type { Listing } from '@/shared/api/types' +import { Link } from 'react-router-dom' const PAGE_SIZE = 12 @@ -32,33 +33,35 @@ function money(cents: number) { function ListingCard({ item }: { item: Listing }) { return ( - }> -
- {item.listing_no} - - {item.in_transaction ? '租赁中' : '可租'} - -
- - {item.title} - -
- {item.server_region || '区服待定'} - {item.login_platform || '登录方式待定'} - {item.rank_level ? {item.rank_level} : null} -
-
- 哈夫币 {item.haf_coin_amount.toLocaleString()} - {item.owner_nickname || '商行认证号主'} -
-
-
- {money(item.price_cent)} - / 单 + + }> +
+ {item.listing_no} + + {item.in_transaction ? '租赁中' : '可租'} +
- 押金 {money(item.deposit_amount_cent)} -
- + + {item.title} + +
+ {item.server_region || '区服待定'} + {item.login_platform || '登录方式待定'} + {item.rank_level ? {item.rank_level} : null} +
+
+ 哈夫币 {item.haf_coin_amount.toLocaleString()} + {item.owner_nickname || '商行认证号主'} +
+
+
+ {money(item.price_cent)} + / 单 +
+ 押金 {money(item.deposit_amount_cent)} +
+ + ) } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index e6ba25b..75c3c08 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -454,6 +454,291 @@ a { font-size: 12px; } +.listing-card-link { + display: block; + height: 100%; + color: inherit; +} + +.listing-card-link:hover { + color: inherit; +} + +.listing-detail-page { + max-width: 1240px; + margin: 0 auto; + padding: 26px 24px 64px; +} + +.listing-detail-page__crumb { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.listing-detail-page__hero { + margin-top: 18px; +} + +.listing-detail-gallery { + padding: 14px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--paper-warm); +} + +.listing-detail-gallery__main { + aspect-ratio: 16 / 10; + overflow: hidden; + border-radius: 8px; + background: #eef1ea; +} + +.listing-detail-gallery__main .ant-image, +.listing-detail-gallery__main img { + width: 100%; + height: 100%; +} + +.listing-detail-gallery__main img { + object-fit: cover; +} + +.listing-detail-gallery__thumbs { + display: flex; + gap: 8px; + margin-top: 10px; + overflow-x: auto; +} + +.listing-detail-gallery__thumbs .ant-image { + flex: 0 0 auto; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 6px; +} + +.listing-detail-gallery__thumbs img { + object-fit: cover; +} + +.listing-detail-gallery__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + min-height: 360px; + border-radius: 8px; + background: var(--paper); + color: var(--brand-ink); + letter-spacing: 0.12em; +} + +.listing-buy-card { + position: sticky; + top: 84px; + border-color: var(--line); + background: var(--paper-warm); +} + +.listing-buy-card__top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.listing-buy-card__title { + margin: 12px 0 10px !important; + color: var(--brand-ink) !important; + font-size: 26px !important; + line-height: 1.35 !important; +} + +.listing-buy-card__price { + display: flex; + align-items: baseline; + gap: 10px; + margin-top: 18px; + padding: 14px 16px; + border-radius: 8px; + background: var(--paper); +} + +.listing-buy-card__price span { + color: var(--ink-muted); + font-size: 12px; +} + +.listing-buy-card__price strong { + color: var(--brand-seal); + font-family: 'Inter', 'DIN Alternate', sans-serif; + font-size: 32px; +} + +.listing-buy-card__meta { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin: 16px 0; +} + +.listing-buy-card__meta div { + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 6px; + background: #fff; +} + +.listing-buy-card__meta span { + color: var(--ink-muted); + font-size: 11px; +} + +.listing-buy-card__meta strong { + color: var(--brand-ink); + font-family: 'Inter', monospace; + font-size: 13px; +} + +.listing-buy-card__notice { + margin-bottom: 16px; +} + +.listing-buy-card__links { + display: flex; + flex-wrap: wrap; + gap: 6px 12px; + margin-top: 12px; +} + +.listing-buy-card__links button { + padding: 0; + border: 0; + background: none; + color: var(--brand-seal); + font-size: 11px; + cursor: pointer; +} + +.listing-buy-card__links button:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.listing-detail-page__body { + margin-top: 24px; +} + +.listing-detail-panel { + margin-bottom: 18px; + border-color: var(--line); + background: var(--paper-warm); +} + +.listing-detail-description { + margin-bottom: 0 !important; + color: var(--ink-secondary); + line-height: 1.85; + white-space: pre-wrap; +} + +.listing-process { + display: flex; + flex-direction: column; +} + +.listing-process__step { + display: flex; + gap: 16px; + padding: 14px 0; + border-bottom: 1px dashed var(--line); +} + +.listing-process__step:last-child { + border-bottom: 0; +} + +.listing-process__step > span { + color: var(--brand-seal); + font-family: 'Inter', monospace; + font-size: 12px; +} + +.listing-process__step strong { + color: var(--brand-ink); + font-size: 14px; +} + +.listing-process__step p { + margin: 6px 0 0; + color: var(--ink-muted); + font-size: 12px; +} + +.listing-trust-card { + border-color: var(--line); + background: var(--paper-warm); +} + +.listing-trust-card__item { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.listing-trust-card__item > .anticon { + margin-top: 2px; + color: var(--brand-seal); + font-size: 18px; +} + +.listing-trust-card__item div { + display: flex; + flex-direction: column; + gap: 4px; +} + +.listing-trust-card__item strong { + color: var(--brand-ink); + font-size: 13px; +} + +.listing-trust-card__item span { + color: var(--ink-muted); + font-size: 11px; + line-height: 1.6; +} + +.listing-agreement-content { + max-height: 62vh; + overflow-y: auto; + color: var(--ink-secondary); + font-size: 12px; + line-height: 1.8; + white-space: pre-wrap; + word-break: break-word; +} + +@media (max-width: 991px) { + .listing-detail-page { + padding: 20px 16px 48px; + } + + .listing-buy-card { + position: static; + } + + .listing-buy-card__meta { + grid-template-columns: 1fr; + } +} + .empty-panel { display: flex; justify-content: center;