feat(pages): 实现商品分类页面及优化轮播图功能

- 新增商品分类页面,包含左侧导航和右侧商品列表
- 实现分类切换和商品展示功能
- 添加骨架屏加载效果和空状态处理
- 优化首页轮播图组件,支持自动播放和触摸滑动
- 调整轮播图高度默认值为300px- 移除旧的热卖商品逻辑,改为获取推荐文章
- 修复医生申请页面用户类型选择功能
- 更新页面标题文本内容
- 添加网站配置获取hook
This commit is contained in:
2025-10-05 09:45:18 +08:00
parent 0a517b1247
commit 7b7bf80bc5
21 changed files with 1153 additions and 123 deletions

View File

@@ -43,6 +43,7 @@ export interface Config {
siteName?: string;
siteLogo?: string;
domain?: string;
apiUrl?: string;
icpNo?: string;
copyright?: string;
loginBgImg?: string;

View File

@@ -9,9 +9,7 @@ import {SERVER_API_URL} from "@/utils/server";
export async function listDictionaries(params?: DictParam) {
const res = await request.get<ApiResult<Dict[]>>(
SERVER_API_URL + '/system/dict',
{
params
}
);
if (res.code === 0) {
return res.data;

View File

@@ -4,7 +4,7 @@ export default {
'pages/cart/cart',
'pages/find/find',
'pages/user/user',
'pages/cms/category/index'
'pages/category/category'
],
"subpackages": [
{
@@ -126,6 +126,12 @@ export default {
selectedIconPath: "assets/tabbar/home-active.png",
text: "首页",
},
{
pagePath: "pages/category/category",
iconPath: "assets/tabbar/category.png",
selectedIconPath: "assets/tabbar/category-active.png",
text: "分类",
},
{
pagePath: "pages/cart/cart",
iconPath: "assets/tabbar/cart.png",

View File

@@ -6,10 +6,12 @@ import './app.scss'
import {loginByOpenId} from "@/api/layout";
import {TenantId} from "@/config/app";
import {saveStorageByLoginUser} from "@/utils/server";
import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation, debugInviteInfo} from "@/utils/invite";
import {configWebsiteField} from "@/api/cms/cmsWebsiteField";
import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation} from "@/utils/invite";
import { useConfig } from "@/hooks/useConfig"; // 引入新的自定义Hook
function App(props: { children: any; }) {
const { refetch: handleTheme } = useConfig(); // 使用新的Hook
const reload = () => {
Taro.login({
success: (res) => {
@@ -38,6 +40,8 @@ function App(props: { children: any; }) {
};
// 可以使用所有的 React Hooks
useEffect(() => {
// 设置主题 (现在由useConfig Hook处理)
handleTheme()
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
@@ -53,13 +57,12 @@ function App(props: { children: any; }) {
// 处理小程序启动参数中的邀请信息
const options = Taro.getLaunchOptionsSync()
handleLaunchOptions(options)
handleTheme()
})
// 处理启动参数
const handleLaunchOptions = (options: any) => {
try {
console.log('=== 小程序启动参数处理开始 ===')
console.log('=== 小程 序启动参数处理开始 ===')
console.log('完整启动参数:', JSON.stringify(options, null, 2))
// 解析邀请参数
@@ -82,10 +85,6 @@ function App(props: { children: any; }) {
})
}, 1000)
// 打印调试信息
setTimeout(() => {
debugInviteInfo()
}, 2000)
} else {
console.log('❌ 未检测到邀请参数')
}
@@ -96,15 +95,6 @@ function App(props: { children: any; }) {
}
}
const handleTheme = () => {
configWebsiteField().then(data => {
// 设置主题
if(data.theme && !Taro.getStorageSync('user_theme')){
Taro.setStorageSync('user_theme', data.theme)
}
})
}
// 对应 onHide
useDidHide(() => {
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,15 +1,25 @@
import {Image, Cell} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
const ArticleList = (props: any) => {
return (
<>
<div className={'px-3'}>
{props.data.map((item, index) => {
<View className={'px-3'}>
{props.data.map((item: any, index: number) => {
return (
<Cell
title={item.title}
title={
<View>
<View className="text-base font-medium mb-1">{item.title}</View>
{item.comments && (
<Text className="text-sm text-gray-500 leading-relaxed">
{item.comments}
</Text>
)}
</View>
}
extra={
<Image src={item.image} mode={'aspectFit'} lazyLoad={false} width={100} height="100"/>
}
@@ -18,7 +28,7 @@ const ArticleList = (props: any) => {
/>
)
})}
</div>
</View>
</>
)
}

View File

@@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '邀请注册',
navigationBarTitleText: '会员注册',
navigationBarTextStyle: 'black'
})

View File

@@ -1,5 +1,5 @@
import {useEffect, useState, useRef} from "react";
import {Loading, CellGroup, Input, Form, Avatar, Button, Space} from '@nutui/nutui-react-taro'
import {Loading, CellGroup, Input, Form, Avatar, Radio, Button, Space, InputNumber, TextArea, ConfigProvider} from '@nutui/nutui-react-taro'
import {Edit} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
@@ -11,6 +11,8 @@ import {User} from "@/api/system/user/model";
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
import {listUserRole, updateUserRole} from "@/api/system/userRole";
import {DictData} from "@/api/system/dict-data/model";
import {listDictData} from "@/api/system/dict-data";
// 类型定义
interface ChooseAvatarEvent {
@@ -19,16 +21,20 @@ interface ChooseAvatarEvent {
};
}
interface InputEvent {
detail: {
value: string;
};
const customTheme = {
nutuiInputnumberButtonWidth: '30px',
nutuiInputnumberButtonHeight: '30px',
nutuiInputnumberButtonBorderRadius: '2px',
nutuiInputnumberButtonBackgroundColor: `#f4f4f4`,
nutuiInputnumberInputHeight: '30px',
nutuiInputnumberInputMargin: '0 2px',
}
const AddUserAddress = () => {
const {user, loginUser} = useUser()
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<User>()
const [userType, setUserType] = useState<DictData[]>()
const formRef = useRef<any>(null)
const reload = async () => {
@@ -47,6 +53,9 @@ const AddUserAddress = () => {
nickname: '',
})
}
listDictData({dictCode: 'UserType'}).then((data) => {
setUserType(data)
})
}
@@ -217,23 +226,6 @@ const AddUserAddress = () => {
}
}
// 获取微信昵称
const getWxNickname = (nickname: string) => {
// 更新表单数据
const updatedFormData = {
...FormData,
nickname: nickname
}
setFormData(updatedFormData);
// 同步更新表单字段
if (formRef.current) {
formRef.current.setFieldsValue({
realName: nickname
})
}
}
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
const {code, encryptedData, iv} = detail
@@ -382,9 +374,13 @@ const AddUserAddress = () => {
>
<View className={'bg-gray-100 h-3'}></View>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
<Input placeholder="邀请人ID" disabled={true}/>
{
FormData?.phone && <Form.Item name="avatar" label="头像" initialValue={FormData?.avatar} required>
<Button open-type="chooseAvatar" style={{height: '58px'}} onChooseAvatar={uploadAvatar}>
<Avatar src={FormData?.avatar || user?.avatar} size="54"/>
</Button>
</Form.Item>
}
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
<View className="flex items-center justify-between">
<Input
@@ -400,22 +396,76 @@ const AddUserAddress = () => {
</Button>
</View>
</Form.Item>
{
FormData?.phone && <Form.Item name="avatar" label="头像" initialValue={FormData?.avatar} required>
<Button open-type="chooseAvatar" style={{height: '58px'}} onChooseAvatar={uploadAvatar}>
<Avatar src={FormData?.avatar || user?.avatar} size="54"/>
</Button>
{FormData?.refereeId && <Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required><Input placeholder="邀请人ID" disabled={true}/></Form.Item>}
</CellGroup>
<View className={'h-3 bg-gray-100'}></View>
<CellGroup style={{padding: '4px 0'}}>
{FormData?.type}
<Form.Item label="用户类型" name="type" initialValue={FormData?.type} required>
<Radio.Group defaultValue="1" direction="horizontal" value={FormData?.type}>
{userType?.map((item) => (
<Radio key={item.value} value={item.value} onChange={() => {
setFormData({
...FormData,
type: item.value
})
}}>
{item.label}
</Radio>
))}
</Radio.Group>
</Form.Item>
}
<Form.Item name="realName" label="昵称" initialValue="" required>
<Input
type="nickname"
className="info-content__input"
placeholder="请获取微信昵称"
value={FormData?.nickname || ''}
onInput={(e: InputEvent) => getWxNickname(e.detail.value)}
<Form.Item label="性别" name="sex" required>
<Radio.Group defaultValue="0" direction="horizontal">
<Radio value="1">
</Radio>
<Radio value="2">
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="真实姓名" name="realName">
<Input placeholder={'请填写真实姓名'} value={FormData?.realName || ''} />
</Form.Item>
<Form.Item label="年龄" name="age" initialValue={18} style={{
display: 'none'
}}>
<ConfigProvider theme={customTheme}>
<InputNumber defaultValue={1} />
</ConfigProvider>
<InputNumber
value={FormData?.age || 0}
/>
</Form.Item>
{FormData?.type == 1 && (
<Form.Item label="个人签名" name="introduction">
<TextArea
placeholder={'个人签名'}
style={{
height: '50px',
backgroundColor: '#fafafa',
padding: '10px',
}}
value={FormData?.introduction || ''}
/>
</Form.Item>
)}
{
FormData?.type == 2 && (
<Form.Item label="医生简介" name="introduction">
<TextArea
placeholder={'医生简介'}
style={{
height: '50px',
backgroundColor: '#fafafa',
padding: '10px',
}}
value={FormData?.introduction || ''}
/>
</Form.Item>
)
}
</CellGroup>
</Form>

View File

@@ -445,7 +445,7 @@ const CustomerIndex = () => {
<Space className="flex justify-end">
<Button
size="small"
onClick={() => navTo(`/dealer/customer/add?id=${customer.applyId}`, true)}
onClick={() => navTo(`/doctor/customer/add?id=${customer.applyId}`, true)}
style={{marginRight: '8px', backgroundColor: '#52c41a', color: 'white'}}
>
@@ -575,7 +575,7 @@ const CustomerIndex = () => {
{/* 客户列表 */}
{renderCustomerList()}
<FixedButton text={'客户报备'} onClick={() => Taro.navigateTo({url: '/dealer/customer/add'})}/>
<FixedButton text={'客户报备'} onClick={() => Taro.navigateTo({url: '/doctor/customer/add'})}/>
</View>
);
};

50
src/hooks/useConfig.ts Normal file
View File

@@ -0,0 +1,50 @@
import { useEffect, useState } from 'react';
import Taro from '@tarojs/taro';
import { configWebsiteField } from '@/api/cms/cmsWebsiteField';
import { Config } from '@/api/cms/cmsWebsiteField/model';
/**
* 自定义Hook用于获取和管理网站配置数据
* @returns {Object} 包含配置数据和加载状态的对象
*/
export const useConfig = () => {
const [config, setConfig] = useState<Config | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchConfig = async () => {
try {
setLoading(true);
const data = await configWebsiteField();
setConfig(data);
Taro.setStorageSync('config', data);
// 设置主题
if (data.theme && !Taro.getStorageSync('user_theme')) {
Taro.setStorageSync('user_theme', data.theme);
}
} catch (err) {
setError(err instanceof Error ? err : new Error('获取配置失败'));
console.error('获取网站配置失败:', err);
} finally {
setLoading(false);
}
};
fetchConfig();
}, []);
return { config, loading, error, refetch: () => {
setLoading(true);
setError(null);
configWebsiteField().then(data => {
setConfig(data);
Taro.setStorageSync('config', data);
setLoading(false);
}).catch(err => {
setError(err instanceof Error ? err : new Error('获取配置失败'));
setLoading(false);
});
}};
};

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '分类'
})

View File

@@ -0,0 +1,425 @@
.category-container {
display: flex;
height: calc(100vh - 1rpx);
background-color: #f5f5f5;
}
/* 左侧分类导航 */
.category-left {
width: 200rpx;
background-color: #fff;
border-right: 2rpx solid #f0f0f0;
position: relative;
z-index: 10;
.category-scroll {
height: calc(100vh - 1rpx);
}
.category-item {
padding: 30rpx 20rpx;
background-color: #fff;
transition: all 0.3s ease;
position: relative;
cursor: pointer;
&:hover {
background-color: #f8f8f8;
}
&.active {
background-color: #fff;
color: #ff6b35;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 50rpx;
background: linear-gradient(180deg, #ff6b35 0%, #ff8f6b 100%);
border-radius: 0 8rpx 8rpx 0;
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.3);
}
&::after {
content: '';
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 2rpx;
background-color: #fff;
z-index: 1;
}
}
.category-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
display: block;
text-align: center;
line-height: 1.2;
transition: color 0.3s ease;
}
.category-count {
font-size: 20rpx;
color: #999;
margin-top: 8rpx;
display: block;
text-align: center;
}
&.active .category-name {
color: #ff6b35;
font-weight: 600;
}
&.active .category-count {
color: #ff6b35;
}
}
}
/* 右侧商品列表 */
.category-right {
flex: 1;
background-color: #fff;
position: relative;
.goods-scroll {
height: calc(100vh - 1rpx);
}
.goods-section {
.section-title {
position: sticky;
top: 0;
background: linear-gradient(135deg, #f8f8f8 0%, #f0f0f0 100%);
padding: 24rpx 30rpx;
border-bottom: 2rpx solid #f0f0f0;
z-index: 10;
text {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.goods-list {
padding: 20rpx 30rpx;
}
.goods-item {
display: flex;
padding: 24rpx 0;
border-bottom: 2rpx solid #f8f8f8;
transition: all 0.3s ease;
border-radius: 12rpx;
margin-bottom: 8rpx;
cursor: pointer;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
&:hover {
background-color: #fafafa;
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.goods-image {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
margin-right: 24rpx;
flex-shrink: 0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
}
}
.goods-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.goods-name {
font-size: 30rpx;
color: #333;
font-weight: 600;
line-height: 1.4;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.goods-desc {
font-size: 24rpx;
color: #666;
line-height: 1.3;
margin-bottom: 12rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.goods-price-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.goods-price {
font-size: 36rpx;
color: #ff6b35;
font-weight: 700;
margin-right: 16rpx;
position: relative;
&::before {
content: '¥';
font-size: 24rpx;
position: relative;
top: -2rpx;
}
}
.goods-original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
position: relative;
&::before {
content: '¥';
font-size: 20rpx;
}
}
}
.goods-stock {
font-size: 22rpx;
color: #999;
background-color: #f8f8f8;
padding: 4rpx 8rpx;
border-radius: 4rpx;
display: inline-block;
width: fit-content;
}
}
}
.empty-section {
padding: 120rpx 30rpx;
text-align: center;
text {
font-size: 28rpx;
color: #999;
position: relative;
&::before {
content: '📋';
display: block;
font-size: 80rpx;
margin-bottom: 20rpx;
}
}
}
}
}
/* 骨架屏样式 */
.category-skeleton {
display: flex;
height: calc(100vh - 1rpx);
.category-left {
width: 200rpx;
background-color: #fff;
border-right: 2rpx solid #f0f0f0;
padding: 20rpx 0;
.skeleton-category-item {
padding: 30rpx 20rpx;
.skeleton-text {
height: 32rpx;
background-color: #f0f0f0;
border-radius: 4rpx;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
}
}
.category-right {
flex: 1;
background-color: #fff;
padding: 30rpx;
.skeleton-goods-item {
display: flex;
padding: 24rpx 0;
border-bottom: 2rpx solid #f8f8f8;
.skeleton-image {
width: 160rpx;
height: 160rpx;
background-color: #f0f0f0;
border-radius: 16rpx;
margin-right: 24rpx;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-info {
flex: 1;
.skeleton-text {
height: 32rpx;
background-color: #f0f0f0;
border-radius: 4rpx;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
}
}
}
}
@keyframes skeleton-loading {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
/* 响应式设计 */
@media (max-width: 750rpx) {
.category-left {
width: 160rpx;
.category-item {
padding: 24rpx 16rpx;
.category-name {
font-size: 26rpx;
}
.category-count {
font-size: 18rpx;
}
}
}
.category-right {
.goods-section {
.goods-list {
padding: 16rpx 20rpx;
}
.goods-item {
padding: 20rpx 0;
.goods-image {
width: 140rpx;
height: 140rpx;
margin-right: 20rpx;
}
.goods-info {
.goods-name {
font-size: 28rpx;
}
.goods-desc {
font-size: 22rpx;
}
.goods-price-row {
.goods-price {
font-size: 32rpx;
}
}
}
}
}
}
}
/* 空状态样式 */
.empty-container {
height: calc(100vh - 1rpx);
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
.empty-content {
text-align: center;
padding: 60rpx 40rpx;
.empty-icon {
font-size: 120rpx;
display: block;
margin-bottom: 30rpx;
}
.empty-title {
font-size: 32rpx;
color: #333;
font-weight: 600;
display: block;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 40rpx;
line-height: 1.4;
}
.empty-action {
background: linear-gradient(135deg, #ff6b35 0%, #ff8f6b 100%);
color: #fff;
padding: 20rpx 40rpx;
border-radius: 50rpx;
font-size: 28rpx;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.4);
}
&:active {
transform: translateY(0);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 53, 0.3);
}
}
}
}

View File

@@ -0,0 +1,270 @@
import Taro from '@tarojs/taro'
import { useShareAppMessage } from "@tarojs/taro"
import { useEffect, useState, useRef, useCallback } from "react"
import { View, Text, ScrollView } from '@tarojs/components'
import { Image } from '@nutui/nutui-react-taro'
import { listCmsNavigation } from "@/api/cms/cmsNavigation"
import { CmsNavigation } from "@/api/cms/cmsNavigation/model"
import { pageShopGoods } from "@/api/shop/shopGoods"
import { ShopGoods } from "@/api/shop/shopGoods/model"
import './category.scss'
function Category() {
const [loading, setLoading] = useState<boolean>(true)
const [categories, setCategories] = useState<CmsNavigation[]>([])
const [selectedCategoryId, setSelectedCategoryId] = useState<number>(0)
const [goods, setGoods] = useState<{ [key: number]: ShopGoods[] }>({})
const [allGoods, setAllGoods] = useState<ShopGoods[]>([])
const rightScrollRef = useRef<any>(null)
const [scrollIntoView, setScrollIntoView] = useState('')
const [isScrollingByClick, setIsScrollingByClick] = useState(false)
// 初始化数据
const initData = async () => {
try {
setLoading(true)
// 获取商品分类
const categoryList = await listCmsNavigation({ model: 'goods' })
if (!categoryList || categoryList.length === 0) {
Taro.showToast({
title: '暂无商品分类',
icon: 'none'
})
setLoading(false)
return
}
setCategories(categoryList)
const firstCategory = categoryList[0]
setSelectedCategoryId(firstCategory.navigationId!)
// 并行获取所有分类的商品数据
const goodsPromises = categoryList.map((category: CmsNavigation) =>
pageShopGoods({ categoryId: category.navigationId }).catch(err => {
console.error(`分类 ${category.title} 商品加载失败:`, err)
return { list: [] }
})
)
const goodsResults = await Promise.all(goodsPromises)
// 组织商品数据
const goodsByCategory: { [key: number]: ShopGoods[] } = {}
categoryList.forEach((category: CmsNavigation, index: number) => {
goodsByCategory[category.navigationId!] = goodsResults[index]?.list || []
})
setGoods(goodsByCategory)
// 获取所有商品用于搜索等功能
try {
const allGoodsRes = await pageShopGoods({})
setAllGoods(allGoodsRes?.list || [])
} catch (err) {
console.error('获取所有商品失败:', err)
}
Taro.setNavigationBarTitle({
title: '商品分类'
})
} catch (error) {
console.error('分类数据加载失败:', error)
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
setLoading(false)
}
}
useEffect(() => {
initData().then()
console.log(allGoods,'allGoods')
}, [])
// 点击左侧分类
const handleCategoryClick = (categoryId: number) => {
setIsScrollingByClick(true)
setSelectedCategoryId(categoryId)
setScrollIntoView(`category-${categoryId}`)
// 延迟重置滚动标志
setTimeout(() => {
setIsScrollingByClick(false)
}, 1000)
}
// 右侧滚动时处理分类切换
const handleRightScroll = useCallback((e: any) => {
console.log(e,'右侧滚动时处理分类切换')
if (isScrollingByClick) return
// 这里可以添加逻辑来检测当前滚动到哪个分类
// 由于小程序限制,暂时简化处理
}, [isScrollingByClick])
// 跳转商品详情
const goToGoodsDetail = (goodsId: number) => {
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${goodsId}` })
}
useShareAppMessage(() => {
return {
title: '商品分类',
path: '/pages/category/category',
success: function () {
console.log('分享成功')
},
fail: function () {
console.log('分享失败')
}
}
})
// 骨架屏组件
const CategorySkeleton = () => (
<View className="category-skeleton" style={{
marginTop: '1rpx',
}}>
<View className="category-left">
{[1, 2, 3, 4, 5].map(i => (
<View key={i} className="skeleton-category-item">
<View className="skeleton-text" />
</View>
))}
</View>
<View className="category-right">
{[1, 2, 3, 4].map(i => (
<View key={i} className="skeleton-goods-item">
<View className="skeleton-image" />
<View className="skeleton-info">
<View className="skeleton-text" style={{ width: '80%', marginBottom: '8px' }} />
<View className="skeleton-text" style={{ width: '60%' }} />
</View>
</View>
))}
</View>
</View>
)
if (loading) {
return <CategorySkeleton />
}
// 空状态处理
if (!categories || categories.length === 0) {
return (
<View className="empty-container">
<View className="empty-content">
<Text className="empty-icon">📋</Text>
<Text className="empty-title"></Text>
<Text className="empty-desc"></Text>
<View className="empty-action" onClick={initData}>
<Text></Text>
</View>
</View>
</View>
)
}
return (
<View className="category-container" style={{
marginTop: '1rpx',
}}>
{/* 左侧分类导航 */}
<View className="category-left">
<ScrollView
className="category-scroll"
scrollY
enhanced
showScrollbar={false}
> {categories.map((category) => (
<View
key={category.navigationId}
className={`category-item ${
selectedCategoryId === category.navigationId ? 'active' : ''
}`}
onClick={() => handleCategoryClick(category.navigationId!)}
>
<Text className="category-name">{category.title}</Text>
</View>
))}
</ScrollView>
</View>
{/* 右侧商品列表 */}
<View className="category-right">
<ScrollView
ref={rightScrollRef}
className="goods-scroll"
scrollY
enhanced
showScrollbar={false}
scrollIntoView={scrollIntoView}
onScroll={handleRightScroll}
>
{categories.map((category) => {
const categoryGoods = goods[category.navigationId!] || []
return (
<View
key={category.navigationId}
id={`category-${category.navigationId}`}
className="goods-section"
>
<View className="section-title">
<Text>{category.title}</Text>
</View>
{categoryGoods.length > 0 ? (
<View className="goods-list">
{categoryGoods.map((item) => (
<View
key={item.goodsId}
className="goods-item"
onClick={() => goToGoodsDetail(item.goodsId!)}
>
<Image
className="goods-image"
src={item.image || ''}
mode="aspectFill"
lazyLoad
width={80}
height={80}
/>
<View className="goods-info">
<Text className="goods-name">{item.name}</Text>
{item.comments && (
<Text className="goods-desc">{item.comments}</Text>
)}
<View className="goods-price-row">
<Text className="goods-price">¥{item.price}</Text>
{item.salePrice && Number(item.salePrice) !== Number(item.price) && (
<Text className="goods-original-price">¥{item.salePrice}</Text>
)}
</View>
{item.stock !== undefined && (
<Text className="goods-stock">
: {item.stock > 0 ? item.stock : '缺货'}
</Text>
)}
</View>
</View>
))}
</View>
) : (
<View className="empty-section">
<Text></Text>
</View>
)}
</View>
)
})}
</ScrollView>
</View>
</View>
)
}
export default Category

View File

@@ -0,0 +1,35 @@
import {Image, Cell} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
const ArticleList = (props: any) => {
return (
<>
<View className={'px-3'}>
{props.data.map((item: any, index: number) => {
return (
<Cell
title={
<View>
<View className="text-base font-medium mb-1">{item.title}</View>
{item.comments && (
<Text className="text-sm text-gray-500 leading-relaxed">
{item.comments}
</Text>
)}
</View>
}
extra={
<Image src={item.image} mode={'aspectFit'} lazyLoad={false} width={100} height="100"/>
}
key={index}
onClick={() => Taro.navigateTo({url: '/cms/detail/index?id=' + item.articleId})}
/>
)
})}
</View>
</>
)
}
export default ArticleList

View File

@@ -0,0 +1,59 @@
import {useEffect, useState} from "react";
import {Tabs, Loading} from '@nutui/nutui-react-taro'
import {pageCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import ArticleList from "./ArticleList";
const ArticleTabs = (props: any) => {
const [loading, setLoading] = useState<boolean>(true)
const [tab1value, setTab1value] = useState<string | number>('0')
const [list, setList] = useState<CmsArticle[]>([])
const reload = async (value) => {
const {data} = props
pageCmsArticle({
categoryId: data[value].navigationId,
page: 1,
status: 0,
limit: 10
}).then((res) => {
res && setList(res?.list || [])
})
.catch(err => {
console.log(err)
})
.finally(() => {
setTab1value(value)
setLoading(false)
})
}
useEffect(() => {
reload(0).then()
}, []);
if (loading) {
return (
<Loading className={'px-2'}></Loading>
)
}
return (
<>
<Tabs
value={tab1value}
onChange={(value) => {
reload(value).then()
}}
>
{props.data?.map((item, index) => {
return (
<Tabs.TabPane title={item.categoryName} key={index}/>
)
})}
</Tabs>
<ArticleList data={list}/>
</>
)
}
export default ArticleTabs

View File

@@ -0,0 +1,31 @@
import { useEffect, useState } from 'react'
import { Swiper } from '@nutui/nutui-react-taro'
import {CmsAd} from "@/api/cms/cmsAd/model";
import {Image} from '@nutui/nutui-react-taro'
import {getCmsAd} from "@/api/cms/cmsAd";
const MyPage = () => {
const [item, setItem] = useState<CmsAd>()
const reload = () => {
getCmsAd(439).then(data => {
setItem(data)
})
}
useEffect(() => {
reload()
}, [])
return (
<>
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
{item?.imageList?.map((item) => (
<Swiper.Item key={item}>
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
</Swiper.Item>
))}
</Swiper>
</>
)
}
export default MyPage

View File

@@ -5,55 +5,123 @@ import {CmsAd} from "@/api/cms/cmsAd/model";
import {Image} from '@nutui/nutui-react-taro'
import {getCmsAdByCode} from "@/api/cms/cmsAd";
import navTo from "@/utils/common";
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {listShopGoods} from "@/api/shop/shopGoods";
import {pageCmsArticle} from "@/api/cms/cmsArticle";
import {CmsArticle} from "@/api/cms/cmsArticle/model";
const MyPage = () => {
const [carouselData, setCarouselData] = useState<CmsAd>()
// const [hotToday, setHotToday] = useState<CmsAd>()
// const [groupBuy, setGroupBuy] = useState<CmsAd>()
const [hotGoods, setHotGoods] = useState<ShopGoods[]>([])
const [hotToday, setHotToday] = useState<CmsAd>()
const [item, setItem] = useState<CmsArticle>()
const [loading, setLoading] = useState(true)
// const [disableSwiper, setDisableSwiper] = useState(false)
// 用于记录触摸开始位置
// const touchStartRef = useRef({x: 0, y: 0})
// 加载数据
const loadData = () => {
const loadData = async () => {
try {
setLoading(true)
// 轮播图
getCmsAdByCode('flash').then(data => {
setCarouselData(data)
})
// 今日热卖素材(上层图片)
// getCmsAd(444).then(data => {
// setHotToday(data)
// })
// 社区拼团素材(下层图片)
// getCmsAd(445).then(data => {
// setGroupBuy(data)
// })
const flash = await getCmsAdByCode('flash')
// 今日热卖
listShopGoods({categoryId: 4424, limit: 2}).then(data => {
setHotGoods(data)
})
const hotToday = await getCmsAdByCode('hot_today')
// 时里动态
const news = await pageCmsArticle({limit:1,recommend:1})
// 赋值
if(flash){
setCarouselData(flash)
}
if(hotToday){
setHotToday(hotToday)
}
if(news && news.list.length > 0){
setItem(news.list[0])
}
} catch (error) {
console.error('Banner数据加载失败:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
loadData()
}, [])
// 轮播图高度,默认200px
const carouselHeight = carouselData?.height || 200;
// 轮播图高度,默认300px
const carouselHeight = carouselData?.height || 300;
// 骨架屏组件
const BannerSkeleton = () => (
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
{/* 左侧轮播图骨架屏 */}
<View style={{width: '50%', height: '100%'}}>
<View
className="bg-gray-200 rounded animate-pulse"
style={{height: `${carouselHeight}px`}}
/>
</View>
{/* 右侧骨架屏 */}
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
{/* 上层骨架屏 */}
<View className="ml-2 bg-white rounded-lg">
<View className="px-3 my-2">
<View className="bg-gray-200 h-4 w-16 rounded animate-pulse"/>
</View>
<View className="px-3 flex" style={{height: '110px'}}>
{[1, 2].map(i => (
<View key={i} className="item flex flex-col mr-4">
<View className="bg-gray-200 rounded animate-pulse" style={{width: 70, height: 70}}/>
<View className="bg-gray-200 h-3 w-16 rounded mt-2 animate-pulse"/>
</View>
))}
</View>
</View>
{/* 下层骨架屏 */}
<View className="ml-2 bg-white rounded-lg mt-3">
<View className="px-3 my-2">
<View className="bg-gray-200 h-4 w-20 rounded animate-pulse"/>
</View>
<View className="rounded-lg px-3 pb-3">
<View className="bg-gray-200 rounded animate-pulse" style={{width: '100%', height: 106}}/>
</View>
</View>
</View>
</View>
)
// 如果正在加载,显示骨架屏
if (loading) {
return <BannerSkeleton />
}
return (
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
{/* 左侧轮播图区域 */}
<View style={{width: '50%', height: '100%'}}>
<View
style={{width: '50%', height: '100%'}}
className="banner-swiper-container"
>
<Swiper
defaultValue={0}
height={carouselHeight}
indicator
style={{height: `${carouselHeight}px`}}
autoPlay
duration={3000}
style={{
height: `${carouselHeight}px`,
touchAction: 'pan-y' // 关键修改:允许垂直滑动
}}
disableTouch={false}
direction="horizontal"
className="custom-swiper"
>
{carouselData?.imageList?.map((img, index) => (
<Swiper.Item key={index}>
{carouselData && carouselData?.imageList?.map((img, index) => (
<Swiper.Item key={index} style={{ touchAction: 'pan-x pan-y' }}>
<Image
width="100%"
height="100%"
@@ -61,7 +129,11 @@ const MyPage = () => {
mode={'scaleToFill'}
onClick={() => navTo(`${img.path}`)}
lazyLoad={false}
style={{height: `${carouselHeight}px`, borderRadius: '4px'}}
style={{
height: `${carouselHeight}px`,
borderRadius: '4px',
touchAction: 'manipulation' // 关键修改:优化触摸操作
}}
/>
</Swiper.Item>
))}
@@ -71,26 +143,26 @@ const MyPage = () => {
{/* 右侧上下图片区域 - 从API获取数据 */}
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
{/* 上层图片 - 使用今日热卖素材 */}
<View className={'ml-2 bg-white rounded-lg'}>
<View className={'ml-2 bg-white rounded-lg shadow-sm'}>
<View className={'px-3 my-2 font-bold text-sm'}></View>
<View className={'px-3 flex'} style={{
<View className={'px-3 flex justify-between'} style={{
height: '110px'
}}>
{
hotGoods.map(item => (
<View className={'item flex flex-col mr-4'}>
hotToday?.imageList?.map(item => (
<View className={'item flex flex-col mr-1'} key={item.url}>
<Image
width={70}
height={70}
src={item.image}
src={item.url}
mode={'scaleToFill'}
lazyLoad={false}
style={{
borderRadius: '4px'
}}
onClick={() => navTo('/shop/category/index?id=4424')}
onClick={() => navTo(item.path)}
/>
<View className={'text-xs py-2'}>¥{item.price}</View>
<View className={'text-xs py-2 text-orange-600 whitespace-nowrap text-center'}>{item.title || '到手价¥9.9'}</View>
</View>
))
}
@@ -98,19 +170,19 @@ const MyPage = () => {
</View>
{/* 下层图片 - 使用社区拼团素材 */}
<View className={'ml-2 bg-white rounded-lg mt-3'}>
<View className={'px-3 my-2 font-bold text-sm'}></View>
<View className={'ml-2 bg-white rounded-lg mt-3 shadow-sm'}>
<View className={'px-3 my-2 font-bold text-sm'}>{item?.overview || item?.categoryName || '推荐文章'}</View>
<View className={'rounded-lg px-3 pb-3'}>
<Image
width={'100%'}
height={100}
src={'https://oss.wsdns.cn/20250919/941c99899e694a7798cab3bb28f1f238.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90'}
height={94}
src={item?.image}
mode={'scaleToFill'}
lazyLoad={false}
style={{
borderRadius: '4px'
}}
onClick={() => navTo('cms/detail/index?id=10109')}
onClick={() => navTo('cms/detail/index?id=' + item?.articleId)}
/>
</View>
</View>
@@ -120,4 +192,3 @@ const MyPage = () => {
}
export default MyPage

View File

@@ -1,4 +0,0 @@
// 订单页面样式
.order-page {
// 订单相关样式
}

View File

@@ -3,18 +3,28 @@ import navTo from "@/utils/common";
import {View, Text} from '@tarojs/components'
import {ArrowRight, Reward, Setting} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
import {useEffect} from "react";
import {useDealerUser} from "@/hooks/useDealerUser";
import {useThemeStyles} from "@/hooks/useTheme";
import { useConfig } from "@/hooks/useConfig";
import {useEffect, useState} from "react";
import {getCmsAdByCode} from "@/api/cms/cmsAd";
import {CmsAd} from "@/api/cms/cmsAd/model"; // 使用新的自定义Hook
const IsDealer = () => {
const themeStyles = useThemeStyles();
const { config } = useConfig(); // 使用新的Hook
const {isSuperAdmin} = useUser();
const {dealerUser} = useDealerUser()
const [register, setRegister] = useState<CmsAd>()
const reload = async () => {
const item = await getCmsAdByCode('register')
setRegister(item)
}
useEffect(() => {
}, [])
reload().then()
}, []);
/**
* 管理中心
@@ -54,7 +64,7 @@ const IsDealer = () => {
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}}
className={'pl-3 text-orange-100 font-medium'}>VIP申请</Text>
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '入驻申请'}</Text>
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
</View>
}
@@ -78,12 +88,12 @@ const IsDealer = () => {
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>VIP</Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{register?.name || '注册会员'}</Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/doctor/apply/add', true)}
onClick={() => navTo(`${register?.path}`, true)}
/>
</View>
</>

View File

@@ -53,7 +53,29 @@ const DEFAULT_CONFIG = {
showError: true
};
let baseUrl = BaseUrl;
// 获取API基础地址的函数
const getBaseUrl = (): string => {
// 尝试从本地存储获取后台配置的API地址
try {
const configStr = Taro.getStorageSync('config');
if (configStr) {
// 如果是字符串,需要解析为对象
const config = typeof configStr === 'string' ? JSON.parse(configStr) : configStr;
console.log('获取后台配置API地址:', config);
// 注意属性名是 ApiUrl首字母大写不是 apiUrl
if (config && config.ApiUrl) {
console.log('使用后台配置的API地址:', config.ApiUrl);
return config.ApiUrl;
}
}
} catch (error) {
console.warn('获取后台配置API地址失败:', error);
}
// 如果后台没有配置API地址则使用本地配置
console.log('使用本地配置的API地址:', BaseUrl);
return BaseUrl;
};
// 开发环境配置
if (process.env.NODE_ENV === 'development') {
@@ -303,8 +325,11 @@ export async function request<T>(options: RequestConfig): Promise<T> {
// 构建完整URL
const buildUrl = (url: string): string => {
// 每次构建URL时都检查最新的baseUrl
const currentBaseUrl = getBaseUrl();
if (url.indexOf('http') === -1) {
return baseUrl + url;
return currentBaseUrl + url;
}
return url;
};