opt: 托盘任务展示优化
This commit is contained in:
parent
2f6582c426
commit
5bbb5bd5fa
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue