Compare commits

...

7 Commits

Author SHA1 Message Date
xinadmin 8f9b54ce76 运营报表优化 2026-09-07 23:09:44 +08:00
xinadmin a85da7a5d1 账单备注调整 2026-09-07 22:37:53 +08:00
xinadmin 1b9e505feb 调整样式 2026-09-06 23:15:13 +08:00
xinadmin 74aee003e7 H5支付 2026-09-04 21:54:10 +08:00
xinadmin e2926b0c70 H5适配 2026-09-04 14:10:33 +08:00
xinadmin 1701169c9d 账单样式 2026-09-03 20:47:55 +08:00
xinadmin e97cc128ec 首页推荐修改 2026-09-03 19:37:42 +08:00
26 changed files with 472 additions and 154 deletions
+2 -2
View File
@@ -4,8 +4,8 @@
flex-shrink: 0; flex-shrink: 0;
&__btn { &__btn {
width: 52rpx; width: 42rpx;
height: 52rpx; height: 42rpx;
border-radius: 50%; border-radius: 50%;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
@@ -1,8 +1,6 @@
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import {View, Text, Image} from '@tarojs/components' import {View, Text, Image} from '@tarojs/components'
import useAuthStore from '@/stores/auth/useAuthStore'
import useCartStore from '@/stores/cart/useCartStore'
import IndexImage from '@/static/images/nav/index.png'; import IndexImage from '@/static/images/nav/index.png';
import IndexActiveImage from '@/static/images/nav/index_active.png'; import IndexActiveImage from '@/static/images/nav/index_active.png';
import CartImage from '@/static/images/nav/cart.png'; import CartImage from '@/static/images/nav/cart.png';
+1
View File
@@ -10,6 +10,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" > <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
<title>订货采购</title> <title>订货采购</title>
<script crossorigin="anonymous" src="https://polyfill.alicdn.com/polyfill.min.js?features=es2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022"></script> <script crossorigin="anonymous" src="https://polyfill.alicdn.com/polyfill.min.js?features=es2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022"></script>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script><%= htmlWebpackPlugin.options.script %></script> <script><%= htmlWebpackPlugin.options.script %></script>
</head> </head>
<body> <body>
-2
View File
@@ -1,5 +1,4 @@
import { View, Text, ScrollView } from '@tarojs/components' import { View, Text, ScrollView } from '@tarojs/components'
import CustomNavBar from '@/components/NavBar'
import './index.less' import './index.less'
/** /**
@@ -9,7 +8,6 @@ import './index.less'
export default function AgreementPage() { export default function AgreementPage() {
return ( return (
<View className='agreement-page'> <View className='agreement-page'>
<CustomNavBar title='用户服务协议' />
<ScrollView scrollY className='agreement-scroll'> <ScrollView scrollY className='agreement-scroll'>
<View className='agreement-content'> <View className='agreement-content'>
<Text className='doc-title'></Text> <Text className='doc-title'></Text>
+10 -2
View File
@@ -115,8 +115,8 @@
} }
&__img { &__img {
width: 72rpx; width: 120rpx;
height: 72rpx; height: 120rpx;
border-radius: 8rpx; border-radius: 8rpx;
background: #f2f3f5; background: #f2f3f5;
flex-shrink: 0; flex-shrink: 0;
@@ -149,6 +149,14 @@
&__amount { &__amount {
font-size: 28rpx; font-size: 28rpx;
color: #ee0a24;
font-weight: 500;
display: block;
margin-top: 4rpx;
}
&__price {
font-size: 24rpx;
color: #323233; color: #323233;
font-weight: 500; font-weight: 500;
display: block; display: block;
+13 -11
View File
@@ -104,12 +104,6 @@ export default function BillDetailPage() {
<Text className='bill-card__value'>{bill.pay_remark}</Text> <Text className='bill-card__value'>{bill.pay_remark}</Text>
</View> </View>
)} )}
{bill.remark && (
<View className='bill-card__row'>
<Text className='bill-card__label'></Text>
<Text className='bill-card__value'>{bill.remark}</Text>
</View>
)}
</View> </View>
{/* ===== 金额构成 ===== */} {/* ===== 金额构成 ===== */}
@@ -141,6 +135,10 @@ export default function BillDetailPage() {
{Number(bill.after_sale) < 0 ? `- ¥${bill.after_sale}` : `${bill.after_sale}`} {Number(bill.after_sale) < 0 ? `- ¥${bill.after_sale}` : `${bill.after_sale}`}
</Text> </Text>
</View> </View>
<View className='bill-card__row'>
<Text className='bill-card__label'></Text>
<Text className='bill-card__value'>{bill.remark || '暂无备注'}</Text>
</View>
<View className='bill-card__row bill-card__row--total'> <View className='bill-card__row bill-card__row--total'>
<Text className='bill-card__label'></Text> <Text className='bill-card__label'></Text>
<Text className='bill-card__total'>{bill.total_amount}</Text> <Text className='bill-card__total'>{bill.total_amount}</Text>
@@ -161,13 +159,17 @@ export default function BillDetailPage() {
/> />
)} )}
<View className='bill-goods__main'> <View className='bill-goods__main'>
<Text className='bill-goods__name'>{item.product_name}</Text> <View className='bill-goods__name'>{item.product_name}</View>
<Text className='bill-goods__spec'> <View className='bill-goods__spec'>
{formatSpec(item.product_spec, item.unit)}{' ¥'}{item.price} {formatSpec(item.product_spec, item.unit)}
</Text> </View>
<View className='bill-goods__spec'>
{formatRetailPrice(item.price, item.spec)} {item.price_unit}
</View>
</View> </View>
<View className='bill-goods__side'> <View className='bill-goods__side'>
<Text className='bill-goods__amount'>{item.price} × {item.quantity}</Text> <Text className='bill-goods__price'>{item.price} × {item.quantity}</Text>
<View className='bill-goods__amount'>{item.amount}</View>
</View> </View>
</View> </View>
))} ))}
+26 -29
View File
@@ -9,16 +9,24 @@
padding-bottom: 160rpx; padding-bottom: 160rpx;
} }
// ===== 待支付汇总 ===== // 底部待支付汇总栏留出空间
.bill-summary { &--pay {
padding-bottom: 180rpx;
}
// ===== 底部待支付汇总栏 =====
.bill-paybar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background: linear-gradient(135deg, #ee0a24, #ff4d4f); padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
border-radius: 16rpx; background: #fff;
padding: 28rpx; box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
margin-bottom: 20rpx;
color: #fff;
&__info { &__info {
display: flex; display: flex;
@@ -26,35 +34,24 @@
} }
&__label { &__label {
font-size: 24rpx; font-size: 22rpx;
opacity: 0.85; color: #969799;
}
&__count {
margin-top: 8rpx;
font-size: 28rpx;
font-weight: 600;
} }
&__amount { &__amount {
font-size: 40rpx; margin-top: 4rpx;
font-size: 36rpx;
color: #ee0a24;
font-weight: 600; font-weight: 600;
} }
&__right { &__btn {
display: flex; padding: 16rpx 56rpx;
flex-direction: column;
align-items: flex-end;
}
&__pay {
margin-top: 12rpx;
padding: 8rpx 32rpx;
background: #fff;
color: #ee0a24;
font-size: 24rpx;
font-weight: 500;
border-radius: 999rpx; border-radius: 999rpx;
background: linear-gradient(135deg, #ee0a24, #ff6034);
color: #fff;
font-size: 28rpx;
font-weight: 500;
} }
} }
+16 -16
View File
@@ -29,7 +29,7 @@ interface CategoryOption {
/** /**
* 账单列表页 * 账单列表页
* 采购单完成后由后台按门店生成(只读);头部 summary 为门店口径待支付汇总(含审核中,不受筛选影响) * 采购单完成后由后台按门店生成(只读);底部汇总栏为门店口径待支付汇总(含审核中,不受筛选影响)
* 支持多选账单合并导出 Excel(可按一级分类过滤商品明细) * 支持多选账单合并导出 Excel(可按一级分类过滤商品明细)
*/ */
export default function BillListPage() { export default function BillListPage() {
@@ -185,22 +185,11 @@ export default function BillListPage() {
[selectedIds, toggleSelectMode], [selectedIds, toggleSelectMode],
) )
return ( /** 底部待支付汇总栏是否可见(多选导出时让位给导出栏) */
<View className={`bill-page ${selectMode ? 'bill-page--select' : ''}`}> const showPayBar = loggedIn && !selectMode && !!summary && summary.unpaid_count > 0
{/* ========== 待支付汇总(门店口径,含审核中) ========== */}
{loggedIn && summary && summary.unpaid_count > 0 && (
<View className='bill-summary'>
<View className='bill-summary__info'>
<Text className='bill-summary__label'></Text>
<Text className='bill-summary__count'>{summary.unpaid_count} </Text>
</View>
<View className='bill-summary__right'>
<Text className='bill-summary__amount'>{summary.unpaid_amount}</Text>
<View className='bill-summary__pay' onClick={goPay}></View>
</View>
</View>
)}
return (
<View className={`bill-page ${selectMode ? 'bill-page--select' : ''} ${showPayBar ? 'bill-page--pay' : ''}`}>
{/* ========== 状态筛选 + 导出入口 ========== */} {/* ========== 状态筛选 + 导出入口 ========== */}
<View className='bill-toolbar'> <View className='bill-toolbar'>
<ScrollView scrollX className='status-scroll'> <ScrollView scrollX className='status-scroll'>
@@ -278,6 +267,17 @@ export default function BillListPage() {
<View className='bill-loading'><Text></Text></View> <View className='bill-loading'><Text></Text></View>
)} )}
{/* ========== 底部待支付汇总栏(门店口径,含审核中) ========== */}
{showPayBar && summary && (
<View className='bill-paybar'>
<View className='bill-paybar__info'>
<Text className='bill-paybar__label'>{summary.unpaid_count} </Text>
<Text className='bill-paybar__amount'>{summary.unpaid_amount}</Text>
</View>
<View className='bill-paybar__btn' onClick={goPay}></View>
</View>
)}
{/* ========== 导出操作栏 ========== */} {/* ========== 导出操作栏 ========== */}
{selectMode && ( {selectMode && (
<View className='export-bar'> <View className='export-bar'>
+2 -3
View File
@@ -59,8 +59,8 @@
} }
&__image { &__image {
width: 150rpx; width: 180rpx;
height: 150rpx; height: 180rpx;
border-radius: 12rpx; border-radius: 12rpx;
background: #f2f3f5; background: #f2f3f5;
flex-shrink: 0; flex-shrink: 0;
@@ -108,7 +108,6 @@
} }
&__spec { &__spec {
margin-top: 10rpx;
font-size: 24rpx; font-size: 24rpx;
color: #969799; color: #969799;
} }
+4 -1
View File
@@ -10,6 +10,7 @@ import {formatRetailPrice, formatSpec} from '@/utils/format'
import type { CartItem } from '@/types/cart' import type { CartItem } from '@/types/cart'
import type { StoreDetail } from '@/types/store' import type { StoreDetail } from '@/types/store'
import './index.less' import './index.less'
import CustomTabBar from "@/components/CustomTabBar";
export default function CartPage() { export default function CartPage() {
const token = useAuthStore(s => s.token) const token = useAuthStore(s => s.token)
@@ -222,7 +223,7 @@ export default function CartPage() {
</View> </View>
<Text className='cart-item__spec'> <Text className='cart-item__spec'>
{formatSpec(item.spec, item.unit)}{' '} {formatSpec(item.spec, item.unit)}{' '}
{formatRetailPrice(item.price, item.spec)} {item.price_unit} <View>{formatRetailPrice(item.price, item.spec)} {item.price_unit}</View>
</Text> </Text>
<View className='cart-item__bottom'> <View className='cart-item__bottom'>
<Text className='cart-item__price'>{item.price}</Text> <Text className='cart-item__price'>{item.price}</Text>
@@ -351,6 +352,8 @@ export default function CartPage() {
</View> </View>
</View> </View>
</Popup> </Popup>
{process.env.TARO_ENV === 'h5' && <CustomTabBar />}
</View> </View>
) )
} }
+14 -3
View File
@@ -235,6 +235,17 @@
color: #969799; color: #969799;
} }
&__loading {
display: flex;
justify-content: center;
padding: 20rpx 0 8rpx;
}
&__loading-text {
font-size: 24rpx;
color: #969799;
}
&__login-btn { &__login-btn {
margin-top: 24rpx; margin-top: 24rpx;
padding: 14rpx 60rpx; padding: 14rpx 60rpx;
@@ -290,7 +301,7 @@
} }
&__bottom { &__bottom {
margin-top: 16rpx; margin-top: 8rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -309,8 +320,8 @@
} }
&__add { &__add {
width: 52rpx; width: 42rpx;
height: 52rpx; height: 42rpx;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #ee0a24, #ff6034); background: linear-gradient(135deg, #ee0a24, #ff6034);
display: flex; display: flex;
+126 -53
View File
@@ -1,22 +1,27 @@
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useRef, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro' import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components' import { View, Text, Image } from '@tarojs/components'
import { Grid, GridItem, Icon, Search, Swiper, SwiperItem } from '@antmjs/vantui' import { Grid, GridItem, Icon, Search, Swiper, SwiperItem } from '@antmjs/vantui'
import useCartStore from '@/stores/cart/useCartStore' import useCartStore from '@/stores/cart/useCartStore'
import { getHomeConfigApi } from '@/services/home' import { getHomeConfigApi } from '@/services/home'
import type { HomeConfig } from '@/services/home' import type { HomeConfig } from '@/services/home'
import { getProductListApi } from '@/services/product' import { getSpecialListApi, normalizeSpecialCart } from '@/services/special'
import { getProductCover } from '@/types/product' import { getProductCover } from '@/types/product'
import type { Product, ProductCartPatch } from '@/types/product' import type { Product, ProductCartPatch } from '@/types/product'
import { getToken } from '@/utils/request'
import CartBall from '@/components/CartBall' import CartBall from '@/components/CartBall'
import CartStepper from '@/components/CartStepper' import CartStepper from '@/components/CartStepper'
import {formatRetailPrice, formatSpec} from '@/utils/format' import {formatRetailPrice, formatSpec} from '@/utils/format'
import './index.less' import './index.less'
import CustomTabBar from "@/components/CustomTabBar";
/** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */ /** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */
const PENDING_CATEGORY_KEY = 'product_category_id' const PENDING_CATEGORY_KEY = 'product_category_id'
const PENDING_KEYWORD_KEY = 'product_keyword' const PENDING_KEYWORD_KEY = 'product_keyword'
/** 特价推荐每页条数 */
const SPECIAL_PAGE_SIZE = 10
/** tabBar 页面路径(link 跳转需改用 switchTab */ /** tabBar 页面路径(link 跳转需改用 switchTab */
const TAB_PATHS = [ const TAB_PATHS = [
'pages/index/index', 'pages/index/index',
@@ -45,8 +50,17 @@ export default function IndexPage() {
/** 首页配置(轮播图 / 宫格导航 / 促销卡片) */ /** 首页配置(轮播图 / 宫格导航 / 促销卡片) */
const [config, setConfig] = useState<HomeConfig>({ banners: [], navs: [], promos: [] }) const [config, setConfig] = useState<HomeConfig>({ banners: [], navs: [], promos: [] })
/** 推荐商品 */ /** 特价推荐商品(后台「客户端配置 → 特价推荐」标记,价格为登录门店的等级价) */
const [products, setProducts] = useState<Product[]>([]) const [specials, setSpecials] = useState<Product[]>([])
/** 特价推荐分页 */
const [specialPage, setSpecialPage] = useState(1)
const [specialHasMore, setSpecialHasMore] = useState(false)
/** 加载更多中(首屏重置不展示,避免已渲染列表下方闪烁) */
const [specialLoading, setSpecialLoading] = useState(false)
/** 特价推荐请求序号(返回 tab 重置与上拉加载并发时,仅采用最后一次响应) */
const specialSeqRef = useRef(0)
/** 是否有「加载更多」请求进行中 */
const specialLoadingRef = useRef(false)
/** 搜索框输入 */ /** 搜索框输入 */
const [keyword, setKeyword] = useState('') const [keyword, setKeyword] = useState('')
@@ -54,7 +68,13 @@ export default function IndexPage() {
useDidShow(() => { useDidShow(() => {
loadHomeConfig() loadHomeConfig()
loadRecommend() loadSpecials(1, true)
})
/** 上拉加载更多特价推荐 */
useReachBottom(() => {
if (!specialHasMore) return
loadSpecials(specialPage + 1, false)
}) })
/** 首页配置聚合数据(响应附带悬浮球汇总) */ /** 首页配置聚合数据(响应附带悬浮球汇总) */
@@ -69,15 +89,37 @@ export default function IndexPage() {
} }
}, [setSummary]) }, [setSummary])
/** 推荐商品 */ /**
const loadRecommend = useCallback(async () => { * 特价推荐商品(reset 时回到第一页整体替换)。
try { * 行结构与 /mini/product/list 一致;响应附带悬浮球汇总
const res = await getProductListApi({ page: 1, pageSize: 10 }) */
setProducts(res.data.data) const loadSpecials = useCallback(
} catch { async (pageNum: number, reset: boolean) => {
// 错误已由 request 层 toast if (!reset && specialLoadingRef.current) return
} const seq = ++specialSeqRef.current
}, []) specialLoadingRef.current = true
if (!reset) setSpecialLoading(true)
try {
const res = await getSpecialListApi({ page: pageNum, pageSize: SPECIAL_PAGE_SIZE })
if (seq !== specialSeqRef.current) return // 已有更新的请求,丢弃本次响应
const { data, total, cart } = res.data
setSpecials(prev => (reset ? data : [...prev, ...data]))
setSpecialPage(pageNum)
setSpecialHasMore(pageNum * SPECIAL_PAGE_SIZE < total)
// 列表响应附带悬浮球汇总(旧版本后端可能未返回)
const summary = normalizeSpecialCart(cart)
if (summary) setSummary(summary)
} catch {
// 错误已由 request 层 toast
} finally {
if (seq === specialSeqRef.current) {
specialLoadingRef.current = false
setSpecialLoading(false)
}
}
},
[setSummary],
)
/** /**
* 后台配置的 link 统一跳转: * 后台配置的 link 统一跳转:
@@ -115,9 +157,9 @@ export default function IndexPage() {
goProduct(keyword.trim()) goProduct(keyword.trim())
}, [goProduct, keyword]) }, [goProduct, keyword])
/** 行内加减购确认后回写推荐商品项的购物车字段 */ /** 行内加减购确认后回写特价商品项的购物车字段 */
const handleRowSync = useCallback((productId: number, patch: ProductCartPatch) => { const handleRowSync = useCallback((productId: number, patch: ProductCartPatch) => {
setProducts(prev => prev.map(p => (p.id === productId ? { ...p, ...patch } : p))) setSpecials(prev => prev.map(p => (p.id === productId ? { ...p, ...patch } : p)))
}, []) }, [])
/** 快捷加购(用返回的购物车行回写,卡片随即展示加减器) */ /** 快捷加购(用返回的购物车行回写,卡片随即展示加减器) */
@@ -135,6 +177,16 @@ export default function IndexPage() {
[addItem, handleRowSync], [addItem, handleRowSync],
) )
/** 无价格时点击:未登录引导登录,已登录但未设等级价提示原因 */
const handlePriceGuide = useCallback((e: any) => {
e.stopPropagation()
if (getToken()) {
Taro.showToast({ title: '该商品暂未设置等级价', icon: 'none' })
} else {
Taro.navigateTo({ url: '/pages/login/index' })
}
}, [])
return ( return (
<View className='home-page'> <View className='home-page'>
{/* ========== 自定义顶部导航栏 ========== */} {/* ========== 自定义顶部导航栏 ========== */}
@@ -237,62 +289,83 @@ export default function IndexPage() {
</View> </View>
)} )}
{/* ========== 推荐商品 ========== */} {/* ========== 特价推荐 ========== */}
<View className='home-recommend'> <View className='home-recommend'>
<View className='home-recommend__header'> <View className='home-recommend__header'>
<View className='home-recommend__title-wrap'> <View className='home-recommend__title-wrap'>
<View className='home-recommend__title-bar' /> <View className='home-recommend__title-bar' />
<Text className='home-recommend__title'></Text> <Text className='home-recommend__title'></Text>
</View> </View>
<Text className='home-recommend__more' onClick={() => goProduct()}> </Text> <Text className='home-recommend__more' onClick={() => goProduct()}> </Text>
</View> </View>
{ products.length === 0 ? ( { specials.length === 0 ? (
<View className='home-recommend__empty'> <View className='home-recommend__empty'>
<Text className='home-recommend__empty-text'></Text> <Text className='home-recommend__empty-text'></Text>
</View> </View>
) : ( ) : (
<View className='product-grid'> <>
{products.map(product => ( <View className='product-grid'>
<View key={product.id} className='product-card' onClick={() => goDetail(product.id)}> {specials.map(product => (
<Image <View key={product.id} className='product-card' onClick={() => goDetail(product.id)}>
className='product-card__image' <Image
src={getProductCover(product) || 'https://img.yzcdn.cn/vant/cat.jpeg'} className='product-card__image'
mode='aspectFill' src={getProductCover(product) || 'https://img.yzcdn.cn/vant/cat.jpeg'}
lazyLoad mode='aspectFill'
/> lazyLoad
<View className='product-card__info'> />
<Text className='product-card__name'>{product.name}</Text> <View className='product-card__info'>
<View className='product-card__spec'> <Text className='product-card__name'>{product.name}</Text>
{formatSpec(product.spec, product.unit)}{' '} <View className='product-card__spec'>
{product.price !== null && <> {formatSpec(product.spec, product.unit)}{' '}
{formatRetailPrice(product.price, product.spec)} {product.price_unit} <View>
</>} {product.price !== null && <>
</View> {formatRetailPrice(product.price, product.spec)} {product.price_unit}
<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>
)}
{/* 已加购展示行内加减器,否则展示快捷加购按钮 */}
{Number(product.cart_quantity ?? 0) > 0 ? (
<CartStepper product={product} onSync={handleRowSync} />
) : (
<View className='product-card__add' onClick={e => handleQuickAdd(product, e)}>
<Text className='product-card__add-icon'></Text>
</View> </View>
)} </View>
<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'
onClick={handlePriceGuide}
>
</Text>
)}
{/* 已加购展示行内加减器,否则展示快捷加购按钮 */}
{Number(product.cart_quantity ?? 0) > 0 ? (
<CartStepper product={product} onSync={handleRowSync} />
) : (
<View className='product-card__add' onClick={e => handleQuickAdd(product, e)}>
<Text className='product-card__add-icon'></Text>
</View>
)}
</View>
</View> </View>
</View> </View>
))}
</View>
{/* 加载更多状态 */}
{specialLoading && (
<View className='home-recommend__loading'>
<Text className='home-recommend__loading-text'></Text>
</View> </View>
))} )}
</View> {!specialHasMore && specialPage > 1 && (
<View className='home-recommend__loading'>
<Text className='home-recommend__loading-text'></Text>
</View>
)}
</>
)} )}
</View> </View>
{/* ========== 购物车悬浮球 ========== */} {/* ========== 购物车悬浮球 ========== */}
<CartBall /> <CartBall />
{process.env.TARO_ENV === 'h5' && <CustomTabBar />}
</View> </View>
) )
} }
+2
View File
@@ -132,6 +132,8 @@
height: 100%; height: 100%;
font-size: 30px; font-size: 30px;
color: #323233; color: #323233;
display: flex;
align-items: center;
} }
.form-input-placeholder { .form-input-placeholder {
-3
View File
@@ -1,7 +1,6 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { View, Text, Button, Input } from '@tarojs/components' import { View, Text, Button, Input } from '@tarojs/components'
import CustomNavBar from '@/components/NavBar'
import useAuthStore from '@/stores/auth/useAuthStore' import useAuthStore from '@/stores/auth/useAuthStore'
import './index.less' import './index.less'
@@ -76,8 +75,6 @@ export default function LoginPage() {
return ( return (
<View className='login-page'> <View className='login-page'>
{/* ========== 导航栏 ========== */}
<CustomNavBar title='登录' />
{/* ========== 内容区域 ========== */} {/* ========== 内容区域 ========== */}
<View className='login-content'> <View className='login-content'>
+3
View File
@@ -8,6 +8,7 @@ import { formatTime } from '@/utils/format'
import { NOTICE_TYPE_MAP } from '@/types/notice' import { NOTICE_TYPE_MAP } from '@/types/notice'
import type { Notice, NoticeType } from '@/types/notice' import type { Notice, NoticeType } from '@/types/notice'
import './index.less' import './index.less'
import CustomTabBar from "@/components/CustomTabBar";
const PAGE_SIZE = 10 const PAGE_SIZE = 10
@@ -123,6 +124,8 @@ export default function MessagePage() {
{loggedIn && finished && notices.length > 0 && ( {loggedIn && finished && notices.length > 0 && (
<View className='message-loading'><Text></Text></View> <View className='message-loading'><Text></Text></View>
)} )}
{process.env.TARO_ENV === 'h5' && <CustomTabBar />}
</View> </View>
) )
} }
+174 -7
View File
@@ -20,10 +20,68 @@ const MAX_VOUCHERS = 3
/** 在线支付(调起 wx.requestPayment)仅微信小程序支持 */ /** 在线支付(调起 wx.requestPayment)仅微信小程序支持 */
const IS_WEAPP = process.env.TARO_ENV === 'weapp' const IS_WEAPP = process.env.TARO_ENV === 'weapp'
/** 支付方式选项(在线支付仅小程序端展示,排在最前) */ /** H5 端处于微信内置浏览器时,可走公众号网页授权 + JSAPI 在线支付 */
const IS_H5_WECHAT =
process.env.TARO_ENV === 'h5' &&
typeof navigator !== 'undefined' &&
/micromessenger/i.test(navigator.userAgent)
/** 在线支付(旺铺网关 JSAPI)是否可用:小程序 / 微信内 H5 */
const ONLINE_PAY_AVAILABLE = IS_WEAPP || IS_H5_WECHAT
/** H5 公众号支付草稿存储 key(授权跳转前暂存账单选择,回跳后恢复) */
const H5_PAY_DRAFT_KEY = 'h5_online_pay_draft'
/** H5 公众号支付草稿(授权回跳页面重载,勾选状态经 sessionStorage 恢复) */
interface H5PayDraft {
bill_ids: number[]
remark?: string
}
/**
* 调起公众号 JSAPI 支付(WeixinJSBridge 未注入时等待 WeixinJSBridgeReady 事件)
* resolve: 'ok' 支付成功 / 'cancel' 用户取消 / 'fail' 调起失败
*/
function invokeWechatJsapiPay(payParams: Record<string, any>): Promise<'ok' | 'cancel' | 'fail'> {
return new Promise(resolve => {
const invoke = () => {
;(window as any).WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{
appId: String(payParams.appId || ''),
timeStamp: String(payParams.timeStamp || ''),
nonceStr: String(payParams.nonceStr || ''),
package: String(payParams.package || ''),
signType: String(payParams.signType || 'RSA'),
paySign: String(payParams.paySign || ''),
},
(res: any) => {
const msg: string = res?.err_msg || ''
if (msg === 'get_brand_wcpay_request:ok') resolve('ok')
else if (msg === 'get_brand_wcpay_request:cancel') resolve('cancel')
else resolve('fail')
},
)
}
if ((window as any).WeixinJSBridge) {
invoke()
} else {
document.addEventListener('WeixinJSBridgeReady', invoke, { once: true })
}
})
}
/** 支付方式选项(在线支付仅小程序/微信内 H5 展示,排在最前) */
const PAY_METHODS: Array<{ value: PayMethod; label: string; icon: string; desc: string }> = [ const PAY_METHODS: Array<{ value: PayMethod; label: string; icon: string; desc: string }> = [
...(IS_WEAPP ...(ONLINE_PAY_AVAILABLE
? [{ value: 4 as PayMethod, label: '微信在线支付', icon: 'wechat', desc: '小程序内直接付款,免上传凭证' }] ? [
{
value: 4 as PayMethod,
label: '微信在线支付',
icon: 'wechat',
desc: IS_WEAPP ? '小程序内直接付款,免上传凭证' : '微信内直接付款,免上传凭证',
},
]
: []), : []),
{ value: 1, label: '微信支付', icon: 'wechat', desc: '扫码完成转账' }, { value: 1, label: '微信支付', icon: 'wechat', desc: '扫码完成转账' },
{ value: 2, label: '支付宝', icon: 'alipay', desc: '扫码完成转账' }, { value: 2, label: '支付宝', icon: 'alipay', desc: '扫码完成转账' },
@@ -45,7 +103,7 @@ export default function PaymentPage() {
const loadingRef = useRef(false) const loadingRef = useRef(false)
const [config, setConfig] = useState<PaymentConfig | null>(null) const [config, setConfig] = useState<PaymentConfig | null>(null)
const [payMethod, setPayMethod] = useState<PayMethod>(IS_WEAPP ? 4 : 1) const [payMethod, setPayMethod] = useState<PayMethod>(ONLINE_PAY_AVAILABLE ? 4 : 1)
const [vouchers, setVouchers] = useState<UploadedFile[]>([]) const [vouchers, setVouchers] = useState<UploadedFile[]>([])
const [remark, setRemark] = useState('') const [remark, setRemark] = useState('')
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
@@ -247,14 +305,123 @@ export default function PaymentPage() {
} }
}, [submitting, selectedIds, remark, loadBills]) }, [submitting, selectedIds, remark, loadBills])
/** 提交入口:按支付方式分发 */ /**
* H5 公众号支付主流程(授权回跳后执行):
* 授权 code 下单(scene=mp,后端换付款人 openid)→ WeixinJSBridge 调起支付 → 主动查询同步结果
* 与小程序端一致:无论成功/取消都跳转支付详情(待支付单可在详情页刷新同步结果)
*/
const runH5OnlinePay = useCallback(
async (billIds: number[], code: string, remarkText?: string) => {
setSubmitting(true)
try {
const res = await createOnlinePaymentApi({
bill_ids: billIds,
code,
scene: 'mp',
remark: remarkText,
})
const { id, payment_no, pay_params } = res.data
const result = await invokeWechatJsapiPay(pay_params)
if (result !== 'ok') {
// 用户取消或调起失败:账单仍锁定在支付单中,进详情页可刷新同步/稍后处理
Taro.showToast({
title: result === 'cancel' ? '已取消支付' : '支付调起失败,请稍后重试',
icon: 'none',
})
setTimeout(() => {
Taro.redirectTo({ url: `/pages/payment-detail/index?id=${id}` })
}, 800)
return
}
// 主动查询同步结果(网关后台通知延迟/丢失时的兜底结账)
let paid = false
try {
const q = await queryOnlinePaymentApi(payment_no)
paid = q.data.status === 1
} catch {
// 查询失败不阻断,进详情页可手动刷新
}
Taro.showToast({ title: paid ? '支付成功' : '支付结果确认中', icon: paid ? 'success' : 'none' })
setTimeout(() => {
Taro.redirectTo({ url: `/pages/payment-detail/index?id=${id}` })
}, 800)
} catch {
// 下单失败:账单可能已被其他端付款/锁定,刷新列表
loadBills(1, true)
} finally {
setSubmitting(false)
}
},
[loadBills],
)
/** H5 公众号支付:处理微信授权回跳(URL 携带 code 且本地存在支付草稿时自动继续支付) */
const h5CallbackRef = useRef(false)
useEffect(() => {
if (!IS_H5_WECHAT || h5CallbackRef.current || !loggedIn) return
const code = new URLSearchParams(window.location.search).get('code')
if (!code) return
// 清理地址栏授权参数,避免刷新/分享带出已失效的 code
window.history.replaceState(null, '', window.location.pathname)
let draft: H5PayDraft | null = null
try {
draft = JSON.parse(window.sessionStorage.getItem(H5_PAY_DRAFT_KEY) || 'null')
window.sessionStorage.removeItem(H5_PAY_DRAFT_KEY)
} catch {
draft = null
}
if (!draft || !Array.isArray(draft.bill_ids) || draft.bill_ids.length === 0) return
h5CallbackRef.current = true
setSelectedIds(draft.bill_ids)
if (draft.remark) setRemark(draft.remark)
runH5OnlinePay(draft.bill_ids, code, draft.remark)
}, [loggedIn, runH5OnlinePay])
/**
* H5 公众号支付入口:暂存支付草稿 → 跳转微信网页授权(snsapi_base 静默授权)
* 授权后回跳本页携带 code,由上方 effect 恢复草稿并继续支付
*/
const handleH5OnlinePay = useCallback(async () => {
if (submitting) return
if (selectedIds.length === 0) {
Taro.showToast({ title: '请选择要付款的账单', icon: 'none' })
return
}
setSubmitting(true)
try {
// mp_appid 可能尚未加载完成,兜底重新拉取
let appid = config?.mp_appid
if (!appid) {
const res = await getPaymentConfigApi()
setConfig(res.data)
appid = res.data.mp_appid
}
if (!appid) {
Taro.showToast({ title: '公众号支付暂未开通,请选择其他支付方式', icon: 'none' })
setSubmitting(false)
return
}
const draft: H5PayDraft = { bill_ids: selectedIds, remark: remark.trim() || undefined }
window.sessionStorage.setItem(H5_PAY_DRAFT_KEY, JSON.stringify(draft))
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname)
window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base#wechat_redirect`
} catch {
setSubmitting(false)
}
}, [submitting, selectedIds, remark, config])
/** 提交入口:按支付方式与端分发(小程序 wx.requestPayment / H5 公众号 JSAPI / 线下凭证) */
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
if (isOnline) { if (isOnline) {
handleOnlinePay() if (IS_H5_WECHAT) {
handleH5OnlinePay()
} else {
handleOnlinePay()
}
} else { } else {
handleVoucherSubmit() handleVoucherSubmit()
} }
}, [isOnline, handleOnlinePay, handleVoucherSubmit]) }, [isOnline, handleOnlinePay, handleH5OnlinePay, handleVoucherSubmit])
/** 当前支付方式的收款展示 */ /** 当前支付方式的收款展示 */
const renderMethodContent = () => { const renderMethodContent = () => {
-2
View File
@@ -1,5 +1,4 @@
import { View, Text, ScrollView } from '@tarojs/components' import { View, Text, ScrollView } from '@tarojs/components'
import CustomNavBar from '@/components/NavBar'
import './index.less' import './index.less'
/** /**
@@ -9,7 +8,6 @@ import './index.less'
export default function PrivacyPage() { export default function PrivacyPage() {
return ( return (
<View className='privacy-page'> <View className='privacy-page'>
<CustomNavBar title='隐私政策' />
<ScrollView scrollY className='privacy-scroll'> <ScrollView scrollY className='privacy-scroll'>
<View className='privacy-content'> <View className='privacy-content'>
<Text className='doc-title'></Text> <Text className='doc-title'></Text>
+4 -8
View File
@@ -3,9 +3,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #f7f8fa; background: #f7f8fa;
// 底部预留给自定义 tabBar(110rpx + 安全区),避免列表被遮挡
padding-bottom: calc(110rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
.product-search { .product-search {
flex-shrink: 0; flex-shrink: 0;
@@ -114,8 +111,8 @@
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04); box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
&__image { &__image {
width: 160rpx; width: 180rpx;
height: 160rpx; height: 180rpx;
border-radius: 12rpx; border-radius: 12rpx;
background: #f2f3f5; background: #f2f3f5;
flex-shrink: 0; flex-shrink: 0;
@@ -139,7 +136,6 @@
} }
&__spec { &__spec {
margin-top: 10rpx;
font-size: 24rpx; font-size: 24rpx;
color: #969799; color: #969799;
} }
@@ -164,8 +160,8 @@
} }
&__add { &__add {
width: 56rpx; width: 42rpx;
height: 56rpx; height: 42rpx;
border-radius: 50%; border-radius: 50%;
background: #ee0a24; background: #ee0a24;
display: flex; display: flex;
+7 -3
View File
@@ -11,6 +11,7 @@ import CartBall from '@/components/CartBall'
import CartStepper from '@/components/CartStepper' import CartStepper from '@/components/CartStepper'
import {formatRetailPrice, formatSpec} from '@/utils/format' import {formatRetailPrice, formatSpec} from '@/utils/format'
import './index.less' import './index.less'
import CustomTabBar from "@/components/CustomTabBar";
const PAGE_SIZE = 10 const PAGE_SIZE = 10
/** 存储 key:首页点击分类/搜索跳转时经本地存储传参(switchTab 无法带参) */ /** 存储 key:首页点击分类/搜索跳转时经本地存储传参(switchTab 无法带参) */
@@ -290,9 +291,11 @@ export default function ProductPage() {
<Text className='product-item__name'>{product.name}</Text> <Text className='product-item__name'>{product.name}</Text>
<Text className='product-item__spec'> <Text className='product-item__spec'>
{formatSpec(product.spec, product.unit)}{' '} {formatSpec(product.spec, product.unit)}{' '}
{product.price !== null && <> <View>
{formatRetailPrice(product.price, product.spec)} {product.price_unit} {product.price !== null && <>
</>} {formatRetailPrice(product.price, product.spec)} {product.price_unit}
</>}
</View>
</Text> </Text>
<View className='product-item__bottom'> <View className='product-item__bottom'>
{product.price !== null ? ( {product.price !== null ? (
@@ -331,6 +334,7 @@ export default function ProductPage() {
{/* ========== 购物车悬浮球 ========== */} {/* ========== 购物车悬浮球 ========== */}
<CartBall /> <CartBall />
{process.env.TARO_ENV === 'h5' && <CustomTabBar />}
</View> </View>
) )
} }
+3
View File
@@ -9,6 +9,7 @@ import type { BillSummary } from '@/services/bill'
import { ORDER_NAV_ITEMS } from '@/types/order' import { ORDER_NAV_ITEMS } from '@/types/order'
import { resolveAvatarUrl } from '@/utils/format' import { resolveAvatarUrl } from '@/utils/format'
import './index.less' import './index.less'
import CustomTabBar from "@/components/CustomTabBar";
/** 菜单项(订单/账单入口已由上方专区承载,后续单独页面开发时在此追加) */ /** 菜单项(订单/账单入口已由上方专区承载,后续单独页面开发时在此追加) */
const MENU_ITEMS = [ const MENU_ITEMS = [
@@ -229,6 +230,8 @@ export default function ProfilePage() {
<Text>退</Text> <Text>退</Text>
</View> </View>
)} )}
{process.env.TARO_ENV === 'h5' && <CustomTabBar />}
</View> </View>
) )
} }
+7 -3
View File
@@ -173,9 +173,13 @@ export default function ReportPage() {
</View> </View>
<View className='report-item__row'> <View className='report-item__row'>
<Text className='report-item__spec'> <Text className='report-item__spec'>
{item.product_spec ? `${item.product_spec} · ` : ''} {item.product_spec ? `${item.product_spec}` : ''}{item.unit}
{item.quantity}{item.unit} </Text>
{parseFloat(item.weight) > 0 ? ` · 称重 ${item.weight}` : ''} </View>
<View className='report-item__row'>
<Text className='report-item__spec'>
{item.quantity}
{parseFloat(item.weight) > 0 ? ` · 重量 ${item.weight}` : ''}
</Text> </Text>
<Text className='report-item__percent'>{item.percent}%</Text> <Text className='report-item__percent'>{item.percent}%</Text>
</View> </View>
+2
View File
@@ -74,6 +74,8 @@ export interface BillItem {
amount: string amount: string
/** 商品首图 URL(无图为空字符串) */ /** 商品首图 URL(无图为空字符串) */
image: string image: string
price_unit: string
spec: string
} }
/** 账单关联订单 */ /** 账单关联订单 */
+12 -3
View File
@@ -42,6 +42,8 @@ export interface PaymentConfig {
wechat_qrcode: string wechat_qrcode: string
alipay_qrcode: string alipay_qrcode: string
bank_info: string bank_info: string
/** 公众号 appid(H5 网页授权取 code 拼授权链接用,配置了公众号支付才返回) */
mp_appid?: string
} }
/** 支付记录(列表行与详情的 payment 字段一致) */ /** 支付记录(列表行与详情的 payment 字段一致) */
@@ -123,7 +125,7 @@ export function getPaymentDetailApi(id: number) {
return get<PaymentDetail>(`/mini/payment/${id}`) return get<PaymentDetail>(`/mini/payment/${id}`)
} }
/** 在线支付下单返回(pay_params 为旺铺网关透传的 wx.requestPayment 调起参数,以网关实际返回为准) */ /** 在线支付下单返回(pay_params 为旺铺网关透传的调起参数:小程序给 wx.requestPaymentH5 公众号给 getBrandWCPayRequest,以网关实际返回为准) */
export interface OnlinePaymentCreateResult { export interface OnlinePaymentCreateResult {
id: number id: number
/** 支付单号(ZF 前缀,= 上送网关的商户订单号 mer_order_id),查询/对账用 */ /** 支付单号(ZF 前缀,= 上送网关的商户订单号 mer_order_id),查询/对账用 */
@@ -131,6 +133,7 @@ export interface OnlinePaymentCreateResult {
/** 应付金额(= 所选账单总额合计,元) */ /** 应付金额(= 所选账单总额合计,元) */
amount: string amount: string
pay_params: { pay_params: {
appId?: string
timeStamp?: string timeStamp?: string
nonceStr?: string nonceStr?: string
package?: string package?: string
@@ -152,9 +155,15 @@ export interface OnlinePaymentQueryResult {
/** /**
* 发起在线支付(合并账单下单):POST /mini/payment/online * 发起在线支付(合并账单下单):POST /mini/payment/online
* code wx.login() 返回的登录凭证(后端换付款人 openid * code:小程序传 wx.login() 登录凭证(不传 scene);
* H5 公众号传网页授权回调 code,需带 scene: 'mp'(后端按场景换付款人 openid)
*/ */
export function createOnlinePaymentApi(data: { bill_ids: number[]; code: string; remark?: string }) { export function createOnlinePaymentApi(data: {
bill_ids: number[]
code: string
scene?: 'mp'
remark?: string
}) {
return post<OnlinePaymentCreateResult>('/mini/payment/online', data) return post<OnlinePaymentCreateResult>('/mini/payment/online', data)
} }
+43
View File
@@ -0,0 +1,43 @@
import { get } from '@/utils/request'
import type { PaginatedData } from '@/types/api'
import type { CartSummary } from '@/types/cart'
import type { Product } from '@/types/product'
/** 特价推荐列表参数 */
export interface SpecialListParams {
page?: number
pageSize?: number
}
/**
* 特价推荐列表响应(分页 + 购物车悬浮球汇总)。
* 行结构与 /mini/product/list 一致:商品基础字段 + price + cart_id/cart_quantity
*/
export interface SpecialListData extends PaginatedData<Product> {
/**
* 购物车悬浮球汇总(未登录返回零值结构;旧版本后端可能未返回,调用方判空)。
* 接口文档示例为 count/quantity/amount 简写,与 /mini/home 等接口的 total_* 命名不一致,
* 统一经 normalizeSpecialCart 转换后再写入 store
*/
cart?: CartSummary | { count: number; quantity: string; amount: string }
}
/** 特价推荐商品列表(免登录;携带门店 token 时返回等级价与购物车字段):GET /mini/special/list */
export function getSpecialListApi(params: SpecialListParams = {}) {
return get<SpecialListData>('/mini/special/list', { data: params })
}
/** 特价推荐响应附带的悬浮球汇总归一化(兼容 count/quantity/amount 与 total_* 两种命名) */
export function normalizeSpecialCart(cart: SpecialListData['cart']): CartSummary | null {
if (!cart) return null
const raw = cart as Record<string, unknown>
const count = raw.total_count ?? raw.count
const quantity = raw.total_quantity ?? raw.quantity
const amount = raw.total_amount ?? raw.amount
if (count == null || quantity == null || amount == null) return null
return {
total_count: Number(count),
total_quantity: String(quantity),
total_amount: String(amount),
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ const LOGIN_PATH = '/pages/login/index'
/** 默认请求超时(ms */ /** 默认请求超时(ms */
const DEFAULT_TIMEOUT = 15000 const DEFAULT_TIMEOUT = 15000
/** 接口根地址(uploadFile 等原生请求同样使用) */ /** 接口根地址(uploadFile 等原生请求同样使用) */
// export const BASE_URL = "http://localhost:8000/index.php" // export const BASE_URL = "http://localhost:8000"
export const BASE_URL = "https://purchase.henanklkj.com/index.php" export const BASE_URL = "https://purchase.henanklkj.com/index.php"
/** /**