| | |
| | | <template> |
| | | <div class="art-full-height flex flex-col gap-5"> |
| | | <section |
| | | class="overflow-hidden rounded-3xl border border-white/10 bg-[linear-gradient(135deg,var(--art-main-bg-color),var(--art-card-bg-color))] p-6 shadow-[0_24px_80px_rgba(15,23,42,0.08)]" |
| | | > |
| | | <div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between"> |
| | | <div class="max-w-3xl"> |
| | | <div |
| | | class="mb-3 inline-flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-600" |
| | | > |
| | | <span class="size-2 rounded-full bg-emerald-500"></span> |
| | | RSF Phase 1 Landing |
| | | <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 |
| | | v-for="item in summaryCardItems" |
| | | :key="item.title" |
| | | class="art-card flex items-start justify-between rounded-3xl px-7 py-6" |
| | | > |
| | | <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> |
| | | <h1 |
| | | class="m-0 text-3xl font-semibold tracking-tight text-[var(--art-gray-900)] md:text-4xl" |
| | | > |
| | | 运行骨架已经切到 `rsf-design` |
| | | </h1> |
| | | <p class="mt-4 max-w-2xl text-sm leading-7 text-[var(--art-gray-600)] md:text-base"> |
| | | 当前入口已经接入真实后端登录、动态菜单和权限链路。这个首页只展示已经可用的 phase-1 |
| | | 能力,不再保留模板里的示例图表和演示数据。 |
| | | </p> |
| | | </div> |
| | | |
| | | <div class="rounded-2xl border border-white/10 bg-white/70 px-4 py-3 backdrop-blur-sm"> |
| | | <p class="text-xs uppercase tracking-[0.24em] text-[var(--art-gray-500)]" |
| | | >Current Entry</p |
| | | > |
| | | <p class="mt-2 text-lg font-semibold text-[var(--art-gray-900)]"> |
| | | {{ currentUserName }} |
| | | </p> |
| | | <p class="mt-1 text-sm text-[var(--art-gray-600)]"> |
| | | {{ currentUserRoleText }} · {{ currentMenuLabel }} |
| | | </p> |
| | | <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> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="grid gap-4 md:grid-cols-3"> |
| | | <ArtStatsCard |
| | | title="已接入后端" |
| | | :count="backendSwitchCount" |
| | | description="登录、用户信息、菜单全部来自 rsf-server" |
| | | icon="ri:server-line" |
| | | /> |
| | | <ArtStatsCard |
| | | title="动态菜单" |
| | | :count="visibleMenuCount" |
| | | description="仅发布 phase-1 允许进入的新入口" |
| | | icon="ri:route-line" |
| | | /> |
| | | <ArtStatsCard |
| | | title="权限链路" |
| | | :count="permissionSignalCount" |
| | | description="角色与权限节点已从真实用户数据恢复" |
| | | icon="ri:shield-check-line" |
| | | /> |
| | | <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> |
| | | </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> |
| | | </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> |
| | | |
| | | <ElEmpty v-if="!locUsageList.length" description="暂无库位使用数据" :image-size="88" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]"> |
| | | <ElCard |
| | | class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]" |
| | | shadow="never" |
| | | > |
| | | <template #header> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">真实运行状态</h2> |
| | | <p class="mt-1 text-xs text-[var(--art-gray-500)]"> |
| | | 这些信息来自当前登录用户和菜单 store |
| | | </p> |
| | | </div> |
| | | <ElTag type="success" effect="light">Backend mode</ElTag> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="grid gap-4 md:grid-cols-2"> |
| | | <div class="rounded-2xl bg-[var(--art-gray-50)] p-4"> |
| | | <p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">User</p> |
| | | <p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]"> |
| | | {{ currentUserName }} |
| | | </p> |
| | | <p class="mt-2 text-sm text-[var(--art-gray-600)]"> |
| | | {{ currentUserRoleText }} |
| | | </p> |
| | | <div class="mt-4 flex flex-wrap gap-2"> |
| | | <ElTag v-for="role in currentRoles" :key="role" type="info" effect="plain"> |
| | | {{ role }} |
| | | </ElTag> |
| | | <ElTag v-if="!currentRoles.length" type="info" effect="plain">No roles</ElTag> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="rounded-2xl bg-[var(--art-gray-50)] p-4"> |
| | | <p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">Permissions</p> |
| | | <p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]"> |
| | | {{ currentAuthorities.length }} auth nodes |
| | | </p> |
| | | <p class="mt-2 text-sm text-[var(--art-gray-600)]"> |
| | | 权限节点直接来自当前用户的真实 `authorities` 载荷,不再依赖模板演示态。 |
| | | </p> |
| | | <div class="mt-4 flex flex-wrap gap-2"> |
| | | <ElTag v-for="item in previewAuthorities" :key="item" type="warning" effect="plain"> |
| | | {{ item }} |
| | | </ElTag> |
| | | <ElTag v-if="!previewAuthorities.length" type="warning" effect="plain"> |
| | | No authorities |
| | | </ElTag> |
| | | </div> |
| | | <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> |
| | | </ElCard> |
| | | |
| | | <ElCard |
| | | class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]" |
| | | shadow="never" |
| | | > |
| | | <template #header> |
| | | <div> |
| | | <h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">菜单接入清单</h2> |
| | | <p class="mt-1 text-xs text-[var(--art-gray-500)]"> |
| | | 当前菜单树用于验证 `rsf-design` 是否真正接住后端发布 |
| | | </p> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="space-y-3"> |
| | | <div |
| | | v-for="item in menuPreview" |
| | | :key="item.key" |
| | | class="flex items-center justify-between rounded-2xl border border-[var(--art-gray-200)] px-4 py-3" |
| | | > |
| | | <div> |
| | | <p class="text-sm font-medium text-[var(--art-gray-900)]">{{ item.title }}</p> |
| | | <p class="mt-1 text-xs text-[var(--art-gray-500)]">{{ item.description }}</p> |
| | | <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> |
| | | <p class="mt-2 text-sm text-g-600">{{ item.subtitle }}</p> |
| | | </div> |
| | | </div> |
| | | <ElTag :type="item.type" effect="light"> |
| | | {{ item.value }} |
| | | </ElTag> |
| | | </ElScrollbar> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.deadStock"> |
| | | <div class="art-card-header"> |
| | | <div class="title"> |
| | | <h4>库存最近动态</h4> |
| | | <p>{{ deadStockSubtitle }}</p> |
| | | </div> |
| | | </div> |
| | | </ElCard> |
| | | |
| | | <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> |
| | | </ElScrollbar> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </template> |
| | |
| | | <script setup> |
| | | import { storeToRefs } from 'pinia' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useMenuStore } from '@/store/modules/menu' |
| | | import { formatMenuTitle } from '@/utils/router' |
| | | import { |
| | | fetchDashboardHeader, |
| | | fetchDashboardTrend, |
| | | fetchDashboardDeadStock, |
| | | fetchDashboardLocUsage, |
| | | fetchDashboardTasks |
| | | } from '@/api/dashboard' |
| | | import { |
| | | EMPTY_SUMMARY, |
| | | buildDashboardDeadStockQuery, |
| | | buildDashboardTaskQuery, |
| | | normalizeDashboardSummary, |
| | | normalizeDashboardTrend, |
| | | normalizeDashboardTaskList, |
| | | normalizeDashboardLocUsage, |
| | | normalizeDashboardDeadStockList, |
| | | withDashboardRequestGuard |
| | | } from './consolePage.helpers' |
| | | |
| | | defineOptions({ name: 'Console' }) |
| | | |
| | | 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 |
| | | }) |
| | | |
| | | const userStore = useUserStore() |
| | | const menuStore = useMenuStore() |
| | | const { getUserInfo } = storeToRefs(userStore) |
| | | const { menuList } = storeToRefs(menuStore) |
| | | |
| | | const currentUser = computed(() => getUserInfo.value || {}) |
| | | const currentUserName = computed(() => { |
| | |
| | | 'RSF User' |
| | | ) |
| | | }) |
| | | const currentRoles = computed(() => { |
| | | const roles = currentUser.value.roles |
| | | if (!Array.isArray(roles)) return [] |
| | | return roles |
| | | .map((role) => { |
| | | if (typeof role === 'string') { |
| | | return role |
| | | } |
| | | return role?.code || role?.name || role?.title || '' |
| | | }) |
| | | .filter(Boolean) |
| | | const currentDateText = computed(() => { |
| | | return new Date().toLocaleDateString('zh-CN', { |
| | | year: 'numeric', |
| | | month: '2-digit', |
| | | day: '2-digit' |
| | | }) |
| | | }) |
| | | const currentAuthorities = computed(() => { |
| | | const authorities = currentUser.value.authorities |
| | | if (!Array.isArray(authorities)) return [] |
| | | return authorities.map((item) => item?.authority || '').filter(Boolean) |
| | | }) |
| | | const currentMenuLabel = computed(() => { |
| | | const firstMenu = menuList.value?.[0] |
| | | return formatMenuTitle(firstMenu?.meta?.title || 'menus.dashboard.console') |
| | | }) |
| | | const currentUserRoleText = computed(() => { |
| | | if (!currentRoles.value.length) { |
| | | return 'No role information yet' |
| | | const summaryCardItems = computed(() => [ |
| | | { |
| | | title: '待入库数量', |
| | | count: summary.value.pendingIn, |
| | | metaLabel: '截至', |
| | | metaValue: currentDateText.value, |
| | | metaTone: 'text-g-500', |
| | | icon: 'ri:pie-chart-line', |
| | | iconBoxClass: 'bg-[var(--el-color-primary-light-9)]', |
| | | iconClass: 'text-[var(--el-color-primary)]' |
| | | }, |
| | | { |
| | | title: '待出库数量', |
| | | count: summary.value.pendingOut, |
| | | metaLabel: '状态', |
| | | metaValue: '当前待执行出库量', |
| | | metaTone: 'text-success', |
| | | icon: 'ri:fire-line', |
| | | iconBoxClass: 'bg-[rgba(255,175,32,0.14)]', |
| | | iconClass: 'text-[#FFAF20]' |
| | | }, |
| | | { |
| | | title: '已入库数量', |
| | | count: summary.value.completedIn, |
| | | metaLabel: '结果', |
| | | metaValue: '累计完成入库结果', |
| | | metaTone: 'text-g-500', |
| | | icon: 'ri:archive-line', |
| | | iconBoxClass: 'bg-[rgba(20,222,186,0.14)]', |
| | | iconClass: 'text-[#14DEBA]' |
| | | }, |
| | | { |
| | | title: '执行中任务', |
| | | count: summary.value.taskQty, |
| | | metaLabel: '当前用户', |
| | | metaValue: currentUserName.value, |
| | | metaTone: 'text-g-500', |
| | | icon: 'ri:progress-2-line', |
| | | iconBoxClass: 'bg-[rgba(139,92,246,0.16)]', |
| | | iconClass: 'text-[#8B5CF6]' |
| | | } |
| | | return `Roles: ${currentRoles.value.join(' / ')}` |
| | | }) |
| | | const backendSwitchCount = computed(() => { |
| | | return currentUserName.value !== 'RSF User' || menuList.value.length > 0 ? 1 : 0 |
| | | }) |
| | | const visibleMenuCount = computed(() => countVisibleMenus(menuList.value)) |
| | | const permissionSignalCount = computed(() => { |
| | | return currentRoles.value.length + currentAuthorities.value.length |
| | | }) |
| | | const previewAuthorities = computed(() => currentAuthorities.value.slice(0, 4)) |
| | | const menuPreview = computed(() => { |
| | | const total = visibleMenuCount.value |
| | | const rootCount = Array.isArray(menuList.value) ? menuList.value.length : 0 |
| | | const firstMenu = menuList.value?.[0] |
| | | const firstChildren = Array.isArray(firstMenu?.children) ? firstMenu.children.length : 0 |
| | | ]) |
| | | const trendDisplayModel = computed(() => { |
| | | const xAxisData = Array.isArray(trendModel.value.xAxisData) ? trendModel.value.xAxisData : [] |
| | | const series = Array.isArray(trendModel.value.series) ? trendModel.value.series : [] |
| | | if (!xAxisData.length || !series.length) { |
| | | return { xAxisData: [], series: [] } |
| | | } |
| | | |
| | | return [ |
| | | { |
| | | key: 'entry', |
| | | title: '入口模式', |
| | | description: '当前页面以 backend mode 运行,菜单由服务端驱动', |
| | | value: 'backend', |
| | | type: 'success' |
| | | }, |
| | | { |
| | | key: 'menus', |
| | | title: '可见菜单', |
| | | description: '已通过动态路由适配后进入前端菜单树', |
| | | value: `${total}`, |
| | | type: 'primary' |
| | | }, |
| | | { |
| | | key: 'root', |
| | | title: '一级目录', |
| | | description: '根级菜单节点数量', |
| | | value: `${rootCount}`, |
| | | type: 'info' |
| | | }, |
| | | { |
| | | key: 'children', |
| | | title: '首个目录子项', |
| | | description: '用于快速确认菜单树已被正确展开', |
| | | value: `${firstChildren}`, |
| | | type: 'warning' |
| | | const bucketSize = Math.max(1, Math.ceil(xAxisData.length / 8)) |
| | | const bucketLabels = [] |
| | | const bucketSeries = series.map((item) => ({ |
| | | ...item, |
| | | data: [] |
| | | })) |
| | | |
| | | for (let index = 0; index < xAxisData.length; index += bucketSize) { |
| | | const labelBucket = xAxisData.slice(index, index + bucketSize) |
| | | bucketLabels.push(labelBucket[labelBucket.length - 1] || '--') |
| | | bucketSeries.forEach((seriesItem, seriesIndex) => { |
| | | const sourceBucket = Array.isArray(series[seriesIndex]?.data) |
| | | ? series[seriesIndex].data.slice(index, index + bucketSize) |
| | | : [] |
| | | const bucketTotal = sourceBucket.reduce((total, value) => total + Number(value || 0), 0) |
| | | seriesItem.data.push(bucketTotal) |
| | | }) |
| | | } |
| | | |
| | | const visibleIndexes = bucketLabels.reduce((indexes, _, index) => { |
| | | const hasData = bucketSeries.some((item) => Number(item.data[index] || 0) > 0) |
| | | if (hasData) { |
| | | indexes.push(index) |
| | | } |
| | | ] |
| | | return indexes |
| | | }, []) |
| | | |
| | | const effectiveIndexes = |
| | | visibleIndexes.length > 0 |
| | | ? visibleIndexes |
| | | : bucketLabels.map((_, index) => index).slice(-8) |
| | | |
| | | return { |
| | | xAxisData: effectiveIndexes.map((index) => bucketLabels[index]), |
| | | series: bucketSeries.map((item) => ({ |
| | | ...item, |
| | | data: effectiveIndexes.map((index) => item.data[index]) |
| | | })) |
| | | } |
| | | }) |
| | | 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'] |
| | | return locUsageList.value.map((item, index) => ({ |
| | | ...item, |
| | | 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() |
| | | }) |
| | | |
| | | function countVisibleMenus(items) { |
| | | if (!Array.isArray(items)) return 0 |
| | | return items.reduce((total, item) => { |
| | | const current = item?.meta?.isHide ? 0 : 1 |
| | | return total + current + countVisibleMenus(item?.children) |
| | | }, 0) |
| | | function loadDashboard() { |
| | | void loadSummarySection() |
| | | void loadTrendSection() |
| | | void loadDeadStockSection() |
| | | void loadLocUsageSection() |
| | | void loadTaskSection() |
| | | } |
| | | |
| | | async function loadSummarySection() { |
| | | sectionLoading.summary = true |
| | | const payload = await withDashboardRequestGuard(fetchDashboardHeader(), null) |
| | | summary.value = normalizeDashboardSummary(payload) |
| | | sectionLoading.summary = false |
| | | } |
| | | |
| | | async function loadTrendSection() { |
| | | sectionLoading.trend = true |
| | | const payload = await withDashboardRequestGuard(fetchDashboardTrend(), null) |
| | | trendModel.value = normalizeDashboardTrend(payload) |
| | | 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) |
| | | locUsageList.value = normalizeDashboardLocUsage(payload) |
| | | 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] || '处理中' |
| | | } |
| | | </script> |