diff --git a/mini-home-api.md b/mini-home-api.md new file mode 100644 index 0000000..e00d2fd --- /dev/null +++ b/mini-home-api.md @@ -0,0 +1,172 @@ +# 小程序首页接口文档 + +> 适用端:微信小程序(h5 仓库,Taro) +> 后台配置入口:PC 后台「客户端配置」菜单(首页轮播图 / 宫格导航 / 促销推荐卡片) +> 更新日期:2026-08-14 + +## 通用约定 + +| 项 | 说明 | +|---|---| +| 根地址 | `BASE_URL`(见 `src/utils/request.ts`,如 `http://localhost:8000/index.php`) | +| 认证 | 请求头 `Authorization: Bearer {token}`,token 来自登录接口,本地存储 key `auth_token` | +| 响应包络 | `{ success: boolean, data: T, msg?: string, showType?: number }` | +| 失败处理 | `success=false` 时 `msg` 为中文错误信息;HTTP 401 表示登录过期,需重新登录 | + +## GET /mini/home + +首页配置聚合接口:一次返回轮播图、宫格导航、促销推荐卡片三组数据,均为**启用状态(status=1)**且按 `sort` 升序(越小越靠前)。 + +- **权限**:需登录(Bearer token) +- **请求参数**:无 + +### 响应示例 + +```json +{ + "success": true, + "msg": "ok", + "data": { + "banners": [ + { + "id": 1, + "title": "新鲜直采", + "image_id": 123, + "link": "/pages/goods/detail?id=1", + "sort": 0, + "image_url": "http://localhost:8000/storage/uploads/2026/08/14/xxx.jpg" + } + ], + "navs": [ + { + "id": 1, + "name": "蔬菜专区", + "image_id": 124, + "link": "/pages/category/index?id=1", + "sort": 0, + "image_url": "http://localhost:8000/storage/uploads/2026/08/14/yyy.png" + } + ], + "promos": [ + { + "id": 1, + "title": "限时特惠", + "sub_title": "全场 8 折起", + "image_id": 125, + "link": "/pages/promo/detail?id=1", + "sort": 0, + "image_url": "http://localhost:8000/storage/uploads/2026/08/14/zzz.jpg" + } + ] + } +} +``` + +### 字段说明 + +**banners(轮播图)** + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | number | 轮播图 ID | +| title | string | 标题(后台维护,可用于无障碍/占位) | +| image_id | number | 图片文件 ID(sys_file),一般无需使用 | +| **image_url** | string \| null | 轮播图片完整 URL,直接用于 ``;未传图时为 null | +| link | string | 小程序页面跳转路径,**空字符串表示点击不跳转** | +| sort | number | 排序值(已按此升序返回,前端无需再排) | + +**navs(宫格导航)** + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | number | 导航 ID | +| name | string | 导航名称(宫格文字) | +| **image_url** | string \| null | 导航图标完整 URL | +| link | string | 小程序页面跳转路径,空字符串不跳转 | +| sort | number | 排序值 | + +**promos(促销推荐卡片)** + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | number | 卡片 ID | +| title | string | 卡片标题 | +| sub_title | string | 副标题/促销文案,可能为空字符串 | +| **image_url** | string \| null | 卡片图片完整 URL | +| link | string | 小程序页面跳转路径,空字符串不跳转 | +| sort | number | 排序值 | + +### 前端接入示例 + +`src/services/home.ts`(新增): + +```ts +import { get } from '@/utils/request' + +/** 首页轮播图项 */ +export interface HomeBanner { + id: number + title: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页宫格导航项 */ +export interface HomeNav { + id: number + name: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页促销推荐卡片 */ +export interface HomePromo { + id: number + title: string + sub_title: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页配置聚合数据 */ +export interface HomeConfig { + banners: HomeBanner[] + navs: HomeNav[] + promos: HomePromo[] +} + +/** 首页配置(轮播图 + 宫格导航 + 促销卡片):GET /mini/home */ +export function getHomeConfigApi() { + return get('/mini/home') +} +``` + +页面中使用(跳转需兼容空链接): + +```tsx +const [config, setConfig] = useState({ banners: [], navs: [], promos: [] }) + +useEffect(() => { + getHomeConfigApi().then(res => setConfig(res.data)) +}, []) + +/** 统一跳转:link 为空不跳转 */ +function handleLink(link: string) { + if (!link) return + Taro.navigateTo({ url: link }) +} +``` + +### 注意事项 + +1. **三组数据均可能为空数组**(后台未配置或全部停用),页面需做空态处理。 +2. `image_url` 可能为 `null`(后台未上传图片),渲染前判空。 +3. `link` 为小程序内部页面路径(以 `/` 开头),用 `Taro.navigateTo` 跳转;若目标为 tabBar 页面需改用 `Taro.switchTab`(建议后台配置时避免填 tabBar 路径)。 +4. 数据实时生效:后台修改后,小程序下次进入首页请求即为最新内容,无缓存。 +5. 接口需登录后调用;未登录(401)会由 request 封装自动跳登录页。 diff --git a/src/app.config.ts b/src/app.config.ts index 1b0503d..b5e1730 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -7,6 +7,7 @@ export default defineAppConfig({ 'pages/profile/index', 'pages/order-list/index', 'pages/statement/index', + 'pages/statement-detail/index', 'pages/settings/index', 'pages/login/index', 'pages/register/index', diff --git a/src/pages/index/index.config.ts b/src/pages/index/index.config.ts index a7c25c7..d178761 100644 --- a/src/pages/index/index.config.ts +++ b/src/pages/index/index.config.ts @@ -1,3 +1,4 @@ export default definePageConfig({ navigationBarTitleText: '首页', + navigationStyle: 'custom', }) diff --git a/src/pages/index/index.less b/src/pages/index/index.less index a5d4b05..de508b3 100644 --- a/src/pages/index/index.less +++ b/src/pages/index/index.less @@ -4,17 +4,62 @@ padding-bottom: calc(140rpx + env(safe-area-inset-bottom)); box-sizing: border-box; - // ===== 顶部搜索 ===== - .home-search { - background: linear-gradient(135deg, #ee0a24, #ff4d4f); - padding: 16rpx 24rpx; + // ===== 自定义顶部导航栏 ===== + .home-header { + background: linear-gradient(160deg, #d60410 0%, #ee0a24 55%, #ff4d4f 100%); + // 底部留白供轮播图上移叠放 + padding-bottom: 88rpx; + + &__bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16rpx 32rpx 8rpx; + } + + &__brand { + display: flex; + align-items: baseline; + } + + &__title { + color: #ffffff; + font-size: 44rpx; + font-weight: 700; + letter-spacing: 4rpx; + } + + &__slogan { + margin-left: 16rpx; + color: rgba(255, 255, 255, 0.75); + font-size: 22rpx; + letter-spacing: 2rpx; + } + + &__notice { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.18); + display: flex; + align-items: center; + justify-content: center; + } + + &__search { + padding: 8rpx 16rpx 0; + } } // ===== 轮播图 ===== .home-banner { - margin: 20rpx 24rpx 0; + // 上移叠在红色导航栏上,形成视觉连贯 + margin: -72rpx 24rpx 0; border-radius: 20rpx; overflow: hidden; + box-shadow: 0 8rpx 24rpx rgba(238, 10, 36, 0.15); + position: relative; + z-index: 2; &__swiper { border-radius: 20rpx; @@ -22,52 +67,127 @@ &__image { width: 100%; - height: 320rpx; + height: 300rpx; + display: block; } &__placeholder { width: 100%; - height: 320rpx; - background: linear-gradient(135deg, #ff9a9e, #ff4d4f); + height: 300rpx; + background: linear-gradient(135deg, #ff8a5c, #ff4d4f); display: flex; + flex-direction: column; align-items: center; justify-content: center; } - &__placeholder-text { + &__placeholder-title { color: #fff; - font-size: 40rpx; - font-weight: 600; + font-size: 44rpx; + font-weight: 700; letter-spacing: 8rpx; } + + &__placeholder-sub { + margin-top: 16rpx; + color: rgba(255, 255, 255, 0.85); + font-size: 24rpx; + letter-spacing: 2rpx; + } } - // ===== 导航菜单 ===== + // ===== 宫格导航 ===== .home-menu { margin: 20rpx 24rpx 0; background: #fff; border-radius: 20rpx; - padding: 20rpx 0 8rpx; + padding: 28rpx 0 16rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04); } .menu-icon { - width: 84rpx; - height: 84rpx; - border-radius: 24rpx; + width: 88rpx; + height: 88rpx; + border-radius: 28rpx; display: flex; align-items: center; justify-content: center; + &__image { + width: 88rpx; + height: 88rpx; + display: block; + } + &__text { color: #fff; - font-size: 36rpx; + font-size: 38rpx; font-weight: 600; } } + // ===== 促销推荐卡片 ===== + .home-promo { + margin: 20rpx 24rpx 0; + } + + .promo-card { + position: relative; + height: 180rpx; + border-radius: 20rpx; + overflow: hidden; + margin-bottom: 16rpx; + + &:last-child { + margin-bottom: 0; + } + + &__bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + &__mask { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, rgba(160, 4, 16, 0.82) 0%, rgba(238, 10, 36, 0.45) 55%, rgba(238, 10, 36, 0) 100%); + } + + &__content { + position: absolute; + left: 32rpx; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + z-index: 2; + } + + &__title { + color: #ffffff; + font-size: 38rpx; + font-weight: 700; + letter-spacing: 2rpx; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.25); + } + + &__sub { + margin-top: 10rpx; + color: rgba(255, 255, 255, 0.9); + font-size: 24rpx; + letter-spacing: 1rpx; + } + } + // ===== 推荐商品 ===== .home-recommend { - margin: 20rpx 24rpx 0; + margin: 28rpx 24rpx 0; &__header { display: flex; @@ -76,6 +196,19 @@ padding: 8rpx 8rpx 20rpx; } + &__title-wrap { + display: flex; + align-items: center; + } + + &__title-bar { + width: 8rpx; + height: 30rpx; + border-radius: 4rpx; + background: linear-gradient(180deg, #ee0a24, #ff6034); + margin-right: 14rpx; + } + &__title { font-size: 32rpx; font-weight: 600; @@ -178,7 +311,7 @@ width: 52rpx; height: 52rpx; border-radius: 50%; - background: #ee0a24; + background: linear-gradient(135deg, #ee0a24, #ff6034); display: flex; align-items: center; justify-content: center; diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 58ecac3..8ec6eab 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -1,46 +1,68 @@ import { useCallback, useMemo, useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' import { View, Text, Image } from '@tarojs/components' -import { Grid, GridItem, Search, Swiper, SwiperItem } from '@antmjs/vantui' +import { Grid, GridItem, Icon, Search, Swiper, SwiperItem } from '@antmjs/vantui' import useAuthStore from '@/stores/auth/useAuthStore' import useCartStore from '@/stores/cart/useCartStore' -import { getCategoriesApi, getProductListApi } from '@/services/product' +import { getHomeConfigApi } from '@/services/home' +import type { HomeConfig } from '@/services/home' +import { getProductListApi } from '@/services/product' import { getProductCover } from '@/types/product' -import type { Category, Product } from '@/types/product' +import type { 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'] +/** tabBar 页面路径(link 跳转需改用 switchTab) */ +const TAB_PATHS = [ + 'pages/index/index', + 'pages/product/index', + 'pages/cart/index', + 'pages/message/index', + 'pages/profile/index', +] + +/** 宫格导航无图时的兜底色块配色(生鲜红橙系) */ +const NAV_COLORS = ['#ee0a24', '#ff7a1a', '#07c160', '#1989fa', '#8a5cf6', '#ff976a', '#00b8d9', '#f56c6c'] + +/** 获取状态栏高度(H5 端为 0) */ +function getStatusBarHeight(): number { + try { + const info = typeof Taro.getWindowInfo === 'function' ? Taro.getWindowInfo() : Taro.getSystemInfoSync() + return info.statusBarHeight || 0 + } catch { + return 0 + } +} export default function IndexPage() { const token = useAuthStore(s => s.token) const addItem = useCartStore(s => s.addItem) - /** 顶级分类(导航菜单前 6 个) */ - const [categories, setCategories] = useState([]) + /** 首页配置(轮播图 / 宫格导航 / 促销卡片) */ + const [config, setConfig] = useState({ banners: [], navs: [], promos: [] }) /** 推荐商品 */ const [products, setProducts] = useState([]) /** 搜索框输入 */ const [keyword, setKeyword] = useState('') const loggedIn = !!token + const statusBarHeight = useMemo(() => getStatusBarHeight(), []) useDidShow(() => { - loadCategories() if (loggedIn) { + loadHomeConfig() loadRecommend() } }) - /** 商品分类 */ - const loadCategories = useCallback(async () => { + /** 首页配置聚合数据(需登录) */ + const loadHomeConfig = useCallback(async () => { try { - const res = await getCategoriesApi() - setCategories(res.data.filter(c => c.parent_id === 0)) + const res = await getHomeConfigApi() + setConfig(res.data) } catch { // 错误已由 request 层 toast } @@ -56,17 +78,26 @@ export default function IndexPage() { } }, []) - /** 轮播数据:取有图商品的前 4 张图 */ - const bannerImages = useMemo( - () => products.filter(p => getProductCover(p)).slice(0, 4), - [products], - ) + /** + * 后台配置的 link 统一跳转: + * - 空字符串不跳转 + * - tabBar 页面用 switchTab,其余用 navigateTo + */ + const handleLink = useCallback((link: string) => { + if (!link) return + const path = link.split('?')[0].replace(/^\//, '') + if (TAB_PATHS.includes(path)) { + Taro.switchTab({ url: `/${path}` }) + } else { + Taro.navigateTo({ url: link }) + } + }, []) - /** 跳转商品页并带上分类/关键词 */ - const goProduct = useCallback((categoryId?: number, kw?: string) => { + /** 跳转商品页并带上关键词 */ + const goProduct = useCallback((kw?: string) => { try { - if (categoryId !== undefined) Taro.setStorageSync(PENDING_CATEGORY_KEY, categoryId) if (kw !== undefined) Taro.setStorageSync(PENDING_KEYWORD_KEY, kw) + Taro.removeStorageSync(PENDING_CATEGORY_KEY) } catch { // noop } @@ -75,7 +106,7 @@ export default function IndexPage() { /** 搜索框聚焦/提交 → 商品页搜索 */ const handleSearchFocus = useCallback(() => { - goProduct(undefined, keyword.trim()) + goProduct(keyword.trim()) }, [goProduct, keyword]) /** 快捷加购 */ @@ -94,82 +125,113 @@ export default function IndexPage() { return ( - {/* ========== 顶部搜索 ========== */} - - setKeyword(String(e.detail))} - onFocus={handleSearchFocus} - onSearch={handleSearchFocus} - /> + {/* ========== 自定义顶部导航栏 ========== */} + + + + 鲜采直供 + 产地直采 · 新鲜直达 + + Taro.switchTab({ url: '/pages/message/index' })}> + + + + + setKeyword(String(e.detail))} + onFocus={handleSearchFocus} + onSearch={handleSearchFocus} + /> + {/* ========== 轮播图 ========== */} - {bannerImages.length > 0 ? ( - bannerImages.map((product, idx) => ( - - goProduct()} - /> - - )) + {config.banners.length > 0 ? ( + config.banners.map(banner => + banner.image_url ? ( + + handleLink(banner.link)} + /> + + ) : null, + ) ) : ( - goProduct()}> - 订货采购 + + 新鲜直采 + 每日凌晨采货 · 天亮前到店 )} - {/* ========== 导航菜单 ========== */} - - - {categories.slice(0, 6).map((cat, idx) => ( - goProduct(cat.id)} - renderIcon={ - - {cat.name.slice(0, 1)} - - } - /> + {/* ========== 宫格导航(一行四个) ========== */} + {config.navs.length > 0 && ( + + + {config.navs.map((nav, idx) => ( + handleLink(nav.link)} + renderIcon={ + nav.image_url ? ( + + ) : ( + + {nav.name.slice(0, 1)} + + ) + } + /> + ))} + + + )} + + {/* ========== 促销推荐卡片 ========== */} + {config.promos.length > 0 && ( + + {config.promos.map(promo => ( + handleLink(promo.link)}> + {promo.image_url && ( + + )} + {promo.title && } + + {promo.title} + {promo.sub_title && {promo.sub_title}} + + ))} - goProduct()} - /> - Taro.navigateTo({ url: '/pages/order-list/index?status=all' })} - /> - - + + )} {/* ========== 推荐商品 ========== */} - 推荐商品 + + + 推荐商品 + goProduct()}>查看更多 › diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 1b634b1..607af04 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -19,7 +19,7 @@ const MENU_ITEMS = [ }, { key: 'statement', - label: '对账单', + label: '账单', icon: 'balance-list-o', onClick: () => Taro.navigateTo({ url: '/pages/statement/index' }), }, diff --git a/src/pages/statement-detail/index.config.ts b/src/pages/statement-detail/index.config.ts new file mode 100644 index 0000000..10bbf0a --- /dev/null +++ b/src/pages/statement-detail/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '账单详情', +}) diff --git a/src/pages/statement-detail/index.less b/src/pages/statement-detail/index.less new file mode 100644 index 0000000..0633f50 --- /dev/null +++ b/src/pages/statement-detail/index.less @@ -0,0 +1,179 @@ +.bill-detail { + min-height: 100vh; + background: #f7f8fa; + padding: 20rpx 24rpx 120rpx; + box-sizing: border-box; + + .bill-card { + background: #fff; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 16rpx; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16rpx; + } + + &__no { + font-size: 30rpx; + font-weight: 600; + color: #323233; + } + + &__status { + font-size: 24rpx; + + &--unpaid { + color: #ee0a24; + } + + &--paid { + color: #07c160; + } + } + + &__row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10rpx 0; + + &--total { + margin-top: 12rpx; + padding-top: 20rpx; + border-top: 1rpx solid #ebedf0; + } + } + + &__label { + font-size: 26rpx; + color: #969799; + } + + &__value { + font-size: 26rpx; + color: #323233; + + &--credit { + color: #07c160; + } + } + + &__total { + font-size: 34rpx; + font-weight: 600; + color: #ee0a24; + } + } + + .bill-section { + &__title { + font-size: 28rpx; + font-weight: 600; + color: #323233; + display: block; + margin-bottom: 8rpx; + } + + &__desc { + font-size: 22rpx; + color: #c8c9cc; + display: block; + margin-bottom: 12rpx; + } + } + + .bill-goods { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14rpx 0; + border-bottom: 1rpx solid #f2f3f5; + + &:last-child { + border-bottom: none; + } + + &__main { + flex: 1; + min-width: 0; + } + + &__name { + font-size: 28rpx; + color: #323233; + display: block; + } + + &__spec { + font-size: 22rpx; + color: #c8c9cc; + display: block; + margin-top: 4rpx; + } + + &__side { + text-align: right; + margin-left: 16rpx; + } + + &__qty { + font-size: 24rpx; + color: #969799; + display: block; + } + + &__amount { + font-size: 28rpx; + color: #323233; + font-weight: 500; + display: block; + margin-top: 4rpx; + } + } + + .bill-order { + display: flex; + align-items: center; + padding: 14rpx 0; + border-bottom: 1rpx solid #f2f3f5; + + &:last-child { + border-bottom: none; + } + + &__no { + font-size: 26rpx; + color: #323233; + flex: 1; + min-width: 0; + } + + &__date { + font-size: 24rpx; + color: #969799; + margin: 0 16rpx; + } + + &__amount { + font-size: 26rpx; + color: #323233; + } + } + + .bill-pay { + position: fixed; + left: 0; + right: 0; + bottom: 0; + padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); + background: #fff; + + &__btn { + border: none; + } + } +} diff --git a/src/pages/statement-detail/index.tsx b/src/pages/statement-detail/index.tsx new file mode 100644 index 0000000..265d277 --- /dev/null +++ b/src/pages/statement-detail/index.tsx @@ -0,0 +1,178 @@ +import { useCallback, useEffect, useState } from 'react' +import Taro, { useRouter } from '@tarojs/taro' +import { View, Text } from '@tarojs/components' +import { Button, Empty } from '@antmjs/vantui' +import { getStatementDetailApi, payStatementApi } from '@/services/statement' +import { STATEMENT_STATUS_MAP } from '@/types/statement' +import type { StatementDetail } from '@/types/statement' +import './index.less' + +/** + * 账单详情 + * 商品汇总(金额直接合并,单价明细见订单记录)+ 回筐计费 + 关联订单 + 付款确认 + */ +export default function StatementDetailPage() { + const router = useRouter() + const id = Number(router.params.id ?? 0) + + const [detail, setDetail] = useState(null) + const [loading, setLoading] = useState(false) + const [paying, setPaying] = useState(false) + + const loadDetail = useCallback(async () => { + if (!id) return + setLoading(true) + try { + const res = await getStatementDetailApi(id) + setDetail(res.data) + } catch { + // 错误已由 request 层 toast + } finally { + setLoading(false) + } + }, [id]) + + useEffect(() => { + loadDetail() + }, [loadDetail]) + + /** 付款确认 */ + const handlePay = useCallback(async () => { + if (!detail || paying) return + const confirm = await Taro.showModal({ + title: '付款确认', + content: `确认已支付账单 ${detail.statement_no} 共 ¥${detail.total_amount}?`, + confirmText: '确认付款', + }) + if (!confirm.confirm) return + setPaying(true) + try { + await payStatementApi(detail.id) + Taro.showToast({ title: '付款确认成功', icon: 'success' }) + loadDetail() + } catch { + // 错误(重复付款等)已由 request 层 toast + } finally { + setPaying(false) + } + }, [detail, paying, loadDetail]) + + if (loading && !detail) { + return + } + if (!detail) { + return + } + + const crateNegative = Number(detail.crate_amount) < 0 + + return ( + + {/* ===== 账单信息 ===== */} + + + {detail.statement_no} + + {STATEMENT_STATUS_MAP[detail.status]} + + + + 账单周期 + {detail.period_start} ~ {detail.period_end} + + + 应结算日期 + {detail.settlement_date ?? '-'} + + {detail.status === 1 && detail.settled_at && ( + + 付款时间 + {detail.settled_at} + + )} + + 商品金额 + ¥{detail.goods_amount} + + + 回筐金额 + + {crateNegative ? `-¥${Math.abs(Number(detail.crate_amount)).toFixed(2)}` : `¥${detail.crate_amount}`} + {crateNegative ? '(抵扣)' : ''} + + + + 账单总额 + ¥{detail.total_amount} + + + + {/* ===== 商品汇总 ===== */} + + 商品汇总({detail.goods?.length ?? 0}) + 同一商品多次订单金额直接合并;单价明细请查看订单记录 + {(detail.goods ?? []).map((item, index) => ( + + + {item.product_name} + {item.product_spec || ''} + + + {item.quantity} {item.unit} + ¥{item.amount} + + + ))} + {(detail.goods ?? []).length === 0 && } + + + {/* ===== 回筐计费 ===== */} + + 回筐计费 + + 周转框 + {detail.box_num} 个{detail.box_num < 0 ? '(抵扣)' : ''} + + + 托盘 + {detail.tray_num} 个{detail.tray_num < 0 ? '(抵扣)' : ''} + + + + {/* ===== 关联订单 ===== */} + + 关联订单({detail.orders?.length ?? 0}) + {(detail.orders ?? []).map(order => ( + Taro.navigateTo({ url: `/pages/order-list/index` })} + > + {order.order_no} + {order.order_date} + ¥{order.total_amount} + + ))} + {(detail.orders ?? []).length === 0 && } + + + {/* ===== 付款按钮 ===== */} + {detail.status === 0 && ( + + + + )} + + ) +} diff --git a/src/pages/statement/index.config.ts b/src/pages/statement/index.config.ts index b942b70..bd3f678 100644 --- a/src/pages/statement/index.config.ts +++ b/src/pages/statement/index.config.ts @@ -1,3 +1,3 @@ export default definePageConfig({ - navigationBarTitleText: '对账单', + navigationBarTitleText: '账单', }) diff --git a/src/pages/statement/index.less b/src/pages/statement/index.less index 8ae33af..c401c66 100644 --- a/src/pages/statement/index.less +++ b/src/pages/statement/index.less @@ -16,12 +16,9 @@ font-weight: 600; } - &__btn { - font-size: 26rpx; - color: #fff; - background: #ee0a24; - padding: 12rpx 28rpx; - border-radius: 999rpx; + &__tip { + font-size: 24rpx; + color: #969799; } } @@ -45,7 +42,7 @@ color: #c8c9cc; } - // ===== 对账单项 ===== + // ===== 账单单项 ===== .statement-item { background: #fff; border-radius: 16rpx; @@ -66,7 +63,14 @@ &__status { font-size: 24rpx; - color: #ee0a24; + + &--unpaid { + color: #ee0a24; + } + + &--paid { + color: #07c160; + } } &__body { @@ -94,46 +98,4 @@ 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 index 288eb0e..6f45eec 100644 --- a/src/pages/statement/index.tsx +++ b/src/pages/statement/index.tsx @@ -1,24 +1,18 @@ 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 { View, Text } from '@tarojs/components' +import { Empty } from '@antmjs/vantui' import useAuthStore from '@/stores/auth/useAuthStore' -import { generateStatementApi, getStatementListApi } from '@/services/statement' +import { 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) @@ -29,15 +23,9 @@ export default function StatementPage() { 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 @@ -69,57 +57,43 @@ export default function StatementPage() { } }) - /** 生成对账单 */ - 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' }) }, []) + const goDetail = useCallback((id: number) => { + Taro.navigateTo({ url: `/pages/statement-detail/index?id=${id}` }) + }, []) + return ( {/* ========== 头部 ========== */} - 对账单 - setShowGen(true)}> - 生成对账单 - + 账单 + 按回款周期自动生成 {/* ========== 列表 ========== */} {!loggedIn ? ( - + 去登录 ) : statements.length === 0 ? ( loading ? ( 加载中... ) : ( - + ) ) : ( statements.map(st => ( - + goDetail(st.id)}> {st.statement_no} - {STATEMENT_STATUS_MAP[st.status]} + + {STATEMENT_STATUS_MAP[st.status]} + @@ -137,44 +111,6 @@ export default function StatementPage() { {loggedIn && finished && statements.length > 0 && ( 没有更多了 )} - - {/* ========== 生成弹层 ========== */} - setShowGen(false)} - > - - 生成对账单 - 按门店客户等级价汇总周期内订单明细 - - 开始日期 - setGenStart(e.detail.value)}> - {genStart} - - - - 结束日期 - setGenEnd(e.detail.value)}> - {genEnd} - - - - - ) } diff --git a/src/services/home.ts b/src/services/home.ts new file mode 100644 index 0000000..a2650d5 --- /dev/null +++ b/src/services/home.ts @@ -0,0 +1,44 @@ +import { get } from '@/utils/request' + +/** 首页轮播图项 */ +export interface HomeBanner { + id: number + title: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页宫格导航项 */ +export interface HomeNav { + id: number + name: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页促销推荐卡片 */ +export interface HomePromo { + id: number + title: string + sub_title: string + image_id: number + image_url: string | null + link: string + sort: number +} + +/** 首页配置聚合数据 */ +export interface HomeConfig { + banners: HomeBanner[] + navs: HomeNav[] + promos: HomePromo[] +} + +/** 首页配置(轮播图 + 宫格导航 + 促销卡片):GET /mini/home */ +export function getHomeConfigApi() { + return get('/mini/home') +} diff --git a/src/services/statement.ts b/src/services/statement.ts index 3c93b84..6f7aa7f 100644 --- a/src/services/statement.ts +++ b/src/services/statement.ts @@ -1,23 +1,23 @@ import { get, post } from '@/utils/request' import type { PaginatedData } from '@/types/api' -import type { Statement, StatementDetail } from '@/types/statement' +import type { PendingContainers, Statement, StatementDetail } from '@/types/statement' -/** 对账单列表(仅本店):GET /mini/statement */ +/** 账单列表(仅本店,按回款周期自动生成):GET /mini/statement */ export function getStatementListApi(params: { page?: number; pageSize?: number } = {}) { return get>('/mini/statement', { data: params }) } -/** 生成对账单:POST /mini/statement/generate */ -export function generateStatementApi(params: { period_start: string; period_end: string }) { - return post<{ - id: number - statement_no: string - total_amount: string - settlement_date: string | null - }>('/mini/statement/generate', params) -} - -/** 对账单详情(校验归属):GET /mini/statement/{id} */ +/** 账单详情(校验归属,含商品汇总/回筐计费/关联订单):GET /mini/statement/{id} */ export function getStatementDetailApi(id: number) { return get(`/mini/statement/${id}`) } + +/** 付款确认:POST /mini/statement/{id}/pay */ +export function payStatementApi(id: number) { + return post<{ id: number; total_amount: string }>(`/mini/statement/${id}/pay`) +} + +/** 当前门店待回筐/托盘台账:GET /mini/statement/pending-containers */ +export function getPendingContainersApi() { + return get('/mini/statement/pending-containers') +} diff --git a/src/types/statement.ts b/src/types/statement.ts index 5f6332b..27c3e83 100644 --- a/src/types/statement.ts +++ b/src/types/statement.ts @@ -1,45 +1,64 @@ -/** 对账单状态:0 待对账 / 1 已对账 / 2 已结算 */ -export type StatementStatus = 0 | 1 | 2 +/** 账单状态:0 待付款 / 1 已付款 */ +export type StatementStatus = 0 | 1 export const STATEMENT_STATUS_MAP: Record = { - 0: '待对账', - 1: '已对账', - 2: '已结算', + 0: '待付款', + 1: '已付款', } -/** 对账单 */ +/** 账单 */ export interface Statement { id: number statement_no: string period_start: string period_end: string + /** 商品金额合计 */ + goods_amount: string + /** 账单计费周转框数(可为负=抵扣) */ + box_num: number + /** 账单计费托盘数(可为负=抵扣) */ + tray_num: number + /** 回筐金额合计(可为负=抵扣) */ + crate_amount: string + /** 账单总金额 = 商品金额 + 回筐金额 */ total_amount: string /** 生成时快照的回款周期 */ payment_cycle_days: number /** 应结算日期 = 周期结束 + 回款周期天 */ settlement_date: string | null status: StatementStatus - reconciled_at: string | null settled_at: string | null remark: string } -/** 对账单明细行 */ -export interface StatementItem { - order_id: number - order_item_id: number - product_id: number +/** 账单商品汇总记录(按商品聚合,不记单价) */ +export interface StatementGoods { product_name: string - price: string + product_spec: string + unit: string quantity: string weight: string amount: string - /** 0 未对账 / 1 已对账 */ - is_reconciled: number - store_remark: string } -/** 对账单详情 */ -export interface StatementDetail extends Statement { - items: StatementItem[] +/** 账单关联订单(下单单价明细在订单记录中查看) */ +export interface StatementOrder { + id: number + order_no: string + order_date: string + total_amount: string +} + +/** 账单详情 */ +export interface StatementDetail extends Statement { + goods: StatementGoods[] + orders: StatementOrder[] +} + +/** 门店待回筐台账 */ +export interface PendingContainers { + pending_box_num: number + pending_tray_num: number + box_price: string + tray_price: string }