feat: 异常处理文档

This commit is contained in:
zouzhiwen 2026-06-02 09:18:45 +08:00
parent 19ffe84785
commit 09b4863fd4
25 changed files with 671 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

View File

@ -0,0 +1,8 @@
{
"docs": [
{
"title": "立库异常处理",
"path": "立库异常处理.md"
}
]
}

View File

View File

@ -0,0 +1,72 @@
# 立库异常处理
### 立库提升机入库异常托盘无法进入提升机
1.在wms平台禁用对应提升机
![CleanShot 2026-06-01 at 17.38.30@2x](assets/CleanShot 2026-06-01 at 17.38.30@2x.png)
2.在智世平台取消智世任务
![CleanShot 2026-05-29 at 09.16.21@2x](assets/CleanShot 2026-05-29 at 09.16.21@2x.png)
![CleanShot 2026-05-29 at 09.17.17@2x](assets/CleanShot 2026-05-29 at 09.17.17@2x.png)
4.智世平台如有未取消的子任务,点击子任务后面的强制完成
![CleanShot 2026-05-29 at 09.18.18@2x](assets/CleanShot 2026-05-29 at 09.18.18@2x.png)
![CleanShot 2026-05-29 at 09.19.05@2x](assets/CleanShot 2026-05-29 at 09.19.05@2x.png)
3.如现场判断需要进入提升机,需要将提升机调至检修模式
4.将托盘平整放在提升机入库口
5.在wms平台取消wms任务
![CleanShot 2026-05-29 at 09.19.59@2x](assets/CleanShot 2026-05-29 at 09.19.59@2x.png)
6.在海康平台绑定站点
一号提升机站点:0211520SS0225040
二号提升机站点:0228800SS0225040
![CleanShot 2026-06-01 at 17.42.58@2x](assets/CleanShot 2026-06-01 at 17.42.58@2x.png)
![CleanShot 2026-06-01 at 17.48.50@2x](assets/CleanShot 2026-06-01 at 17.48.50@2x.png)
7.在海康平台建立点到点的搬运任务,看A3和A4哪个站点无货就出到哪个站点
任务类型:Q7RUKU1
![CleanShot 2026-06-01 at 17.52.10@2x](assets/CleanShot 2026-06-01 at 17.52.10@2x.png)
8.小车将托盘搬走后,如提升机为检修模式需要恢复为自动模式,将提升机复位初始化启动
9.托盘到达站点并叉下后在wms平台操作解绑,并启用提升机
![CleanShot 2026-05-29 at 09.30.29@2x](assets/CleanShot 2026-05-29 at 09.30.29@2x.png)
![CleanShot 2026-06-01 at 18.00.12@2x](assets/CleanShot 2026-06-01 at 18.00.12@2x.png)
### 拣货站扫码后小车不回库
1.在wms平台查看子任务状态是否正常
如果HK_STATION_RELEASE这一步的状态为待释放说明没有接收到回库指令,此时可以点击旁边的回库按钮
如果遇见其他情况联系技术排查
![CleanShot 2026-06-01 at 18.01.18@2x](assets/CleanShot 2026-06-01 at 18.01.18@2x.png)
### 立库错误码(14:238)
![28a28b2190bab4e9b22c521e9c0ff0f3](assets/28a28b2190bab4e9b22c521e9c0ff0f3-9787914.png)
操作下系统复位

34
src/api/docs.ts Normal file
View File

@ -0,0 +1,34 @@
export interface DocManifestItem {
title: string
path: string
}
export interface DocManifest {
docs: DocManifestItem[]
}
const docsBase = `${import.meta.env.BASE_URL}docs/`
function normalizeDocPath(path: string) {
return path.replace(/^\/+/, '')
}
export function getDocsBase() {
return docsBase
}
export async function getDocManifest() {
const response = await fetch(`${docsBase}manifest.json`, { cache: 'no-cache' })
if (!response.ok) {
throw new Error('文档清单读取失败')
}
return response.json() as Promise<DocManifest>
}
export async function getDocContent(path: string) {
const response = await fetch(`${docsBase}${encodeURI(normalizeDocPath(path))}`, { cache: 'no-cache' })
if (!response.ok) {
throw new Error('文档内容读取失败')
}
return response.text()
}

View File

