diff --git a/src/app.config.ts b/src/app.config.ts index 422e995..919353f 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -7,6 +7,7 @@ export default defineAppConfig({ 'pages/message/index', 'pages/profile/index', 'pages/order-list/index', + 'pages/report/index', 'pages/bill/index', 'pages/bill-detail/index', 'pages/payment/index', diff --git a/src/pages/profile/index.less b/src/pages/profile/index.less index 18ddc89..65edce0 100644 --- a/src/pages/profile/index.less +++ b/src/pages/profile/index.less @@ -144,6 +144,53 @@ } } + // ===== 运营报表入口 ===== + .report-entry { + margin-top: 12rpx; + border-top: 1rpx solid #f2f3f5; + padding-top: 20rpx; + display: flex; + align-items: center; + + &__icon { + width: 84rpx; + height: 84rpx; + border-radius: 24rpx; + background: #fff0f0; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + &__info { + flex: 1; + min-width: 0; + margin-left: 20rpx; + display: flex; + flex-direction: column; + } + + &__title { + font-size: 28rpx; + color: #323233; + } + + &__desc { + margin-top: 6rpx; + font-size: 22rpx; + color: #969799; + } + + &__arrow { + font-size: 32rpx; + color: #c8c9cc; + line-height: 1; + margin-left: 12rpx; + flex-shrink: 0; + } + } + // ===== 账单入口 ===== .bill-entry { display: flex; diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index eefe04d..0c53c77 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -80,6 +80,11 @@ export default function ProfilePage() { Taro.navigateTo({ url: `/pages/order-list/index?status=${status ?? 'all'}` }) }, []) + /** 运营报表入口 → 运营报表页 */ + const goReport = useCallback(() => { + Taro.navigateTo({ url: '/pages/report/index' }) + }, []) + /** 账单入口 → 账单列表页 */ const goBill = useCallback(() => { Taro.navigateTo({ url: '/pages/bill/index' }) @@ -162,6 +167,17 @@ export default function ProfilePage() { ))} + {/* 运营报表入口 */} + + + + + + 运营报表 + 按周/月统计采购金额与单品占比 + + + {/* ========== 我的账单 ========== */} diff --git a/src/pages/report/index.config.ts b/src/pages/report/index.config.ts new file mode 100644 index 0000000..8cf1aec --- /dev/null +++ b/src/pages/report/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '运营报表', +}) diff --git a/src/pages/report/index.less b/src/pages/report/index.less new file mode 100644 index 0000000..d3c3664 --- /dev/null +++ b/src/pages/report/index.less @@ -0,0 +1,222 @@ +.report-page { + min-height: 100vh; + background: #f7f8fa; + padding: 20rpx 24rpx 60rpx; + box-sizing: border-box; + + // ===== 周期切换 ===== + .period-bar { + display: flex; + gap: 16rpx; + } + + .period-chip { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 14rpx 0; + border-radius: 999rpx; + background: #fff; + font-size: 26rpx; + color: #323233; + + &.active { + background: #ee0a24; + color: #fff; + font-weight: 600; + } + } + + // ===== 汇总卡片 ===== + .report-summary { + margin-top: 20rpx; + background: linear-gradient(135deg, #ee0a24, #ff4d4f); + border-radius: 20rpx; + padding: 32rpx 28rpx; + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + + &__label { + font-size: 24rpx; + opacity: 0.85; + } + + &__amount { + margin-top: 12rpx; + font-size: 56rpx; + font-weight: 700; + line-height: 1.2; + } + + &__range { + margin-top: 12rpx; + font-size: 22rpx; + opacity: 0.85; + } + + &__meta { + margin-top: 28rpx; + width: 100%; + display: flex; + border-top: 1rpx solid rgba(255, 255, 255, 0.25); + padding-top: 24rpx; + } + + &__meta-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + } + + &__meta-value { + font-size: 34rpx; + font-weight: 600; + } + + &__meta-label { + margin-top: 6rpx; + font-size: 22rpx; + opacity: 0.85; + } + } + + // ===== 单品排行 ===== + .report-list { + margin-top: 20rpx; + background: #fff; + border-radius: 20rpx; + padding: 24rpx 28rpx 8rpx; + + &__header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 8rpx; + } + + &__title { + font-size: 30rpx; + font-weight: 600; + } + + &__desc { + font-size: 22rpx; + color: #969799; + } + } + + .report-item { + display: flex; + padding: 24rpx 0; + border-bottom: 1rpx solid #f2f3f5; + + &:last-child { + border-bottom: none; + } + + &__rank { + width: 44rpx; + height: 44rpx; + border-radius: 12rpx; + background: #f2f3f5; + color: #969799; + font-size: 24rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-top: 4rpx; + + &--top { + background: #fff0f0; + color: #ee0a24; + font-weight: 600; + } + } + + &__main { + flex: 1; + min-width: 0; + margin-left: 20rpx; + } + + &__row { + display: flex; + align-items: center; + justify-content: space-between; + } + + &__name { + font-size: 28rpx; + color: #323233; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__amount { + font-size: 30rpx; + color: #ee0a24; + font-weight: 600; + margin-left: 16rpx; + flex-shrink: 0; + } + + &__spec { + margin-top: 8rpx; + font-size: 22rpx; + color: #969799; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__percent { + margin-top: 8rpx; + font-size: 24rpx; + color: #323233; + margin-left: 16rpx; + flex-shrink: 0; + } + + &__bar { + margin-top: 14rpx; + height: 8rpx; + border-radius: 999rpx; + background: #f2f3f5; + overflow: hidden; + } + + &__bar-inner { + height: 100%; + border-radius: 999rpx; + background: linear-gradient(90deg, #ff8a8f, #ee0a24); + } + } + + // ===== 空态 / 加载中 ===== + .report-empty { + margin-top: 60rpx; + + &__btn { + margin-top: 20rpx; + font-size: 26rpx; + padding: 14rpx 48rpx; + border-radius: 999rpx; + background: #ee0a24; + color: #fff; + } + } + + .report-loading { + padding: 60rpx 0; + text-align: center; + font-size: 26rpx; + color: #969799; + } +} diff --git a/src/pages/report/index.tsx b/src/pages/report/index.tsx new file mode 100644 index 0000000..128cf08 --- /dev/null +++ b/src/pages/report/index.tsx @@ -0,0 +1,221 @@ +import { useCallback, useRef, useState } from 'react' +import Taro, { useDidShow } from '@tarojs/taro' +import { View, Text } from '@tarojs/components' +import { Calendar, Empty } from '@antmjs/vantui' +import useAuthStore from '@/stores/auth/useAuthStore' +import { getPurchaseReportApi } from '@/services/report' +import type { PurchaseReport, PurchaseReportParams, ReportPreset } from '@/services/report' +import './index.less' + +/** 周期选项 key(custom 为前端伪预设:选中自定义区间后生效) */ +type PeriodKey = ReportPreset + +/** 周期切换 chips */ +const PERIOD_TABS: Array<{ key: PeriodKey; label: string }> = [ + { key: 'week', label: '本周' }, + { key: 'last_week', label: '上周' }, + { key: 'month', label: '本月' }, + { key: 'last_month', label: '上月' }, + { key: 'custom', label: '自定义' }, +] + +/** 自定义区间可选范围:2020-01-01 ~ 今天(进行中的周期由后端封顶今天) */ +const MIN_DATE = new Date(2020, 0, 1).getTime() +const MAX_DATE = Date.now() + +/** Date → Y-m-d */ +function formatDate(d: Date): string { + const m = `${d.getMonth() + 1}`.padStart(2, '0') + const day = `${d.getDate()}`.padStart(2, '0') + return `${d.getFullYear()}-${m}-${day}` +} + +/** + * 运营报表页 + * 按周期(本周/上周/本月/上月/自定义区间)统计门店采购总金额与单品累计金额占比; + * 数据为下单快照口径,已取消/已删除订单不计入 + */ +export default function ReportPage() { + const token = useAuthStore(s => s.token) + + const [period, setPeriod] = useState('month') + /** 自定义区间(period=custom 时使用) */ + const [range, setRange] = useState<{ start: string; end: string } | null>(null) + const [report, setReport] = useState(null) + const [loading, setLoading] = useState(false) + const [showCalendar, setShowCalendar] = useState(false) + const loadingRef = useRef(false) + + const loggedIn = !!token + + /** 拉取报表 */ + const load = useCallback( + async (params: PurchaseReportParams) => { + if (!loggedIn || loadingRef.current) return + loadingRef.current = true + setLoading(true) + try { + const res = await getPurchaseReportApi(params) + setReport(res.data) + } catch { + // 错误已由 request 层 toast + } finally { + loadingRef.current = false + setLoading(false) + } + }, + [loggedIn], + ) + + useDidShow(() => { + load( + period === 'custom' && range + ? { start_date: range.start, end_date: range.end } + : { preset: period === 'custom' ? 'month' : period }, + ) + }) + + /** 切换周期;自定义打开日历选择区间 */ + const handlePeriodTap = useCallback( + (key: PeriodKey) => { + if (key === 'custom') { + setShowCalendar(true) + return + } + if (key === period) return + setPeriod(key) + load({ preset: key }) + }, + [period, load], + ) + + /** 日历确认区间 → 自定义区间查询(优先于 preset) */ + const handleCalendarConfirm = useCallback( + (e: { detail: { value: Date | Date[] } }) => { + const value = Array.isArray(e.detail.value) ? e.detail.value : [e.detail.value] + const [start, end] = value + if (!start || !end) return + const next = { start: formatDate(start), end: formatDate(end) } + setRange(next) + setPeriod('custom') + setShowCalendar(false) + load({ start_date: next.start, end_date: next.end }) + }, + [load], + ) + + const goLogin = useCallback(() => { + Taro.navigateTo({ url: '/pages/login/index' }) + }, []) + + return ( + + {/* ========== 周期切换 ========== */} + + {PERIOD_TABS.map(tab => ( + handlePeriodTap(tab.key)} + > + {tab.label} + + ))} + + + {!loggedIn ? ( + + 去登录 + + ) : ( + <> + {/* ========== 汇总卡片 ========== */} + {report && ( + + 采购总金额(元) + ¥{report.total_amount} + + {report.start_date} ~ {report.end_date} + + + + {report.order_count} + 订货单数 + + + {report.item_count} + 单品数 + + + {report.total_quantity} + 订货总量 + + + + )} + + {/* ========== 单品排行 ========== */} + {report && report.items.length > 0 && ( + + + 单品采购排行 + 共 {report.item_count} 种,按金额降序 + + {report.items.map((item, index) => ( + + + {index + 1} + + + + {item.product_name} + ¥{item.amount} + + + + {item.product_spec ? `${item.product_spec} · ` : ''} + 累计 {item.quantity}{item.unit} + {parseFloat(item.weight) > 0 ? ` · 称重 ${item.weight}` : ''} + + {item.percent}% + + + + + + + ))} + + )} + + {/* ========== 空态 / 加载中 ========== */} + {(!report || report.items.length === 0) && ( + loading ? ( + 加载中... + ) : ( + + ) + )} + + )} + + {/* ========== 自定义区间日历 ========== */} + setShowCalendar(false)} + onConfirm={handleCalendarConfirm} + /> + + ) +} diff --git a/src/services/report.ts b/src/services/report.ts new file mode 100644 index 0000000..da4884a --- /dev/null +++ b/src/services/report.ts @@ -0,0 +1,61 @@ +import { get } from '@/utils/request' + +/** + * 统计周期预设: + * week 本周 / last_week 上周 / month 本月 / last_month 上月; + * custom 仅出现在响应中(传 start_date + end_date 自定义区间时生效,优先于 preset) + */ +export type ReportPreset = 'week' | 'last_week' | 'month' | 'last_month' | 'custom' + +/** 单品累计行(按金额降序) */ +export interface PurchaseReportItem { + product_id: number + /** 品名(下单时快照) */ + product_name: string + /** 规格/包规(快照) */ + product_spec: string + /** 计价单位(快照) */ + unit: string + /** 周期内累计订货量 */ + quantity: number + /** 周期内累计重量(3 位小数,未称重为 0.000) */ + weight: string + /** 周期内累计采购金额(元,2 位小数字符串) */ + amount: string + /** 金额占比(%,1 位小数;如 8.8 表示 8.8%) */ + percent: number +} + +/** 采购运营报表 */ +export interface PurchaseReport { + /** 实际生效的周期预设 */ + preset: ReportPreset + /** 实际统计开始日期(Y-m-d,进行中的周期封顶为今天) */ + start_date: string + /** 实际统计结束日期(Y-m-d) */ + end_date: string + /** 周期内采购总金额(元,2 位小数字符串) */ + total_amount: string + /** 周期内订货总量(各单品数量之和) */ + total_quantity: number + /** 周期内有效订货单数 */ + order_count: number + /** 单品个数(= items 长度) */ + item_count: number + /** 单品累计列表,按金额降序 */ + items: PurchaseReportItem[] +} + +/** 报表查询参数:自定义区间(start_date + end_date 需成对)优先于 preset,preset 缺省为 month */ +export interface PurchaseReportParams { + preset?: Exclude + /** 自定义开始日期(Y-m-d) */ + start_date?: string + /** 自定义结束日期(Y-m-d),不得早于 start_date */ + end_date?: string +} + +/** 采购运营报表:GET /mini/report/purchase(仅当前门店自身数据) */ +export function getPurchaseReportApi(params: PurchaseReportParams = {}) { + return get('/mini/report/purchase', { data: params }) +}