#
zhou zhou
9 天以前 523365960513f297024a419f94b2b42eccd9456f
rsf-design/src/views/dashboard/console/index.vue
@@ -1,143 +1,114 @@
<template>
  <div class="art-full-height flex flex-col gap-6">
    <section class="grid gap-6 md:grid-cols-2 xl:grid-cols-4" v-loading="sectionLoading.summary">
  <div class="art-full-height flex flex-col gap-6 lg:min-h-0 lg:overflow-hidden lg:gap-2">
    <section class="grid gap-3 md:grid-cols-2 lg:grid-cols-4 lg:shrink-0" v-loading="sectionLoading.summary">
      <div
        v-for="item in summaryCardItems"
        :key="item.title"
        class="art-card flex items-start justify-between rounded-3xl px-7 py-6"
        class="art-card flex items-start justify-between rounded-3xl px-4 py-4 lg:px-4 lg:py-3 xl:px-5"
      >
        <div class="min-w-0 pr-6">
          <p class="text-sm font-medium text-g-700">{{ item.title }}</p>
          <ArtCountTo class="mt-3 block text-[2.3rem] font-semibold leading-none text-g-900" :target="item.count" :duration="1400" />
          <div class="mt-4 flex items-center gap-2 text-sm">
            <span :class="item.metaTone">{{ item.metaLabel }}</span>
            <span class="text-g-500">{{ item.metaValue }}</span>
        <div class="min-w-0 flex-1 pr-3">
          <p class="truncate text-sm font-medium text-g-700 lg:text-[13px]">{{ item.title }}</p>
          <ArtCountTo
            class="mt-1 block truncate text-[1.95rem] font-semibold leading-none text-g-900 lg:text-[1.72rem] xl:text-[1.92rem]"
            :target="item.count"
            :duration="1400"
          />
          <div class="mt-1.5 flex items-center gap-1 text-xs lg:text-[12px] xl:text-sm">
            <span :class="item.metaTone" class="shrink-0">{{ item.metaLabel }}</span>
            <span class="truncate text-g-500">{{ item.metaValue }}</span>
          </div>
        </div>
        <div class="flex size-13 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass">
          <ArtSvgIcon :icon="item.icon" class="text-2xl" :class="item.iconClass" />
        <div class="flex size-10 shrink-0 items-center justify-center rounded-2xl lg:size-9 xl:size-11" :class="item.iconBoxClass">
          <ArtSvgIcon :icon="item.icon" class="text-xl xl:text-2xl" :class="item.iconClass" />
        </div>
      </div>
    </section>
    <section class="grid gap-6 xl:grid-cols-[1.35fr_1fr]">
      <div class="art-card h-115 overflow-hidden p-6 box-border">
        <div class="art-card-header">
          <div class="title">
            <h4>近 30 天出入库趋势</h4>
            <p>真实链路 <span class="text-success">已接通</span></p>
    <section class="flex min-h-0 flex-1 flex-col gap-2">
      <div class="grid min-h-0 flex-1 gap-2 lg:grid-cols-[1.35fr_1fr]">
        <div class="art-card box-border flex h-full min-h-0 flex-col overflow-hidden p-4 lg:p-5">
          <div class="art-card-header">
            <div class="title">
              <h4>近 30 天出入库趋势</h4>
              <p>真实链路 <span class="text-success">已接通</span></p>
            </div>
          </div>
          <div class="mt-3 min-h-0 flex-1">
            <ArtBarChart
              height="100%"
              :loading="sectionLoading.trend"
              :data="trendChartSeries"
              :x-axis-data="trendChartXAxisData"
              :show-axis-line="false"
              :show-legend="true"
              :show-split-line="true"
              legend-position="top"
              bar-width="38%"
            />
          </div>
        </div>
        <div class="h-[calc(100%-4.5rem)]">
          <ArtBarChart
            height="22rem"
            :loading="sectionLoading.trend"
            :data="trendChartSeries"
            :x-axis-data="trendChartXAxisData"
            :show-axis-line="false"
            :show-legend="true"
            :show-split-line="true"
            legend-position="top"
            bar-width="38%"
          />
        </div>
      </div>
      <div class="art-card h-115 overflow-hidden p-6 box-border" v-loading="sectionLoading.locUsage">
        <div class="art-card-header">
          <div class="title">
            <h4>库位使用分布</h4>
            <p>{{ usageLegendCount }} 个维度</p>
        <div class="art-card box-border flex h-full min-h-0 flex-col overflow-hidden p-4 lg:p-5" v-loading="sectionLoading.locUsage">
          <div class="art-card-header">
            <div class="title">
              <h4>库位使用分布</h4>
              <p>{{ usageLegendCount }} 个维度</p>
            </div>
          </div>
        </div>
        <div class="grid h-[calc(100%-4.5rem)] gap-6 lg:grid-cols-[1fr_0.95fr] lg:items-center">
          <ArtRingChart
            height="21rem"
            :data="locUsageList"
            center-text="库位占比"
            :show-legend="false"
            :show-label="false"
          />
          <div class="space-y-1">
            <div
              v-for="item in usageLegend"
              :key="item.name"
              class="flex items-center justify-between border-b border-[var(--art-border-color)] py-4 last:border-b-0"
            >
              <div class="flex items-center gap-3">
                <span class="size-2.5 rounded-full" :style="{ backgroundColor: item.color }"></span>
                <span class="text-sm text-[var(--art-gray-900)]">{{ item.name }}</span>
              </div>
              <span class="text-sm font-medium text-[var(--art-gray-700)]">{{ item.value }}%</span>
          <div class="mt-3 grid min-h-0 flex-1 gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.92fr)] lg:items-center">
            <div class="min-h-0 h-full">
              <ArtRingChart
                height="100%"
                :data="locUsageList"
                center-text="库位占比"
                :show-legend="false"
                :show-label="false"
              />
            </div>
            <ElEmpty v-if="!locUsageList.length" description="暂无库位使用数据" :image-size="88" />
          </div>
        </div>
      </div>
    </section>
    <section class="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
      <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.tasks">
        <div class="art-card-header">
          <div class="title">
            <h4>执行中任务</h4>
            <p>{{ taskSubtitle }}</p>
          </div>
        </div>
        <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden">
          <ElScrollbar>
            <div
              v-for="item in taskCardItems"
              :key="`${item.time}-${item.title}`"
              class="flex gap-4 border-b border-g-300 py-4 last:border-b-0"
            >
              <div class="flex flex-col items-center pt-1">
                <span class="size-3 rounded-full bg-[var(--el-color-primary)]"></span>
                <span class="mt-2 min-h-10 w-px bg-[var(--art-border-color)]"></span>
              </div>
              <div class="min-w-0 flex-1">
                <p class="text-xs text-g-500">{{ item.time }}</p>
                <div class="mt-2 flex items-center gap-2">
                  <p class="truncate text-base font-medium text-[var(--art-gray-900)]">
                    {{ item.title }}
                  </p>
                  <ElTag size="small" effect="light" :type="item.tagType">{{ item.tagText }}</ElTag>
            <div class="min-h-0 overflow-hidden">
              <div
                v-for="item in usageLegend"
                :key="item.name"
                class="flex items-center justify-between gap-3 border-b border-[var(--art-border-color)] py-3 last:border-b-0"
              >
                <div class="flex items-center gap-3">
                  <span class="size-2.5 rounded-full" :style="{ backgroundColor: item.color }"></span>
                  <span class="truncate text-sm text-[var(--art-gray-900)]">{{ item.name }}</span>
                </div>
                <p class="mt-2 text-sm text-g-600">{{ item.subtitle }}</p>
                <span class="text-sm font-medium text-[var(--art-gray-700)]">{{ item.value }}%</span>
              </div>
              <ElEmpty v-if="!locUsageList.length" description="暂无库位使用数据" :image-size="88" />
            </div>
          </ElScrollbar>
          </div>
        </div>
      </div>
      <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.deadStock">
      <div class="art-card box-border flex shrink-0 flex-col p-4 lg:p-3.5">
        <div class="art-card-header">
          <div class="title">
            <h4>库存最近动态</h4>
            <p>{{ deadStockSubtitle }}</p>
            <h4>快捷跳转</h4>
            <p>快速进入任务页和库存页</p>
          </div>
        </div>
        <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden">
          <ElScrollbar>
            <div
              v-for="item in stockCardItems"
              :key="`${item.title}-${item.time}`"
              class="flex items-center gap-4 border-b border-g-300 py-4 last:border-b-0"
            >
              <div class="size-12 rounded-2xl bg-[var(--el-color-primary-light-9)] flex-cc">
                <ArtSvgIcon :icon="item.icon" class="text-xl text-[var(--el-color-primary)]" />
              </div>
              <div class="min-w-0 flex-1">
                <p class="truncate text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
                <p class="mt-1 text-sm text-g-500">{{ item.status }}</p>
              </div>
              <div class="max-w-40 text-right text-sm text-g-500">{{ item.time }}</div>
        <div class="mt-2.5 grid gap-2 md:grid-cols-2">
          <button
            v-for="item in quickLinkCards"
            :key="item.title"
            type="button"
            class="flex items-start justify-between rounded-3xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-4 py-3 text-left transition hover:border-[var(--el-color-primary-light-5)] hover:bg-[var(--el-color-primary-light-9)]"
            @click="navigateTo(item.path)"
          >
            <div class="min-w-0 pr-4">
              <p class="text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
              <p class="mt-0.5 text-sm text-g-500">{{ item.description }}</p>
            </div>
          </ElScrollbar>
            <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass">
              <ArtSvgIcon :icon="item.icon" class="text-xl" :class="item.iconClass" />
            </div>
          </button>
        </div>
      </div>
    </section>