@ -43,6 +43,10 @@
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
<template #title>任务生成</template> <template #title>任务生成</template>
</el-menu-item> </el-menu-item>
<el-menu-item index="/docs/exception">
<el-icon><Document /></el-icon>
<template #title>异常处理</template>
</el-menu-item>
</el-menu> </el-menu>
</el-aside> </el-aside>
@ -172,7 +176,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useDevice } from '@/composables/useDevice' import { useDevice } from '@/composables/useDevice'
import { Monitor, List, SetUp, Plus, DataLine, Fold, Expand, User, ArrowDown, Refresh, Tickets, Box } from '@element-plus/icons-vue' import { Monitor, List, SetUp, Plus, DataLine, Fold, Expand, User, ArrowDown, Refresh, Tickets, Box, Document } from '@element-plus/icons-vue'
import { useTabsStore } from '@/stores/tabs' import { useTabsStore } from '@/stores/tabs'
const { isMobile } = useDevice() const { isMobile } = useDevice()
@ -203,7 +207,7 @@ const userStore = useUserStore()
const isMonitorMode = computed(() => userStore.isMonitorMode()) const isMonitorMode = computed(() => userStore.isMonitorMode())
const keepAliveNames = computed(() => { const keepAliveNames = computed(() => {
if (isMonitorMode.value) { if (isMonitorMode.value) {
return ['TaskChainList', 'TaskChainDetail', 'ElevatorList', 'StationQueueView', 'PalletCacheStationView'] return ['TaskChainList', 'TaskChainDetail', 'ElevatorList', 'StationQueueView', 'PalletCacheStationView', 'ExceptionDocsView']
} }
return [ return [
'DashboardView', 'DashboardView',
@ -214,6 +218,7 @@ const keepAliveNames = computed(() => {
'PalletCacheStationView', 'PalletCacheStationView',
'VendorLoadView', 'VendorLoadView',
'TaskGenerateView', 'TaskGenerateView',
'ExceptionDocsView',
] ]
}) })
@ -338,6 +343,7 @@ const tabs = computed(() => [
{ path: '/station/pallet-cache', label: '缓存站', icon: Box }, { path: '/station/pallet-cache', label: '缓存站', icon: Box },
...(!isMonitorMode.value ? [{ path: '/vendor/load', label: '厂商负载', icon: DataLine }] : []), ...(!isMonitorMode.value ? [{ path: '/vendor/load', label: '厂商负载', icon: DataLine }] : []),
...(!isMonitorMode.value ? [{ path: '/generate', label: '生成', icon: Plus }] : []), ...(!isMonitorMode.value ? [{ path: '/generate', label: '生成', icon: Plus }] : []),
{ path: '/docs/exception', label: '异常', icon: Document },
]) ])
const activeMenu = computed(() => { const activeMenu = computed(() => {
@ -345,6 +351,7 @@ const activeMenu = computed(() => {
if (path.startsWith('/task/chain')) return '/task/chain' if (path.startsWith('/task/chain')) return '/task/chain'
if (path.startsWith('/station/queue')) return '/station/queue' if (path.startsWith('/station/queue')) return '/station/queue'
if (path.startsWith('/station/pallet-cache')) return '/station/pallet-cache' if (path.startsWith('/station/pallet-cache')) return '/station/pallet-cache'
if (path.startsWith('/docs/exception')) return '/docs/exception'
return path return path
}) })
@ -353,6 +360,7 @@ const activeTab = computed(() => {
if (path.startsWith('/task/chain')) return '/task/chain' if (path.startsWith('/task/chain')) return '/task/chain'
if (path.startsWith('/station/queue')) return '/station/queue' if (path.startsWith('/station/queue')) return '/station/queue'
if (path.startsWith('/station/pallet-cache')) return '/station/pallet-cache' if (path.startsWith('/station/pallet-cache')) return '/station/pallet-cache'
if (path.startsWith('/docs/exception')) return '/docs/exception'
return path return path
}) })

View File

@ -70,6 +70,12 @@ const router = createRouter({
component: () => import('@/views/generate/TaskGenerateView.vue'), component: () => import('@/views/generate/TaskGenerateView.vue'),
meta: { title: '任务生成' }, meta: { title: '任务生成' },
}, },
{
path: 'docs/exception',
name: 'ExceptionDocs',
component: () => import('@/views/docs/ExceptionDocsView.vue'),
meta: { title: '异常处理' },
},
], ],
}, },
], ],

View File

