diff --git a/apps/mini-user/README.md b/apps/mini-user/README.md index 5db4bbf..75d1abc 100644 --- a/apps/mini-user/README.md +++ b/apps/mini-user/README.md @@ -1,6 +1,6 @@ # @dukang/mini-user -杜康好客 C 端微信小程序(Taro 4 + React)。 +杜康好客 C 端微信小程序(Taro 4 + React)。UI 对齐 `apps/h5-user`。 ## 启动 @@ -19,27 +19,22 @@ pnpm --filter @dukang/mini-user dev:weapp | 环节 | 文件 | 说明 | |------|------|------| | **编译注入** | `config/index.ts` → `defineConstants.TARO_APP_API_ORIGIN` | 默认 `https://dkapi.runxian.top`;可用环境变量 `VITE_API_TARGET` 覆盖 | -| **运行时拼装** | `src/lib/api.ts` → `resolveApiBase()` | `{origin}/api/v1`,例如 `https://dkapi.runxian.top/api/v1` | +| **运行时拼装** | `src/lib/api.ts` → `resolveApiBase()` | `{origin}/api/v1` | -本地联调 API(`localhost:3000`)时,启动前设置环境变量并重新编译: +## 页面结构(18 页) -```powershell -$env:VITE_API_TARGET = "http://localhost:3000" -pnpm dev:mini-user:weapp -``` +**Tab**:首页 / 门店 / 好客权益 / 我的 -微信小程序还需在公众平台配置 request 合法域名:`dkapi.runxian.top`。 +**栈页**:商品详情、门店详情、确认订单、收银台、订单列表/详情、地址列表/编辑、客服、权益明细、核销/核销码/成功、登录 + +## 布局约定 + +- 统一 `PageShell`(`tab` / `scroll` / `sub` / `plain`)注入刘海屏 CSS 变量 +- Tab 顶栏:`TabMainHeader`;详情滚动顶栏:`PageNavBar`;子页:`SubPageHeader` +- 样式:`src/styles/*.css` 按业务域拆分,入口 `app.css` ## 约定 -- `X-Client-App: USER_MINI`(与 `USER_H5` 共用用户端 API;JWT 与 Header 一致) -- 功能对照源:`apps/h5-user` + `pages/user/` 原型 -- 底栏使用本地 PNG 图标(weapp 无法加载 Google Material 字体) -- Tab 页:`navigationStyle: custom` + `TabMainHeader`,对齐 h5 浅色顶栏 - -## 迁移路线(建议) - -1. 登录(短信 → 小程序 `wx.login` + 手机号组件) -2. 商品详情 / 下单 / 支付 -3. 订单 3 Tab、权益、核销码 -4. 地址管理、门店详情 +- `X-Client-App: USER_MINI` +- 底栏原生 `tabBar` + PNG(weapp);H5 内嵌 `UserTabBar` +- 交易/核销页面以 UI 壳为主,部分 API 可后续加深 diff --git a/apps/mini-user/config/dev.ts b/apps/mini-user/config/dev.ts deleted file mode 100644 index 92f3462..0000000 --- a/apps/mini-user/config/dev.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { UserConfigExport } from '@tarojs/cli'; - -/** - * dev:weapp 下 Vite 会用 prod 模式替换 react 引用,可能导致 useState 等 hooks 失效。 - * @see https://docs.taro.zone/docs/config-detail#minidebugreact - */ -export default { - mini: { - debugReact: true, - }, -} satisfies UserConfigExport; diff --git a/apps/mini-user/config/index.ts b/apps/mini-user/config/index.ts index eb9731b..571a2c6 100644 --- a/apps/mini-user/config/index.ts +++ b/apps/mini-user/config/index.ts @@ -39,6 +39,8 @@ export default defineConfig(async () => ({ enable: false, }, mini: { + /** dev:weapp 预览模式需开启,否则 React hooks 在 Vite 下会失效 */ + debugReact: process.env.NODE_ENV !== 'production', postcss: { pxtransform: { enable: true, config: {} }, cssModules: { enable: false }, diff --git a/apps/mini-user/package.json b/apps/mini-user/package.json index accfa60..0e6e508 100644 --- a/apps/mini-user/package.json +++ b/apps/mini-user/package.json @@ -5,7 +5,7 @@ "description": "杜康好客 · C 端用户微信小程序(Taro)", "scripts": { "dev": "taro build --type h5 --watch", - "dev:weapp": "taro build --type weapp --watch", + "dev:weapp": "taro build --type weapp --watch --mode development", "build": "taro build --type h5", "build:weapp": "taro build --type weapp", "lint": "echo ok" diff --git a/apps/mini-user/src/app.config.ts b/apps/mini-user/src/app.config.ts index 25c1ed6..57d7d6c 100644 --- a/apps/mini-user/src/app.config.ts +++ b/apps/mini-user/src/app.config.ts @@ -4,6 +4,19 @@ export default defineAppConfig({ 'pages/stores/index', 'pages/benefit/index', 'pages/mine/index', + 'pages/product-detail/index', + 'pages/store-detail/index', + 'pages/order-confirm/index', + 'pages/pay/index', + 'pages/orders/index', + 'pages/order-detail/index', + 'pages/addresses/index', + 'pages/address-edit/index', + 'pages/customer-service/index', + 'pages/benefit-detail/index', + 'pages/redeem/index', + 'pages/redeem-code/index', + 'pages/redeem-success/index', 'pages/login/index', ], window: { diff --git a/apps/mini-user/src/app.css b/apps/mini-user/src/app.css index 66f3c0b..eab96a0 100644 --- a/apps/mini-user/src/app.css +++ b/apps/mini-user/src/app.css @@ -1,5 +1,16 @@ @import './styles/tokens.css'; +@import './styles/base.css'; +@import './styles/nav-bar.css'; @import './styles/home.css'; +@import './styles/stores.css'; +@import './styles/benefit.css'; +@import './styles/mine.css'; +@import './styles/product-detail.css'; +@import './styles/store-detail.css'; +@import './styles/login.css'; +@import './styles/order.css'; +@import './styles/address.css'; +@import './styles/redeem.css'; page, body { @@ -8,95 +19,3 @@ body { font-family: var(--font-body); margin: 0; } - -.u-page { - min-height: 100vh; - padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px)); - background: var(--color-background); - box-sizing: border-box; -} - -.u-page--tab { - padding-bottom: calc(88px + env(safe-area-inset-bottom, 0px)); -} - -.u-card { - background: var(--color-card); - border-radius: var(--radius-lg); - margin: 12px var(--space-page); - padding: 16px; - box-shadow: var(--shadow-card); - box-sizing: border-box; -} - -.u-muted { - color: var(--color-subtle-gray); - font-size: 13px; -} - -.u-title { - font-family: var(--font-headline); - font-size: 18px; - font-weight: 600; - margin: 0 0 8px; - color: var(--color-on-surface); -} - -.u-btn { - display: flex; - align-items: center; - justify-content: center; - border: none; - border-radius: var(--radius-md); - padding: 12px 18px; - font-size: 15px; - font-weight: 600; - line-height: 1.2; - background: var(--color-heritage-red); - color: #fff; -} - -.u-btn--block { - width: 100%; -} - -.u-btn:active { - opacity: 0.85; -} - -.u-empty { - text-align: center; - padding: 48px 24px; - color: var(--color-subtle-gray); - font-size: 14px; -} - -.u-store-row { - display: flex; - gap: 12px; - align-items: center; - padding: 14px 0; - border-bottom: 1px solid var(--color-surface-container); -} - -.u-store-row:last-child { - border-bottom: none; -} - -.u-store-avatar { - width: 48px; - height: 48px; - border-radius: 12px; - background: rgba(166, 29, 36, 0.08); - flex-shrink: 0; -} - -.u-store-name { - font-size: 15px; - font-weight: 600; - color: var(--color-on-surface); -} - -.u-safe-top { - padding-top: env(safe-area-inset-top, 0px); -} diff --git a/apps/mini-user/src/components/PageNavBar.tsx b/apps/mini-user/src/components/PageNavBar.tsx new file mode 100644 index 0000000..11e8992 --- /dev/null +++ b/apps/mini-user/src/components/PageNavBar.tsx @@ -0,0 +1,48 @@ +import type { ReactNode } from 'react'; +import { View, Text } from '@tarojs/components'; +import { navBarStyle, useNavBarMetrics } from '../lib/nav-bar'; + +type PageNavBarProps = { + title: string; + solid?: boolean; + titleVisible?: boolean; + onBack?: () => void; + right?: ReactNode; +}; + +/** 内页自定义导航栏(返回 + 标题 + 右侧操作),适配刘海屏 */ +export default function PageNavBar({ + title, + solid = false, + titleVisible = true, + onBack, + right, +}: PageNavBarProps) { + const metrics = useNavBarMetrics(); + + return ( + + + {onBack ? ( + + + + ) : ( + + )} + + {title} + + {right ?? } + + + ); +} diff --git a/apps/mini-user/src/components/PageShell.tsx b/apps/mini-user/src/components/PageShell.tsx new file mode 100644 index 0000000..a30666d --- /dev/null +++ b/apps/mini-user/src/components/PageShell.tsx @@ -0,0 +1,39 @@ +import type { PropsWithChildren, ReactNode } from 'react'; +import { View } from '@tarojs/components'; +import { pageShellCssVars, useNavBarMetrics } from '../lib/nav-bar'; + +type PageShellVariant = 'tab' | 'scroll' | 'sub' | 'plain'; + +type PageShellProps = PropsWithChildren<{ + variant?: PageShellVariant; + className?: string; + /** 栈页固定底栏时额外底部留白 */ + hasFixedFooter?: boolean; +}>; + +/** + * 页面壳:唯一注入刘海屏 CSS 变量;统一 min-height / 底栏安全区。 + * 顶栏组件自行调用 navBarStyle,不要在根节点再套 navBarStyle。 + */ +export default function PageShell({ + variant = 'tab', + className = '', + hasFixedFooter = false, + children, +}: PageShellProps) { + const metrics = useNavBarMetrics(); + const classes = [ + 'page-shell', + `page-shell--${variant}`, + hasFixedFooter ? 'page-shell--fixed-footer' : '', + className, + ] + .filter(Boolean) + .join(' '); + + return ( + + {children as ReactNode} + + ); +} diff --git a/apps/mini-user/src/components/ProductCarousel.tsx b/apps/mini-user/src/components/ProductCarousel.tsx new file mode 100644 index 0000000..16ea93f --- /dev/null +++ b/apps/mini-user/src/components/ProductCarousel.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import { View, Image, Swiper, SwiperItem } from '@tarojs/components'; + +type ProductCarouselProps = { + images: string[]; + alt: string; + variant?: 'home' | 'detail' | 'store'; +}; + +/** 商品/门店轮播(对齐 h5 ProductCarousel 三变体) */ +export default function ProductCarousel({ images, alt, variant = 'detail' }: ProductCarouselProps) { + const slides = images.length > 0 ? images : ['']; + const [activeIndex, setActiveIndex] = useState(0); + const prefix = + variant === 'store' ? 'store-detail-carousel' : variant === 'home' ? 'home-carousel' : 'detail-carousel'; + + return ( + + 1} + onChange={(e) => setActiveIndex(e.detail.current)} + > + {slides.map((src, index) => ( + + {src ? ( + {alt} + ) : ( + + )} + + ))} + + {slides.length > 1 ? ( + + {slides.map((_, index) => ( + + ))} + + ) : null} + + ); +} diff --git a/apps/mini-user/src/components/RegionPicker.tsx b/apps/mini-user/src/components/RegionPicker.tsx new file mode 100644 index 0000000..91e180e --- /dev/null +++ b/apps/mini-user/src/components/RegionPicker.tsx @@ -0,0 +1,49 @@ +import { View, Text } from '@tarojs/components'; + +type RegionPickerProps = { + open: boolean; + valueLabel?: string; + onClose: () => void; + onConfirm?: (label: string) => void; +}; + +/** + * 区域选择弹层占位(门店/地址后续可接真实级联数据)。 + * 当前提供「郑州市」确认交互,避免阻断主流程。 + */ +export default function RegionPicker({ + open, + valueLabel = '郑州市', + onClose, + onConfirm, +}: RegionPickerProps) { + if (!open) return null; + + return ( + + e.stopPropagation()}> + + + 取消 + + 选择区域 + { + onConfirm?.(valueLabel); + onClose(); + }} + > + 确定 + + + + {valueLabel} + + 完整省市区级联后续接入 + + + + + ); +} diff --git a/apps/mini-user/src/components/SubPageHeader.tsx b/apps/mini-user/src/components/SubPageHeader.tsx new file mode 100644 index 0000000..9fe7762 --- /dev/null +++ b/apps/mini-user/src/components/SubPageHeader.tsx @@ -0,0 +1,43 @@ +import type { ReactNode } from 'react'; +import { View, Text } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import { navBarStyle, useNavBarMetrics } from '../lib/nav-bar'; + +type SubPageHeaderProps = { + title: string; + onBack?: () => void; + right?: ReactNode; +}; + +/** 子页面固定实底顶栏(确认订单 / 地址 / 支付 / 订单列表等) */ +export default function SubPageHeader({ title, onBack, right }: SubPageHeaderProps) { + const metrics = useNavBarMetrics(); + + function handleBack() { + if (onBack) { + onBack(); + return; + } + const pages = Taro.getCurrentPages(); + if (pages.length > 1) { + Taro.navigateBack(); + return; + } + Taro.switchTab({ url: '/pages/home/index' }); + } + + return ( + + + + + + {title} + {right ? {right} : null} + + + ); +} diff --git a/apps/mini-user/src/components/TabMainHeader.tsx b/apps/mini-user/src/components/TabMainHeader.tsx index 5c9d35a..131e0dc 100644 --- a/apps/mini-user/src/components/TabMainHeader.tsx +++ b/apps/mini-user/src/components/TabMainHeader.tsx @@ -1,16 +1,25 @@ import type { ReactNode } from 'react'; import { View, Text } from '@tarojs/components'; +import { navBarStyle, useNavBarMetrics } from '../lib/nav-bar'; type TabMainHeaderProps = { title: string; extra?: ReactNode; }; +/** Tab 页顶栏:适配刘海屏 + 微信胶囊避让 */ export default function TabMainHeader({ title, extra }: TabMainHeaderProps) { + const metrics = useNavBarMetrics(); + return ( - - {title} - {extra ? {extra} : null} + + + {title} + {extra ? {extra} : null} + ); } diff --git a/apps/mini-user/src/custom-tab-bar/index.tsx b/apps/mini-user/src/custom-tab-bar/index.tsx deleted file mode 100644 index 4b4c65d..0000000 --- a/apps/mini-user/src/custom-tab-bar/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from 'react'; -import UserTabBar from '../components/UserTabBar'; - -/** 小程序 custom-tab-bar 入口;H5 由各 Tab 页直接渲染 UserTabBar */ -export default class CustomTabBar extends Component { - state = { selected: 0 }; - - setSelected(index: number) { - this.setState({ selected: index }); - } - - render() { - return ; - } -} diff --git a/apps/mini-user/src/lib/api.ts b/apps/mini-user/src/lib/api.ts index 2a7dec1..acb9dea 100644 --- a/apps/mini-user/src/lib/api.ts +++ b/apps/mini-user/src/lib/api.ts @@ -1,5 +1,6 @@ import Taro from '@tarojs/taro'; import { ClientApp } from '@dukang/shared-types'; +import { goLogin } from './auth-nav'; function resolveApiBase(): string { const origin = @@ -52,7 +53,7 @@ export function isLoggedIn(): boolean { } export function redirectToLogin() { - Taro.navigateTo({ url: '/pages/login/index' }); + goLogin(); } export function logout() { @@ -119,4 +120,5 @@ export type UserProfile = { id: string; phone?: string | null; nickname?: string | null; + avatarUrl?: string | null; }; diff --git a/apps/mini-user/src/lib/auth-nav.ts b/apps/mini-user/src/lib/auth-nav.ts new file mode 100644 index 0000000..e8497c1 --- /dev/null +++ b/apps/mini-user/src/lib/auth-nav.ts @@ -0,0 +1,60 @@ +import Taro from '@tarojs/taro'; + +const TAB_PAGES = new Set([ + '/pages/home/index', + '/pages/stores/index', + '/pages/benefit/index', + '/pages/mine/index', +]); + +function currentPagePath(): string { + const pages = Taro.getCurrentPages(); + const cur = pages[pages.length - 1] as + | { route?: string; options?: Record } + | undefined; + if (!cur?.route) return ''; + const path = cur.route.startsWith('/') ? cur.route : `/${cur.route}`; + if (path.includes('/pages/login/')) return ''; + const opts = cur.options ?? {}; + const qs = Object.entries(opts) + .filter(([, v]) => v != null && v !== '') + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) + .join('&'); + return qs ? `${path}?${qs}` : path; +} + +/** 跳转登录页;默认带回当前页作为 return */ +export function goLogin(returnPath?: string) { + const returnTo = returnPath ?? currentPagePath(); + const url = returnTo + ? `/pages/login/index?return=${encodeURIComponent(returnTo)}` + : '/pages/login/index'; + Taro.navigateTo({ url }).catch(() => { + Taro.redirectTo({ url }); + }); +} + +/** 登录成功后回到 return 页,或回退 / 首页 */ +export function finishLoginNavigate(returnTo?: string) { + const raw = (returnTo || '').trim(); + const target = raw ? decodeURIComponent(raw) : ''; + const pathOnly = target.split('?')[0]; + + if (pathOnly && TAB_PAGES.has(pathOnly)) { + Taro.switchTab({ url: pathOnly }); + return; + } + if (pathOnly && pathOnly.startsWith('/pages/')) { + Taro.redirectTo({ url: target }).catch(() => { + Taro.reLaunch({ url: pathOnly }); + }); + return; + } + + const pages = Taro.getCurrentPages(); + if (pages.length > 1) { + Taro.navigateBack(); + return; + } + Taro.switchTab({ url: '/pages/home/index' }); +} diff --git a/apps/mini-user/src/lib/nav-bar.ts b/apps/mini-user/src/lib/nav-bar.ts new file mode 100644 index 0000000..9d79c8b --- /dev/null +++ b/apps/mini-user/src/lib/nav-bar.ts @@ -0,0 +1,82 @@ +import { useMemo } from 'react'; +import Taro from '@tarojs/taro'; + +export type NavBarMetrics = { + /** 状态栏高度(刘海/灵动岛上方) */ + statusBarHeight: number; + /** 导航栏总高度 = 状态栏 + 内容区 */ + navBarHeight: number; + /** 标题栏内容区高度(与胶囊对齐) */ + navContentHeight: number; + /** 右侧留白,避免与微信胶囊按钮重叠 */ + navBarPaddingRight: number; +}; + +const H5_FALLBACK: NavBarMetrics = { + statusBarHeight: 0, + navBarHeight: 56, + navContentHeight: 56, + navBarPaddingRight: 16, +}; + +const WEAPP_FALLBACK: NavBarMetrics = { + statusBarHeight: 20, + navBarHeight: 64, + navContentHeight: 44, + navBarPaddingRight: 96, +}; + +/** 计算小程序自定义导航栏尺寸(对齐微信胶囊按钮) */ +export function getNavBarMetrics(): NavBarMetrics { + if (process.env.TARO_ENV === 'h5') { + return H5_FALLBACK; + } + + try { + const win = Taro.getWindowInfo?.() ?? Taro.getSystemInfoSync(); + const menu = Taro.getMenuButtonBoundingClientRect(); + const statusBarHeight = win.statusBarHeight ?? WEAPP_FALLBACK.statusBarHeight; + const navContentHeight = + menu.height > 0 + ? (menu.top - statusBarHeight) * 2 + menu.height + : WEAPP_FALLBACK.navContentHeight; + const navBarHeight = statusBarHeight + navContentHeight; + const navBarPaddingRight = + menu.width > 0 + ? Math.max(win.windowWidth - menu.left + 8, 16) + : WEAPP_FALLBACK.navBarPaddingRight; + + return { + statusBarHeight, + navBarHeight, + navContentHeight, + navBarPaddingRight, + }; + } catch { + return WEAPP_FALLBACK; + } +} + +export function useNavBarMetrics(): NavBarMetrics { + return useMemo(() => getNavBarMetrics(), []); +} + +/** 仅注入 CSS 变量,供 PageShell / sticky 子元素使用(不加 height/padding) */ +export function pageShellCssVars(metrics: NavBarMetrics): Record { + return { + '--nav-bar-height': `${metrics.navBarHeight}px`, + '--nav-content-height': `${metrics.navContentHeight}px`, + '--nav-status-bar-height': `${metrics.statusBarHeight}px`, + '--nav-padding-right': `${metrics.navBarPaddingRight}px`, + }; +} + +/** 顶栏自身样式:statusBar padding + 总高 + 右侧胶囊避让 */ +export function navBarStyle(metrics: NavBarMetrics): Record { + return { + paddingTop: `${metrics.statusBarHeight}px`, + height: `${metrics.navBarHeight}px`, + paddingRight: `${metrics.navBarPaddingRight}px`, + ...pageShellCssVars(metrics), + }; +} diff --git a/apps/mini-user/src/lib/product-images.ts b/apps/mini-user/src/lib/product-images.ts index c7a5644..4e5274b 100644 --- a/apps/mini-user/src/lib/product-images.ts +++ b/apps/mini-user/src/lib/product-images.ts @@ -30,4 +30,16 @@ export function getProductImages(source?: ProductImageSource | null): string[] { return PRODUCT_IMAGE_FALLBACK ? [PRODUCT_IMAGE_FALLBACK] : []; } +/** 详情页顶部轮播 */ +export function getProductCarouselImages(source?: ProductImageSource | null): string[] { + const carousel = uniqueUrls(source?.carouselUrls ?? []); + if (carousel.length > 0) return carousel; + return getProductImages(source); +} + +/** 详情页图文长图 */ +export function getProductDetailImages(source?: ProductImageSource | null): string[] { + return uniqueUrls(source?.detailImageUrls ?? []); +} + export const FALLBACK_CITY_CODE = '410100'; diff --git a/apps/mini-user/src/lib/wechat-auth.ts b/apps/mini-user/src/lib/wechat-auth.ts new file mode 100644 index 0000000..d2ded0c --- /dev/null +++ b/apps/mini-user/src/lib/wechat-auth.ts @@ -0,0 +1,15 @@ +import type { WechatLoginResult } from '@dukang/shared-types'; +import Taro from '@tarojs/taro'; +import { request } from './api'; + +/** 小程序微信授权登录:Taro.login → /auth/login/wechat */ +export async function loginWithWechat(): Promise { + const res = await Taro.login(); + if (!res.code) { + throw new Error(res.errMsg || '微信登录失败,未获取到 code'); + } + return request('/auth/login/wechat', { + method: 'POST', + data: { code: res.code, platform: 'mini' }, + }); +} diff --git a/apps/mini-user/src/pages/address-edit/index.config.ts b/apps/mini-user/src/pages/address-edit/index.config.ts new file mode 100644 index 0000000..434bbce --- /dev/null +++ b/apps/mini-user/src/pages/address-edit/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationStyle: 'custom', + navigationBarTitleText: '编辑地址', +}); diff --git a/apps/mini-user/src/pages/address-edit/index.tsx b/apps/mini-user/src/pages/address-edit/index.tsx new file mode 100644 index 0000000..40eaf89 --- /dev/null +++ b/apps/mini-user/src/pages/address-edit/index.tsx @@ -0,0 +1,74 @@ +import { useState } from 'react'; +import { View, Text, Input, Textarea } from '@tarojs/components'; +import Taro, { useRouter } from '@tarojs/taro'; +import PageShell from '../../components/PageShell'; +import SubPageHeader from '../../components/SubPageHeader'; +import { toast } from '../../lib/api'; + +export default function AddressEditPage() { + const router = useRouter(); + const isEdit = !!router.params.id; + const [name, setName] = useState(''); + const [phone, setPhone] = useState(''); + const [region, setRegion] = useState('河南省 郑州市'); + const [detail, setDetail] = useState(''); + + function save() { + if (!name.trim() || !phone.trim() || !detail.trim()) { + toast('请完善地址信息'); + return; + } + toast(isEdit ? '地址已更新(UI 壳)' : '地址已新增(UI 壳)', 'success'); + setTimeout(() => Taro.navigateBack(), 600); + } + + return ( + + + + + 收货人 + setName(e.detail.value)} + /> + + + 手机号 + setPhone(e.detail.value)} + /> + + + 所在地区 + toast('区域选择器后续接入')} + > + {region || '请选择省市区'} + + + + 详细地址 +