label-report.vue 9.16 KB
<template>
  <view class="page">
    <view class="header-hero" :style="{ paddingTop: statusBarHeight + 'px' }">
      <view class="top-bar">
        <view class="top-left" @click="goBack">
          <AppIcon name="chevronLeft" size="sm" color="white" />
        </view>
        <view class="top-center">
          <text class="page-title">Label Report</text>
          <LocationPicker />
        </view>
        <view class="top-right" @click="isMenuOpen = true">
          <AppIcon name="menu" size="sm" color="white" />
        </view>
      </view>
    </view>

    <scroll-view class="content" scroll-y>
      <!-- 指标卡 -->
      <view class="metrics-grid">
        <view class="metric-card">
          <text class="metric-label">Total Labels Printed</text>
          <text class="metric-value">2,543</text>
          <text class="metric-trend up">+20.1% from last month</text>
        </view>
        <view class="metric-card">
          <text class="metric-label">Most Printed Category</text>
          <text class="metric-value">Dairy</text>
          <text class="metric-trend">450 labels generated</text>
        </view>
        <view class="metric-card">
          <text class="metric-label">Top Product</text>
          <text class="metric-value">Whole Milk</text>
          <text class="metric-trend">182 labels generated</text>
        </view>
        <view class="metric-card">
          <text class="metric-label">Avg. Daily Prints</text>
          <text class="metric-value">85</text>
          <text class="metric-trend up">+12% from last week</text>
        </view>
      </view>

      <!-- 柱状图 -->
      <view class="section">
        <text class="section-title">Labels by Category</text>
        <text class="section-desc">Distribution of printed labels across product categories.</text>
        <view class="bar-chart">
          <view v-for="item in categoryData" :key="item.name" class="bar-row">
            <text class="bar-label">{{ item.name }}</text>
            <view class="bar-track">
              <view class="bar-fill" :style="{ width: item.percent + '%' }" />
            </view>
            <text class="bar-value">{{ item.value }}</text>
          </view>
        </view>
      </view>

      <!-- 走势图 -->
      <view class="section">
        <text class="section-title">Print Volume Trends</text>
        <text class="section-desc">Daily label printing volume for the last 7 days.</text>
        <view class="line-chart">
          <view class="chart-bars">
            <view v-for="(pct, i) in trendData" :key="i" class="day-bar-wrap">
              <view class="day-bar" :style="{ height: pct + '%' }" />
            </view>
          </view>
          <view class="chart-labels">
            <text v-for="d in dayLabels" :key="d" class="day-label">{{ d }}</text>
          </view>
        </view>
      </view>

      <!-- 最常用产品列表 -->
      <view class="section">
        <text class="section-title">Most Used Products</text>
        <view class="product-table">
          <view class="product-row header">
            <text class="col-name">Product Name</text>
            <text class="col-cat">Category</text>
            <text class="col-total">Total Printed</text>
            <text class="col-pct">Usage %</text>
          </view>
          <view v-for="p in topProducts" :key="p.name" class="product-row">
            <text class="col-name">{{ p.name }}</text>
            <text class="col-cat">{{ p.category }}</text>
            <text class="col-total">{{ p.total }}</text>
            <text class="col-pct">{{ p.usage }}%</text>
          </view>
        </view>
      </view>
    </scroll-view>

    <SideMenu v-model="isMenuOpen" />
  </view>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import AppIcon from '../../components/AppIcon.vue'
import SideMenu from '../../components/SideMenu.vue'
import LocationPicker from '../../components/LocationPicker.vue'
import { getStatusBarHeight } from '../../utils/statusBar'

const statusBarHeight = getStatusBarHeight()
const isMenuOpen = ref(false)

const categoryDataRaw = [
  { name: 'Dairy', value: 450 },
  { name: 'Meat', value: 380 },
  { name: 'Bakery', value: 320 },
  { name: 'Deli', value: 280 },
  { name: 'Produce', value: 220 },
  { name: 'Beverage', value: 180 },
]

const categoryData = computed(() => {
  const max = Math.max(...categoryDataRaw.map(d => d.value))
  return categoryDataRaw.map(d => ({ ...d, percent: (d.value / max) * 100 }))
})

const trendDataRaw = [72, 85, 78, 92, 88, 95, 90]
const trendData = computed(() => {
  const max = Math.max(...trendDataRaw)
  return trendDataRaw.map(v => (v / max) * 100)
})

const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

const topProducts = ref([
  { name: 'Whole Milk', category: 'Dairy', total: 182, usage: '7.2' },
  { name: 'Ground Beef 80/20', category: 'Meat', total: 145, usage: '5.7' },
  { name: 'Chicken Breast', category: 'Meat', total: 132, usage: '5.2' },
  { name: 'Sliced Ham', category: 'Deli', total: 98, usage: '3.8' },
])

