feat(mobile): 新增解忧商行移动端首页、详情与个人中心
This commit is contained in:
@@ -4,9 +4,10 @@
|
||||
|
||||
## 当前状态
|
||||
|
||||
**Phase 0 已完成,Phase 1 基础切片已完成。** 后端已复制并跑通空库迁移、构建、健康检查与全量测试;前端已完成 SVG 品牌资产、用户首页/登录/商品列表、管理员验证码登录、强制改密、浅色后台 Shell 与真实仪表盘。
|
||||
**Phase 0 已完成,Phase 1 基础切片已完成。** 后端已复制并跑通空库迁移、构建、健康检查与全量测试;前端已完成 SVG 品牌资产、用户首页/登录/商品列表、移动端 `/m` 首页/详情/个人中心、管理员验证码登录、强制改密、浅色后台 Shell 与真实仪表盘。
|
||||
|
||||
- 前端:`http://localhost:5289/`
|
||||
- 移动端:`http://localhost:5289/m`
|
||||
- 后台:`http://localhost:5289/console-jy8k3m/login`
|
||||
- 后端:`http://localhost:18089/`
|
||||
- 健康检查:`http://localhost:18089/health/ready`
|
||||
|
||||
+25
-6
@@ -89,9 +89,22 @@
|
||||
`AppRouter` 已改为 `React.lazy + Suspense`:
|
||||
|
||||
- 首页商品列表、用户登录、后台登录、后台改密、后台仪表盘、后台 Shell 分别按路由加载。
|
||||
- 移动端 Layout、首页、详情、个人中心独立分包。
|
||||
- 后台代码不会在用户首页首屏直接执行。
|
||||
- 已移除会提前触发后台模块加载的顶层 `import()`。
|
||||
|
||||
### 1.8 移动端 `/m` 基础切片
|
||||
|
||||
已引入 `antd-mobile` + `antd-mobile-icons`:
|
||||
|
||||
- `MobileLayout`:品牌顶栏、底部 TabBar(首页 / 订单 / 消息 / 我的)、详情页自动隐藏 TabBar。
|
||||
- `MobileHomePage`:深绿品牌首屏、搜索、快捷筛选、PullToRefresh、商品卡列表、空/错误状态。
|
||||
- `MobileListingDetailPage`:商品图、价格/押金/哈夫币、账号说明、资产摘要、在线时间、固定底部操作栏。
|
||||
- `MobileProfilePage`:登录状态、实名状态、订单/消息入口、退出登录。
|
||||
- `/m/orders`、`/m/messages` 先用统一占位页,下一批迁移真实业务。
|
||||
- 移动端 CSS 变量已接入解忧商行品牌色,不使用 Vant/旧站视觉。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. 验证结果
|
||||
@@ -126,6 +139,10 @@ listings = ok,返回 1 件联调商品
|
||||
```text
|
||||
/console-jy8k3m/login HTTP 200
|
||||
/favicon.svg HTTP 200
|
||||
/m HTTP 200
|
||||
/m/listings/1 HTTP 200
|
||||
/m/login HTTP 200
|
||||
/api/listings/1 code=ok,返回联调商品详情
|
||||
```
|
||||
|
||||
---
|
||||
@@ -149,9 +166,11 @@ listings = ok,返回 1 件联调商品
|
||||
当前生产构建:
|
||||
|
||||
```text
|
||||
dist/assets/index-*.js 884.51 kB │ gzip 288.64 kB
|
||||
dist/assets/AdminLayout-*.js 36.74 kB │ gzip 12.98 kB
|
||||
dist/assets/AdminDashboardPage-*.js 179.67 kB │ gzip 58.44 kB
|
||||
dist/assets/index-*.js 886.81 kB │ gzip 289.33 kB
|
||||
dist/assets/AdminLayout-*.js 36.75 kB │ gzip 12.98 kB
|
||||
dist/assets/AdminDashboardPage-*.js 179.71 kB │ gzip 58.46 kB
|
||||
dist/assets/MobileHomePage-*.js 36.96 kB │ gzip 13.37 kB
|
||||
dist/assets/MobileListingDetailPage-*.js 27.31 kB │ gzip 10.70 kB
|
||||
```
|
||||
|
||||
**问题:** 首屏主包仍偏大,超过 Phase 1 目标。
|
||||
@@ -162,19 +181,19 @@ dist/assets/AdminDashboardPage-*.js 179.67 kB │ gzip 58.44 kB
|
||||
|
||||
## 5. 已知限制
|
||||
|
||||
1. 移动端 `/m` 尚未开始迁移。
|
||||
1. 移动端订单与消息页仍是占位,下单、客服会话尚未接入。
|
||||
2. 后台 43 个页面目前只有仪表盘是真实页面,其余为统一占位。
|
||||
3. 分销后台尚未做品牌轻改。
|
||||
4. 后台菜单权限码基于旧项目推断,后续需与后端权限表逐一核对。
|
||||
5. SVG 中文尚未转曲。
|
||||
6. 首屏包体仍偏大。
|
||||
7. 本批次尚未提交 Git。
|
||||
7. 本移动端批次尚未提交 Git。
|
||||
|
||||
---
|
||||
|
||||
## 6. 建议下一批次
|
||||
|
||||
1. 移动端 `/m` 首页 + 商品列表 + 底部导航。
|
||||
1. 移动端订单列表、订单详情、消息与聊天。
|
||||
2. 后台列表页原型:DataTable、FilterBar、详情 Drawer、批量操作。
|
||||
3. 先迁移后台订单列表与商品审核列表。
|
||||
4. 分销后台主题轻改。
|
||||
|
||||
+1
-1
@@ -352,7 +352,7 @@ hfb_jysh/
|
||||
|
||||
### Phase 1:品牌与设计系统(1 周)
|
||||
|
||||
> 状态:**基础切片已完成(2026-10-30)**,验收见 [`Phase1-验收报告.md`](./Phase1-验收报告.md)。已完成 SVG 品牌资产、管理员会话、后台登录/强制改密/Shell/仪表盘与路由懒加载;移动端与后台列表页原型继续。
|
||||
> 状态:**基础切片已完成(2026-10-30)**,验收见 [`Phase1-验收报告.md`](./Phase1-验收报告.md)。已完成 SVG 品牌资产、管理员会话、后台登录/强制改密/Shell/仪表盘、路由懒加载与移动端 `/m` 首页/详情/个人中心;后台列表页原型与移动订单/消息继续。
|
||||
|
||||
- 重绘 logo SVG:横版、方形印章、单色、反白。
|
||||
- 落地 Design Token:颜色、字体、间距、圆角、阴影、动效。
|
||||
|
||||
Generated
+303
@@ -11,6 +11,8 @@
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"antd": "^5.27.6",
|
||||
"antd-mobile": "^5.43.0",
|
||||
"antd-mobile-icons": "^0.3.0",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"react": "^19.1.1",
|
||||
@@ -865,6 +867,31 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.8.0",
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -1108,6 +1135,18 @@
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/rafz": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
|
||||
"integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-spring/types": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
|
||||
"integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
@@ -1618,6 +1657,24 @@
|
||||
"@types/react": "^19.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@use-gesture/core": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.0.tgz",
|
||||
"integrity": "sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@use-gesture/react": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.0.tgz",
|
||||
"integrity": "sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@use-gesture/core": "10.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
|
||||
@@ -1651,6 +1708,26 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ahooks": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.11.0.tgz",
|
||||
"integrity": "sha512-mQML7DrxZo7xvxm145NTMowE9m75tflSayKjlwEMzwQSRFguweNCdCSIq88aQ4RRfVMugBrH8AtGresJwqMy5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0",
|
||||
"dayjs": "^1.9.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"screenfull": "^5.0.0",
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd": {
|
||||
"version": "5.29.3",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz",
|
||||
@@ -1716,6 +1793,160 @@
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd-mobile": {
|
||||
"version": "5.43.0",
|
||||
"resolved": "https://registry.npmjs.org/antd-mobile/-/antd-mobile-5.43.0.tgz",
|
||||
"integrity": "sha512-Pe8P/hHmv1vDHzbGuT6C23SJPeiH7ryESi001eiNqEm9WzEgyKhV60T69Mt26fUvkF7k6Wd75cMcdDziGYeJbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.4.2",
|
||||
"@rc-component/mini-decimal": "^1.1.0",
|
||||
"@react-spring/web": "~9.6.1",
|
||||
"@use-gesture/react": "10.3.0",
|
||||
"ahooks": "^3.7.6",
|
||||
"antd-mobile-icons": "^0.3.0",
|
||||
"antd-mobile-v5-count": "^1.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"deepmerge": "^4.3.1",
|
||||
"nano-memoize": "^3.0.16",
|
||||
"rc-field-form": "^1.34.2",
|
||||
"rc-segmented": "~2.4.1",
|
||||
"rc-util": "^5.44.4",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"react-is": "^18.2.0",
|
||||
"runes2": "^1.1.2",
|
||||
"staged-components": "^1.1.3",
|
||||
"tslib": "^2.5.0",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd-mobile-icons": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/antd-mobile-icons/-/antd-mobile-icons-0.3.0.tgz",
|
||||
"integrity": "sha512-rqINQpJWZWrva9moCd1Ye695MZYWmqLPE+bY8d2xLRy7iSQwPsinCdZYjpUPp2zL/LnKYSyXxP2ut2A+DC+whQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/antd-mobile-v5-count": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/antd-mobile-v5-count/-/antd-mobile-v5-count-1.0.1.tgz",
|
||||
"integrity": "sha512-YGsiEDCPUDz3SzfXi6gLZn/HpeSMW+jgPc4qiYUr1fSopg3hkUie2TnooJdExgfiETHefH3Ggs58He0OVfegLA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/antd-mobile/node_modules/@react-spring/web": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.6.1.tgz",
|
||||
"integrity": "sha512-X2zR6q2Z+FjsWfGAmAXlQaoUHbPmfuCaXpuM6TcwXPpLE1ZD4A1eys/wpXboFQmDkjnrlTmKvpVna1MjWpZ5Hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.6.1",
|
||||
"@react-spring/core": "~9.6.1",
|
||||
"@react-spring/shared": "~9.6.1",
|
||||
"@react-spring/types": "~9.6.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd-mobile/node_modules/@react-spring/web/node_modules/@react-spring/animated": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
|
||||
"integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/shared": "~9.6.1",
|
||||
"@react-spring/types": "~9.6.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd-mobile/node_modules/@react-spring/web/node_modules/@react-spring/core": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
|
||||
"integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.6.1",
|
||||
"@react-spring/rafz": "~9.6.1",
|
||||
"@react-spring/shared": "~9.6.1",
|
||||
"@react-spring/types": "~9.6.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-spring/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd-mobile/node_modules/@react-spring/web/node_modules/@react-spring/shared": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
|
||||
"integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-spring/rafz": "~9.6.1",
|
||||
"@react-spring/types": "~9.6.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd-mobile/node_modules/rc-field-form": {
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.44.0.tgz",
|
||||
"integrity": "sha512-el7w87fyDUsca63Y/s8qJcq9kNkf/J5h+iTdqG5WsSHLH0e6Usl7QuYSmSVzJMgtp40mOVZIY/W/QP9zwrp1FA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"async-validator": "^4.1.0",
|
||||
"rc-util": "^5.32.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd-mobile/node_modules/rc-segmented": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.4.1.tgz",
|
||||
"integrity": "sha512-KUi+JJFdKnumV9iXlm+BJ00O4NdVBp2TEexLCk6bK1x/RH83TvYKQMzIz/7m3UTRPD08RM/8VG/JNjWgWbd4cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.1",
|
||||
"classnames": "^2.2.1",
|
||||
"rc-motion": "^2.4.4",
|
||||
"rc-util": "^5.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd-mobile/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/antd-mobile/node_modules/staged-components": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/staged-components/-/staged-components-1.1.3.tgz",
|
||||
"integrity": "sha512-9EIswzDqjwlEu+ymkV09TTlJfzSbKgEnNteUnZSTxkpMgr5Wx2CzzA9WcMFWBNCldqVPsHVnRGGrApduq2Se5A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd/node_modules/@ant-design/colors": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz",
|
||||
@@ -1757,6 +1988,12 @@
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -1947,6 +2184,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -2269,6 +2515,12 @@
|
||||
"integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
|
||||
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -2311,6 +2563,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -2357,6 +2615,12 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nano-memoize": {
|
||||
"version": "3.0.16",
|
||||
"resolved": "https://registry.npmjs.org/nano-memoize/-/nano-memoize-3.0.16.tgz",
|
||||
"integrity": "sha512-JyK96AKVGAwVeMj3MoMhaSXaUNqgMbCRSQB3trUV8tYZfWEzqUBKdK1qJpfuNXgKeHOx1jv/IEYTM659ly7zUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.20",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
|
||||
@@ -3077,6 +3341,12 @@
|
||||
"react": "^19.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.1.tgz",
|
||||
@@ -3183,12 +3453,30 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/runes2": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.10.tgz",
|
||||
"integrity": "sha512-4Y1en7X+3i0pf9NDPMEerT1CkZlExpQJPnEPlrBXzXSIHO3etyZR6Wbpl+hHWVYRFnhPVCyPHaJks6GcCFgEiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
|
||||
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/screenfull": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
|
||||
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
|
||||
@@ -3268,6 +3556,12 @@
|
||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -3320,6 +3614,15 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz",
|
||||
"integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"antd": "^5.27.6",
|
||||
"antd-mobile": "^5.43.0",
|
||||
"antd-mobile-icons": "^0.3.0",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"react": "^19.1.1",
|
||||
|
||||
@@ -11,6 +11,27 @@ const ListingListPage = lazy(() =>
|
||||
const LoginPage = lazy(() =>
|
||||
import('@/features/auth/LoginPage').then(module => ({ default: module.LoginPage }))
|
||||
)
|
||||
const MobileLayout = lazy(() =>
|
||||
import('@/layouts/MobileLayout').then(module => ({ default: module.MobileLayout }))
|
||||
)
|
||||
const MobileHomePage = lazy(() =>
|
||||
import('@/features/mobile/MobileHomePage').then(module => ({ default: module.MobileHomePage }))
|
||||
)
|
||||
const MobileListingDetailPage = lazy(() =>
|
||||
import('@/features/mobile/MobileListingDetailPage').then(module => ({
|
||||
default: module.MobileListingDetailPage,
|
||||
}))
|
||||
)
|
||||
const MobileProfilePage = lazy(() =>
|
||||
import('@/features/mobile/MobileProfilePage').then(module => ({
|
||||
default: module.MobileProfilePage,
|
||||
}))
|
||||
)
|
||||
const MobilePlaceholderPage = lazy(() =>
|
||||
import('@/features/mobile/MobilePlaceholderPage').then(module => ({
|
||||
default: module.MobilePlaceholderPage,
|
||||
}))
|
||||
)
|
||||
const AdminLoginPage = lazy(() =>
|
||||
import('@/features/admin/AdminLoginPage').then(module => ({ default: module.AdminLoginPage }))
|
||||
)
|
||||
@@ -60,6 +81,32 @@ export function AppRouter() {
|
||||
</Route>
|
||||
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/m/login" element={<LoginPage />} />
|
||||
|
||||
<Route path="/m" element={<MobileLayout />}>
|
||||
<Route index element={<MobileHomePage />} />
|
||||
<Route path="listings" element={<MobileHomePage />} />
|
||||
<Route path="listings/:id" element={<MobileListingDetailPage />} />
|
||||
<Route
|
||||
path="orders"
|
||||
element={
|
||||
<MobilePlaceholderPage
|
||||
title="订单中心"
|
||||
description="订单列表、交接、归还和结账会在下一批迁移到这里。"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="messages"
|
||||
element={
|
||||
<MobilePlaceholderPage
|
||||
title="消息中心"
|
||||
description="订单群聊、客服会话和系统通知会在下一批迁移到这里。"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="profile" element={<MobileProfilePage />} />
|
||||
</Route>
|
||||
|
||||
<Route path={`${adminBase}/login`} element={<AdminLoginPage />} />
|
||||
<Route path={adminBase} element={<AdminGuard />}>
|
||||
|
||||
@@ -29,7 +29,8 @@ export function LoginPage() {
|
||||
await login(values.phone.trim(), values.password)
|
||||
message.success('登录成功')
|
||||
const redirect = searchParams.get('redirect')
|
||||
navigate(redirect && redirect.startsWith('/') ? redirect : '/', { replace: true })
|
||||
const fallbackPath = window.location.pathname.startsWith('/m') ? '/m' : '/'
|
||||
navigate(redirect && redirect.startsWith('/') ? redirect : fallbackPath, { replace: true })
|
||||
} catch (err) {
|
||||
const text =
|
||||
err instanceof Error && err.message ? err.message : '登录没有完成,请核对手机号和密码'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { apiClient, unwrapData } from '@/shared/api/client'
|
||||
import type { ApiResponse, PublicListingPage } from '@/shared/api/types'
|
||||
import type { ApiResponse, Listing, PublicListingPage } from '@/shared/api/types'
|
||||
|
||||
export interface ListingQuery {
|
||||
page?: number
|
||||
@@ -30,3 +30,15 @@ export function useListings(query: ListingQuery) {
|
||||
queryFn: () => fetchListings(query),
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchListingDetail(id: number | string) {
|
||||
return unwrapData<Listing>(apiClient.get<ApiResponse<Listing>>(`/listings/${id}`))
|
||||
}
|
||||
|
||||
export function useListingDetail(id?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['listing-detail', id],
|
||||
queryFn: () => fetchListingDetail(id as string),
|
||||
enabled: Boolean(id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Empty, ErrorBlock, PullToRefresh, SearchBar, Skeleton, Tag } from 'antd-mobile'
|
||||
import { RightOutline, SearchOutline } from 'antd-mobile-icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { useListings } from '@/features/listings/api'
|
||||
import type { Listing } from '@/shared/api/types'
|
||||
|
||||
const quickFilters = [
|
||||
{ label: '全部', keyword: '' },
|
||||
{ label: '国服', keyword: '国服' },
|
||||
{ label: '高资产', keyword: '高资产' },
|
||||
{ label: '可租', keyword: '' },
|
||||
]
|
||||
|
||||
function money(cents: number) {
|
||||
return `¥${(cents / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function MobileListingCard({ item }: { item: Listing }) {
|
||||
return (
|
||||
<Link to={`/m/listings/${item.id}`} className="mobile-listing-card">
|
||||
<div className="mobile-listing-card__cover">
|
||||
{item.cover_url ? (
|
||||
<img src={item.cover_url} alt={item.title} loading="lazy" />
|
||||
) : (
|
||||
<BrandLogo height={38} variant="seal" />
|
||||
)}
|
||||
</div>
|
||||
<div className="mobile-listing-card__body">
|
||||
<div className="mobile-listing-card__top">
|
||||
<span className="mobile-listing-card__no">{item.listing_no}</span>
|
||||
<Tag color={item.in_transaction ? 'default' : 'success'} fill="outline">
|
||||
{item.in_transaction ? '租赁中' : '可租'}
|
||||
</Tag>
|
||||
</div>
|
||||
<h3>{item.title}</h3>
|
||||
<div className="mobile-listing-card__tags">
|
||||
<span>{item.server_region || '区服待定'}</span>
|
||||
<span>{item.rank_level || '段位待定'}</span>
|
||||
<span>哈夫币 {item.haf_coin_amount.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="mobile-listing-card__bottom">
|
||||
<div>
|
||||
<strong>{money(item.price_cent)}</strong>
|
||||
<small>押金 {money(item.deposit_amount_cent)}</small>
|
||||
</div>
|
||||
<span className="mobile-listing-card__more">
|
||||
查看 <RightOutline />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileHomePage() {
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const { data, isPending, isError, refetch } = useListings({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
keyword,
|
||||
})
|
||||
|
||||
const applyFilter = (value: string) => {
|
||||
setKeyword(value)
|
||||
setSearchValue(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mobile-home">
|
||||
<section className="mobile-hero">
|
||||
<div className="mobile-hero__eyebrow">JIEYOU TRADING HOUSE</div>
|
||||
<h1>
|
||||
租得明白,
|
||||
<br />
|
||||
还得安心。
|
||||
</h1>
|
||||
<p>实名审核 · 流程留痕 · 账单逐项列明</p>
|
||||
<SearchBar
|
||||
className="mobile-search"
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onSearch={value => setKeyword(value.trim())}
|
||||
placeholder="搜索区服、段位、装备或商品号"
|
||||
searchIcon={<SearchOutline />}
|
||||
/>
|
||||
<div className="mobile-chips">
|
||||
{quickFilters.map(item => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.label}
|
||||
className={keyword === item.keyword && item.label !== '全部' ? 'is-active' : ''}
|
||||
onClick={() => applyFilter(item.keyword)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-trust">
|
||||
<span>实名审核</span>
|
||||
<span>交接留痕</span>
|
||||
<span>争议可申诉</span>
|
||||
</section>
|
||||
|
||||
<section className="mobile-list-section">
|
||||
<div className="mobile-section-head">
|
||||
<div>
|
||||
<span className="mobile-eyebrow">CURATED ACCOUNTS</span>
|
||||
<h2>本周柜选</h2>
|
||||
</div>
|
||||
<span className="mobile-count">共 {data?.total ?? 0} 件</span>
|
||||
</div>
|
||||
|
||||
{isError ? (
|
||||
<div className="mobile-error">
|
||||
<ErrorBlock
|
||||
status="disconnected"
|
||||
title="商品没有加载出来"
|
||||
description="后端服务可能暂时不可用,你的账号和订单不受影响。"
|
||||
/>
|
||||
<Button color="primary" size="small" onClick={() => void refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<PullToRefresh onRefresh={() => refetch().then(() => undefined)}>
|
||||
{isPending ? (
|
||||
<div className="mobile-skeleton-list">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div className="mobile-skeleton-card" key={index}>
|
||||
<Skeleton animated className="mobile-skeleton-cover" />
|
||||
<Skeleton.Paragraph lineCount={3} animated />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : data?.items.length ? (
|
||||
<div className="mobile-listing-list">
|
||||
{data.items.map(item => (
|
||||
<MobileListingCard item={item} key={item.id} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mobile-empty">
|
||||
<Empty description="还没有符合条件的商品" />
|
||||
<Button size="small" color="primary" onClick={() => applyFilter('')}>
|
||||
查看全部
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PullToRefresh>
|
||||
</section>
|
||||
|
||||
<section className="mobile-rules">
|
||||
<span className="mobile-eyebrow">RULES, IN PLAIN WORDS</span>
|
||||
<h2>先把规则说明白</h2>
|
||||
<p>押金、租金、额外消耗和退款规则都会在订单里逐项列出。结账备注不参与系统结算。</p>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { Button, ErrorBlock, Skeleton, Tag, Toast } from 'antd-mobile'
|
||||
import { LeftOutline, PhoneFill } from 'antd-mobile-icons'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { useListingDetail } from '@/features/listings/api'
|
||||
|
||||
function money(cents: number) {
|
||||
return `¥${(cents / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function assetValue(value: unknown) {
|
||||
if (value && typeof value === 'object') {
|
||||
const record = value as Record<string, unknown>
|
||||
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)
|
||||
}
|
||||
|
||||
export function MobileListingDetailPage() {
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams()
|
||||
const { data, isPending, isError, refetch } = useListingDetail(id)
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="mobile-detail mobile-detail--loading">
|
||||
<Skeleton animated className="mobile-detail__cover-skeleton" />
|
||||
<Skeleton.Paragraph lineCount={6} animated />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<div className="mobile-detail mobile-detail--error">
|
||||
<ErrorBlock status="disconnected" title="商品没有加载出来" description="请返回列表后重试。" />
|
||||
<Button color="primary" onClick={() => void refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const onlineTime =
|
||||
data.asset_summary && typeof data.asset_summary === 'object'
|
||||
? (data.asset_summary.online_time as unknown)
|
||||
: undefined
|
||||
const assetEntries = Object.entries(data.asset_summary || {}).filter(
|
||||
([key]) => key !== 'online_time'
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mobile-detail">
|
||||
<button type="button" className="mobile-detail__back" onClick={() => navigate(-1)}>
|
||||
<LeftOutline /> 返回
|
||||
</button>
|
||||
|
||||
<div className="mobile-detail__cover">
|
||||
{data.cover_url ? (
|
||||
<img src={data.cover_url} alt={data.title} />
|
||||
) : (
|
||||
<BrandLogo height={52} variant="seal" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section className="mobile-detail__head">
|
||||
<div className="mobile-detail__no">{data.listing_no}</div>
|
||||
<h1>{data.title}</h1>
|
||||
<div className="mobile-detail__tags">
|
||||
<Tag color={data.in_transaction ? 'default' : 'success'} fill="outline">
|
||||
{data.in_transaction ? '租赁中' : '可租'}
|
||||
</Tag>
|
||||
<Tag fill="outline">{data.server_region || '区服待定'}</Tag>
|
||||
<Tag fill="outline">{data.login_platform || '登录方式待定'}</Tag>
|
||||
<Tag fill="outline">{data.rank_level || '段位待定'}</Tag>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-detail__price-card">
|
||||
<div>
|
||||
<span>租金</span>
|
||||
<strong>{money(data.price_cent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>押金</span>
|
||||
<strong>{money(data.deposit_amount_cent)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>哈夫币</span>
|
||||
<strong>{data.haf_coin_amount.toLocaleString()}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-detail__section">
|
||||
<h2>账号说明</h2>
|
||||
<p>{data.description || '号主暂未填写补充说明。'}</p>
|
||||
</section>
|
||||
|
||||
<section className="mobile-detail__section">
|
||||
<h2>资产摘要</h2>
|
||||
<dl className="mobile-detail__assets">
|
||||
{assetEntries.length ? (
|
||||
assetEntries.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<dt>{key}</dt>
|
||||
<dd>{assetValue(value)}</dd>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>
|
||||
<dt>资产</dt>
|
||||
<dd>未填写</dd>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<dt>在线时间</dt>
|
||||
<dd>{assetValue(onlineTime)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="mobile-detail__notice">
|
||||
<strong>先看清规则,再决定要不要租</strong>
|
||||
<p>押金、租金、额外消耗和退款规则会在下单页逐项列出;交接、归还、结账全程留痕。</p>
|
||||
</section>
|
||||
|
||||
<div className="mobile-detail__actions">
|
||||
<Button
|
||||
block
|
||||
onClick={() => {
|
||||
Toast.show({ content: '客服接入将在下一批完成' })
|
||||
}}
|
||||
>
|
||||
<PhoneFill /> 联系客服
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
Toast.show({ content: '下单流程将在下一批接入' })
|
||||
}}
|
||||
>
|
||||
确认租赁
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Button } from 'antd-mobile'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
|
||||
export function MobilePlaceholderPage({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="mobile-placeholder">
|
||||
<BrandLogo height={50} variant="seal" />
|
||||
<span className="mobile-eyebrow">PHASE 1 · COMING NEXT</span>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
<Button color="primary" onClick={() => navigate('/m')}>
|
||||
回柜选看看
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Button, List } from 'antd-mobile'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
export function MobileProfilePage() {
|
||||
const navigate = useNavigate()
|
||||
const user = useSessionStore(state => state.user)
|
||||
const logout = useSessionStore(state => state.logout)
|
||||
|
||||
return (
|
||||
<div className="mobile-profile">
|
||||
<section className="mobile-profile__head">
|
||||
<BrandLogo height={42} variant="seal" />
|
||||
<div>
|
||||
<h1>{user ? user.nickname || '我的商行' : '还没登录'}</h1>
|
||||
<p>{user ? user.phone : '登录后查看订单、钱包与实名状态'}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<List className="mobile-profile__list">
|
||||
<List.Item extra={user ? '已登录' : '未登录'}>账号状态</List.Item>
|
||||
<List.Item extra={user?.realname_status === 'verified' ? '已实名' : '未实名'}>
|
||||
实名认证
|
||||
</List.Item>
|
||||
<List.Item onClick={() => navigate('/m/orders')}>我的订单</List.Item>
|
||||
<List.Item onClick={() => navigate('/m/messages')}>消息中心</List.Item>
|
||||
</List>
|
||||
|
||||
<div className="mobile-profile__actions">
|
||||
{user ? (
|
||||
<Button
|
||||
block
|
||||
onClick={async () => {
|
||||
await logout()
|
||||
navigate('/m')
|
||||
}}
|
||||
>
|
||||
退出登录
|
||||
</Button>
|
||||
) : (
|
||||
<Button block color="primary" onClick={() => navigate('/m/login')}>
|
||||
登录 / 注册
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'antd-mobile/es/global'
|
||||
|
||||
import { TabBar } from 'antd-mobile'
|
||||
import { AppOutline, MessageOutline, UnorderedListOutline, UserOutline } from 'antd-mobile-icons'
|
||||
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const tabs = [
|
||||
{ key: '/m', title: '首页', icon: <AppOutline /> },
|
||||
{ key: '/m/orders', title: '订单', icon: <UnorderedListOutline /> },
|
||||
{ key: '/m/messages', title: '消息', icon: <MessageOutline /> },
|
||||
{ key: '/m/profile', title: '我的', icon: <UserOutline /> },
|
||||
]
|
||||
|
||||
function activeTab(pathname: string) {
|
||||
if (pathname.startsWith('/m/orders') || pathname.startsWith('/m/chats')) return '/m/orders'
|
||||
if (pathname.startsWith('/m/messages')) return '/m/messages'
|
||||
if (pathname.startsWith('/m/profile')) return '/m/profile'
|
||||
return '/m'
|
||||
}
|
||||
|
||||
export function MobileLayout() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const user = useSessionStore(state => state.user)
|
||||
const hideTabbar = /^\/m\/listings\/[^/]+$/.test(location.pathname)
|
||||
|
||||
return (
|
||||
<div className="mobile-shell">
|
||||
<header className="mobile-header">
|
||||
<Link to="/m" className="mobile-header__brand" aria-label="解忧商行移动端首页">
|
||||
<BrandLogo height={30} />
|
||||
</Link>
|
||||
<div className="mobile-header__right">
|
||||
{user ? (
|
||||
<Link to="/m/profile" className="mobile-header__user">
|
||||
{user.nickname || '我的'}
|
||||
</Link>
|
||||
) : (
|
||||
<Link to="/m/login" className="mobile-header__login">
|
||||
登录
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mobile-content">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<div className="mobile-tabbar" hidden={hideTabbar}>
|
||||
<TabBar
|
||||
activeKey={activeTab(location.pathname)}
|
||||
onChange={key => navigate(key)}
|
||||
safeArea
|
||||
>
|
||||
{tabs.map(tab => (
|
||||
<TabBar.Item key={tab.key} icon={tab.icon} title={tab.title} />
|
||||
))}
|
||||
</TabBar>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1317,3 +1317,644 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile storefront */
|
||||
.mobile-shell {
|
||||
--adm-color-primary: #1e4d42;
|
||||
--adm-color-success: #2f7d5c;
|
||||
--adm-color-warning: #c17a2b;
|
||||
--adm-color-danger: #a64332;
|
||||
--adm-font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding-bottom: calc(72px + env(safe-area-inset-bottom));
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 58px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(255, 253, 248, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.mobile-header__brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-header__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mobile-header__login,
|
||||
.mobile-header__user {
|
||||
padding: 7px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
color: var(--brand-ink);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-header__login {
|
||||
border-color: var(--brand-ink);
|
||||
background: var(--brand-ink);
|
||||
color: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-content {
|
||||
min-height: calc(100vh - 130px);
|
||||
}
|
||||
|
||||
.mobile-tabbar {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
border-top: 1px solid var(--line);
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-tabbar[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-tabbar .adm-tab-bar-item {
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.mobile-tabbar .adm-tab-bar-item-active {
|
||||
color: var(--brand-ink);
|
||||
}
|
||||
|
||||
.mobile-tabbar .adm-tab-bar-item-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mobile-home {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.mobile-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 28px 18px 24px;
|
||||
background: var(--brand-ink);
|
||||
color: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-hero::after {
|
||||
position: absolute;
|
||||
right: -70px;
|
||||
bottom: -100px;
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
border: 1px solid rgba(236, 208, 177, 0.22);
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.mobile-hero__eyebrow {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--brand-seal-line);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
.mobile-hero h1 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 12px 0 10px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.24;
|
||||
}
|
||||
|
||||
.mobile-hero p {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0 0 18px;
|
||||
color: #d8e2dd;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.mobile-search {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
--background: #fffdf8;
|
||||
--border-radius: 6px;
|
||||
--height: 42px;
|
||||
--padding-left: 12px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.mobile-chips {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.mobile-chips::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-chips button {
|
||||
flex: 0 0 auto;
|
||||
padding: 7px 13px;
|
||||
border: 1px solid rgba(236, 208, 177, 0.38);
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: #d8e2dd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-chips button.is-active {
|
||||
border-color: var(--brand-seal-line);
|
||||
background: var(--brand-seal-line);
|
||||
color: var(--brand-ink);
|
||||
}
|
||||
|
||||
.mobile-trust {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-trust span {
|
||||
padding: 12px 4px;
|
||||
border-right: 1px solid var(--line);
|
||||
color: var(--ink-secondary);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mobile-trust span:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.mobile-list-section {
|
||||
padding: 24px 16px 8px;
|
||||
}
|
||||
|
||||
.mobile-section-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.mobile-eyebrow {
|
||||
display: block;
|
||||
color: var(--brand-seal);
|
||||
font-size: 9px !important;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
.mobile-section-head h2,
|
||||
.mobile-rules h2,
|
||||
.mobile-placeholder h1,
|
||||
.mobile-profile h1 {
|
||||
margin: 5px 0 0;
|
||||
color: var(--brand-ink);
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.mobile-count {
|
||||
color: var(--ink-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mobile-listing-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-listing-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-listing-card__cover {
|
||||
display: flex;
|
||||
flex: 0 0 108px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
background: #eef1ea;
|
||||
}
|
||||
|
||||
.mobile-listing-card__cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.mobile-listing-card__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-listing-card__top,
|
||||
.mobile-listing-card__bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-listing-card__no {
|
||||
color: var(--ink-muted);
|
||||
font-family: 'Inter', monospace;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mobile-listing-card__body h3 {
|
||||
display: -webkit-box;
|
||||
margin: 7px 0 6px;
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.mobile-listing-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mobile-listing-card__tags span {
|
||||
padding: 2px 5px;
|
||||
border-radius: 2px;
|
||||
background: #eef2ec;
|
||||
color: var(--brand-ink);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mobile-listing-card__bottom {
|
||||
margin-top: auto;
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.mobile-listing-card__bottom strong {
|
||||
color: var(--brand-seal);
|
||||
font-family: 'Inter', 'DIN Alternate', sans-serif;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.mobile-listing-card__bottom small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: var(--ink-muted);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mobile-listing-card__more {
|
||||
color: var(--ink-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mobile-skeleton-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-skeleton-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-skeleton-cover {
|
||||
flex: 0 0 108px;
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.mobile-skeleton-card .adm-skeleton-paragraph {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mobile-empty,
|
||||
.mobile-error {
|
||||
padding: 36px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mobile-error .adm-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mobile-rules {
|
||||
margin: 22px 16px 12px;
|
||||
padding: 22px 18px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-rules p {
|
||||
margin: 10px 0 0;
|
||||
color: var(--ink-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.mobile-detail {
|
||||
padding: 14px 16px 108px;
|
||||
}
|
||||
|
||||
.mobile-detail--loading,
|
||||
.mobile-detail--error {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.mobile-detail--error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mobile-detail--error .adm-button {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--ink-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__cover,
|
||||
.mobile-detail__cover-skeleton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #eef1ea;
|
||||
}
|
||||
|
||||
.mobile-detail__cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.mobile-detail__head {
|
||||
padding: 18px 0 14px;
|
||||
}
|
||||
|
||||
.mobile-detail__no {
|
||||
color: var(--ink-muted);
|
||||
font-family: 'Inter', monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mobile-detail__head h1 {
|
||||
margin: 6px 0 10px;
|
||||
color: var(--ink);
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.mobile-detail__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mobile-detail__price-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.mobile-detail__price-card > div {
|
||||
padding: 14px 10px;
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-detail__price-card span,
|
||||
.mobile-detail__price-card strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-detail__price-card span {
|
||||
margin-bottom: 5px;
|
||||
color: var(--ink-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mobile-detail__price-card strong {
|
||||
color: var(--brand-ink);
|
||||
font-family: 'Inter', 'DIN Alternate', sans-serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.mobile-detail__section {
|
||||
padding: 20px 0 0;
|
||||
}
|
||||
|
||||
.mobile-detail__section h2 {
|
||||
margin: 0 0 10px;
|
||||
color: var(--brand-ink);
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mobile-detail__section p {
|
||||
margin: 0;
|
||||
color: var(--ink-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.mobile-detail__assets {
|
||||
margin: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.mobile-detail__assets > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-detail__assets dt {
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.mobile-detail__assets dd {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mobile-detail__notice {
|
||||
margin-top: 22px;
|
||||
padding: 16px;
|
||||
border-left: 2px solid var(--brand-seal);
|
||||
background: #f1ece2;
|
||||
}
|
||||
|
||||
.mobile-detail__notice strong {
|
||||
color: var(--brand-ink);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mobile-detail__notice p {
|
||||
margin: 7px 0 0;
|
||||
color: var(--ink-secondary);
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mobile-detail__actions {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--line);
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 70vh;
|
||||
padding: 44px 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mobile-placeholder .mobile-eyebrow {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mobile-placeholder p {
|
||||
max-width: 320px;
|
||||
margin: 14px 0 22px;
|
||||
color: var(--ink-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.mobile-profile {
|
||||
padding: 22px 16px;
|
||||
}
|
||||
|
||||
.mobile-profile__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--brand-ink);
|
||||
}
|
||||
|
||||
.mobile-profile__head h1 {
|
||||
margin: 0 0 4px;
|
||||
color: #f0f4f2;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.mobile-profile__head p {
|
||||
margin: 0;
|
||||
color: #b9cbc3;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mobile-profile__list {
|
||||
margin-top: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--paper-warm);
|
||||
--border-inner: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.mobile-profile__actions {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.mobile-shell {
|
||||
border-right: 1px solid var(--line);
|
||||
border-left: 1px solid var(--line);
|
||||
box-shadow: 0 0 60px rgba(29, 36, 33, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user