@ -0,0 +1,540 @@
<template>
<div class="docs-page">
<aside class="docs-sidebar">
<div class="docs-panel">
<div class="docs-panel__head">
<span class="docs-panel__title">异常处理</span>
</div>
<el-input
v-model="keyword"
class="docs-search"
clearable
placeholder="搜索异常、步骤、错误码"
:prefix-icon="Search"
/>
<div class="docs-list">
<div v-for="doc in docs" :key="doc.path" class="docs-list__entry">
<button
type="button"
:class="['docs-list__item', { 'is-active': activeDocPath === doc.path }]"
@click="selectDoc(doc.path)"
>
<el-icon><Document /></el-icon>
<span>{{ doc.title }}</span>
</button>
<div v-if="activeDocPath === doc.path && level3Toc.length" class="docs-heading-list">
<button
v-for="item in level3Toc"
:key="item.id"
type="button"
class="docs-heading-list__item"
@click="jumpTo(item.id)"
>
{{ item.text }}
</button>
</div>
</div>
</div>
</div>
<div v-if="keyword.trim()" class="docs-panel">
<div class="docs-panel__head">
<span class="docs-panel__title">搜索结果</span>
<span class="docs-panel__count">{{ searchResults.length }}</span>
</div>
<div v-if="searchResults.length" class="search-results">
<button
v-for="item in searchResults"
:key="item.id"
type="button"
class="search-result"
@click="jumpTo(item.anchor)"
>
<span class="search-result__title">{{ item.title }}</span>
<span class="search-result__text">{{ item.preview }}</span>
</button>
</div>
<el-empty v-else description="未找到匹配内容" :image-size="80" />
</div>
</aside>
<main v-loading="loading" class="docs-main">
<div v-if="loadError" class="docs-error">
<el-empty :description="loadError" />
</div>
<article v-else class="docs-article" v-html="renderedHtml" />
</main>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { Document, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getDocContent, getDocManifest, getDocsBase, type DocManifestItem } from '@/api/docs'
defineOptions({ name: 'ExceptionDocsView' })
interface TocItem {
id: string
level: number
text: string
}
interface SearchSection {
id: string
anchor: string
title: string
content: string
}
const docs = ref<DocManifestItem[]>([])
const activeDocPath = ref('')
const rawMarkdown = ref('')
const loading = ref(false)
const loadError = ref('')
const keyword = ref('')
const toc = ref<TocItem[]>([])
const searchSections = ref<SearchSection[]>([])
const renderedHtml = computed(() => renderMarkdown(rawMarkdown.value, activeDocPath.value))
const level3Toc = computed(() => toc.value.filter((item) => item.level === 3))
const searchResults = computed(() => {
const q = keyword.value.trim().toLowerCase()
if (!q) return []
return searchSections.value
.filter((item) => `${item.title}\n${item.content}`.toLowerCase().includes(q))
.map((item) => ({
id: item.id,
anchor: item.anchor,
title: item.title,
preview: buildPreview(item.content, q),
}))
})
watch(rawMarkdown, (value) => {
toc.value = extractToc(value)
searchSections.value = buildSearchSections(value)
})
onMounted(async () => {
await loadManifest()
})
async function loadManifest() {
loading.value = true
loadError.value = ''
try {
const manifest = await getDocManifest()
docs.value = manifest.docs
if (docs.value.length) {
await selectDoc(docs.value[0].path)
}
} catch (error) {
loadError.value = error instanceof Error ? error.message : '文档加载失败'
ElMessage.error(loadError.value)
} finally {
loading.value = false
}
}
async function selectDoc(path: string) {
if (activeDocPath.value === path && rawMarkdown.value) return
loading.value = true
loadError.value = ''
try {
activeDocPath.value = path
rawMarkdown.value = await getDocContent(path)
requestAnimationFrame(() => {
const firstTitle = toc.value[0]?.id
if (firstTitle) jumpTo(firstTitle)
})
} catch (error) {
loadError.value = error instanceof Error ? error.message : '文档加载失败'
ElMessage.error(loadError.value)
} finally {
loading.value = false
}
}
function extractToc(markdown: string) {
const counters = new Map<string, number>()
return markdown
.split('\n')
.map((line) => {
const match = /^(#{1,4})\s+(.+)$/.exec(line.trim())
if (!match) return null
const text = stripMarkdown(match[2])
return {
id: buildHeadingId(text, counters),
level: match[1].length,
text,
}
})
.filter((item): item is TocItem => Boolean(item))
}
function buildSearchSections(markdown: string) {
const counters = new Map<string, number>()
const sections: SearchSection[] = []
let current: SearchSection | null = null
markdown.split('\n').forEach((line) => {
const trimmed = line.trim()
const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed)
if (heading) {
const title = stripMarkdown(heading[2])
current = {
id: buildHeadingId(title, counters),
anchor: '',
title,
content: '',
}
current.anchor = current.id
sections.push(current)
return
}
if (!current || !trimmed) return
current.content += `${stripMarkdown(trimmed)}\n`
})
return sections
}
function renderMarkdown(markdown: string, docPath: string) {
const counters = new Map<string, number>()
const html: string[] = []
markdown.split('\n').forEach((line) => {
const trimmed = line.trim()
if (!trimmed) return
const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed)
if (heading) {
const level = heading[1].length
const text = stripMarkdown(heading[2])
const id = buildHeadingId(text, counters)
html.push(`<h${level} id="${id}">${escapeHtml(text)}</h${level}>`)
return
}
const image = /^!\[([^\]]*)]\(([^)]+)\)$/.exec(trimmed)
if (image) {
const alt = escapeHtml(image[1])
const src = escapeHtml(resolveDocAsset(docPath, image[2]))
html.push(`<figure><img src="${src}" alt="${alt}" loading="lazy" /><figcaption>${alt}</figcaption></figure>`)
return
}
const step = /^(\d+)\.(.+)$/.exec(trimmed)
if (step) {
html.push(`<p class="doc-step"><span>${escapeHtml(step[1])}</span>${escapeHtml(step[2].trim())}</p>`)
return
}
html.push(`<p>${escapeHtml(trimmed)}</p>`)
})
return html.join('')
}
function resolveDocAsset(docPath: string, assetPath: string) {
if (/^(https?:)?\/\//.test(assetPath) || assetPath.startsWith('data:')) return assetPath
const docDir = docPath.includes('/') ? docPath.slice(0, docPath.lastIndexOf('/') + 1) : ''
const normalized = `${docDir}${assetPath}`.replace(/^\.\//, '')
return `${getDocsBase()}${encodeURI(normalized)}`
}
function buildHeadingId(text: string, counters: Map<string, number>) {
const base = text
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/^-+|-+$/g, '') || 'section'
const count = counters.get(base) ?? 0
counters.set(base, count + 1)
return count ? `${base}-${count + 1}` : base
}
function buildPreview(content: string, q: string) {
const compact = content.replace(/\s+/g, ' ').trim()
const index = compact.toLowerCase().indexOf(q)
if (index < 0) return compact.slice(0, 80)
return compact.slice(Math.max(0, index - 24), index + q.length + 56)
}
function stripMarkdown(value: string) {
return value
.replace(/^!\[([^\]]*)]\([^)]+\)$/, '$1')
.replace(/\[([^\]]+)]\([^)]+\)/g, '$1')
.replace(/[*_`#>]/g, '')
.trim()
}
function escapeHtml(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function jumpTo(id: string) {
requestAnimationFrame(() => {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}
</script>
<style scoped>
.docs-page {
display: grid;
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
gap: 16px;
min-height: 100%;
}
.docs-sidebar {
position: sticky;
top: 20px;
align-self: start;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
max-height: calc(100vh - 40px);
overflow-y: auto;
}
.docs-panel,
.docs-main {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 6px;
}
.docs-panel {
padding: 14px;
}
.docs-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.docs-panel__title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.docs-panel__count {
min-width: 22px;
height: 22px;
padding: 0 7px;
border-radius: 11px;
background: #ecf5ff;
color: #409eff;
font-size: 12px;
line-height: 22px;
text-align: center;
}
.docs-search {
margin-bottom: 12px;
}
.docs-list,
.search-results {
display: flex;
flex-direction: column;
gap: 6px;
}
.docs-list__item,
.search-result,
.docs-heading-list__item {
width: 100%;
border: 0;
background: transparent;
color: #606266;
cursor: pointer;
text-align: left;
}
.docs-list__item {
display: flex;
align-items: center;
gap: 8px;
min-height: 38px;
padding: 8px 10px;
border-radius: 6px;
font-size: 14px;
}
.docs-list__item:hover,
.docs-list__item.is-active {
background: #ecf5ff;
color: #409eff;
}
.docs-heading-list {
display: flex;
flex-direction: column;
gap: 2px;
margin: 4px 0 6px 28px;
padding-left: 10px;
border-left: 2px solid #e4e7ed;
}
.docs-heading-list__item {
padding: 7px 8px;
border-radius: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
.docs-heading-list__item:hover {
background: #f5f7fa;
color: #409eff;
}
.search-result {
padding: 10px;
border-radius: 6px;
}
.search-result:hover {
background: #f5f7fa;
}
.search-result__title,
.search-result__text {
display: block;
}
.search-result__title {
margin-bottom: 4px;
color: #303133;
font-size: 13px;
font-weight: 600;
}
.search-result__text {
color: #909399;
font-size: 12px;
line-height: 1.5;
}
.docs-main {
position: relative;
min-width: 0;
padding: 0;
overflow: hidden;
}
.docs-error {
padding: 48px 16px;
}
.docs-article {
max-width: 980px;
padding: 28px 36px 56px;
}
:deep(.docs-article h1),
:deep(.docs-article h2),
:deep(.docs-article h3),
:deep(.docs-article h4) {
scroll-margin-top: 74px;
color: #1f2d3d;
line-height: 1.35;
}
:deep(.docs-article h1) {
margin-bottom: 28px;
font-size: 28px;
}
:deep(.docs-article h2) {
margin: 32px 0 16px;
font-size: 22px;
}
:deep(.docs-article h3) {
margin: 28px 0 14px;
font-size: 18px;
}
:deep(.docs-article h4) {
margin: 22px 0 12px;
font-size: 16px;
}
:deep(.docs-article p) {
margin: 12px 0;
color: #303133;
font-size: 15px;
line-height: 1.8;
}
:deep(.docs-article .doc-step) {
display: flex;
align-items: flex-start;
gap: 10px;
}
:deep(.docs-article .doc-step span) {
flex: 0 0 auto;
min-width: 24px;
height: 24px;
margin-top: 2px;
border-radius: 12px;
background: #409eff;
color: #fff;
font-size: 13px;
line-height: 24px;
text-align: center;
}
:deep(.docs-article figure) {
margin: 18px 0 24px;
}
:deep(.docs-article img) {
display: block;
max-width: 100%;
border: 1px solid #dcdfe6;
border-radius: 6px;
background: #f5f7fa;
}
:deep(.docs-article figcaption) {
margin-top: 8px;
color: #909399;
font-size: 12px;
}
@media (max-width: 900px) {
.docs-page {
grid-template-columns: 1fr;
}
.docs-sidebar {
position: static;
order: 1;
max-height: none;
overflow: visible;
}
.docs-main {
order: 2;
}
.docs-article {
padding: 22px 18px 44px;
}
}
</style>

View File

@ -1 +1 @@
{"root":["./src/main.ts","./src/api/auth.ts","./src/api/elevator.ts","./src/api/generate.ts","./src/api/palletcachestation.ts","./src/api/stationqueue.ts","./src/api/task.ts","./src/api/vendorload.ts","./src/composables/usedevice.ts","./src/constants/chainstatus.ts","./src/constants/taskstatus.ts","./src/router/index.ts","./src/stores/tabs.ts","./src/stores/user.ts","./src/utils/request.ts","./src/app.vue","./src/layouts/adminlayout.vue","./src/views/dashboard/dashboardview.vue","./src/views/elevator/elevatorlist.vue","./src/views/generate/taskgenerateview.vue","./src/views/login/loginview.vue","./src/views/station/palletcachestationview.vue","./src/views/station/stationqueueview.vue","./src/views/task/taskchaindetail.vue","./src/views/task/taskchainlist.vue","./src/views/vendor/vendorloadview.vue","./env.d.ts"],"version":"5.7.3"} {"root":["./src/main.ts","./src/api/auth.ts","./src/api/docs.ts","./src/api/elevator.ts","./src/api/generate.ts","./src/api/palletcachestation.ts","./src/api/stationqueue.ts","./src/api/task.ts","./src/api/vendorload.ts","./src/composables/usedevice.ts","./src/constants/chainstatus.ts","./src/constants/taskstatus.ts","./src/router/index.ts","./src/stores/tabs.ts","./src/stores/user.ts","./src/utils/request.ts","./src/app.vue","./src/layouts/adminlayout.vue","./src/views/dashboard/dashboardview.vue","./src/views/docs/exceptiondocsview.vue","./src/views/elevator/elevatorlist.vue","./src/views/generate/taskgenerateview.vue","./src/views/login/loginview.vue","./src/views/station/palletcachestationview.vue","./src/views/station/stationqueueview.vue","./src/views/task/taskchaindetail.vue","./src/views/task/taskchainlist.vue","./src/views/vendor/vendorloadview.vue","./env.d.ts"],"version":"5.7.3"}