@@ -145,44 +116,55 @@
</template>
<script setup>
  import { useRouter } from 'vue-router'
  import { storeToRefs } from 'pinia'
  import { useUserStore } from '@/store/modules/user'
  import {
    fetchDashboardHeader,
    fetchDashboardTrend,
    fetchDashboardDeadStock,
    fetchDashboardLocUsage,
    fetchDashboardTasks
    fetchDashboardLocUsage
  } from '@/api/dashboard'
  import {
    EMPTY_SUMMARY,
    buildDashboardDeadStockQuery,
    buildDashboardTaskQuery,
    normalizeDashboardSummary,
    normalizeDashboardTrend,
    normalizeDashboardTaskList,
    normalizeDashboardLocUsage,
    normalizeDashboardDeadStockList,
    withDashboardRequestGuard
  } from './consolePage.helpers'
  defineOptions({ name: 'Console' })
  const router = useRouter()
  const summary = ref({ ...EMPTY_SUMMARY })
  const trendModel = ref(normalizeDashboardTrend())
  const taskList = ref([])
  const deadStockList = ref([])
  const locUsageList = ref([])
  const sectionLoading = reactive({
    summary: false,
    trend: false,
    deadStock: false,
    locUsage: false,
    tasks: false
    locUsage: false
  })
  const userStore = useUserStore()
  const { getUserInfo } = storeToRefs(userStore)
  const quickLinkCards = [
    {
      title: '任务页',
      description: '查看和处理任务管理数据',
      path: '/manager/task',
      icon: 'ri:task-line',
      iconBoxClass: 'bg-[var(--el-color-primary-light-9)]',
      iconClass: 'text-[var(--el-color-primary)]'
    },
    {
      title: '库存页',
      description: '查看当前库存与库存明细',
      path: '/manager/stock',
      icon: 'ri:archive-stack-line',
      iconBoxClass: 'bg-[rgba(20,222,186,0.14)]',
      iconClass: 'text-[#14DEBA]'
    }
  ]
  const currentUser = computed(() => getUserInfo.value || {})
  const currentUserName = computed(() => {
@@ -291,8 +273,6 @@
  })
  const trendChartXAxisData = computed(() => trendDisplayModel.value.xAxisData)
  const trendChartSeries = computed(() => trendDisplayModel.value.series)
  const taskSubtitle = computed(() => `最近 ${taskList.value.length} 条任务动态`)
  const deadStockSubtitle = computed(() => `最近 ${deadStockList.value.length} 条库存记录`)
  const usageLegendCount = computed(() => locUsageList.value.length)
  const usageLegend = computed(() => {
    const palette = ['#5B8FF9', '#5AD8A6', '#5D7092', '#F6BD16', '#E8684A', '#6DC8EC']
@@ -301,16 +281,6 @@
      color: palette[index % palette.length]
    }))
  })
  const taskCardItems = computed(() =>
    taskList.value.slice(0, 6).map((item) => ({
      title: item.title,
      time: item.time,
      subtitle: item.status,
      tagText: resolveTaskTagText(item.status),
      tagType: resolveTaskTagType(item.class)
    }))
  )
  const stockCardItems = computed(() => deadStockList.value.slice(0, 6))
  onMounted(() => {
    loadDashboard()
@@ -319,9 +289,7 @@
  function loadDashboard() {
    void loadSummarySection()
    void loadTrendSection()
    void loadDeadStockSection()
    void loadLocUsageSection()
    void loadTaskSection()
  }
  async function loadSummarySection() {
@@ -338,16 +306,6 @@
    sectionLoading.trend = false
  }
  async function loadDeadStockSection() {
    sectionLoading.deadStock = true
    const payload = await withDashboardRequestGuard(
      fetchDashboardDeadStock(buildDashboardDeadStockQuery()),
      {}
    )
    deadStockList.value = normalizeDashboardDeadStockList(payload || {})
    sectionLoading.deadStock = false
  }
  async function loadLocUsageSection() {
    sectionLoading.locUsage = true
    const payload = await withDashboardRequestGuard(fetchDashboardLocUsage(), null)
@@ -355,29 +313,8 @@
    sectionLoading.locUsage = false
  }
  async function loadTaskSection() {
    sectionLoading.tasks = true
    const payload = await withDashboardRequestGuard(
      fetchDashboardTasks(buildDashboardTaskQuery()),
      {}
    )
    taskList.value = normalizeDashboardTaskList(payload || {})
    sectionLoading.tasks = false
  }
  function resolveTaskTagType(statusClass) {
    const text = String(statusClass || '')
    if (text.includes('emerald')) return 'success'
    if (text.includes('rose')) return 'danger'
    return 'primary'
  }
  function resolveTaskTagText(statusText) {
    const text = String(statusText || '')
      .replace(/\b\d+\./g, '')
      .replace(/\s+/g, ' ')
      .trim()
    const parts = text.split('·').map((item) => item.trim()).filter(Boolean)
    return parts[parts.length - 1] || '处理中'
  function navigateTo(path) {
    if (!path) return
    router.push(path)
  }
</script>