From 69b72a9c1c53caa4242e059906976aef7119529d Mon Sep 17 00:00:00 2001
From: liu <2302563948@qq.com>
Date: Fri, 21 Aug 2026 12:04:39 +0800
Subject: [PATCH] =?UTF-8?q?=E8=BF=90=E8=90=A5=E6=8A=A5=E8=A1=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app.config.ts | 1 +
src/pages/profile/index.less | 47 +++++++
src/pages/profile/index.tsx | 16 +++
src/pages/report/index.config.ts | 3 +
src/pages/report/index.less | 222 +++++++++++++++++++++++++++++++
src/pages/report/index.tsx | 221 ++++++++++++++++++++++++++++++
src/services/report.ts | 61 +++++++++
7 files changed, 571 insertions(+)
create mode 100644 src/pages/report/index.config.ts
create mode 100644 src/pages/report/index.less
create mode 100644 src/pages/report/index.tsx
create mode 100644 src/services/report.ts
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 })
+}