登录页面

This commit is contained in:
2026-07-12 15:17:54 +08:00
parent becf91da52
commit bfac6c6663
63 changed files with 4662 additions and 431 deletions
@@ -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 (
<View
className={`page-nav-bar${solid ? ' page-nav-bar--solid' : ''}`}
style={navBarStyle(metrics)}
>
<View
className="page-nav-bar__content"
style={{ height: `${metrics.navContentHeight}px` }}
>
{onBack ? (
<View className="page-nav-bar__btn" onClick={onBack}>
<Text className="page-nav-bar__icon"></Text>
</View>
) : (
<View className="page-nav-bar__btn page-nav-bar__btn--placeholder" />
)}
<Text
className={`page-nav-bar__title${titleVisible ? ' page-nav-bar__title--visible' : ''}`}
>
{title}
</Text>
{right ?? <View className="page-nav-bar__btn page-nav-bar__btn--placeholder" />}
</View>
</View>
);
}
@@ -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 (
<View className={classes} style={pageShellCssVars(metrics)}>
{children as ReactNode}
</View>
);
}
@@ -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 (
<View className={`${prefix}-wrap`}>
<Swiper
className={prefix}
circular={slides.length > 1}
onChange={(e) => setActiveIndex(e.detail.current)}
>
{slides.map((src, index) => (
<SwiperItem key={`${src}-${index}`} className={`${prefix}-item`}>
{src ? (
<Image className={`${prefix}-image`} src={src} mode="aspectFill" alt={alt} />
) : (
<View className={`${prefix}-placeholder`} />
)}
</SwiperItem>
))}
</Swiper>
{slides.length > 1 ? (
<View className={`${prefix}-dots`}>
{slides.map((_, index) => (
<View
key={index}
className={`${prefix}-dot${index === activeIndex ? ` ${prefix}-dot--active` : ''}`}
/>
))}
</View>
) : null}
</View>
);
}
@@ -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 (
<View className="region-picker-mask" onClick={onClose}>
<View className="region-picker-sheet" onClick={(e) => e.stopPropagation()}>
<View className="region-picker-head">
<Text className="region-picker-cancel" onClick={onClose}>
</Text>
<Text className="region-picker-title"></Text>
<Text
className="region-picker-ok"
onClick={() => {
onConfirm?.(valueLabel);
onClose();
}}
>
</Text>
</View>
<View className="region-picker-body">
<Text className="region-picker-item region-picker-item--active">{valueLabel}</Text>
<Text className="u-muted" style={{ display: 'block', marginTop: 12, textAlign: 'center' }}>
</Text>
</View>
</View>
</View>
);
}
@@ -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 (
<View className="sub-page-header" style={navBarStyle(metrics)}>
<View
className="sub-page-header__content"
style={{ height: `${metrics.navContentHeight}px` }}
>
<View className="sub-page-header__back" onClick={handleBack}>
<Text className="sub-page-header__back-icon"></Text>
</View>
<Text className="sub-page-header__title">{title}</Text>
{right ? <View className="sub-page-header__right">{right}</View> : null}
</View>
</View>
);
}
@@ -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 (
<View className="tab-main-header">
<Text className="tab-main-header__title">{title}</Text>
{extra ? <View className="tab-main-header__extra">{extra}</View> : null}
<View className="tab-main-header" style={navBarStyle(metrics)}>
<View
className="tab-main-header__content"
style={{ height: `${metrics.navContentHeight}px` }}
>
<Text className="tab-main-header__title">{title}</Text>
{extra ? <View className="tab-main-header__extra">{extra}</View> : null}
</View>
</View>
);
}