opt: 托盘任务展示优化

This commit is contained in:
zouzhiwen 2026-06-11 16:00:27 +08:00
parent 2f6582c426
commit 5bbb5bd5fa
3 changed files with 216 additions and 15 deletions

View File

@ -122,7 +122,7 @@
</div>
</div>
</div>
<el-main class="layout-main">
<el-main ref="layoutMainRef" class="layout-main">
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveNames" :max="20">
<component :is="Component" :key="componentKey" />
@ -149,7 +149,7 @@
</div>
</header>
<main class="mobile-main">
<main ref="mobileMainRef" class="mobile-main">
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveNames" :max="20">
<component :is="Component" :key="componentKey" />
@ -172,7 +172,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useDevice } from '@/composables/useDevice'
@ -184,6 +184,10 @@ const route = useRoute()
const router = useRouter()
const tabsStore = useTabsStore()
const refreshKeys = ref<Record<string, number>>({})
const layoutMainRef = ref<{ $el?: HTMLElement } | HTMLElement | null>(null)
const mobileMainRef = ref<HTMLElement | null>(null)
const scrollPositions = ref<Record<string, number>>({})
const skipNextScrollSavePaths = new Set<string>()
const tabMenuVisible = ref(false)
const tabMenuX = ref(0)
const tabMenuY = ref(0)
@ -200,7 +204,11 @@ const componentKey = computed(() => {
})
function refreshCurrentTab() {
clearScrollPosition(route.fullPath)
refreshKeys.value[route.path] = (refreshKeys.value[route.path] ?? 0) + 1
nextTick(() => {
restoreScrollPosition(route.fullPath)
})
}
const userStore = useUserStore()
@ -241,6 +249,66 @@ watch(
{ immediate: true },
)
watch(
() => route.fullPath,
async (fullPath, oldFullPath) => {
if (oldFullPath) {
saveScrollPosition(oldFullPath)
}
await nextTick()
restoreScrollPosition(fullPath)
},
)
function getScrollContainer() {
if (isMobile.value) return mobileMainRef.value
const el = layoutMainRef.value
if (!el) return null
return el instanceof HTMLElement ? el : el.$el ?? null
}
function saveScrollPosition(path: string) {
if (shouldSkipScrollSave(path)) return
const container = getScrollContainer()
if (!container) return
scrollPositions.value[path] = container.scrollTop
}
function restoreScrollPosition(path: string) {
const container = getScrollContainer()
if (!container) return
container.scrollTop = scrollPositions.value[path] ?? 0
}
function clearScrollPosition(path: string, skipNextSave = false) {
if (skipNextSave) {
skipNextScrollSavePaths.add(path)
}
Object.keys(scrollPositions.value).forEach((key) => {
if (key === path || key.startsWith(`${path}?`)) {
delete scrollPositions.value[key]
}
})
}
function keepOnlyScrollPosition(path: string) {
Object.keys(scrollPositions.value).forEach((key) => {
if (key !== path && !key.startsWith(`${path}?`)) {
delete scrollPositions.value[key]
}
})
}
function shouldSkipScrollSave(path: string) {
for (const skipPath of skipNextScrollSavePaths) {
if (path === skipPath || path.startsWith(`${skipPath}?`)) {
skipNextScrollSavePaths.delete(skipPath)
return true
}
}
return false
}
function goTab(path: string) {
if (route.path === path) return
router.push(path)
@ -252,6 +320,7 @@ function closeTab(path: string) {
const next = tabsStore.tabs[idx - 1] ?? tabsStore.tabs[idx + 1]
const nextPath = next ? next.path : '/dashboard'
tabsStore.remove(path)
clearScrollPosition(path, route.path === path)
if (route.path === path) {
router.push(nextPath)
}
@ -271,7 +340,9 @@ function closeOthersTab() {
tabMenuVisible.value = false
if (tabsStore.tabs.length <= 1) return
tabsStore.closeOthers(path)
keepOnlyScrollPosition(path)
if (route.path !== path) {
skipNextScrollSavePaths.add(route.fullPath)
router.push(path)
}
tabsStore.saveToStorage(path)
@ -285,6 +356,9 @@ function closeRightTab() {
if (idx < 0 || idx >= tabsStore.tabs.length - 1) return
const closedPaths = tabsStore.tabs.slice(idx + 1).map((t) => t.path)
tabsStore.closeRight(idx)
closedPaths.forEach((closedPath) => {
clearScrollPosition(closedPath, closedPath === route.path)
})
const needNav = closedPaths.includes(route.path)
if (needNav) {
router.push(path)

View File

@ -64,9 +64,14 @@
</el-descriptions-item>
<el-descriptions-item label="当前出库任务链">
<template v-if="elevator.outboundWorkingTask">
<el-link type="primary" @click="goChainDetail(elevator.outboundWorkingTask)">
{{ elevator.outboundWorkingTask }}
</el-link>
<span class="chain-inline">
<span v-if="chainTargetCodeText(elevator.outboundWorkingTask)" class="chain-target-tag">
{{ chainTargetCodeText(elevator.outboundWorkingTask) }}
</span>
<el-link type="primary" @click="goChainDetail(elevator.outboundWorkingTask)">
{{ elevator.outboundWorkingTask }}
</el-link>
</span>
</template>
<template v-else>
<span class="text-muted"></span>
@ -100,6 +105,9 @@
class="task-chain-tag"
@click="goChainDetail(code)"
>
<span v-if="chainTargetCodeText(code)" class="chain-target-tag">
{{ chainTargetCodeText(code) }}
</span>
{{ code }}
</el-tag>
</div>
@ -151,6 +159,9 @@
class="m-working"
@click="goChainDetail(elevator.outboundWorkingTask)"
>
<span v-if="chainTargetCodeText(elevator.outboundWorkingTask)" class="m-chain-target-tag">
{{ chainTargetCodeText(elevator.outboundWorkingTask) }}
</span>
{{ elevator.outboundWorkingTask }}
</span>
<span v-else class="m-point-value"></span>
@ -182,6 +193,9 @@
class="m-task-chain-tag"
@click="goChainDetail(code)"
>
<span v-if="chainTargetCodeText(code)" class="m-chain-target-tag">
{{ chainTargetCodeText(code) }}
</span>
{{ code }}
</el-tag>
</div>
@ -230,6 +244,7 @@ import {
getElevatorList,
type ElevatorVO,
} from '@/api/elevator'
import { getChainDetail } from '@/api/task'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
@ -242,17 +257,47 @@ const isMonitorMode = computed(() => userStore.isMonitorMode())
const router = useRouter()
const elevators = ref<ElevatorVO[]>([])
const refreshing = ref(false)
const chainTargetCodeMap = ref<Record<string, string>>({})
async function loadData() {
refreshing.value = true
chainTargetCodeMap.value = {}
try {
const res = await getElevatorList()
elevators.value = res.data || []
await loadChainTargetCodes(elevators.value)
} catch { /* handled by interceptor */ } finally {
refreshing.value = false
}
}
async function loadChainTargetCodes(list: ElevatorVO[]) {
const codes = [
...new Set(
list
.flatMap((elevator) => [
elevator.outboundWorkingTask,
...(elevator.taskChains ?? []),
])
.filter((code) => !!code?.trim()),
),
]
await Promise.all(
codes.map(async (code) => {
try {
const detailRes = await getChainDetail(code)
chainTargetCodeMap.value[code] = detailRes.data.targetCode || '-'
} catch {
chainTargetCodeMap.value[code] = ''
}
}),
)
}
function chainTargetCodeText(code: string) {
return chainTargetCodeMap.value[code] || ''
}
function goChainDetail(code: string) {
router.push(`/task/chain/${code}`)
}
@ -372,6 +417,9 @@ onMounted(loadData)
}
.m-working {
display: inline-flex;
align-items: center;
gap: 5px;
color: #409eff;
cursor: pointer;
word-break: break-all;
@ -384,10 +432,36 @@ onMounted(loadData)
gap: 6px;
}
.chain-inline {
display: inline-flex;
align-items: center;
gap: 6px;
}
.task-chain-tag {
display: inline-flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.chain-target-tag {
display: inline-flex;
align-items: center;
max-width: 120px;
height: 18px;
padding: 0 6px;
overflow: hidden;
color: var(--el-color-success);
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
background: var(--el-color-success-light-9);
border: 1px solid var(--el-color-success-light-7);
border-radius: 4px;
}
.text-muted {
color: #909399;
font-size: 12px;
@ -411,9 +485,29 @@ onMounted(loadData)
}
.m-task-chain-tag {
display: inline-flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.m-chain-target-tag {
display: inline-flex;
align-items: center;
max-width: 100px;
height: 18px;
padding: 0 6px;
overflow: hidden;
color: var(--el-color-success);
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
background: var(--el-color-success-light-9);
border: 1px solid var(--el-color-success-light-7);
border-radius: 4px;
}
.m-elevator-points {
background: #f8f9fb;
border-radius: 8px;

View File

@ -98,6 +98,9 @@
class="chain-row"
@click="goChainDetail(c)"
>
<span v-if="chainTargetCodeText(c)" class="chain-row__target">
{{ chainTargetCodeText(c) }}
</span>
<span class="chain-row__code">{{ c }}</span>
<span class="chain-row__sep" aria-hidden="true"></span>
<span class="chain-row__containers">{{ chainContainersText(c) }}</span>
@ -129,8 +132,13 @@ const warehouse = ref('WH01')
const snapshots = ref<ExtensionResourceSnapshotVO[]>([])
const loading = ref(false)
/** 任务链号 -> 容器串(冒号拼接);载入中 / 失败为占位标记 */
const chainContainersLabel = ref<Record<string, string>>({})
interface ChainSummaryLabel {
containers: string
targetCode: string
}
/** 任务链号 -> 任务链摘要;载入中 / 失败为占位标记 */
const chainSummaryLabel = ref<Record<string, ChainSummaryLabel | string>>({})
function capacityPercentage(resource: ExtensionResourceSnapshotVO): number {
if (resource.capacity <= 0) return 0
@ -166,10 +174,17 @@ function collectContainersFromDetail(d: TaskChainDetailVO): string[] {
}
function chainContainersText(code: string): string {
const v = chainContainersLabel.value[code]
const v = chainSummaryLabel.value[code]
if (v === '__loading__') return '容器载入中...'
if (v === '__error__') return '容器加载失败'
return v ?? '-'
if (typeof v === 'object') return v.containers
return '-'
}
function chainTargetCodeText(code: string): string {
const v = chainSummaryLabel.value[code]
if (typeof v === 'object') return v.targetCode || '-'
return ''
}
async function loadData() {
@ -179,7 +194,7 @@ async function loadData() {
return
}
loading.value = true
chainContainersLabel.value = {}
chainSummaryLabel.value = {}
try {
const res = await getExtensionResourceSnapshots(wh)
snapshots.value = res.data || []
@ -187,22 +202,25 @@ async function loadData() {
...new Set(snapshots.value.flatMap((s) => s.reservedChainCodes ?? []).filter((c) => !!c?.trim())),
]
for (const c of codes) {
chainContainersLabel.value[c] = '__loading__'
chainSummaryLabel.value[c] = '__loading__'
}
await Promise.all(
codes.map(async (code) => {
try {
const detailRes = await getChainDetail(code)
const list = collectContainersFromDetail(detailRes.data)
chainContainersLabel.value[code] = list.length ? list.join(':') : '-'
chainSummaryLabel.value[code] = {
containers: list.length ? list.join(':') : '-',
targetCode: detailRes.data.targetCode || '-',
}
} catch {
chainContainersLabel.value[code] = '__error__'
chainSummaryLabel.value[code] = '__error__'
}
}),
)
} catch {
snapshots.value = []
chainContainersLabel.value = {}
chainSummaryLabel.value = {}
} finally {
loading.value = false
}
@ -449,6 +467,21 @@ onMounted(loadData)
flex-shrink: 0;
}
.chain-row__target {
flex-shrink: 0;
max-width: 140px;
padding: 1px 6px;
overflow: hidden;
color: var(--el-color-success);
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
background: var(--el-color-success-light-9);
border: 1px solid var(--el-color-success-light-7);
border-radius: 4px;
}
.chain-row__sep {
font-size: 13px;
color: var(--el-text-color-placeholder);