diff --git a/src/app.config.ts b/src/app.config.ts index bffab9c..18d4310 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -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: { diff --git a/src/pages/index/index.less b/src/pages/index/index.less index 2d74013..a5d4b05 100644 --- a/src/pages/index/index.less +++ b/src/pages/index/index.less @@ -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; } } } diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index ebed02c..58ecac3 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -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(null) - /** 顶级分类快捷入口 */ + /** 顶级分类(导航菜单前 6 个) */ const [categories, setCategories] = useState([]) - /** 最新通知 */ - const [notices, setNotices] = useState([]) - const [unreadCount, setUnreadCount] = useState(0) + /** 推荐商品 */ + const [products, setProducts] = useState([]) + /** 搜索框输入 */ + 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 ( - {/* ========== 门店信息卡片 ========== */} - - {!loggedIn ? ( - - - 欢迎使用订货采购系统 - 登录后即可查看门店与价格信息 - - 去登录 - - ) : ( - <> - - {user?.avatar ? ( - - ) : ( - {user?.nickname?.[0] || '用'} - )} - - {user?.nickname} - {user?.phone ? ( - {user.phone} - ) : ( - 未绑定手机号,点击去绑定 - )} + {/* ========== 顶部搜索 ========== */} + + setKeyword(String(e.detail))} + onFocus={handleSearchFocus} + onSearch={handleSearchFocus} + /> + + + {/* ========== 轮播图 ========== */} + + + {bannerImages.length > 0 ? ( + bannerImages.map((product, idx) => ( + + goProduct()} + /> + + )) + ) : ( + + goProduct()}> + 订货采购 - - - {user?.type === 0 ? ( - 绑定手机号 - ) : user?.store ? ( - - 门店 · {user.store.name} - {user.store.level && {user.store.level.name}} - - ) : user?.supplier ? ( - - 供应商 · {user.supplier.name} - - ) : ( - 绑定身份 - )} - - - )} + + )} + - {/* ========== 本月订货汇总 ========== */} - - - 订货汇总 - {summary?.period_label ?? '—'} - - - - ¥{summary?.total_amount ?? '0.00'} - 订货金额 - - - {summary?.total_quantity ?? '0.00'} - 订货数量 - - - {summary?.order_count ?? 0} - 订单笔数 - - - - - {/* ========== 快捷入口 ========== */} - - - Taro.switchTab({ url: '/pages/product/index' })} /> - Taro.switchTab({ url: '/pages/cart/index' })} /> - Taro.switchTab({ url: '/pages/profile/index' })} /> - Taro.switchTab({ url: '/pages/profile/index' })} /> + {/* ========== 导航菜单 ========== */} + + + {categories.slice(0, 6).map((cat, idx) => ( + goProduct(cat.id)} + renderIcon={ + + {cat.name.slice(0, 1)} + + } + /> + ))} + goProduct()} + /> + Taro.navigateTo({ url: '/pages/order-list/index?status=all' })} + /> - {/* ========== 商品分类 ========== */} - {categories.length > 0 && ( - - - 商品分类 - Taro.switchTab({ url: '/pages/product/index' })}>查看全部 › - - + {/* ========== 推荐商品 ========== */} + + + 推荐商品 + goProduct()}>查看更多 › - )} - {/* ========== 通知 ========== */} - - - 通知 - {unreadCount > 0 && 未读 {unreadCount} 条} - - {notices.length === 0 ? ( - - 暂无通知 + {!loggedIn ? ( + + 登录后查看商品与价格 + Taro.navigateTo({ url: '/pages/login/index' })} + > + 去登录 + + + ) : products.length === 0 ? ( + + 暂无商品 ) : ( - notices.map(notice => ( - handleNoticeTap(notice)}> - {notice.is_read === 0 && } - - {notice.title} - {notice.content} + + {products.map(product => ( + goProduct()}> + + + {product.name} + {product.spec} / {product.unit} + + {product.price !== null ? ( + ¥{product.price} + ) : ( + 价格待定 + )} + handleQuickAdd(product, e)}> + + + + - - )) + ))} + )} ) } - -/** 横向滚动的分类入口 */ -function ScrollX({ items, onTap }: { items: Category[]; onTap: (id: number) => void }) { - return ( - - {items.map(item => ( - onTap(item.id)}> - {item.name} - - ))} - - ) -} diff --git a/src/pages/message/index.config.ts b/src/pages/message/index.config.ts index a2af952..4f6fcae 100644 --- a/src/pages/message/index.config.ts +++ b/src/pages/message/index.config.ts @@ -1,3 +1,3 @@ export default definePageConfig({ - navigationBarTitleText: '首页' -}) \ No newline at end of file + navigationBarTitleText: '消息', +}) diff --git a/src/pages/message/index.less b/src/pages/message/index.less index e69de29..454cba1 100644 --- a/src/pages/message/index.less +++ b/src/pages/message/index.less @@ -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; + } + } +} diff --git a/src/pages/message/index.tsx b/src/pages/message/index.tsx index d7fa857..c117240 100644 --- a/src/pages/message/index.tsx +++ b/src/pages/message/index.tsx @@ -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 = { + 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([]) + 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 ( - - - 上面的按钮的颜色已经通过全局主题重写覆盖了,参见src/styles/index.less + + {/* ========== 未读概览 ========== */} + + 消息中心 + {loggedIn && unreadCount > 0 && ( + 未读 {unreadCount} 条 + )} + + + {/* ========== 通知列表 ========== */} + {!loggedIn ? ( + + 去登录 + + ) : notices.length === 0 ? ( + loading ? ( + 加载中... + ) : ( + + ) + ) : ( + notices.map(notice => ( + handleRead(notice)}> + {notice.is_read === 0 && } + + + {notice.title} + + {NOTICE_TYPE_MAP[notice.type]} + + + {notice.content} + {formatTime(notice.created_at)} + + + )) + )} + + {loggedIn && finished && notices.length > 0 && ( + 没有更多了 + )} ) } - -// export default class Index extends Component { - -// componentWillMount () { } - -// componentDidMount () { } - -// componentWillUnmount () { } - -// componentDidShow () { } - -// componentDidHide () { } - -// render () { -// return ( -// -// -// 上面的按钮的颜色已经通过全局主题重写覆盖了,参见src/style/index.less -// -// ) -// } -// } diff --git a/src/pages/order-list/index.config.ts b/src/pages/order-list/index.config.ts new file mode 100644 index 0000000..99d0ded --- /dev/null +++ b/src/pages/order-list/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '我的订单', +}) diff --git a/src/pages/order-list/index.less b/src/pages/order-list/index.less new file mode 100644 index 0000000..a2ed2a4 --- /dev/null +++ b/src/pages/order-list/index.less @@ -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; + } + } +} diff --git a/src/pages/order-list/index.tsx b/src/pages/order-list/index.tsx new file mode 100644 index 0000000..20ec81b --- /dev/null +++ b/src/pages/order-list/index.tsx @@ -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(undefined) + const [orders, setOrders] = useState([]) + 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(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 ( + + {/* ========== 状态筛选 ========== */} + + handleStatusTap(undefined)} + > + 全部 + + {ORDER_NAV_ITEMS.map(item => ( + handleStatusTap(item.status)} + > + {item.label} + + ))} + + + {/* ========== 订单列表 ========== */} + {!loggedIn ? ( + + 去登录 + + ) : orders.length === 0 ? ( + loading ? ( + 加载中... + ) : ( + + ) + ) : ( + orders.map(order => ( + handleOrderTap(order)}> + + {order.order_no} + {ORDER_STATUS_MAP[order.status]} + + + {order.order_date} + + 共 {order.total_quantity} 件 + ¥{order.total_amount} + + + {order.remark && 备注:{order.remark}} + {order.status === 0 && ( + { + e.stopPropagation() + handleCancelOrder(order) + }} + > + 取消订单 + + )} + + )) + )} + + {loggedIn && finished && orders.length > 0 && ( + 没有更多了 + )} + + {/* ========== 订单详情弹层 ========== */} + setShowDetail(false)} + > + {orderDetail && ( + + {orderDetail.order_no} + + {orderDetail.order_date} + {ORDER_STATUS_MAP[orderDetail.status]} + + + {(orderDetail.items ?? []).map(item => ( + + + {item.product_name} + + {item.product_spec} ¥{item.price} × {item.quantity} + + + ¥{item.amount} + + ))} + + + 合计 ¥{orderDetail.total_amount} + + + )} + + + ) +} diff --git a/src/pages/product/index.tsx b/src/pages/product/index.tsx index 65ab376..263eaa0 100644 --- a/src/pages/product/index.tsx +++ b/src/pages/product/index.tsx @@ -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(() => { diff --git a/src/pages/profile/index.less b/src/pages/profile/index.less index 2f719a7..32ed7d8 100644 --- a/src/pages/profile/index.less +++ b/src/pages/profile/index.less @@ -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; - } - } } diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 08e2805..1541a7d 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -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(null) - const [orders, setOrders] = useState([]) - 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(null) - - // ===== 对账单 ===== - const [statements, setStatements] = useState([]) - - // ===== 生成对账单弹层 ===== - 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 ( - {/* ========== 用户卡片 ========== */} + {/* ========== 用户信息块 ========== */} {!loggedIn ? ( @@ -266,226 +130,50 @@ export default function ProfilePage() { ) : user?.supplier ? ( 供应商 · {user.supplier.name} ) : ( - 身份待绑定,联系客服或绑定手机号 + {getTypeLabel(user?.type ?? 0)},联系客服或绑定手机号 )} )} + {/* ========== 订单总汇 ========== */} + + + 订单总汇 + handleOrderNav()}>全部 › + + + {ORDER_NAV_ITEMS.map(item => ( + handleOrderNav(item.status)}> + + + + {item.label} + + ))} + + + + {/* ========== 功能菜单 ========== */} + + {MENU_ITEMS.map(item => ( + + + + {item.label} + + + + ))} + + + {/* ========== 退出登录 ========== */} {loggedIn && ( - <> - {/* ========== 我的订单 ========== */} - - - 我的订单 - 共 {orderTotal} 单 - - - {ORDER_STATUS_FILTERS.map(item => ( - handleFilterTap(item.value)} - > - {item.label} - - ))} - - - {orders.length === 0 ? ( - orderLoading ? ( - 加载中... - ) : ( - - ) - ) : ( - orders.map(order => ( - handleOrderTap(order)}> - - {order.order_no} - {ORDER_STATUS_MAP[order.status]} - - - {order.order_date} - - 共 {order.total_quantity} 件 - ¥{order.total_amount} - - - {order.remark && 备注:{order.remark}} - {order.status === 0 && ( - { - e.stopPropagation() - handleCancelOrder(order) - }} - > - 取消订单 - - )} - - )) - )} - {orderFinished && orders.length > 0 && ( - 没有更多了 - )} - - - {/* ========== 对账单 ========== */} - - - 对账单 - setShowGen(true)}>生成对账单 - - {statements.length === 0 ? ( - 暂无对账单 - ) : ( - statements.map(st => ( - - - {st.statement_no} - {STATEMENT_STATUS_MAP[st.status]} - - - - {st.period_start} ~ {st.period_end} - - ¥{st.total_amount} - - {st.settlement_date && ( - 应结算:{st.settlement_date} - )} - - )) - )} - - - {/* ========== 设置 ========== */} - - - 门店设置 - - setShowCycle(true)}> - 回款周期(天) - 设置 › - - - - {/* ========== 退出登录 ========== */} - - 退出登录 - - + + 退出登录 + )} - - {/* ========== 订单详情弹层 ========== */} - setShowDetail(false)} - > - {orderDetail && ( - - {orderDetail.order_no} - - {orderDetail.order_date} - {ORDER_STATUS_MAP[orderDetail.status]} - - - {(orderDetail.items ?? []).map(item => ( - - - {item.product_name} - - {item.product_spec} ¥{item.price} × {item.quantity} - - - ¥{item.amount} - - ))} - - - 合计 ¥{orderDetail.total_amount} - - - )} - - - {/* ========== 生成对账单弹层 ========== */} - setShowGen(false)} - > - - 生成对账单 - - 开始日期 - setGenStart(e.detail.value)}> - {genStart} - - - - 结束日期 - setGenEnd(e.detail.value)}> - {genEnd} - - - - - - - {/* ========== 回款周期弹层 ========== */} - setShowCycle(false)} - > - - 修改回款周期 - 0 = 当天结算;该值影响后续生成对账单的应结算日期 - setCycleInput(e.detail.value)} - /> - - - ) } diff --git a/src/pages/settings/index.config.ts b/src/pages/settings/index.config.ts new file mode 100644 index 0000000..ca71aee --- /dev/null +++ b/src/pages/settings/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '设置', +}) diff --git a/src/pages/settings/index.less b/src/pages/settings/index.less new file mode 100644 index 0000000..b1cb639 --- /dev/null +++ b/src/pages/settings/index.less @@ -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; + } + } +} diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx new file mode 100644 index 0000000..05855bf --- /dev/null +++ b/src/pages/settings/index.tsx @@ -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 ( + + {/* ========== 门店设置 ========== */} + + + 门店设置 + + setShowCycle(true)}> + 回款周期(天) + 设置 › + + handlePlaceholder('收货信息')}> + 收货信息 + + + + + {/* ========== 通用 ========== */} + + handlePlaceholder('清除缓存')}> + 清除缓存 + + + handlePlaceholder('用户协议')}> + 用户协议 + + + handlePlaceholder('隐私政策')}> + 隐私政策 + + + handlePlaceholder('关于我们')}> + 关于我们 + + + + + {/* ========== 回款周期弹层 ========== */} + setShowCycle(false)} + > + + 修改回款周期 + 0 = 当天结算;该值影响后续生成对账单的应结算日期 + setCycleInput(e.detail.value)} + /> + + + + + ) +} diff --git a/src/pages/statement/index.config.ts b/src/pages/statement/index.config.ts new file mode 100644 index 0000000..b942b70 --- /dev/null +++ b/src/pages/statement/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '对账单', +}) diff --git a/src/pages/statement/index.less b/src/pages/statement/index.less new file mode 100644 index 0000000..8ae33af --- /dev/null +++ b/src/pages/statement/index.less @@ -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; + } + } +} diff --git a/src/pages/statement/index.tsx b/src/pages/statement/index.tsx new file mode 100644 index 0000000..288eb0e --- /dev/null +++ b/src/pages/statement/index.tsx @@ -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([]) + 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 ( + + {/* ========== 头部 ========== */} + + 对账单 + setShowGen(true)}> + 生成对账单 + + + + {/* ========== 列表 ========== */} + {!loggedIn ? ( + + 去登录 + + ) : statements.length === 0 ? ( + loading ? ( + 加载中... + ) : ( + + ) + ) : ( + statements.map(st => ( + + + {st.statement_no} + {STATEMENT_STATUS_MAP[st.status]} + + + + {st.period_start} ~ {st.period_end} + + ¥{st.total_amount} + + {st.settlement_date && ( + 应结算:{st.settlement_date} + )} + + )) + )} + + {loggedIn && finished && statements.length > 0 && ( + 没有更多了 + )} + + {/* ========== 生成弹层 ========== */} + setShowGen(false)} + > + + 生成对账单 + 按门店客户等级价汇总周期内订单明细 + + 开始日期 + setGenStart(e.detail.value)}> + {genStart} + + + + 结束日期 + setGenEnd(e.detail.value)}> + {genEnd} + + + + + + + ) +} diff --git a/src/types/order.ts b/src/types/order.ts index ea50082..28ff210 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -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