init
This commit is contained in:
+4
-1
@@ -3,8 +3,11 @@ export default defineAppConfig({
|
||||
'pages/index/index',
|
||||
'pages/product/index',
|
||||
'pages/cart/index',
|
||||
'pages/profile/index',
|
||||
'pages/message/index',
|
||||
'pages/profile/index',
|
||||
'pages/order-list/index',
|
||||
'pages/statement/index',
|
||||
'pages/settings/index',
|
||||
'pages/login/index',
|
||||
],
|
||||
window: {
|
||||
|
||||
+137
-188
@@ -1,166 +1,84 @@
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 140rpx;
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
|
||||
// ===== 门店信息卡片 =====
|
||||
.store-card {
|
||||
// ===== 顶部搜索 =====
|
||||
.home-search {
|
||||
background: linear-gradient(135deg, #ee0a24, #ff4d4f);
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
// ===== 轮播图 =====
|
||||
.home-banner {
|
||||
margin: 20rpx 24rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&__row {
|
||||
&__swiper {
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
background: linear-gradient(135deg, #ff9a9e, #ff4d4f);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
flex-shrink: 0;
|
||||
|
||||
&--text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 34rpx;
|
||||
&__placeholder-text {
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__identity {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
font-size: 24rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
|
||||
&--level {
|
||||
background: #ffd21e;
|
||||
color: #7a5b00;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
font-size: 26rpx;
|
||||
padding: 12rpx 30rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #fff;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
margin-left: auto;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 汇总卡片 =====
|
||||
.summary-card {
|
||||
margin-top: 20rpx;
|
||||
// ===== 导航菜单 =====
|
||||
.home-menu {
|
||||
margin: 20rpx 24rpx 0;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
padding: 20rpx 0 8rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__text {
|
||||
color: #fff;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 推荐商品 =====
|
||||
.home-recommend {
|
||||
margin: 20rpx 24rpx 0;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 8rpx 20rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__period {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #323233;
|
||||
|
||||
&--strong {
|
||||
color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 通用区块卡片 =====
|
||||
.section-card {
|
||||
margin-top: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__more {
|
||||
@@ -169,76 +87,107 @@
|
||||
}
|
||||
|
||||
&__empty {
|
||||
padding: 40rpx 0;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 80rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #c8c9cc;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__login-btn {
|
||||
margin-top: 24rpx;
|
||||
padding: 14rpx 60rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 分类横向滚动 =====
|
||||
.category-scroll {
|
||||
white-space: nowrap;
|
||||
margin: 0 -28rpx;
|
||||
padding: 0 28rpx;
|
||||
box-sizing: border-box;
|
||||
// ===== 商品卡片(两列网格) =====
|
||||
.product-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.category-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 14rpx 30rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f7f8fa;
|
||||
border: 1rpx solid #ebedf0;
|
||||
.product-card {
|
||||
width: 342rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
background: #f2f3f5;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding: 16rpx 20rpx 20rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 通知 =====
|
||||
.notice-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: #ee0a24;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
&__spec {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__price {
|
||||
font-size: 30rpx;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
|
||||
&--none {
|
||||
color: #c8c9cc;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__add {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
background: #ee0a24;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__add-icon {
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+160
-175
@@ -1,60 +1,42 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import { Grid, GridItem } from '@antmjs/vantui'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Grid, GridItem, Search, Swiper, SwiperItem } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import useCartStore from '@/stores/cart/useCartStore'
|
||||
import { getOrderSummaryApi } from '@/services/order'
|
||||
import { getCategoriesApi } from '@/services/product'
|
||||
import { getNoticeListApi, readNoticeApi } from '@/services/notice'
|
||||
import { resolveAvatarUrl } from '@/utils/format'
|
||||
import type { Category } from '@/types/product'
|
||||
import type { SummaryGroup } from '@/types/order'
|
||||
import type { Notice } from '@/types/notice'
|
||||
import { getCategoriesApi, getProductListApi } from '@/services/product'
|
||||
import { getProductCover } from '@/types/product'
|
||||
import type { Category, Product } from '@/types/product'
|
||||
import './index.less'
|
||||
|
||||
/** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */
|
||||
const PENDING_CATEGORY_KEY = 'product_category_id'
|
||||
const PENDING_KEYWORD_KEY = 'product_keyword'
|
||||
|
||||
/** 分类菜单色块配色 */
|
||||
const MENU_COLORS = ['#ee0a24', '#ff9f43', '#07c160', '#1989fa', '#8a5cf6', '#00b8d9']
|
||||
|
||||
export default function IndexPage() {
|
||||
const user = useAuthStore(s => s.user)
|
||||
const token = useAuthStore(s => s.token)
|
||||
const fetchCart = useCartStore(s => s.fetchCart)
|
||||
const addItem = useCartStore(s => s.addItem)
|
||||
|
||||
/** 本月订货汇总(取当前月份分组,无则取最近一组) */
|
||||
const [summary, setSummary] = useState<SummaryGroup | null>(null)
|
||||
/** 顶级分类快捷入口 */
|
||||
/** 顶级分类(导航菜单前 6 个) */
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
/** 最新通知 */
|
||||
const [notices, setNotices] = useState<Notice[]>([])
|
||||
const [unreadCount, setUnreadCount] = useState(0)
|
||||
/** 推荐商品 */
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
/** 搜索框输入 */
|
||||
const [keyword, setKeyword] = useState('')
|
||||
|
||||
const loggedIn = !!token && !!user
|
||||
const loggedIn = !!token
|
||||
|
||||
useDidShow(() => {
|
||||
if (!loggedIn) return
|
||||
// 静默同步购物车角标
|
||||
fetchCart().catch(() => {})
|
||||
loadSummary()
|
||||
loadCategories()
|
||||
loadNotices()
|
||||
if (loggedIn) {
|
||||
loadRecommend()
|
||||
}
|
||||
})
|
||||
|
||||
/** 本月订货汇总 */
|
||||
const loadSummary = useCallback(async () => {
|
||||
try {
|
||||
const res = await getOrderSummaryApi('month')
|
||||
const groups = res.data.groups
|
||||
if (!groups.length) {
|
||||
setSummary(null)
|
||||
return
|
||||
}
|
||||
const now = new Date()
|
||||
const thisMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
||||
setSummary(groups.find(g => g.period_label === thisMonth) ?? groups[groups.length - 1])
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 商品分类(顶级) */
|
||||
/** 商品分类 */
|
||||
const loadCategories = useCallback(async () => {
|
||||
try {
|
||||
const res = await getCategoriesApi()
|
||||
@@ -64,173 +46,176 @@ export default function IndexPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 最新通知 + 未读数 */
|
||||
const loadNotices = useCallback(async () => {
|
||||
/** 推荐商品(默认排序第一页) */
|
||||
const loadRecommend = useCallback(async () => {
|
||||
try {
|
||||
const res = await getNoticeListApi({ page: 1, pageSize: 5 })
|
||||
setNotices(res.data.data)
|
||||
setUnreadCount(res.data.unread_count)
|
||||
const res = await getProductListApi({ page: 1, pageSize: 10 })
|
||||
setProducts(res.data.data)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 点击分类 → 商品页(switchTab 无法传参,经本地存储传递) */
|
||||
const handleCategoryTap = useCallback((id: number) => {
|
||||
/** 轮播数据:取有图商品的前 4 张图 */
|
||||
const bannerImages = useMemo(
|
||||
() => products.filter(p => getProductCover(p)).slice(0, 4),
|
||||
[products],
|
||||
)
|
||||
|
||||
/** 跳转商品页并带上分类/关键词 */
|
||||
const goProduct = useCallback((categoryId?: number, kw?: string) => {
|
||||
try {
|
||||
Taro.setStorageSync('product_category_id', id)
|
||||
if (categoryId !== undefined) Taro.setStorageSync(PENDING_CATEGORY_KEY, categoryId)
|
||||
if (kw !== undefined) Taro.setStorageSync(PENDING_KEYWORD_KEY, kw)
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
Taro.switchTab({ url: '/pages/product/index' })
|
||||
}, [])
|
||||
|
||||
/** 点击通知 → 标记已读 */
|
||||
const handleNoticeTap = useCallback(
|
||||
async (notice: Notice) => {
|
||||
if (notice.is_read === 1) return
|
||||
/** 搜索框聚焦/提交 → 商品页搜索 */
|
||||
const handleSearchFocus = useCallback(() => {
|
||||
goProduct(undefined, keyword.trim())
|
||||
}, [goProduct, keyword])
|
||||
|
||||
/** 快捷加购 */
|
||||
const handleQuickAdd = useCallback(
|
||||
async (product: Product, e: any) => {
|
||||
e.stopPropagation()
|
||||
try {
|
||||
await readNoticeApi(notice.id)
|
||||
setNotices(prev => prev.map(n => (n.id === notice.id ? { ...n, is_read: 1 } : n)))
|
||||
setUnreadCount(count => Math.max(0, count - 1))
|
||||
await addItem(product.id, 1)
|
||||
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
// 错误(未设等级价等)已由 request 层 toast
|
||||
}
|
||||
},
|
||||
[],
|
||||
[addItem],
|
||||
)
|
||||
|
||||
const goLogin = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/login/index' })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='home-page'>
|
||||
{/* ========== 门店信息卡片 ========== */}
|
||||
<View className='store-card'>
|
||||
{!loggedIn ? (
|
||||
<View className='store-card__row'>
|
||||
<View className='store-card__info'>
|
||||
<Text className='store-card__name'>欢迎使用订货采购系统</Text>
|
||||
<Text className='store-card__desc'>登录后即可查看门店与价格信息</Text>
|
||||
</View>
|
||||
<View className='store-card__btn' onClick={goLogin}>去登录</View>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View className='store-card__row'>
|
||||
{user?.avatar ? (
|
||||
<Image className='store-card__avatar' src={resolveAvatarUrl(user.avatar)} mode='aspectFill' />
|
||||
) : (
|
||||
<View className='store-card__avatar store-card__avatar--text'>{user?.nickname?.[0] || '用'}</View>
|
||||
)}
|
||||
<View className='store-card__info'>
|
||||
<Text className='store-card__name'>{user?.nickname}</Text>
|
||||
{user?.phone ? (
|
||||
<Text className='store-card__desc'>{user.phone}</Text>
|
||||
) : (
|
||||
<Text className='store-card__desc' onClick={goLogin}>未绑定手机号,点击去绑定</Text>
|
||||
)}
|
||||
{/* ========== 顶部搜索 ========== */}
|
||||
<View className='home-search'>
|
||||
<Search
|
||||
value={keyword}
|
||||
placeholder='搜索商品名称/规格'
|
||||
shape='round'
|
||||
background='transparent'
|
||||
onChange={e => setKeyword(String(e.detail))}
|
||||
onFocus={handleSearchFocus}
|
||||
onSearch={handleSearchFocus}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* ========== 轮播图 ========== */}
|
||||
<View className='home-banner'>
|
||||
<Swiper
|
||||
className='home-banner__swiper'
|
||||
height='320rpx'
|
||||
autoPlay={3000}
|
||||
loop
|
||||
paginationVisible
|
||||
paginationColor='#ffffff'
|
||||
>
|
||||
{bannerImages.length > 0 ? (
|
||||
bannerImages.map((product, idx) => (
|
||||
<SwiperItem key={product.id}>
|
||||
<Image
|
||||
className='home-banner__image'
|
||||
src={getProductCover(product)}
|
||||
mode='aspectFill'
|
||||
onClick={() => goProduct()}
|
||||
/>
|
||||
</SwiperItem>
|
||||
))
|
||||
) : (
|
||||
<SwiperItem>
|
||||
<View className='home-banner__placeholder' onClick={() => goProduct()}>
|
||||
<Text className='home-banner__placeholder-text'>订货采购</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='store-card__identity'>
|
||||
{user?.type === 0 ? (
|
||||
<View className='store-card__btn' onClick={goLogin}>绑定手机号</View>
|
||||
) : user?.store ? (
|
||||
<View className='store-card__tags'>
|
||||
<Text className='store-card__tag'>门店 · {user.store.name}</Text>
|
||||
{user.store.level && <Text className='store-card__tag store-card__tag--level'>{user.store.level.name}</Text>}
|
||||
</View>
|
||||
) : user?.supplier ? (
|
||||
<View className='store-card__tags'>
|
||||
<Text className='store-card__tag'>供应商 · {user.supplier.name}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='store-card__btn' onClick={goLogin}>绑定身份</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</SwiperItem>
|
||||
)}
|
||||
</Swiper>
|
||||
</View>
|
||||
|
||||
{/* ========== 本月订货汇总 ========== */}
|
||||
<View className='summary-card'>
|
||||
<View className='summary-card__header'>
|
||||
<Text className='summary-card__title'>订货汇总</Text>
|
||||
<Text className='summary-card__period'>{summary?.period_label ?? '—'}</Text>
|
||||
</View>
|
||||
<View className='summary-card__body'>
|
||||
<View className='summary-card__item'>
|
||||
<Text className='summary-card__value summary-card__value--strong'>¥{summary?.total_amount ?? '0.00'}</Text>
|
||||
<Text className='summary-card__label'>订货金额</Text>
|
||||
</View>
|
||||
<View className='summary-card__item'>
|
||||
<Text className='summary-card__value'>{summary?.total_quantity ?? '0.00'}</Text>
|
||||
<Text className='summary-card__label'>订货数量</Text>
|
||||
</View>
|
||||
<View className='summary-card__item'>
|
||||
<Text className='summary-card__value'>{summary?.order_count ?? 0}</Text>
|
||||
<Text className='summary-card__label'>订单笔数</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 快捷入口 ========== */}
|
||||
<View className='section-card'>
|
||||
<Grid columnNum={4} iconSize={52} border={false}>
|
||||
<GridItem icon='apps-o' text='商品中心' onClick={() => Taro.switchTab({ url: '/pages/product/index' })} />
|
||||
<GridItem icon='shopping-cart-o' text='购物车' onClick={() => Taro.switchTab({ url: '/pages/cart/index' })} />
|
||||
<GridItem icon='orders-o' text='我的订单' onClick={() => Taro.switchTab({ url: '/pages/profile/index' })} />
|
||||
<GridItem icon='balance-list-o' text='对账单' onClick={() => Taro.switchTab({ url: '/pages/profile/index' })} />
|
||||
{/* ========== 导航菜单 ========== */}
|
||||
<View className='home-menu'>
|
||||
<Grid columnNum={4} border={false} iconSize={52}>
|
||||
{categories.slice(0, 6).map((cat, idx) => (
|
||||
<GridItem
|
||||
key={cat.id}
|
||||
text={cat.name}
|
||||
onClick={() => goProduct(cat.id)}
|
||||
renderIcon={
|
||||
<View className='menu-icon' style={{ background: MENU_COLORS[idx % MENU_COLORS.length] }}>
|
||||
<Text className='menu-icon__text'>{cat.name.slice(0, 1)}</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<GridItem
|
||||
icon='apps-o'
|
||||
text='全部商品'
|
||||
onClick={() => goProduct()}
|
||||
/>
|
||||
<GridItem
|
||||
icon='orders-o'
|
||||
text='我的订单'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/order-list/index?status=all' })}
|
||||
/>
|
||||
</Grid>
|
||||
</View>
|
||||
|
||||
{/* ========== 商品分类 ========== */}
|
||||
{categories.length > 0 && (
|
||||
<View className='section-card'>
|
||||
<View className='section-card__header'>
|
||||
<Text className='section-card__title'>商品分类</Text>
|
||||
<Text className='section-card__more' onClick={() => Taro.switchTab({ url: '/pages/product/index' })}>查看全部 ›</Text>
|
||||
</View>
|
||||
<ScrollX items={categories} onTap={handleCategoryTap} />
|
||||
{/* ========== 推荐商品 ========== */}
|
||||
<View className='home-recommend'>
|
||||
<View className='home-recommend__header'>
|
||||
<Text className='home-recommend__title'>推荐商品</Text>
|
||||
<Text className='home-recommend__more' onClick={() => goProduct()}>查看更多 ›</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 通知 ========== */}
|
||||
<View className='section-card'>
|
||||
<View className='section-card__header'>
|
||||
<Text className='section-card__title'>通知</Text>
|
||||
{unreadCount > 0 && <Text className='section-card__more'>未读 {unreadCount} 条</Text>}
|
||||
</View>
|
||||
{notices.length === 0 ? (
|
||||
<View className='section-card__empty'>
|
||||
<Text className='section-card__empty-text'>暂无通知</Text>
|
||||
{!loggedIn ? (
|
||||
<View className='home-recommend__empty'>
|
||||
<Text className='home-recommend__empty-text'>登录后查看商品与价格</Text>
|
||||
<View
|
||||
className='home-recommend__login-btn'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}
|
||||
>
|
||||
去登录
|
||||
</View>
|
||||
</View>
|
||||
) : products.length === 0 ? (
|
||||
<View className='home-recommend__empty'>
|
||||
<Text className='home-recommend__empty-text'>暂无商品</Text>
|
||||
</View>
|
||||
) : (
|
||||
notices.map(notice => (
|
||||
<View key={notice.id} className='notice-item' onClick={() => handleNoticeTap(notice)}>
|
||||
{notice.is_read === 0 && <View className='notice-item__dot' />}
|
||||
<View className='notice-item__content'>
|
||||
<Text className='notice-item__title'>{notice.title}</Text>
|
||||
<Text className='notice-item__desc' numberOfLines={1}>{notice.content}</Text>
|
||||
<View className='product-grid'>
|
||||
{products.map(product => (
|
||||
<View key={product.id} className='product-card' onClick={() => goProduct()}>
|
||||
<Image
|
||||
className='product-card__image'
|
||||
src={getProductCover(product) || 'https://img.yzcdn.cn/vant/cat.jpeg'}
|
||||
mode='aspectFill'
|
||||
lazyLoad
|
||||
/>
|
||||
<View className='product-card__info'>
|
||||
<Text className='product-card__name'>{product.name}</Text>
|
||||
<Text className='product-card__spec'>{product.spec} / {product.unit}</Text>
|
||||
<View className='product-card__bottom'>
|
||||
{product.price !== null ? (
|
||||
<Text className='product-card__price'>¥{product.price}</Text>
|
||||
) : (
|
||||
<Text className='product-card__price product-card__price--none'>价格待定</Text>
|
||||
)}
|
||||
<View className='product-card__add' onClick={e => handleQuickAdd(product, e)}>
|
||||
<Text className='product-card__add-icon'>+</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
/** 横向滚动的分类入口 */
|
||||
function ScrollX({ items, onTap }: { items: Category[]; onTap: (id: number) => void }) {
|
||||
return (
|
||||
<ScrollView scrollX className='category-scroll'>
|
||||
{items.map(item => (
|
||||
<View key={item.id} className='category-chip' onClick={() => onTap(item.id)}>
|
||||
<Text className='category-chip__name'>{item.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '首页'
|
||||
})
|
||||
navigationBarTitleText: '消息',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
.message-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 160rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// ===== 头部 =====
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 8rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__unread {
|
||||
font-size: 24rpx;
|
||||
color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
.message-empty {
|
||||
padding-top: 120rpx;
|
||||
|
||||
&__btn {
|
||||
margin-top: 24rpx;
|
||||
padding: 14rpx 60rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.message-loading {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
// ===== 通知项 =====
|
||||
.notice-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&__dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: #ee0a24;
|
||||
margin-top: 12rpx;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--order {
|
||||
color: #ee0a24;
|
||||
background: #fff0f0;
|
||||
}
|
||||
|
||||
&--price {
|
||||
color: #1989fa;
|
||||
background: #e8f3ff;
|
||||
}
|
||||
|
||||
&--system {
|
||||
color: #969799;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
color: #646566;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__time {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #c8c9cc;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+122
-28
@@ -1,34 +1,128 @@
|
||||
import { View } from '@tarojs/components'
|
||||
import { Button } from '@antmjs/vantui'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Empty } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { getNoticeListApi, readNoticeApi } from '@/services/notice'
|
||||
import { formatTime } from '@/utils/format'
|
||||
import { NOTICE_TYPE_MAP } from '@/types/notice'
|
||||
import type { Notice, NoticeType } from '@/types/notice'
|
||||
import './index.less'
|
||||
|
||||
export default function Index() {
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
/** 通知类型样式 */
|
||||
const NOTICE_TYPE_CLASS: Record<NoticeType, string> = {
|
||||
order: 'notice-item__tag--order',
|
||||
price: 'notice-item__tag--price',
|
||||
system: 'notice-item__tag--system',
|
||||
}
|
||||
|
||||
export default function MessagePage() {
|
||||
const token = useAuthStore(s => s.token)
|
||||
|
||||
const [notices, setNotices] = useState<Notice[]>([])
|
||||
const [unreadCount, setUnreadCount] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const loadingRef = useRef(false)
|
||||
|
||||
const loggedIn = !!token
|
||||
|
||||
/** 拉取通知列表 */
|
||||
const loadList = useCallback(
|
||||
async (pageNum: number, reset: boolean) => {
|
||||
if (!loggedIn || loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getNoticeListApi({ page: pageNum, pageSize: PAGE_SIZE })
|
||||
const { data, unread_count, total } = res.data
|
||||
setNotices(prev => (reset ? data : [...prev, ...data]))
|
||||
setUnreadCount(unread_count)
|
||||
setPage(pageNum)
|
||||
setFinished(pageNum * PAGE_SIZE >= total)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[loggedIn],
|
||||
)
|
||||
|
||||
useDidShow(() => {
|
||||
loadList(1, true)
|
||||
})
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!finished && !loadingRef.current && loggedIn) {
|
||||
loadList(page + 1, false)
|
||||
}
|
||||
})
|
||||
|
||||
/** 点击通知 → 标记已读 */
|
||||
const handleRead = useCallback(async (notice: Notice) => {
|
||||
if (notice.is_read === 1) return
|
||||
try {
|
||||
await readNoticeApi(notice.id)
|
||||
setNotices(prev =>
|
||||
prev.map(n => (n.id === notice.id ? { ...n, is_read: 1 } : n)),
|
||||
)
|
||||
setUnreadCount(count => Math.max(0, count - 1))
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
}, [])
|
||||
|
||||
const goLogin = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/login/index' })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='index'>
|
||||
<View><Button type='primary'>Hello world!</Button></View>
|
||||
<View>上面的按钮的颜色已经通过全局主题重写覆盖了,参见src/styles/index.less</View>
|
||||
<View className='message-page'>
|
||||
{/* ========== 未读概览 ========== */}
|
||||
<View className='message-header'>
|
||||
<Text className='message-header__title'>消息中心</Text>
|
||||
{loggedIn && unreadCount > 0 && (
|
||||
<Text className='message-header__unread'>未读 {unreadCount} 条</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== 通知列表 ========== */}
|
||||
{!loggedIn ? (
|
||||
<Empty description='登录后查看消息通知' className='message-empty'>
|
||||
<View className='message-empty__btn' onClick={goLogin}>去登录</View>
|
||||
</Empty>
|
||||
) : notices.length === 0 ? (
|
||||
loading ? (
|
||||
<View className='message-loading'><Text>加载中...</Text></View>
|
||||
) : (
|
||||
<Empty description='暂无通知' className='message-empty' />
|
||||
)
|
||||
) : (
|
||||
notices.map(notice => (
|
||||
<View key={notice.id} className='notice-item' onClick={() => handleRead(notice)}>
|
||||
{notice.is_read === 0 && <View className='notice-item__dot' />}
|
||||
<View className='notice-item__content'>
|
||||
<View className='notice-item__header'>
|
||||
<Text className='notice-item__title'>{notice.title}</Text>
|
||||
<Text className={`notice-item__tag ${NOTICE_TYPE_CLASS[notice.type]}`}>
|
||||
{NOTICE_TYPE_MAP[notice.type]}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='notice-item__desc'>{notice.content}</Text>
|
||||
<Text className='notice-item__time'>{formatTime(notice.created_at)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
{loggedIn && finished && notices.length > 0 && (
|
||||
<View className='message-loading'><Text>没有更多了</Text></View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// export default class Index extends Component {
|
||||
|
||||
// componentWillMount () { }
|
||||
|
||||
// componentDidMount () { }
|
||||
|
||||
// componentWillUnmount () { }
|
||||
|
||||
// componentDidShow () { }
|
||||
|
||||
// componentDidHide () { }
|
||||
|
||||
// render () {
|
||||
// return (
|
||||
// <View className='index'>
|
||||
// <View><Button type='primary'>Hello world!</Button></View>
|
||||
// <View>上面的按钮的颜色已经通过全局主题重写覆盖了,参见src/style/index.less</View>
|
||||
// </View>
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的订单',
|
||||
})
|
||||
@@ -0,0 +1,195 @@
|
||||
.order-list-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// ===== 状态筛选 =====
|
||||
.status-scroll {
|
||||
white-space: nowrap;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
display: inline-flex;
|
||||
padding: 12rpx 28rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #fff;
|
||||
font-size: 26rpx;
|
||||
color: #646566;
|
||||
|
||||
&.active {
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.order-empty {
|
||||
padding-top: 120rpx;
|
||||
|
||||
&__btn {
|
||||
margin-top: 24rpx;
|
||||
padding: 14rpx 60rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.order-loading {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
// ===== 订单项 =====
|
||||
.order-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__no {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 24rpx;
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__amounts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__qty {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__remark {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
margin-top: 16rpx;
|
||||
display: inline-flex;
|
||||
padding: 8rpx 24rpx;
|
||||
border: 1rpx solid #ee0a24;
|
||||
color: #ee0a24;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 详情弹层 =====
|
||||
.detail-popup {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__status {
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
&__list {
|
||||
max-height: 480rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-spec {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&-amount {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__total {
|
||||
font-size: 30rpx;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow, useReachBottom, useRouter } from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { Empty, Popup } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { cancelOrderApi, getOrderDetailApi, getOrderListApi } from '@/services/order'
|
||||
import { ORDER_NAV_ITEMS, ORDER_STATUS_MAP } from '@/types/order'
|
||||
import type { Order, OrderStatus } from '@/types/order'
|
||||
import './index.less'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
/**
|
||||
* 订单列表页(框架)
|
||||
* 入口:/pages/order-list/index?status=0|1|2|3|9|all
|
||||
* status 与 ORDER_NAV_ITEMS 映射(业务语言 → 后端枚举),缺省为全部
|
||||
*/
|
||||
export default function OrderListPage() {
|
||||
const router = useRouter()
|
||||
const token = useAuthStore(s => s.token)
|
||||
|
||||
/** 当前状态筛选(undefined = 全部) */
|
||||
const [status, setStatus] = useState<number | undefined>(undefined)
|
||||
const [orders, setOrders] = useState<Order[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const loadingRef = useRef(false)
|
||||
|
||||
/** 订单详情弹层 */
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
const [orderDetail, setOrderDetail] = useState<Order | null>(null)
|
||||
|
||||
const loggedIn = !!token
|
||||
|
||||
/** 解析路由参数中的 status('all' / 数字 / 缺省 → undefined) */
|
||||
const parseStatus = useCallback((raw?: string): number | undefined => {
|
||||
if (!raw || raw === 'all' || raw === '') return undefined
|
||||
const num = Number(raw)
|
||||
return Number.isNaN(num) ? undefined : num
|
||||
}, [])
|
||||
|
||||
/** 拉取订单列表 */
|
||||
const loadOrders = useCallback(
|
||||
async (pageNum: number, reset: boolean, statusParam?: number) => {
|
||||
if (!loggedIn || loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getOrderListApi({
|
||||
status: statusParam,
|
||||
page: pageNum,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
const { data, total: totalCount } = res.data
|
||||
setOrders(prev => (reset ? data : [...prev, ...data]))
|
||||
setTotal(totalCount)
|
||||
setPage(pageNum)
|
||||
setFinished(pageNum * PAGE_SIZE >= totalCount)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[loggedIn],
|
||||
)
|
||||
|
||||
useDidShow(() => {
|
||||
const statusParam = parseStatus(router.params.status)
|
||||
setStatus(statusParam)
|
||||
loadOrders(1, true, statusParam)
|
||||
})
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!finished && !loadingRef.current && loggedIn) {
|
||||
loadOrders(page + 1, false, status)
|
||||
}
|
||||
})
|
||||
|
||||
/** 切换状态筛选 */
|
||||
const handleStatusTap = useCallback(
|
||||
(value?: number) => {
|
||||
setStatus(value)
|
||||
setFinished(false)
|
||||
loadOrders(1, true, value)
|
||||
},
|
||||
[loadOrders],
|
||||
)
|
||||
|
||||
/** 查看订单详情 */
|
||||
const handleOrderTap = useCallback(async (order: Order) => {
|
||||
try {
|
||||
const res = await getOrderDetailApi(order.id)
|
||||
setOrderDetail(res.data)
|
||||
setShowDetail(true)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 取消订单(仅待汇总可取消) */
|
||||
const handleCancelOrder = useCallback(
|
||||
(order: Order) => {
|
||||
Taro.showModal({
|
||||
title: '取消订单',
|
||||
content: `确定取消订单 ${order.order_no} 吗?`,
|
||||
confirmColor: '#ee0a24',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
cancelOrderApi(order.id)
|
||||
.then(() => {
|
||||
Taro.showToast({ title: '订单已取消', icon: 'success' })
|
||||
loadOrders(1, true, status)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
[loadOrders, status],
|
||||
)
|
||||
|
||||
const goLogin = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/login/index' })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='order-list-page'>
|
||||
{/* ========== 状态筛选 ========== */}
|
||||
<ScrollView scrollX className='status-scroll'>
|
||||
<View
|
||||
className={`status-chip ${status === undefined ? 'active' : ''}`}
|
||||
onClick={() => handleStatusTap(undefined)}
|
||||
>
|
||||
<Text>全部</Text>
|
||||
</View>
|
||||
{ORDER_NAV_ITEMS.map(item => (
|
||||
<View
|
||||
key={item.key}
|
||||
className={`status-chip ${status === item.status ? 'active' : ''}`}
|
||||
onClick={() => handleStatusTap(item.status)}
|
||||
>
|
||||
<Text>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* ========== 订单列表 ========== */}
|
||||
{!loggedIn ? (
|
||||
<Empty description='登录后查看订单' className='order-empty'>
|
||||
<View className='order-empty__btn' onClick={goLogin}>去登录</View>
|
||||
</Empty>
|
||||
) : orders.length === 0 ? (
|
||||
loading ? (
|
||||
<View className='order-loading'><Text>加载中...</Text></View>
|
||||
) : (
|
||||
<Empty description='暂无订单' className='order-empty' />
|
||||
)
|
||||
) : (
|
||||
orders.map(order => (
|
||||
<View key={order.id} className='order-item' onClick={() => handleOrderTap(order)}>
|
||||
<View className='order-item__header'>
|
||||
<Text className='order-item__no'>{order.order_no}</Text>
|
||||
<Text className='order-item__status'>{ORDER_STATUS_MAP[order.status]}</Text>
|
||||
</View>
|
||||
<View className='order-item__body'>
|
||||
<Text className='order-item__date'>{order.order_date}</Text>
|
||||
<View className='order-item__amounts'>
|
||||
<Text className='order-item__qty'>共 {order.total_quantity} 件</Text>
|
||||
<Text className='order-item__amount'>¥{order.total_amount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{order.remark && <Text className='order-item__remark'>备注:{order.remark}</Text>}
|
||||
{order.status === 0 && (
|
||||
<View
|
||||
className='order-item__cancel'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleCancelOrder(order)
|
||||
}}
|
||||
>
|
||||
取消订单
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
{loggedIn && finished && orders.length > 0 && (
|
||||
<View className='order-loading'><Text>没有更多了</Text></View>
|
||||
)}
|
||||
|
||||
{/* ========== 订单详情弹层 ========== */}
|
||||
<Popup
|
||||
show={showDetail}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowDetail(false)}
|
||||
>
|
||||
{orderDetail && (
|
||||
<View className='detail-popup'>
|
||||
<Text className='detail-popup__title'>{orderDetail.order_no}</Text>
|
||||
<View className='detail-popup__meta'>
|
||||
<Text>{orderDetail.order_date}</Text>
|
||||
<Text className='detail-popup__status'>{ORDER_STATUS_MAP[orderDetail.status]}</Text>
|
||||
</View>
|
||||
<ScrollView scrollY className='detail-popup__list'>
|
||||
{(orderDetail.items ?? []).map(item => (
|
||||
<View key={item.id} className='detail-popup__item'>
|
||||
<View className='detail-popup__item-info'>
|
||||
<Text className='detail-popup__item-name'>{item.product_name}</Text>
|
||||
<Text className='detail-popup__item-spec'>
|
||||
{item.product_spec} ¥{item.price} × {item.quantity}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='detail-popup__item-amount'>¥{item.amount}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
<View className='detail-popup__footer'>
|
||||
<Text className='detail-popup__total'>合计 ¥{orderDetail.total_amount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -9,8 +9,9 @@ import type { Category, Product } from '@/types/product'
|
||||
import './index.less'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
/** 存储 key:首页点击分类跳转时经本地存储传参(switchTab 无法带参) */
|
||||
/** 存储 key:首页点击分类/搜索跳转时经本地存储传参(switchTab 无法带参) */
|
||||
const PENDING_CATEGORY_KEY = 'product_category_id'
|
||||
const PENDING_KEYWORD_KEY = 'product_keyword'
|
||||
|
||||
export default function ProductPage() {
|
||||
const addItem = useCartStore(s => s.addItem)
|
||||
@@ -51,16 +52,16 @@ export default function ProductPage() {
|
||||
[activeChild, activeTop],
|
||||
)
|
||||
|
||||
/** 拉取商品列表 */
|
||||
/** 拉取商品列表(keywordOverride 用于状态未更新时显式传本次搜索词) */
|
||||
const fetchList = useCallback(
|
||||
async (pageNum: number, reset: boolean) => {
|
||||
async (pageNum: number, reset: boolean, keywordOverride?: string) => {
|
||||
if (listLoadingRef.current) return
|
||||
listLoadingRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getProductListApi({
|
||||
category_id: effectiveCategoryId,
|
||||
keyword: searchKey || undefined,
|
||||
keyword: keywordOverride ?? (searchKey || undefined),
|
||||
page: pageNum,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
@@ -108,8 +109,20 @@ export default function ProductPage() {
|
||||
}, [])
|
||||
|
||||
useDidShow(() => {
|
||||
// 处理首页搜索跳转带入的关键词
|
||||
let pendingKeyword: string | null = null
|
||||
try {
|
||||
pendingKeyword = Taro.getStorageSync(PENDING_KEYWORD_KEY) || null
|
||||
Taro.removeStorageSync(PENDING_KEYWORD_KEY)
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
if (pendingKeyword) {
|
||||
setInputKey(pendingKeyword)
|
||||
setSearchKey(pendingKeyword)
|
||||
}
|
||||
loadCategories()
|
||||
fetchList(1, true)
|
||||
fetchList(1, true, pendingKeyword ?? undefined)
|
||||
})
|
||||
|
||||
useReachBottom(() => {
|
||||
|
||||
+37
-294
@@ -4,7 +4,7 @@
|
||||
padding: 20rpx 24rpx 160rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// ===== 用户卡片 =====
|
||||
// ===== 用户信息块 =====
|
||||
.profile-card {
|
||||
background: linear-gradient(135deg, #ee0a24, #ff4d4f);
|
||||
border-radius: 20rpx;
|
||||
@@ -92,11 +92,15 @@
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
|
||||
&--menu {
|
||||
padding: 8rpx 28rpx;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@@ -104,188 +108,69 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__total {
|
||||
&__more {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__action {
|
||||
font-size: 24rpx;
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 订单筛选 chips =====
|
||||
.order-filter-scroll {
|
||||
white-space: nowrap;
|
||||
margin: 0 -28rpx 8rpx;
|
||||
padding: 0 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
// ===== 订单总汇 =====
|
||||
.order-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.order-filter-chip {
|
||||
display: inline-flex;
|
||||
padding: 10rpx 24rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f7f8fa;
|
||||
font-size: 24rpx;
|
||||
color: #646566;
|
||||
|
||||
&.active {
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
&__item {
|
||||
width: 33.33%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 订单项 =====
|
||||
.order-item {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&__header {
|
||||
&__icon {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #fff0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__no {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 24rpx;
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
&__body {
|
||||
&__label {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__amounts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__qty {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__remark {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
margin-top: 16rpx;
|
||||
display: inline-flex;
|
||||
padding: 8rpx 24rpx;
|
||||
border: 1rpx solid #ee0a24;
|
||||
color: #ee0a24;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 对账单项 =====
|
||||
.statement-item {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__no {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 24rpx;
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__period {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__settle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 设置 =====
|
||||
.setting-cell {
|
||||
// ===== 功能菜单 =====
|
||||
.menu-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
padding: 28rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-left: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 26rpx;
|
||||
color: #969799;
|
||||
&__arrow {
|
||||
font-size: 32rpx;
|
||||
color: #c8c9cc;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,146 +184,4 @@
|
||||
font-size: 30rpx;
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
// ===== 订单详情弹层 =====
|
||||
.detail-popup {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__status {
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
&__list {
|
||||
max-height: 480rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-spec {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&-amount {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__total {
|
||||
font-size: 30rpx;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 生成对账单弹层 =====
|
||||
.gen-popup {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__row {
|
||||
margin-top: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 28rpx;
|
||||
color: #646566;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 回款周期弹层 =====
|
||||
.cycle-popup {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__input {
|
||||
margin-top: 24rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+80
-392
@@ -1,26 +1,41 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
|
||||
import { View, Text, Image, Input, Picker, ScrollView } from '@tarojs/components'
|
||||
import { Button, Empty, Popup } from '@antmjs/vantui'
|
||||
import { useCallback } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Icon } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { getUserInfoApi } from '@/services/auth'
|
||||
import { cancelOrderApi, getOrderDetailApi, getOrderListApi } from '@/services/order'
|
||||
import { generateStatementApi, getStatementListApi } from '@/services/statement'
|
||||
import { updatePaymentCycleApi } from '@/services/store'
|
||||
import { ORDER_NAV_ITEMS } from '@/types/order'
|
||||
import { resolveAvatarUrl } from '@/utils/format'
|
||||
import { ORDER_STATUS_FILTERS, ORDER_STATUS_MAP } from '@/types/order'
|
||||
import { STATEMENT_STATUS_MAP } from '@/types/statement'
|
||||
import type { Order, OrderStatus } from '@/types/order'
|
||||
import type { Statement } from '@/types/statement'
|
||||
import type { UserType } from '@/types/user'
|
||||
import './index.less'
|
||||
|
||||
const ORDER_PAGE_SIZE = 10
|
||||
|
||||
/** 日期 → Y-m-d */
|
||||
function toYMD(d: Date): string {
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||
}
|
||||
/** 菜单项(后续单独页面开发时在此追加) */
|
||||
const MENU_ITEMS = [
|
||||
{
|
||||
key: 'orders',
|
||||
label: '所有订单',
|
||||
icon: 'orders-o',
|
||||
onClick: () => Taro.navigateTo({ url: '/pages/order-list/index?status=all' }),
|
||||
},
|
||||
{
|
||||
key: 'statement',
|
||||
label: '对账单',
|
||||
icon: 'balance-list-o',
|
||||
onClick: () => Taro.navigateTo({ url: '/pages/statement/index' }),
|
||||
},
|
||||
{
|
||||
key: 'message',
|
||||
label: '消息中心',
|
||||
icon: 'chat-o',
|
||||
onClick: () => Taro.switchTab({ url: '/pages/message/index' }),
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: '设置',
|
||||
icon: 'setting-o',
|
||||
onClick: () => Taro.navigateTo({ url: '/pages/settings/index' }),
|
||||
},
|
||||
]
|
||||
|
||||
export default function ProfilePage() {
|
||||
const user = useAuthStore(s => s.user)
|
||||
@@ -30,176 +45,25 @@ export default function ProfilePage() {
|
||||
|
||||
const loggedIn = !!token && !!user
|
||||
|
||||
// ===== 我的订单 =====
|
||||
const [orderFilter, setOrderFilter] = useState<OrderStatus | null>(null)
|
||||
const [orders, setOrders] = useState<Order[]>([])
|
||||
const [orderPage, setOrderPage] = useState(1)
|
||||
const [orderTotal, setOrderTotal] = useState(0)
|
||||
const [orderLoading, setOrderLoading] = useState(false)
|
||||
const [orderFinished, setOrderFinished] = useState(false)
|
||||
const orderLoadingRef = useRef(false)
|
||||
|
||||
// ===== 订单详情弹层 =====
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
const [orderDetail, setOrderDetail] = useState<Order | null>(null)
|
||||
|
||||
// ===== 对账单 =====
|
||||
const [statements, setStatements] = useState<Statement[]>([])
|
||||
|
||||
// ===== 生成对账单弹层 =====
|
||||
const [showGen, setShowGen] = useState(false)
|
||||
const [genStart, setGenStart] = useState(() => toYMD(new Date(Date.now() - 30 * 864e5)))
|
||||
const [genEnd, setGenEnd] = useState(() => toYMD(new Date()))
|
||||
const [genLoading, setGenLoading] = useState(false)
|
||||
|
||||
// ===== 回款周期弹层 =====
|
||||
const [showCycle, setShowCycle] = useState(false)
|
||||
const [cycleInput, setCycleInput] = useState('1')
|
||||
const [cycleLoading, setCycleLoading] = useState(false)
|
||||
|
||||
/** 拉取订单列表(filter 显式传入,避免状态异步导致的旧值) */
|
||||
const loadOrders = useCallback(
|
||||
async (reset: boolean, filter: OrderStatus | null = orderFilter) => {
|
||||
if (!loggedIn) return
|
||||
if (orderLoadingRef.current) return
|
||||
orderLoadingRef.current = true
|
||||
setOrderLoading(true)
|
||||
try {
|
||||
const nextPage = reset ? 1 : orderPage + 1
|
||||
const res = await getOrderListApi({
|
||||
status: filter ?? undefined,
|
||||
page: nextPage,
|
||||
pageSize: ORDER_PAGE_SIZE,
|
||||
})
|
||||
const { data, total } = res.data
|
||||
setOrders(prev => (reset ? data : [...prev, ...data]))
|
||||
setOrderTotal(total)
|
||||
setOrderPage(nextPage)
|
||||
setOrderFinished(nextPage * ORDER_PAGE_SIZE >= total)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
orderLoadingRef.current = false
|
||||
setOrderLoading(false)
|
||||
}
|
||||
},
|
||||
[loggedIn, orderFilter, orderPage],
|
||||
)
|
||||
|
||||
/** 拉取对账单 */
|
||||
const loadStatements = useCallback(async () => {
|
||||
if (!loggedIn) return
|
||||
try {
|
||||
const res = await getStatementListApi({ page: 1, pageSize: 5 })
|
||||
setStatements(res.data.data)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
}, [loggedIn])
|
||||
|
||||
useDidShow(() => {
|
||||
if (!loggedIn) {
|
||||
setOrders([])
|
||||
setStatements([])
|
||||
return
|
||||
}
|
||||
if (!loggedIn) return
|
||||
// 刷新用户信息(门店/客户等级可能变化)
|
||||
getUserInfoApi()
|
||||
.then(res => setUser(res.data))
|
||||
.catch(() => {})
|
||||
loadOrders(true)
|
||||
loadStatements()
|
||||
})
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!orderFinished && !orderLoadingRef.current && loggedIn) {
|
||||
loadOrders(false)
|
||||
}
|
||||
})
|
||||
|
||||
/** 切换订单状态筛选:立即按新筛选重新加载 */
|
||||
const handleFilterTap = useCallback(
|
||||
(value: OrderStatus | null) => {
|
||||
setOrderFilter(value)
|
||||
setOrderPage(1)
|
||||
setOrderFinished(false)
|
||||
loadOrders(true, value)
|
||||
},
|
||||
[loadOrders],
|
||||
)
|
||||
|
||||
/** 查看订单详情 */
|
||||
const handleOrderTap = useCallback(async (order: Order) => {
|
||||
try {
|
||||
const res = await getOrderDetailApi(order.id)
|
||||
setOrderDetail(res.data)
|
||||
setShowDetail(true)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
/** 身份标签 */
|
||||
const getTypeLabel = useCallback((type: UserType): string => {
|
||||
if (type === 1) return '门店'
|
||||
if (type === 2) return '供应商'
|
||||
return '待绑定'
|
||||
}, [])
|
||||
|
||||
/** 取消订单(仅待汇总) */
|
||||
const handleCancelOrder = useCallback(
|
||||
(order: Order) => {
|
||||
Taro.showModal({
|
||||
title: '取消订单',
|
||||
content: `确定取消订单 ${order.order_no} 吗?`,
|
||||
confirmColor: '#ee0a24',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
cancelOrderApi(order.id)
|
||||
.then(() => {
|
||||
Taro.showToast({ title: '订单已取消', icon: 'success' })
|
||||
loadOrders(true)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
[loadOrders],
|
||||
)
|
||||
|
||||
/** 生成对账单 */
|
||||
const handleGenerate = useCallback(async () => {
|
||||
if (genStart > genEnd) {
|
||||
Taro.showToast({ title: '结束日期不能早于开始日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (genLoading) return
|
||||
setGenLoading(true)
|
||||
try {
|
||||
await generateStatementApi({ period_start: genStart, period_end: genEnd })
|
||||
Taro.showToast({ title: '对账单已生成', icon: 'success' })
|
||||
setShowGen(false)
|
||||
loadStatements()
|
||||
} catch {
|
||||
// 错误(周期内无订单等)已由 request 层 toast
|
||||
} finally {
|
||||
setGenLoading(false)
|
||||
}
|
||||
}, [genStart, genEnd, genLoading, loadStatements])
|
||||
|
||||
/** 保存回款周期 */
|
||||
const handleSaveCycle = useCallback(async () => {
|
||||
const days = Number(cycleInput)
|
||||
if (!Number.isInteger(days) || days < 0) {
|
||||
Taro.showToast({ title: '请输入不小于 0 的整数', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (cycleLoading) return
|
||||
setCycleLoading(true)
|
||||
try {
|
||||
await updatePaymentCycleApi(days)
|
||||
Taro.showToast({ title: '回款周期已更新', icon: 'success' })
|
||||
setShowCycle(false)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
setCycleLoading(false)
|
||||
}
|
||||
}, [cycleInput, cycleLoading])
|
||||
/** 订单总汇 → 订单列表页(按状态) */
|
||||
const handleOrderNav = useCallback((status?: number) => {
|
||||
Taro.navigateTo({ url: `/pages/order-list/index?status=${status ?? 'all'}` })
|
||||
}, [])
|
||||
|
||||
/** 退出登录 */
|
||||
const handleLogout = useCallback(() => {
|
||||
@@ -222,7 +86,7 @@ export default function ProfilePage() {
|
||||
|
||||
return (
|
||||
<View className='profile-page'>
|
||||
{/* ========== 用户卡片 ========== */}
|
||||
{/* ========== 用户信息块 ========== */}
|
||||
<View className='profile-card'>
|
||||
{!loggedIn ? (
|
||||
<View className='profile-card__row'>
|
||||
@@ -266,226 +130,50 @@ export default function ProfilePage() {
|
||||
) : user?.supplier ? (
|
||||
<Text className='profile-card__tag'>供应商 · {user.supplier.name}</Text>
|
||||
) : (
|
||||
<Text className='profile-card__tag'>身份待绑定,联系客服或绑定手机号</Text>
|
||||
<Text className='profile-card__tag'>{getTypeLabel(user?.type ?? 0)},联系客服或绑定手机号</Text>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== 订单总汇 ========== */}
|
||||
<View className='profile-section'>
|
||||
<View className='profile-section__header'>
|
||||
<Text className='profile-section__title'>订单总汇</Text>
|
||||
<Text className='profile-section__more' onClick={() => handleOrderNav()}>全部 ›</Text>
|
||||
</View>
|
||||
<View className='order-nav'>
|
||||
{ORDER_NAV_ITEMS.map(item => (
|
||||
<View key={item.key} className='order-nav__item' onClick={() => handleOrderNav(item.status)}>
|
||||
<View className='order-nav__icon'>
|
||||
<Icon name={item.icon} size={26} color='#ee0a24' />
|
||||
</View>
|
||||
<Text className='order-nav__label'>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 功能菜单 ========== */}
|
||||
<View className='profile-section profile-section--menu'>
|
||||
{MENU_ITEMS.map(item => (
|
||||
<View key={item.key} className='menu-cell' onClick={item.onClick}>
|
||||
<View className='menu-cell__left'>
|
||||
<Icon name={item.icon} size={20} color='#ee0a24' />
|
||||
<Text className='menu-cell__label'>{item.label}</Text>
|
||||
</View>
|
||||
<Text className='menu-cell__arrow'>›</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* ========== 退出登录 ========== */}
|
||||
{loggedIn && (
|
||||
<>
|
||||
{/* ========== 我的订单 ========== */}
|
||||
<View className='profile-section'>
|
||||
<View className='profile-section__header'>
|
||||
<Text className='profile-section__title'>我的订单</Text>
|
||||
<Text className='profile-section__total'>共 {orderTotal} 单</Text>
|
||||
</View>
|
||||
<ScrollView scrollX className='order-filter-scroll'>
|
||||
{ORDER_STATUS_FILTERS.map(item => (
|
||||
<View
|
||||
key={item.label}
|
||||
className={`order-filter-chip ${orderFilter === item.value ? 'active' : ''}`}
|
||||
onClick={() => handleFilterTap(item.value)}
|
||||
>
|
||||
<Text>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{orders.length === 0 ? (
|
||||
orderLoading ? (
|
||||
<View className='profile-section__loading'><Text>加载中...</Text></View>
|
||||
) : (
|
||||
<Empty description='暂无订单' className='profile-section__empty' />
|
||||
)
|
||||
) : (
|
||||
orders.map(order => (
|
||||
<View key={order.id} className='order-item' onClick={() => handleOrderTap(order)}>
|
||||
<View className='order-item__header'>
|
||||
<Text className='order-item__no'>{order.order_no}</Text>
|
||||
<Text className='order-item__status'>{ORDER_STATUS_MAP[order.status]}</Text>
|
||||
</View>
|
||||
<View className='order-item__body'>
|
||||
<Text className='order-item__date'>{order.order_date}</Text>
|
||||
<View className='order-item__amounts'>
|
||||
<Text className='order-item__qty'>共 {order.total_quantity} 件</Text>
|
||||
<Text className='order-item__amount'>¥{order.total_amount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{order.remark && <Text className='order-item__remark'>备注:{order.remark}</Text>}
|
||||
{order.status === 0 && (
|
||||
<View
|
||||
className='order-item__cancel'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleCancelOrder(order)
|
||||
}}
|
||||
>
|
||||
取消订单
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
{orderFinished && orders.length > 0 && (
|
||||
<View className='profile-section__loading'><Text>没有更多了</Text></View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== 对账单 ========== */}
|
||||
<View className='profile-section'>
|
||||
<View className='profile-section__header'>
|
||||
<Text className='profile-section__title'>对账单</Text>
|
||||
<Text className='profile-section__action' onClick={() => setShowGen(true)}>生成对账单</Text>
|
||||
</View>
|
||||
{statements.length === 0 ? (
|
||||
<View className='profile-section__loading'><Text>暂无对账单</Text></View>
|
||||
) : (
|
||||
statements.map(st => (
|
||||
<View key={st.id} className='statement-item'>
|
||||
<View className='statement-item__header'>
|
||||
<Text className='statement-item__no'>{st.statement_no}</Text>
|
||||
<Text className='statement-item__status'>{STATEMENT_STATUS_MAP[st.status]}</Text>
|
||||
</View>
|
||||
<View className='statement-item__body'>
|
||||
<Text className='statement-item__period'>
|
||||
{st.period_start} ~ {st.period_end}
|
||||
</Text>
|
||||
<Text className='statement-item__amount'>¥{st.total_amount}</Text>
|
||||
</View>
|
||||
{st.settlement_date && (
|
||||
<Text className='statement-item__settle'>应结算:{st.settlement_date}</Text>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== 设置 ========== */}
|
||||
<View className='profile-section'>
|
||||
<View className='profile-section__header'>
|
||||
<Text className='profile-section__title'>门店设置</Text>
|
||||
</View>
|
||||
<View className='setting-cell' onClick={() => setShowCycle(true)}>
|
||||
<Text className='setting-cell__label'>回款周期(天)</Text>
|
||||
<Text className='setting-cell__value'>设置 ›</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 退出登录 ========== */}
|
||||
<View className='profile-logout' onClick={handleLogout}>
|
||||
<Text>退出登录</Text>
|
||||
</View>
|
||||
</>
|
||||
<View className='profile-logout' onClick={handleLogout}>
|
||||
<Text>退出登录</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 订单详情弹层 ========== */}
|
||||
<Popup
|
||||
show={showDetail}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowDetail(false)}
|
||||
>
|
||||
{orderDetail && (
|
||||
<View className='detail-popup'>
|
||||
<Text className='detail-popup__title'>{orderDetail.order_no}</Text>
|
||||
<View className='detail-popup__meta'>
|
||||
<Text>{orderDetail.order_date}</Text>
|
||||
<Text className='detail-popup__status'>{ORDER_STATUS_MAP[orderDetail.status]}</Text>
|
||||
</View>
|
||||
<ScrollView scrollY className='detail-popup__list'>
|
||||
{(orderDetail.items ?? []).map(item => (
|
||||
<View key={item.id} className='detail-popup__item'>
|
||||
<View className='detail-popup__item-info'>
|
||||
<Text className='detail-popup__item-name'>{item.product_name}</Text>
|
||||
<Text className='detail-popup__item-spec'>
|
||||
{item.product_spec} ¥{item.price} × {item.quantity}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='detail-popup__item-amount'>¥{item.amount}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
<View className='detail-popup__footer'>
|
||||
<Text className='detail-popup__total'>合计 ¥{orderDetail.total_amount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Popup>
|
||||
|
||||
{/* ========== 生成对账单弹层 ========== */}
|
||||
<Popup
|
||||
show={showGen}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowGen(false)}
|
||||
>
|
||||
<View className='gen-popup'>
|
||||
<Text className='gen-popup__title'>生成对账单</Text>
|
||||
<View className='gen-popup__row'>
|
||||
<Text className='gen-popup__label'>开始日期</Text>
|
||||
<Picker mode='date' value={genStart} end={genEnd} onChange={e => setGenStart(e.detail.value)}>
|
||||
<View className='gen-popup__value'>{genStart}</View>
|
||||
</Picker>
|
||||
</View>
|
||||
<View className='gen-popup__row'>
|
||||
<Text className='gen-popup__label'>结束日期</Text>
|
||||
<Picker mode='date' value={genEnd} start={genStart} onChange={e => setGenEnd(e.detail.value)}>
|
||||
<View className='gen-popup__value'>{genEnd}</View>
|
||||
</Picker>
|
||||
</View>
|
||||
<Button
|
||||
type='danger'
|
||||
block
|
||||
round
|
||||
loading={genLoading}
|
||||
className='gen-popup__submit'
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
生成
|
||||
</Button>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* ========== 回款周期弹层 ========== */}
|
||||
<Popup
|
||||
show={showCycle}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowCycle(false)}
|
||||
>
|
||||
<View className='cycle-popup'>
|
||||
<Text className='cycle-popup__title'>修改回款周期</Text>
|
||||
<Text className='cycle-popup__desc'>0 = 当天结算;该值影响后续生成对账单的应结算日期</Text>
|
||||
<Input
|
||||
className='cycle-popup__input'
|
||||
type='number'
|
||||
value={cycleInput}
|
||||
placeholder='请输入回款周期天数'
|
||||
onInput={e => setCycleInput(e.detail.value)}
|
||||
/>
|
||||
<Button
|
||||
type='danger'
|
||||
block
|
||||
round
|
||||
loading={cycleLoading}
|
||||
className='cycle-popup__submit'
|
||||
onClick={handleSaveCycle}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</View>
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '设置',
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
.settings-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// ===== 区块 =====
|
||||
.settings-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 8rpx 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&__header {
|
||||
padding: 24rpx 0 8rpx;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 26rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 回款周期弹层 =====
|
||||
.cycle-popup {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__input {
|
||||
margin-top: 24rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { View, Text, Input } from '@tarojs/components'
|
||||
import { Button, Popup } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { updatePaymentCycleApi } from '@/services/store'
|
||||
import './index.less'
|
||||
|
||||
/**
|
||||
* 设置页(框架)
|
||||
* 门店设置:回款周期;后续在此追加更多设置项
|
||||
*/
|
||||
export default function SettingsPage() {
|
||||
const token = useAuthStore(s => s.token)
|
||||
|
||||
/** 回款周期弹层 */
|
||||
const [showCycle, setShowCycle] = useState(false)
|
||||
const [cycleInput, setCycleInput] = useState('1')
|
||||
const [cycleLoading, setCycleLoading] = useState(false)
|
||||
|
||||
const loggedIn = !!token
|
||||
|
||||
useDidShow(() => {
|
||||
// 预留:进入设置页时刷新门店设置
|
||||
})
|
||||
|
||||
/** 保存回款周期 */
|
||||
const handleSaveCycle = useCallback(async () => {
|
||||
const days = Number(cycleInput)
|
||||
if (!Number.isInteger(days) || days < 0) {
|
||||
Taro.showToast({ title: '请输入不小于 0 的整数', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (cycleLoading) return
|
||||
setCycleLoading(true)
|
||||
try {
|
||||
await updatePaymentCycleApi(days)
|
||||
Taro.showToast({ title: '回款周期已更新', icon: 'success' })
|
||||
setShowCycle(false)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
setCycleLoading(false)
|
||||
}
|
||||
}, [cycleInput, cycleLoading])
|
||||
|
||||
/** 占位菜单:后续单独页面开发 */
|
||||
const handlePlaceholder = useCallback((label: string) => {
|
||||
Taro.showToast({ title: `${label}开发中`, icon: 'none' })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='settings-page'>
|
||||
{/* ========== 门店设置 ========== */}
|
||||
<View className='settings-section'>
|
||||
<View className='settings-section__header'>
|
||||
<Text className='settings-section__title'>门店设置</Text>
|
||||
</View>
|
||||
<View className='setting-cell' onClick={() => setShowCycle(true)}>
|
||||
<Text className='setting-cell__label'>回款周期(天)</Text>
|
||||
<Text className='setting-cell__value'>设置 ›</Text>
|
||||
</View>
|
||||
<View className='setting-cell' onClick={() => handlePlaceholder('收货信息')}>
|
||||
<Text className='setting-cell__label'>收货信息</Text>
|
||||
<Text className='setting-cell__value'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 通用 ========== */}
|
||||
<View className='settings-section'>
|
||||
<View className='setting-cell' onClick={() => handlePlaceholder('清除缓存')}>
|
||||
<Text className='setting-cell__label'>清除缓存</Text>
|
||||
<Text className='setting-cell__value'>›</Text>
|
||||
</View>
|
||||
<View className='setting-cell' onClick={() => handlePlaceholder('用户协议')}>
|
||||
<Text className='setting-cell__label'>用户协议</Text>
|
||||
<Text className='setting-cell__value'>›</Text>
|
||||
</View>
|
||||
<View className='setting-cell' onClick={() => handlePlaceholder('隐私政策')}>
|
||||
<Text className='setting-cell__label'>隐私政策</Text>
|
||||
<Text className='setting-cell__value'>›</Text>
|
||||
</View>
|
||||
<View className='setting-cell' onClick={() => handlePlaceholder('关于我们')}>
|
||||
<Text className='setting-cell__label'>关于我们</Text>
|
||||
<Text className='setting-cell__value'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 回款周期弹层 ========== */}
|
||||
<Popup
|
||||
show={showCycle}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowCycle(false)}
|
||||
>
|
||||
<View className='cycle-popup'>
|
||||
<Text className='cycle-popup__title'>修改回款周期</Text>
|
||||
<Text className='cycle-popup__desc'>0 = 当天结算;该值影响后续生成对账单的应结算日期</Text>
|
||||
<Input
|
||||
className='cycle-popup__input'
|
||||
type='number'
|
||||
value={cycleInput}
|
||||
placeholder='请输入回款周期天数'
|
||||
onInput={e => setCycleInput(e.detail.value)}
|
||||
/>
|
||||
<Button
|
||||
type='danger'
|
||||
block
|
||||
round
|
||||
loading={cycleLoading}
|
||||
className='cycle-popup__submit'
|
||||
onClick={handleSaveCycle}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</View>
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '对账单',
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
.statement-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// ===== 头部 =====
|
||||
.statement-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 8rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
background: #ee0a24;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.statement-empty {
|
||||
padding-top: 120rpx;
|
||||
|
||||
&__btn {
|
||||
margin-top: 24rpx;
|
||||
padding: 14rpx 60rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.statement-loading {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
// ===== 对账单项 =====
|
||||
.statement-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__no {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 24rpx;
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__period {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__settle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 生成弹层 =====
|
||||
.gen-popup {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__row {
|
||||
margin-top: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 28rpx;
|
||||
color: #646566;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
|
||||
import { View, Text, Picker } from '@tarojs/components'
|
||||
import { Button, Empty, Popup } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { generateStatementApi, getStatementListApi } from '@/services/statement'
|
||||
import { STATEMENT_STATUS_MAP } from '@/types/statement'
|
||||
import type { Statement } from '@/types/statement'
|
||||
import './index.less'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
/** 日期 → Y-m-d */
|
||||
function toYMD(d: Date): string {
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 对账单页(框架)
|
||||
* 门店自助:对账单列表 + 按日期区间生成
|
||||
*/
|
||||
export default function StatementPage() {
|
||||
const token = useAuthStore(s => s.token)
|
||||
|
||||
const [statements, setStatements] = useState<Statement[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const loadingRef = useRef(false)
|
||||
|
||||
/** 生成弹层 */
|
||||
const [showGen, setShowGen] = useState(false)
|
||||
const [genStart, setGenStart] = useState(() => toYMD(new Date(Date.now() - 30 * 864e5)))
|
||||
const [genEnd, setGenEnd] = useState(() => toYMD(new Date()))
|
||||
const [genLoading, setGenLoading] = useState(false)
|
||||
|
||||
const loggedIn = !!token
|
||||
|
||||
/** 拉取对账单列表 */
|
||||
const loadList = useCallback(
|
||||
async (pageNum: number, reset: boolean) => {
|
||||
if (!loggedIn || loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getStatementListApi({ page: pageNum, pageSize: PAGE_SIZE })
|
||||
const { data, total } = res.data
|
||||
setStatements(prev => (reset ? data : [...prev, ...data]))
|
||||
setPage(pageNum)
|
||||
setFinished(pageNum * PAGE_SIZE >= total)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[loggedIn],
|
||||
)
|
||||
|
||||
useDidShow(() => {
|
||||
loadList(1, true)
|
||||
})
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!finished && !loadingRef.current && loggedIn) {
|
||||
loadList(page + 1, false)
|
||||
}
|
||||
})
|
||||
|
||||
/** 生成对账单 */
|
||||
const handleGenerate = useCallback(async () => {
|
||||
if (genStart > genEnd) {
|
||||
Taro.showToast({ title: '结束日期不能早于开始日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (genLoading) return
|
||||
setGenLoading(true)
|
||||
try {
|
||||
await generateStatementApi({ period_start: genStart, period_end: genEnd })
|
||||
Taro.showToast({ title: '对账单已生成', icon: 'success' })
|
||||
setShowGen(false)
|
||||
loadList(1, true)
|
||||
} catch {
|
||||
// 错误(周期内无订单等)已由 request 层 toast
|
||||
} finally {
|
||||
setGenLoading(false)
|
||||
}
|
||||
}, [genStart, genEnd, genLoading, loadList])
|
||||
|
||||
const goLogin = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/login/index' })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='statement-page'>
|
||||
{/* ========== 头部 ========== */}
|
||||
<View className='statement-header'>
|
||||
<Text className='statement-header__title'>对账单</Text>
|
||||
<View className='statement-header__btn' onClick={() => setShowGen(true)}>
|
||||
<Text>生成对账单</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 列表 ========== */}
|
||||
{!loggedIn ? (
|
||||
<Empty description='登录后查看对账单' className='statement-empty'>
|
||||
<View className='statement-empty__btn' onClick={goLogin}>去登录</View>
|
||||
</Empty>
|
||||
) : statements.length === 0 ? (
|
||||
loading ? (
|
||||
<View className='statement-loading'><Text>加载中...</Text></View>
|
||||
) : (
|
||||
<Empty description='暂无对账单' className='statement-empty' />
|
||||
)
|
||||
) : (
|
||||
statements.map(st => (
|
||||
<View key={st.id} className='statement-item'>
|
||||
<View className='statement-item__header'>
|
||||
<Text className='statement-item__no'>{st.statement_no}</Text>
|
||||
<Text className='statement-item__status'>{STATEMENT_STATUS_MAP[st.status]}</Text>
|
||||
</View>
|
||||
<View className='statement-item__body'>
|
||||
<Text className='statement-item__period'>
|
||||
{st.period_start} ~ {st.period_end}
|
||||
</Text>
|
||||
<Text className='statement-item__amount'>¥{st.total_amount}</Text>
|
||||
</View>
|
||||
{st.settlement_date && (
|
||||
<Text className='statement-item__settle'>应结算:{st.settlement_date}</Text>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
{loggedIn && finished && statements.length > 0 && (
|
||||
<View className='statement-loading'><Text>没有更多了</Text></View>
|
||||
)}
|
||||
|
||||
{/* ========== 生成弹层 ========== */}
|
||||
<Popup
|
||||
show={showGen}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowGen(false)}
|
||||
>
|
||||
<View className='gen-popup'>
|
||||
<Text className='gen-popup__title'>生成对账单</Text>
|
||||
<Text className='gen-popup__desc'>按门店客户等级价汇总周期内订单明细</Text>
|
||||
<View className='gen-popup__row'>
|
||||
<Text className='gen-popup__label'>开始日期</Text>
|
||||
<Picker mode='date' value={genStart} end={genEnd} onChange={e => setGenStart(e.detail.value)}>
|
||||
<View className='gen-popup__value'>{genStart}</View>
|
||||
</Picker>
|
||||
</View>
|
||||
<View className='gen-popup__row'>
|
||||
<Text className='gen-popup__label'>结束日期</Text>
|
||||
<Picker mode='date' value={genEnd} start={genStart} onChange={e => setGenEnd(e.detail.value)}>
|
||||
<View className='gen-popup__value'>{genEnd}</View>
|
||||
</Picker>
|
||||
</View>
|
||||
<Button
|
||||
type='danger'
|
||||
block
|
||||
round
|
||||
loading={genLoading}
|
||||
className='gen-popup__submit'
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
生成
|
||||
</Button>
|
||||
</View>
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,20 @@ export const ORDER_STATUS_FILTERS: Array<{ value: OrderStatus | null; label: str
|
||||
{ value: 9, label: '已取消' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 我的页「订单总汇」导航(业务语言 → 后端枚举)
|
||||
* status 映射 StoreOrder.status:0 待汇总 / 1 已汇总 / 2 配送中 / 3 已完成 / 9 已取消
|
||||
* 注意:后端当前无「待付款」状态(status 缺省 = 不传参,显示全部),待后端补充后在此对齐
|
||||
*/
|
||||
export const ORDER_NAV_ITEMS: Array<{ key: string; label: string; status?: number; icon: string }> = [
|
||||
{ key: 'pending_review', label: '待审核', status: 0, icon: 'clock-o' },
|
||||
{ key: 'pending_stock', label: '待配货', status: 1, icon: 'logistics' },
|
||||
{ key: 'stocking', label: '配货中', status: 2, icon: 'van-o' },
|
||||
{ key: 'pending_pay', label: '待付款', icon: 'pending-payment' },
|
||||
{ key: 'completed', label: '已完成', status: 3, icon: 'completed' },
|
||||
{ key: 'all', label: '我的订单', icon: 'orders-o' },
|
||||
]
|
||||
|
||||
/** 门店订单 */
|
||||
export interface Order {
|
||||
id: number
|
||||
|
||||
Reference in New Issue
Block a user