feat(home): 重构首页界面并更新API配置

- 移除底部导航栏中的"基地生活"选项卡
- 切换开发环境API地址为线上测试接口
- 添加完整的首页样式定义,包括英雄区域、商品卡片、快捷入口等
- 重构首页组件结构,集成商品列表、分类标签页和交互功能
- 更新主题管理逻辑,支持多种主题模式和用户ID兼容处理
- 添加商品数据获取和展示功能,实现首页内容动态加载
This commit is contained in:
2026-01-15 10:12:49 +08:00
parent 039af32fc3
commit 0770eb1699
6 changed files with 597 additions and 58 deletions

View File

@@ -2,8 +2,8 @@
export const ENV_CONFIG = { export const ENV_CONFIG = {
// 开发环境 // 开发环境
development: { development: {
API_BASE_URL: 'http://127.0.0.1:9200/api', // API_BASE_URL: 'http://127.0.0.1:9200/api',
// API_BASE_URL: 'https://cms-api.websoft.top/api', API_BASE_URL: 'https://cms-api.websoft.top/api',
APP_NAME: '开发环境', APP_NAME: '开发环境',
DEBUG: 'true', DEBUG: 'true',
}, },

View File

@@ -116,12 +116,6 @@ export default {
selectedIconPath: "assets/tabbar/home-active.png", selectedIconPath: "assets/tabbar/home-active.png",
text: "首页", text: "首页",
}, },
{
pagePath: "pages/category/index",
iconPath: "assets/tabbar/category.png",
selectedIconPath: "assets/tabbar/category-active.png",
text: "基地生活",
},
{ {
pagePath: "pages/cart/cart", pagePath: "pages/cart/cart",
iconPath: "assets/tabbar/cart.png", iconPath: "assets/tabbar/cart.png",

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useCallback } from 'react'
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients' import { gradientThemes, type GradientTheme, gradientUtils } from '@/styles/gradients'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
export interface UseThemeReturn { export interface UseThemeReturn {
@@ -14,28 +14,42 @@ export interface UseThemeReturn {
* 提供主题切换和状态管理功能 * 提供主题切换和状态管理功能
*/ */
export const useTheme = (): UseThemeReturn => { export const useTheme = (): UseThemeReturn => {
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0]) const getSavedThemeName = useCallback((): string => {
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true) try {
return Taro.getStorageSync('user_theme') || 'nature'
// 获取当前主题 } catch {
const getCurrentTheme = (): GradientTheme => { return 'nature'
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
if (savedTheme === 'auto') {
// 自动主题根据用户ID生成
const userId = Taro.getStorageSync('userId') || '1'
return gradientUtils.getThemeByUserId(userId)
} else {
// 手动选择的主题
return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
} }
} }, [])
const getStoredUserId = useCallback((): number => {
try {
const raw = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId')
const asNumber = typeof raw === 'number' ? raw : parseInt(String(raw || '1'), 10)
return Number.isFinite(asNumber) ? asNumber : 1
} catch {
return 1
}
}, [])
const resolveTheme = useCallback(
(themeName: string): GradientTheme => {
if (themeName === 'auto') {
return gradientUtils.getThemeByUserId(getStoredUserId())
}
return gradientThemes.find(t => t.name === themeName) || gradientUtils.getThemeByName('nature') || gradientThemes[0]
},
[getStoredUserId]
)
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(() => getSavedThemeName() === 'auto')
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(() => resolveTheme(getSavedThemeName()))
// 初始化主题 // 初始化主题
useEffect(() => { useEffect(() => {
const savedTheme = Taro.getStorageSync('user_theme') || 'auto' const savedTheme = getSavedThemeName()
setIsAutoTheme(savedTheme === 'auto') setIsAutoTheme(savedTheme === 'auto')
setCurrentTheme(getCurrentTheme()) setCurrentTheme(resolveTheme(savedTheme))
}, []) }, [])
// 设置主题 // 设置主题
@@ -43,7 +57,7 @@ export const useTheme = (): UseThemeReturn => {
try { try {
Taro.setStorageSync('user_theme', themeName) Taro.setStorageSync('user_theme', themeName)
setIsAutoTheme(themeName === 'auto') setIsAutoTheme(themeName === 'auto')
setCurrentTheme(getCurrentTheme()) setCurrentTheme(resolveTheme(themeName))
} catch (error) { } catch (error) {
console.error('保存主题失败:', error) console.error('保存主题失败:', error)
} }
@@ -51,7 +65,7 @@ export const useTheme = (): UseThemeReturn => {
// 刷新主题(用于自动主题模式下用户信息变更时) // 刷新主题(用于自动主题模式下用户信息变更时)
const refreshTheme = () => { const refreshTheme = () => {
setCurrentTheme(getCurrentTheme()) setCurrentTheme(resolveTheme(getSavedThemeName()))
} }
return { return {

View File

@@ -4,6 +4,376 @@ page {
background: linear-gradient(to bottom, #e9fff2, #ffffff); background: linear-gradient(to bottom, #e9fff2, #ffffff);
} }
.home-page {
padding: 24rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
}
.home-hero {
position: relative;
overflow: hidden;
border-radius: 28rpx;
background: linear-gradient(180deg, #bfefff 0%, #eafaff 40%, #fff7ec 100%);
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
}
.home-hero__bg {
position: absolute;
inset: 0;
background:
radial-gradient(360rpx 240rpx at 18% 16%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)),
radial-gradient(320rpx 220rpx at 84% 18%, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0)),
linear-gradient(180deg, rgba(0, 207, 255, 0.12), rgba(0, 0, 0, 0));
pointer-events: none;
}
.home-hero__content {
position: relative;
display: flex;
justify-content: space-between;
gap: 18rpx;
padding: 26rpx 24rpx 28rpx;
min-height: 320rpx;
}
.home-hero__left {
flex: 1;
min-width: 0;
}
.home-hero__topRow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16rpx;
}
.home-hero__brand {
flex: none;
display: inline-flex;
align-items: center;
padding: 8rpx 14rpx;
border-radius: 999rpx;
background: rgba(255, 214, 84, 0.92);
color: #2a2a2a;
font-weight: 700;
font-size: 24rpx;
line-height: 1;
}
.home-hero__brandText {
line-height: 1;
}
.home-hero__tag {
flex: none;
display: inline-flex;
align-items: center;
padding: 10rpx 18rpx;
border-radius: 18rpx;
background: linear-gradient(90deg, #22d64a 0%, #7df4b0 100%);
box-shadow: 0 14rpx 24rpx rgba(36, 202, 148, 0.22);
}
.home-hero__tagText {
font-size: 56rpx;
font-weight: 900;
color: #ffffff;
line-height: 1;
}
.home-hero__date {
flex: 1;
min-width: 0;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10rpx 14rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.75);
}
.home-hero__dateText {
font-size: 26rpx;
font-weight: 700;
color: #1a1a1a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.home-hero__headline {
margin-top: 22rpx;
}
.home-hero__headlineText {
display: block;
font-size: 42rpx;
font-weight: 900;
color: #0b0b0b;
letter-spacing: 0.5px;
line-height: 1.15;
}
.home-hero__right {
width: 200rpx;
display: flex;
justify-content: flex-end;
align-items: flex-end;
}
.home-hero__bottle {
position: relative;
width: 190rpx;
height: 250rpx;
border-radius: 28rpx;
background:
radial-gradient(240rpx 360rpx at 60% 30%, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.18)),
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.1));
border: 2rpx solid rgba(255, 255, 255, 0.65);
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.12);
}
.home-hero__bottleCap {
position: absolute;
top: 14rpx;
left: 50%;
transform: translateX(-50%);
width: 88rpx;
height: 26rpx;
border-radius: 999rpx;
background: linear-gradient(180deg, #d7e6f3, #b0cadd);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.12);
}
.home-hero__bottleLabel {
position: absolute;
left: 18rpx;
right: 18rpx;
bottom: 30rpx;
padding: 12rpx 12rpx;
border-radius: 18rpx;
background: linear-gradient(90deg, rgba(0, 150, 255, 0.18), rgba(0, 255, 210, 0.18));
border: 2rpx solid rgba(255, 255, 255, 0.45);
}
.home-hero__bottleLabelText {
font-size: 30rpx;
font-weight: 800;
color: rgba(0, 80, 140, 0.95);
text-align: center;
display: block;
}
.ticket-card {
margin-top: 18rpx;
border-radius: 22rpx;
overflow: hidden;
background: #ffffff;
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
}
.ticket-card__head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18rpx 20rpx;
background: linear-gradient(90deg, #22d64a 0%, #7df4b0 100%);
}
.ticket-card__title {
color: #ffffff;
font-weight: 800;
font-size: 28rpx;
}
.ticket-card__count {
color: rgba(255, 255, 255, 0.92);
font-size: 24rpx;
}
.ticket-card__countNum {
color: #ffffff;
font-weight: 900;
}
.ticket-card__body {
padding: 20rpx 10rpx 22rpx;
}
.shortcut-grid {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12rpx;
}
.shortcut-grid__item {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.shortcut-grid__icon {
width: 88rpx;
height: 88rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
color: #20c26a;
border: 2rpx solid rgba(32, 194, 106, 0.35);
}
.shortcut-grid__text {
font-size: 24rpx;
color: #333333;
}
.home-tabs {
margin-top: 18rpx;
}
.home-tabs__inner {
display: flex;
gap: 18rpx;
padding: 0 4rpx;
}
.home-tabs__item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: transparent;
}
.home-tabs__item--active {
background: rgba(32, 194, 106, 0.16);
}
.home-tabs__itemText {
font-size: 28rpx;
color: #2a2a2a;
white-space: nowrap;
}
.home-tabs__item--active .home-tabs__itemText {
color: #16b65a;
font-weight: 800;
}
.goods-grid {
margin-top: 18rpx;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18rpx;
}
.goods-card {
border-radius: 22rpx;
overflow: hidden;
background: #ffffff;
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
}
.goods-card__imgWrap {
padding: 18rpx 18rpx 0;
}
.goods-card__img {
width: 100%;
height: 280rpx;
border-radius: 18rpx;
background: #f4f4f4;
}
.goods-card__body {
padding: 18rpx 18rpx 20rpx;
}
.goods-card__title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 26rpx;
font-weight: 700;
color: #1c1c1c;
min-height: 72rpx;
}
.goods-card__meta {
margin-top: 10rpx;
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 10rpx;
}
.goods-card__sold {
font-size: 22rpx;
color: #9a9a9a;
white-space: nowrap;
}
.goods-card__price {
display: flex;
align-items: baseline;
gap: 4rpx;
color: #27c86b;
white-space: nowrap;
}
.goods-card__priceUnit {
font-size: 22rpx;
font-weight: 800;
}
.goods-card__priceValue {
font-size: 36rpx;
font-weight: 900;
}
.goods-card__actions {
margin-top: 16rpx;
display: flex;
gap: 14rpx;
}
.goods-card__btn {
flex: 1;
height: 64rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
}
.goods-card__btn--ghost {
border: 2rpx solid rgba(32, 194, 106, 0.7);
background: #ffffff;
}
.goods-card__btn--primary {
background: linear-gradient(90deg, #24d34c 0%, #6df09a 100%);
}
.goods-card__btnText {
font-size: 24rpx;
font-weight: 700;
color: #18b85a;
white-space: nowrap;
}
.goods-card__btnText--primary {
color: #ffffff;
}
.buy-btn{ .buy-btn{
height: 70px; height: 70px;
background: linear-gradient(to bottom, #1cd98a, #24ca94); background: linear-gradient(to bottom, #1cd98a, #24ca94);

View File

@@ -1,19 +1,17 @@
import Header from './Header'; import Header from './Header'
import BestSellers from './BestSellers'; import Taro, { useShareAppMessage } from '@tarojs/taro'
import Taro from '@tarojs/taro'; import { View, Text, Image, ScrollView } from '@tarojs/components'
import {useShareAppMessage} from "@tarojs/taro" import { useEffect, useMemo, useState, type ReactNode } from 'react'
import {useEffect, useState} from "react"; import { Cart, Coupon, Gift, Ticket } from '@nutui/icons-react-taro'
import {getShopInfo} from "@/api/layout"; import { getShopInfo } from '@/api/layout'
import Menu from "./Menu"; import { checkAndHandleInviteRelation, hasPendingInvite } from '@/utils/invite'
import Banner from "./Banner"; import { pageShopGoods } from '@/api/shop/shopGoods'
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite"; import type { ShopGoods } from '@/api/shop/shopGoods/model'
import './index.scss' import './index.scss'
function Home() { function Home() {
// 吸顶状态 const [activeTab, setActiveTab] = useState('推荐')
// const [stickyStatus, setStickyStatus] = useState<boolean>(false) const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
// Tabs粘性状态
const [_, setTabsStickyStatus] = useState<boolean>(false)
useShareAppMessage(() => { useShareAppMessage(() => {
// 获取当前用户ID用于生成邀请链接 // 获取当前用户ID用于生成邀请链接
@@ -85,9 +83,7 @@ function Home() {
// } // }
// 处理Tabs粘性状态变化 // 处理Tabs粘性状态变化
const handleTabsStickyChange = (isSticky: boolean) => { // const handleTabsStickyChange = (isSticky: boolean) => {}
setTabsStickyStatus(isSticky)
}
const reload = () => { const reload = () => {
@@ -99,6 +95,10 @@ function Home() {
}) })
pageShopGoods({}).then(res => {
setGoodsList(res?.list || [])
})
// 检查是否有待处理的邀请关系 - 异步处理,不阻塞页面加载 // 检查是否有待处理的邀请关系 - 异步处理,不阻塞页面加载
if (hasPendingInvite()) { if (hasPendingInvite()) {
console.log('检测到待处理的邀请关系') console.log('检测到待处理的邀请关系')
@@ -152,16 +152,177 @@ function Home() {
}); });
}, []); }, []);
const tabs = useMemo(() => ['推荐', '桶装水', '优惠组合', '购机套餐', '饮水设备'], [])
const shortcuts = useMemo<
Array<{ key: string; title: string; icon: ReactNode; onClick: () => void }>
>(
() => [
{
key: 'ticket',
title: '充值水票',
icon: <Ticket size={30} />,
onClick: () => Taro.navigateTo({ url: '/user/wallet/wallet' }),
},
{
key: 'order',
title: '立即订水',
icon: <Cart size={30} />,
onClick: () => Taro.switchTab({ url: '/pages/category/index' }),
},
{
key: 'invite',
title: '邀请有礼',
icon: <Gift size={30} />,
onClick: () => Taro.navigateTo({ url: '/dealer/qrcode/index' }),
},
{
key: 'coupon',
title: '领券中心',
icon: <Coupon size={30} />,
onClick: () => Taro.navigateTo({ url: '/coupon/index' }),
},
],
[]
)
const visibleGoods = useMemo(() => {
// 先按效果图展示两列卡片,数据不够时也保持布局稳定
const list = goodsList || []
if (list.length <= 6) return list
return list.slice(0, 6)
}, [goodsList])
return ( return (
<> <>
{/* Header区域 - 现在由Header组件内部处理吸顶逻辑 */} {/* Header区域 - 现在由Header组件内部处理吸顶逻辑 */}
<Header /> <Header />
<div className={'flex flex-col mt-12'}> <View className="home-page">
<Menu/> {/* 顶部活动主视觉 */}
<Banner/> <View className="home-hero">
<BestSellers onStickyChange={handleTabsStickyChange}/> <View className="home-hero__bg" />
</div> <View className="home-hero__content">
<View className="home-hero__left">
<View className="home-hero__topRow">
<View className="home-hero__brand">
<Text className="home-hero__brandText"></Text>
</View>
<View className="home-hero__tag">
<Text className="home-hero__tagText"></Text>
</View>
<View className="home-hero__date">
<Text className="home-hero__dateText">202X年X月X日 - X月X日</Text>
</View>
</View>
<View className="home-hero__headline">
<Text className="home-hero__headlineText"></Text>
<Text className="home-hero__headlineText">15L</Text>
</View>
</View>
<View className="home-hero__right">
<View className="home-hero__bottle">
<View className="home-hero__bottleCap" />
<View className="home-hero__bottleLabel">
<Text className="home-hero__bottleLabelText"></Text>
</View>
</View>
</View>
</View>
</View>
{/* 电子水票 */}
<View className="ticket-card">
<View className="ticket-card__head">
<Text className="ticket-card__title"></Text>
<Text className="ticket-card__count">
<Text className="ticket-card__countNum">0</Text>
</Text>
</View>
<View className="ticket-card__body">
<View className="shortcut-grid">
{shortcuts.map((item) => (
<View
key={item.key}
className="shortcut-grid__item"
onClick={item.onClick}
>
<View className="shortcut-grid__icon">{item.icon}</View>
<Text className="shortcut-grid__text">{item.title}</Text>
</View>
))}
</View>
</View>
</View>
{/* 分类Tabs */}
<ScrollView className="home-tabs" scrollX enableFlex>
<View className="home-tabs__inner">
{tabs.map((tab) => {
const active = tab === activeTab
return (
<View
key={tab}
className={`home-tabs__item ${active ? 'home-tabs__item--active' : ''}`}
onClick={() => setActiveTab(tab)}
>
<Text className="home-tabs__itemText">{tab}</Text>
</View>
)
})}
</View>
</ScrollView>
{/* 商品列表 */}
<View className="goods-grid">
{visibleGoods.map((item) => (
<View key={item.goodsId} className="goods-card">
<View className="goods-card__imgWrap">
<Image
className="goods-card__img"
src={item.image}
mode="aspectFill"
lazyLoad={false}
onClick={() =>
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
}
/>
</View>
<View className="goods-card__body">
<Text className="goods-card__title">{item.name}</Text>
<View className="goods-card__meta">
<Text className="goods-card__sold">:{item.sales || 0}</Text>
<View className="goods-card__price">
<Text className="goods-card__priceUnit"></Text>
<Text className="goods-card__priceValue">{item.price}</Text>
</View>
</View>
<View className="goods-card__actions">
<View
className="goods-card__btn goods-card__btn--ghost"
onClick={() => Taro.navigateTo({ url: '/user/coupon/index' })}
>
<Text className="goods-card__btnText"></Text>
</View>
<View
className="goods-card__btn goods-card__btn--primary"
onClick={() =>
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
}
>
<Text className="goods-card__btnText goods-card__btnText--primary"></Text>
</View>
</View>
</View>
</View>
))}
</View>
</View>
</> </>
) )
} }

View File

@@ -11,13 +11,13 @@ const ThemeSelector: React.FC = () => {
// 获取当前主题 // 获取当前主题
useEffect(() => { useEffect(() => {
const savedTheme = Taro.getStorageSync('user_theme') || 'auto' const savedTheme = Taro.getStorageSync('user_theme') || 'nature'
setSelectedTheme(savedTheme) setSelectedTheme(savedTheme)
if (savedTheme === 'auto') { if (savedTheme === 'auto') {
// 自动主题根据用户ID生成 // 自动主题根据用户ID生成
const userId = Taro.getStorageSync('userId') || '1' const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1'
const theme = gradientUtils.getThemeByUserId(userId) const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10))
setCurrentTheme(theme) setCurrentTheme(theme)
} else { } else {
// 手动选择的主题 // 手动选择的主题
@@ -33,8 +33,8 @@ const ThemeSelector: React.FC = () => {
setSelectedTheme(themeName) setSelectedTheme(themeName)
if (themeName === 'auto') { if (themeName === 'auto') {
const userId = Taro.getStorageSync('userId') || '1' const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1'
const theme = gradientUtils.getThemeByUserId(userId) const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10))
setCurrentTheme(theme) setCurrentTheme(theme)
} else { } else {
const theme = gradientThemes.find(t => t.name === themeName) const theme = gradientThemes.find(t => t.name === themeName)
@@ -61,8 +61,8 @@ const ThemeSelector: React.FC = () => {
// 预览主题 // 预览主题
const previewTheme = (themeName: string) => { const previewTheme = (themeName: string) => {
if (themeName === 'auto') { if (themeName === 'auto') {
const userId = Taro.getStorageSync('userId') || '1' const userId = Taro.getStorageSync('UserId') ?? Taro.getStorageSync('userId') ?? '1'
const theme = gradientUtils.getThemeByUserId(userId) const theme = gradientUtils.getThemeByUserId(typeof userId === 'number' ? userId : parseInt(String(userId), 10))
setCurrentTheme(theme) setCurrentTheme(theme)
} else { } else {
const theme = gradientThemes.find(t => t.name === themeName) const theme = gradientThemes.find(t => t.name === themeName)