This commit is contained in:
liu
2026-08-07 08:45:25 +08:00
parent 39b789154c
commit 0203c84357
19 changed files with 1652 additions and 1085 deletions
+137 -188
View File
@@ -1,166 +1,84 @@
.home-page {
min-height: 100vh;
background: #f7f8fa;
padding: 20rpx 24rpx 140rpx;
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
// ===== 门店信息卡片 =====
.store-card {
// ===== 顶部搜索 =====
.home-search {
background: linear-gradient(135deg, #ee0a24, #ff4d4f);
padding: 16rpx 24rpx;
}
// ===== 轮播图 =====
.home-banner {
margin: 20rpx 24rpx 0;
border-radius: 20rpx;
padding: 28rpx;
color: #fff;
overflow: hidden;
&__row {
&__swiper {
border-radius: 20rpx;
}
&__image {
width: 100%;
height: 320rpx;
}
&__placeholder {
width: 100%;
height: 320rpx;
background: linear-gradient(135deg, #ff9a9e, #ff4d4f);
display: flex;
align-items: center;
justify-content: center;
}
&__avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
flex-shrink: 0;
&--text {
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
font-weight: 600;
}
}
&__info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
min-width: 0;
}
&__name {
font-size: 34rpx;
&__placeholder-text {
color: #fff;
font-size: 40rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__desc {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.85;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__identity {
margin-top: 24rpx;
display: flex;
align-items: center;
}
&__tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
&__tag {
font-size: 24rpx;
padding: 6rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.22);
&--level {
background: #ffd21e;
color: #7a5b00;
}
}
&__btn {
font-size: 26rpx;
padding: 12rpx 30rpx;
border-radius: 999rpx;
background: #fff;
color: #ee0a24;
font-weight: 600;
margin-left: auto;
letter-spacing: 8rpx;
}
}
// ===== 汇总卡片 =====
.summary-card {
margin-top: 20rpx;
// ===== 导航菜单 =====
.home-menu {
margin: 20rpx 24rpx 0;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 28rpx;
padding: 20rpx 0 8rpx;
}
.menu-icon {
width: 84rpx;
height: 84rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
&__text {
color: #fff;
font-size: 36rpx;
font-weight: 600;
}
}
// ===== 推荐商品 =====
.home-recommend {
margin: 20rpx 24rpx 0;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8rpx 8rpx 20rpx;
}
&__title {
font-size: 30rpx;
font-weight: 600;
}
&__period {
font-size: 24rpx;
color: #969799;
}
&__body {
margin-top: 20rpx;
display: flex;
}
&__item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
&__value {
font-size: 32rpx;
font-weight: 600;
color: #323233;
&--strong {
color: #ee0a24;
}
}
&__label {
margin-top: 8rpx;
font-size: 24rpx;
color: #969799;
}
}
// ===== 通用区块卡片 =====
.section-card {
margin-top: 20rpx;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 28rpx;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
&__title {
font-size: 30rpx;
font-weight: 600;
}
&__more {
@@ -169,76 +87,107 @@
}
&__empty {
padding: 40rpx 0;
background: #fff;
border-radius: 20rpx;
padding: 80rpx 0;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
&__empty-text {
font-size: 26rpx;
color: #c8c9cc;
color: #969799;
}
&__login-btn {
margin-top: 24rpx;
padding: 14rpx 60rpx;
background: #ee0a24;
color: #fff;
font-size: 28rpx;
border-radius: 999rpx;
}
}
// ===== 分类横向滚动 =====
.category-scroll {
white-space: nowrap;
margin: 0 -28rpx;
padding: 0 28rpx;
box-sizing: border-box;
// ===== 商品卡片(两列网格) =====
.product-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.category-chip {
display: inline-flex;
align-items: center;
padding: 14rpx 30rpx;
margin-right: 16rpx;
border-radius: 999rpx;
background: #f7f8fa;
border: 1rpx solid #ebedf0;
.product-card {
width: 342rpx;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
margin-bottom: 16rpx;
&__image {
width: 100%;
height: 300rpx;
background: #f2f3f5;
display: block;
}
&__info {
padding: 16rpx 20rpx 20rpx;
}
&__name {
font-size: 26rpx;
color: #323233;
}
}
// ===== 通知 =====
.notice-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f2f3f5;
&:last-child {
border-bottom: none;
}
&__dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: #ee0a24;
margin-right: 16rpx;
flex-shrink: 0;
}
&__content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
&__title {
font-size: 28rpx;
color: #323233;
font-weight: 500;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__desc {
margin-top: 6rpx;
font-size: 24rpx;
&__spec {
margin-top: 8rpx;
font-size: 22rpx;
color: #969799;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__bottom {
margin-top: 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
&__price {
font-size: 30rpx;
color: #ee0a24;
font-weight: 600;
&--none {
color: #c8c9cc;
font-weight: 400;
font-size: 24rpx;
}
}
&__add {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #ee0a24;
display: flex;
align-items: center;
justify-content: center;
}
&__add-icon {
color: #fff;
font-size: 30rpx;
line-height: 1;
}
}
}
+160 -175
View File
@@ -1,60 +1,42 @@
import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text, Image, ScrollView } from '@tarojs/components'
import { Grid, GridItem } from '@antmjs/vantui'
import { View, Text, Image } from '@tarojs/components'
import { Grid, GridItem, Search, Swiper, SwiperItem } from '@antmjs/vantui'
import useAuthStore from '@/stores/auth/useAuthStore'
import useCartStore from '@/stores/cart/useCartStore'
import { getOrderSummaryApi } from '@/services/order'
import { getCategoriesApi } from '@/services/product'
import { getNoticeListApi, readNoticeApi } from '@/services/notice'
import { resolveAvatarUrl } from '@/utils/format'
import type { Category } from '@/types/product'
import type { SummaryGroup } from '@/types/order'
import type { Notice } from '@/types/notice'
import { getCategoriesApi, getProductListApi } from '@/services/product'
import { getProductCover } from '@/types/product'
import type { Category, Product } from '@/types/product'
import './index.less'
/** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */
const PENDING_CATEGORY_KEY = 'product_category_id'
const PENDING_KEYWORD_KEY = 'product_keyword'
/** 分类菜单色块配色 */
const MENU_COLORS = ['#ee0a24', '#ff9f43', '#07c160', '#1989fa', '#8a5cf6', '#00b8d9']
export default function IndexPage() {
const user = useAuthStore(s => s.user)
const token = useAuthStore(s => s.token)
const fetchCart = useCartStore(s => s.fetchCart)
const addItem = useCartStore(s => s.addItem)
/** 本月订货汇总(取当前月份分组,无则取最近一组 */
const [summary, setSummary] = useState<SummaryGroup | null>(null)
/** 顶级分类快捷入口 */
/** 顶级分类(导航菜单前 6 个 */
const [categories, setCategories] = useState<Category[]>([])
/** 最新通知 */
const [notices, setNotices] = useState<Notice[]>([])
const [unreadCount, setUnreadCount] = useState(0)
/** 推荐商品 */
const [products, setProducts] = useState<Product[]>([])
/** 搜索框输入 */
const [keyword, setKeyword] = useState('')
const loggedIn = !!token && !!user
const loggedIn = !!token
useDidShow(() => {
if (!loggedIn) return
// 静默同步购物车角标
fetchCart().catch(() => {})
loadSummary()
loadCategories()
loadNotices()
if (loggedIn) {
loadRecommend()
}
})
/** 本月订货汇总 */
const loadSummary = useCallback(async () => {
try {
const res = await getOrderSummaryApi('month')
const groups = res.data.groups
if (!groups.length) {
setSummary(null)
return
}
const now = new Date()
const thisMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
setSummary(groups.find(g => g.period_label === thisMonth) ?? groups[groups.length - 1])
} catch {
// 错误已由 request 层 toast
}
}, [])
/** 商品分类(顶级) */
/** 商品分类 */
const loadCategories = useCallback(async () => {
try {
const res = await getCategoriesApi()
@@ -64,173 +46,176 @@ export default function IndexPage() {
}
}, [])
/** 最新通知 + 未读数 */
const loadNotices = useCallback(async () => {
/** 推荐商品(默认排序第一页) */
const loadRecommend = useCallback(async () => {
try {
const res = await getNoticeListApi({ page: 1, pageSize: 5 })
setNotices(res.data.data)
setUnreadCount(res.data.unread_count)
const res = await getProductListApi({ page: 1, pageSize: 10 })
setProducts(res.data.data)
} catch {
// 错误已由 request 层 toast
}
}, [])
/** 点击分类 → 商品页(switchTab 无法传参,经本地存储传递) */
const handleCategoryTap = useCallback((id: number) => {
/** 轮播数据:取有图商品的前 4 张图 */
const bannerImages = useMemo(
() => products.filter(p => getProductCover(p)).slice(0, 4),
[products],
)
/** 跳转商品页并带上分类/关键词 */
const goProduct = useCallback((categoryId?: number, kw?: string) => {
try {
Taro.setStorageSync('product_category_id', id)
if (categoryId !== undefined) Taro.setStorageSync(PENDING_CATEGORY_KEY, categoryId)
if (kw !== undefined) Taro.setStorageSync(PENDING_KEYWORD_KEY, kw)
} catch {
// noop
}
Taro.switchTab({ url: '/pages/product/index' })
}, [])
/** 点击通知 → 标记已读 */
const handleNoticeTap = useCallback(
async (notice: Notice) => {
if (notice.is_read === 1) return
/** 搜索框聚焦/提交 → 商品页搜索 */
const handleSearchFocus = useCallback(() => {
goProduct(undefined, keyword.trim())
}, [goProduct, keyword])
/** 快捷加购 */
const handleQuickAdd = useCallback(
async (product: Product, e: any) => {
e.stopPropagation()
try {
await readNoticeApi(notice.id)
setNotices(prev => prev.map(n => (n.id === notice.id ? { ...n, is_read: 1 } : n)))
setUnreadCount(count => Math.max(0, count - 1))
await addItem(product.id, 1)
Taro.showToast({ title: '已加入购物车', icon: 'success' })
} catch {
// 错误已由 request 层 toast
// 错误(未设等级价等)已由 request 层 toast
}
},
[],
[addItem],
)
const goLogin = useCallback(() => {
Taro.navigateTo({ url: '/pages/login/index' })
}, [])
return (
<View className='home-page'>
{/* ========== 门店信息卡片 ========== */}
<View className='store-card'>
{!loggedIn ? (
<View className='store-card__row'>
<View className='store-card__info'>
<Text className='store-card__name'>使</Text>
<Text className='store-card__desc'></Text>
</View>
<View className='store-card__btn' onClick={goLogin}></View>
</View>
) : (
<>
<View className='store-card__row'>
{user?.avatar ? (
<Image className='store-card__avatar' src={resolveAvatarUrl(user.avatar)} mode='aspectFill' />
) : (
<View className='store-card__avatar store-card__avatar--text'>{user?.nickname?.[0] || '用'}</View>
)}
<View className='store-card__info'>
<Text className='store-card__name'>{user?.nickname}</Text>
{user?.phone ? (
<Text className='store-card__desc'>{user.phone}</Text>
) : (
<Text className='store-card__desc' onClick={goLogin}></Text>
)}
{/* ========== 顶部搜索 ========== */}
<View className='home-search'>
<Search
value={keyword}
placeholder='搜索商品名称/规格'
shape='round'
background='transparent'
onChange={e => setKeyword(String(e.detail))}
onFocus={handleSearchFocus}
onSearch={handleSearchFocus}
/>
</View>
{/* ========== 轮播图 ========== */}
<View className='home-banner'>
<Swiper
className='home-banner__swiper'
height='320rpx'
autoPlay={3000}
loop
paginationVisible
paginationColor='#ffffff'
>
{bannerImages.length > 0 ? (
bannerImages.map((product, idx) => (
<SwiperItem key={product.id}>
<Image
className='home-banner__image'
src={getProductCover(product)}
mode='aspectFill'
onClick={() => goProduct()}
/>
</SwiperItem>
))
) : (
<SwiperItem>
<View className='home-banner__placeholder' onClick={() => goProduct()}>
<Text className='home-banner__placeholder-text'></Text>
</View>
</View>
<View className='store-card__identity'>
{user?.type === 0 ? (
<View className='store-card__btn' onClick={goLogin}></View>
) : user?.store ? (
<View className='store-card__tags'>
<Text className='store-card__tag'> · {user.store.name}</Text>
{user.store.level && <Text className='store-card__tag store-card__tag--level'>{user.store.level.name}</Text>}
</View>
) : user?.supplier ? (
<View className='store-card__tags'>
<Text className='store-card__tag'> · {user.supplier.name}</Text>
</View>
) : (
<View className='store-card__btn' onClick={goLogin}></View>
)}
</View>
</>
)}
</SwiperItem>
)}
</Swiper>
</View>
{/* ========== 本月订货汇总 ========== */}
<View className='summary-card'>
<View className='summary-card__header'>
<Text className='summary-card__title'></Text>
<Text className='summary-card__period'>{summary?.period_label ?? '—'}</Text>
</View>
<View className='summary-card__body'>
<View className='summary-card__item'>
<Text className='summary-card__value summary-card__value--strong'>{summary?.total_amount ?? '0.00'}</Text>
<Text className='summary-card__label'></Text>
</View>
<View className='summary-card__item'>
<Text className='summary-card__value'>{summary?.total_quantity ?? '0.00'}</Text>
<Text className='summary-card__label'></Text>
</View>
<View className='summary-card__item'>
<Text className='summary-card__value'>{summary?.order_count ?? 0}</Text>
<Text className='summary-card__label'></Text>
</View>
</View>
</View>
{/* ========== 快捷入口 ========== */}
<View className='section-card'>
<Grid columnNum={4} iconSize={52} border={false}>
<GridItem icon='apps-o' text='商品中心' onClick={() => Taro.switchTab({ url: '/pages/product/index' })} />
<GridItem icon='shopping-cart-o' text='购物车' onClick={() => Taro.switchTab({ url: '/pages/cart/index' })} />
<GridItem icon='orders-o' text='我的订单' onClick={() => Taro.switchTab({ url: '/pages/profile/index' })} />
<GridItem icon='balance-list-o' text='对账单' onClick={() => Taro.switchTab({ url: '/pages/profile/index' })} />
{/* ========== 导航菜单 ========== */}
<View className='home-menu'>
<Grid columnNum={4} border={false} iconSize={52}>
{categories.slice(0, 6).map((cat, idx) => (
<GridItem
key={cat.id}
text={cat.name}
onClick={() => goProduct(cat.id)}
renderIcon={
<View className='menu-icon' style={{ background: MENU_COLORS[idx % MENU_COLORS.length] }}>
<Text className='menu-icon__text'>{cat.name.slice(0, 1)}</Text>
</View>
}
/>
))}
<GridItem
icon='apps-o'
text='全部商品'
onClick={() => goProduct()}
/>
<GridItem
icon='orders-o'
text='我的订单'
onClick={() => Taro.navigateTo({ url: '/pages/order-list/index?status=all' })}
/>
</Grid>
</View>
{/* ========== 商品分类 ========== */}
{categories.length > 0 && (
<View className='section-card'>
<View className='section-card__header'>
<Text className='section-card__title'></Text>
<Text className='section-card__more' onClick={() => Taro.switchTab({ url: '/pages/product/index' })}> </Text>
</View>
<ScrollX items={categories} onTap={handleCategoryTap} />
{/* ========== 推荐商品 ========== */}
<View className='home-recommend'>
<View className='home-recommend__header'>
<Text className='home-recommend__title'></Text>
<Text className='home-recommend__more' onClick={() => goProduct()}> </Text>
</View>
)}
{/* ========== 通知 ========== */}
<View className='section-card'>
<View className='section-card__header'>
<Text className='section-card__title'></Text>
{unreadCount > 0 && <Text className='section-card__more'> {unreadCount} </Text>}
</View>
{notices.length === 0 ? (
<View className='section-card__empty'>
<Text className='section-card__empty-text'></Text>
{!loggedIn ? (
<View className='home-recommend__empty'>
<Text className='home-recommend__empty-text'></Text>
<View
className='home-recommend__login-btn'
onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}
>
</View>
</View>
) : products.length === 0 ? (
<View className='home-recommend__empty'>
<Text className='home-recommend__empty-text'></Text>
</View>
) : (
notices.map(notice => (
<View key={notice.id} className='notice-item' onClick={() => handleNoticeTap(notice)}>
{notice.is_read === 0 && <View className='notice-item__dot' />}
<View className='notice-item__content'>
<Text className='notice-item__title'>{notice.title}</Text>
<Text className='notice-item__desc' numberOfLines={1}>{notice.content}</Text>
<View className='product-grid'>
{products.map(product => (
<View key={product.id} className='product-card' onClick={() => goProduct()}>
<Image
className='product-card__image'
src={getProductCover(product) || 'https://img.yzcdn.cn/vant/cat.jpeg'}
mode='aspectFill'
lazyLoad
/>
<View className='product-card__info'>
<Text className='product-card__name'>{product.name}</Text>
<Text className='product-card__spec'>{product.spec} / {product.unit}</Text>
<View className='product-card__bottom'>
{product.price !== null ? (
<Text className='product-card__price'>{product.price}</Text>
) : (
<Text className='product-card__price product-card__price--none'></Text>
)}
<View className='product-card__add' onClick={e => handleQuickAdd(product, e)}>
<Text className='product-card__add-icon'></Text>
</View>
</View>
</View>
</View>
</View>
))
))}
</View>
)}
</View>
</View>
)
}
/** 横向滚动的分类入口 */
function ScrollX({ items, onTap }: { items: Category[]; onTap: (id: number) => void }) {
return (
<ScrollView scrollX className='category-scroll'>
{items.map(item => (
<View key={item.id} className='category-chip' onClick={() => onTap(item.id)}>
<Text className='category-chip__name'>{item.name}</Text>
</View>
))}
</ScrollView>
)
}