const goBack = () => {
  const pages = getCurrentPages()
  if (pages.length > 1) {
    uni.navigateBack()
  } else {
    uni.redirectTo({ url: '/pages/index/index' })
  }
}
</script>

<style scoped>
.page {
  min-height: 100vh;
  background: #f3f4f6;
  display: flex;
  flex-direction: column;
}

.header-hero {
  background: linear-gradient(135deg, #1F3A8A, #142a6c);
  padding: 16rpx 32rpx 24rpx;
}

.top-bar {
  height: 96rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.top-left, .top-right {
  width: 64rpx;
  height: 64rpx;
  border-radius: 999rpx;
  background: rgba(255, 255, 255, 0.15);
  display: flex;
  align-items: center;
  justify-content: center;
}

.top-center {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.page-title {
  font-size: 34rpx;
  font-weight: 600;
  color: #fff;
}

.content {
  flex: 1;
  padding: 24rpx 28rpx 40rpx;
  box-sizing: border-box;
}

.metrics-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20rpx;
  margin-bottom: 32rpx;
}

.metric-card {
  background: #fff;
  padding: 28rpx;
  border-radius: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  border: 1rpx solid #e5e7eb;
}

.metric-label {
  font-size: 24rpx;
  color: #6b7280;
  display: block;
  margin-bottom: 12rpx;
  line-height: 1.5;
}

.metric-value {
  font-size: 38rpx;
  font-weight: 700;
  color: #111827;
  display: block;
  margin-bottom: 8rpx;
  letter-spacing: -0.02em;
}

.metric-trend {
  font-size: 24rpx;
  color: #6b7280;
}

.metric-trend.up {
  color: #16a34a;
  font-weight: 500;
}

.section {
  margin-bottom: 32rpx;
}

.section-title {
  font-size: 32rpx;
  font-weight: 600;
  color: #111827;
  display: block;
  margin-bottom: 8rpx;
}

.section-desc {
  font-size: 26rpx;
  color: #6b7280;
  display: block;
  margin-bottom: 24rpx;
  line-height: 1.5;
}

.bar-chart {
  background: #fff;
  padding: 28rpx;
  border-radius: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  border: 1rpx solid #e5e7eb;
}

.bar-row {
  display: flex;
  align-items: center;
  gap: 16rpx;
  margin-bottom: 24rpx;
}

.bar-row:last-child {
  margin-bottom: 0;
}

.bar-label {
  width: 120rpx;
  font-size: 26rpx;
  color: #374151;
  flex-shrink: 0;
  font-weight: 500;
}

.bar-track {
  flex: 1;
  height: 36rpx;
  background: #f3f4f6;
  border-radius: 10rpx;
  overflow: hidden;
}

.bar-fill {
  height: 100%;
  background: linear-gradient(90deg, #1F3A8A, #1447E6);
  border-radius: 10rpx;
  transition: width 0.3s ease;
}

.bar-value {
  width: 88rpx;
  text-align: right;
  font-size: 26rpx;
  font-weight: 600;
  color: #111827;
}

.line-chart {
  background: #fff;
  padding: 28rpx;
  border-radius: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  border: 1rpx solid #e5e7eb;
}

.chart-bars {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12rpx;
  height: 220rpx;
  margin-bottom: 20rpx;
}

.day-bar-wrap {
  flex: 1;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.day-bar {
  width: 100%;
  max-width: 52rpx;
  min-height: 12rpx;
  background: linear-gradient(180deg, #1F3A8A, #1447E6);
  border-radius: 10rpx 10rpx 0 0;
  transition: height 0.3s ease;
}

.chart-labels {
  display: flex;
  justify-content: space-between;
  gap: 12rpx;
}

.day-label {
  flex: 1;
  padding: 0 4rpx;
  font-size: 24rpx;
  color: #6b7280;
  text-align: center;
}

.product-table {
  background: #fff;
  border-radius: 20rpx;
  overflow: hidden;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  border: 1rpx solid #e5e7eb;
}

.product-row {
  display: flex;
  padding: 24rpx 28rpx;
  border-bottom: 1rpx solid #e5e7eb;
  font-size: 28rpx;
}

.product-row:last-child {
  border-bottom: none;
}

.product-row.header {
  background: #fafbfc;
  font-weight: 600;
  color: #6b7280;
  font-size: 24rpx;
}

.col-name { flex: 1; min-width: 0; }
.col-cat { flex: 0 0 140rpx; }
.col-total { flex: 0 0 120rpx; text-align: right; }
.col-pct { flex: 0 0 88rpx; text-align: right; }

.product-row:not(.header) .col-name { color: #111827; font-weight: 500; }
.product-row:not(.header) .col-cat { color: #6b7280; }
.product-row:not(.header) .col-total { color: #111827; }
.product-row:not(.header) .col-pct { color: #1F3A8A; font-weight: 600; }
</style>