Files
SoonMachine/app/src/renderer/src/views/DistributeRunningView.vue
T
24kycj 3168daedf4 完善分发收集续做、打印机状态查询与任务字段对齐
- 完成/失败态统一卡位13自动续做,修复主进程失败时 mode 被重置导致续做被拒
- 接入 SAPI_PrinterCheckstatus,Header 轻量刷状态,Init 后一次 GetPrinterInfo
- print_flag 取自 soon.flag,path_file 目录级 staging,加密狗与格式化选项对齐
- 失败页并入运行页,移除独立 DistributeFailedView

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 12:16:03 +08:00

541 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<AppShell>
<AppHeader :mode="headerMode">
<div v-if="phase === 'failed'" class="c-nav-group">
<NavButton icon="arrow-left" label="返回" @click="onFailedBack" />
<NavButton icon="redo" label="重置" variant="primary" @click="onFailedReset" />
</div>
<NavButton
v-else-if="phase === 'completed'"
icon="home"
label="返回"
@click="onReturn"
/>
<NavButton v-else icon="stop" label="停止" variant="stop" @click="onStop" />
</AppHeader>
<main class="app-shell__main l-main-full">
<section class="l-hero-container">
<div class="m-left-panel">
<div class="c-status-panel">
<template v-if="phase === 'failed'">
<h2 class="c-status-title is-error">任务失败</h2>
<p class="c-status-sub">{{ failedSub }}</p>
<p class="c-status-counter">
任务已经完成<span class="ok">{{ successCount }}</span>其中失败次数是<span
class="err"
>{{ failCount }}</span
>
</p>
<p v-if="failureErrorText" class="m-error-detail">{{ failureErrorText }}</p>
</template>
<template v-else>
<h2 class="c-status-title" :class="{ 'is-looping': phase === 'running' }">
{{ statusTitle }}
</h2>
<p class="c-status-sub">{{ statusSub }}</p>
<p class="c-status-counter">
任务已经完成<span class="ok">{{ successCount }}</span>其中失败次数是<span
class="err"
>{{ failCount }}</span
>
</p>
</template>
</div>
<WorkflowSteps
v-if="phase === 'failed'"
mode="failed"
:variant="isCollect ? 'collect' : 'distribute'"
:failed-step="failureStep"
/>
<WorkflowSteps
v-else
:active-step="workflowStep"
:variant="isCollect ? 'collect' : 'distribute'"
mode="running"
/>
</div>
<div class="m-right-panel">
<div v-if="phase === 'failed'" class="m-error-icon">
<AppIcon name="warning" size="xl" />
</div>
<div v-else class="m-progress-circle">
<svg viewBox="0 0 100 100">
<circle class="bg" cx="50" cy="50" r="45" />
<circle
class="fill"
cx="50"
cy="50"
r="45"
:style="{ strokeDashoffset: strokeOffset }"
/>
</svg>
<div class="m-progress-value">{{ displayProgress }}%</div>
</div>
</div>
</section>
</main>
<AppFooter />
</AppShell>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { notify } from '@/composables/useNotify'
import AppShell from '@/layouts/AppShell.vue'
import AppHeader from '@/components/AppHeader.vue'
import AppFooter from '@/components/AppFooter.vue'
import NavButton from '@/components/NavButton.vue'
import AppIcon from '@/components/AppIcon.vue'
import WorkflowSteps from '@/components/WorkflowSteps.vue'
import { useJobStore } from '@/stores/job'
import { useCollectStore } from '@/stores/collect'
import { useDistributeFormStore } from '@/stores/distributeForm'
import { useAppStore } from '@/stores/app'
import {
dllJobCancel,
dllPrinterErrorStr,
dllUsbCopy,
onCardPositionTick,
onJobPollTick,
onUsbPollTick,
pollCardPositionStart,
pollCardPositionStop,
pollJobStart,
pollJobStop,
pollUsbStop
} from '@/api/cardsoon'
import { createDistributeJob } from '@/utils/createDistributeJob'
import { validateJobPreflight } from '@/utils/validateJobPreflight'
import { mapJobStateToUi } from '@/utils/job-state'
import { POSITION_PREPARE } from '@shared/card-position'
import {
USB_TASK_COPYING,
USB_TASK_PREPARING,
clampUsbCopyProgress,
usbTaskStatusHint
} from '@shared/usb-copy-state'
import type { CardPositionPollPayload, JobPollPayload, UsbPollPayload } from '@/types/ipc'
const CIRCLE_LEN = 283
const route = useRoute()
const router = useRouter()
const jobStore = useJobStore()
const collectStore = useCollectStore()
const formStore = useDistributeFormStore()
const appStore = useAppStore()
const isCollect = computed(() => route.name === 'collect-running')
const headerMode = computed(() => (isCollect.value ? '数据收集模式' : '数据分发模式'))
const successCount = computed(() =>
isCollect.value ? collectStore.successCount : jobStore.successCount
)
const failCount = computed(() => (isCollect.value ? collectStore.failCount : jobStore.failCount))
const phase = ref<'running' | 'completed' | 'failed'>('running')
const progress = ref(0)
const strokeOffset = ref(CIRCLE_LEN)
const workflowStep = ref(1)
const failureStep = ref(1)
const failureErrorText = ref('')
const waitCard = ref(false)
const collectHint = ref('')
let unsubJob: (() => void) | null = null
let unsubUsb: (() => void) | null = null
let unsubCard: (() => void) | null = null
let finishing = false
let resubmitting = false
let lastCardPosition = -1
let usbAwaitNewCycle = false
let queryFailStreak = 0
let usbQueryFailStreak = 0
const displayProgress = computed(() => Math.round(progress.value))
const failedSub = computed(() =>
isCollect.value
? '请检查读卡器与卡片,插入备卡位可自动重试'
: '请检查设备故障,插入备卡位可自动重试'
)
const statusTitle = computed(() => {
if (phase.value === 'completed') {
return isCollect.value ? '收集已完成' : '任务已完成'
}
if (isCollect.value) {
return collectHint.value || '数据收集中'
}
return waitCard.value ? '等待插卡' : '任务执行中'
})
const statusSub = computed(() => {
if (phase.value === 'completed') {
return isCollect.value
? '请点击返回,或插入备卡位继续下一张'
: '请点击返回,或插入备卡位自动开始下一张'
}
if (isCollect.value) {
return collectHint.value || '正在从卡片读取并写入导入目录'
}
return waitCard.value ? '请插入数据卡,任务将自动执行' : '任务执行中,请稍候'
})
function setProgress(value: number): void {
const p = Math.min(100, Math.max(0, value))
progress.value = p
strokeOffset.value = CIRCLE_LEN - (CIRCLE_LEN * p) / 100
}
function clearPollListeners(): void {
unsubJob?.()
unsubJob = null
unsubUsb?.()
unsubUsb = null
unsubCard?.()
unsubCard = null
}
async function releasePolls(resetMode: boolean): Promise<void> {
clearPollListeners()
await pollCardPositionStop()
if (isCollect.value) {
await pollUsbStop({ resetMode })
} else {
await pollJobStop({ resetMode })
}
}
async function stopTaskPollsOnly(): Promise<void> {
unsubJob?.()
unsubJob = null
unsubUsb?.()
unsubUsb = null
if (isCollect.value) {
await pollUsbStop({ resetMode: false })
} else {
await pollJobStop({ resetMode: false })
}
}
async function startCardPositionWatch(): Promise<void> {
await pollCardPositionStop()
const cardStarted = await pollCardPositionStart()
if (!cardStarted.ok) {
notify.warning(cardStarted.message || '无法监控卡位,请手动点击返回或重新提交')
}
unsubCard?.()
unsubCard = onCardPositionTick((payload) =>
applyCardPosition(payload as CardPositionPollPayload)
)
}
async function enterWaitPhase(next: 'completed' | 'failed', errorText = ''): Promise<void> {
if (phase.value !== next) {
phase.value = next
if (next === 'completed') {
setProgress(100)
workflowStep.value = isCollect.value ? 3 : 4
} else {
failureStep.value = workflowStep.value
failureErrorText.value = errorText
if (!failureErrorText.value && !isCollect.value) {
try {
const r = await dllPrinterErrorStr(-1)
failureErrorText.value = r.ok && r.data?.text ? r.data.text : ''
} catch {
failureErrorText.value = ''
}
}
jobStore.clearActiveJob()
}
} else if (next === 'failed' && errorText) {
failureErrorText.value = errorText
}
await stopTaskPollsOnly()
appStore.setMode(isCollect.value ? 'usbCopying' : 'distributing')
lastCardPosition = -1
await startCardPositionWatch()
}
async function enterFailedPhase(errorText = ''): Promise<void> {
await enterWaitPhase('failed', errorText)
}
async function enterCompletedPhase(): Promise<void> {
await enterWaitPhase('completed')
}
function canAutoResubmit(): boolean {
return phase.value === 'completed' || phase.value === 'failed'
}
function applyCardPosition(p: CardPositionPollPayload): void {
if (!canAutoResubmit() || resubmitting) return
if (p.queryCode !== 0) return
const pos = p.position
const enteredPrepare = pos === POSITION_PREPARE && lastCardPosition !== POSITION_PREPARE
lastCardPosition = pos
if (!enteredPrepare) return
void resubmitTask()
}
async function resubmitTask(): Promise<void> {
if (resubmitting || !canAutoResubmit()) return
const fromWaitPhase = phase.value
resubmitting = true
await pollCardPositionStop()
unsubCard?.()
unsubCard = null
phase.value = 'running'
setProgress(0)
workflowStep.value = 1
collectHint.value = ''
waitCard.value = false
queryFailStreak = 0
usbQueryFailStreak = 0
const backToWait = async (message?: string): Promise<void> => {
resubmitting = false
if (message) notify.error(message)
if (fromWaitPhase === 'failed') {
await enterFailedPhase(message || failureErrorText.value)
} else {
await enterCompletedPhase()
}
}
try {
if (isCollect.value) {
const dest = collectStore.destPath.trim()
if (!dest) {
await backToWait('导入目录无效,无法继续')
return
}
const r = await dllUsbCopy(dest, collectStore.cardOutput, { resubmit: true })
if (!r.ok) {
await backToWait(r.message || '重新提交收集任务失败')
return
}
usbAwaitNewCycle = true
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
return
}
const err = await validateJobPreflight(formStore)
if (err) {
notify.warning(err)
await backToWait()
return
}
const created = await createDistributeJob(formStore, { resubmit: true })
if (!created.ok) {
await backToWait(created.message)
return
}
jobStore.setActiveJob(created.jobId)
const started = await pollJobStart(created.jobId)
if (!started.ok) {
await backToWait(started.message || '启动任务轮询失败')
return
}
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
} finally {
resubmitting = false
}
}
function applyJobProgress(p: JobPollPayload): void {
if (finishing || phase.value !== 'running') return
if (p.queryErrorCode !== 0) {
queryFailStreak += 1
if (queryFailStreak < 3) return
jobStore.failCount += 1
void enterFailedPhase(`查询任务失败: ${p.queryErrorCode}`)
return
}
queryFailStreak = 0
setProgress(p.progress)
const ui = mapJobStateToUi(p.jobState)
workflowStep.value = ui.workflowStep
waitCard.value = ui.hint === 'waitCard'
if (p.failed) {
jobStore.failCount += 1
void enterFailedPhase()
return
}
if (p.cancelled) {
void finishDistribute(false)
return
}
if (p.finished) {
jobStore.successCount += 1
void enterCompletedPhase()
return
}
}
function applyUsbProgress(p: UsbPollPayload): void {
if (finishing || phase.value !== 'running') return
if (p.queryCode !== 0) {
usbQueryFailStreak += 1
if (usbQueryFailStreak < 3) return
collectStore.failCount += 1
void enterFailedPhase(`查询 USB 任务失败: ${p.queryCode}`)
return
}
usbQueryFailStreak = 0
if (usbAwaitNewCycle && p.queryCode === 0 && p.taskStatus === USB_TASK_PREPARING) {
usbAwaitNewCycle = false
}
if (p.failed) {
collectStore.failCount += 1
void enterFailedPhase(p.errorMessage || usbTaskStatusHint(p.taskStatus))
return
}
if (p.success) {
collectStore.successCount += 1
workflowStep.value = 3
collectHint.value = usbTaskStatusHint(p.taskStatus)
notify.success('USB 收集完成')
void enterCompletedPhase()
return
}
const copyProgress = clampUsbCopyProgress(p.progress)
workflowStep.value = p.taskStatus === USB_TASK_COPYING ? 2 : 1
collectHint.value = usbTaskStatusHint(p.taskStatus)
setProgress(copyProgress)
}
async function finishDistribute(
toHome: boolean,
redirect: '/home' | '/distribute/config' = toHome ? '/home' : '/distribute/config'
): Promise<void> {
if (finishing) return
finishing = true
const skipCancel = phase.value === 'completed' || phase.value === 'failed'
await releasePolls(true)
const id = jobStore.jobId
if (id && !skipCancel) {
const r = await dllJobCancel(id)
if (!r.ok) {
notify.warning(r.message || '取消任务时出现问题')
}
}
jobStore.clearActiveJob()
appStore.setMode('ready')
await router.push(redirect)
}
async function finishCollect(toHome: boolean): Promise<void> {
if (finishing) return
finishing = true
await releasePolls(true)
appStore.setMode('ready')
await router.push(toHome ? '/home' : '/collect')
}
onMounted(async () => {
if (isCollect.value) {
if (appStore.mode !== 'usbCopying') {
router.replace('/collect')
return
}
workflowStep.value = 1
setProgress(0)
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
return
}
if (!jobStore.jobId) {
router.replace('/distribute/config')
return
}
appStore.setMode('distributing')
setProgress(0)
const started = await pollJobStart(jobStore.jobId)
if (!started.ok) {
notify.error(started.message || '启动任务轮询失败')
await finishDistribute(false)
return
}
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
})
onUnmounted(() => {
if (finishing || phase.value !== 'running') return
void releasePolls(true)
if (isCollect.value && appStore.mode === 'usbCopying') {
appStore.setMode('ready')
} else if (!isCollect.value && appStore.mode === 'distributing') {
appStore.setMode('ready')
}
})
async function leaveFailedPage(redirect: '/collect' | '/distribute/config' | '/home'): Promise<void> {
finishing = true
await releasePolls(true)
appStore.setMode('ready')
await router.push(redirect)
}
async function onFailedBack(): Promise<void> {
if (isCollect.value) {
await leaveFailedPage('/collect')
return
}
await leaveFailedPage('/distribute/config')
}
async function onFailedReset(): Promise<void> {
if (isCollect.value) {
collectStore.reset()
} else {
formStore.reset()
jobStore.reset()
}
await leaveFailedPage('/home')
}
async function onReturn(): Promise<void> {
if (isCollect.value) {
await finishCollect(false)
return
}
await finishDistribute(false)
}
async function onStop(): Promise<void> {
if (isCollect.value) {
await finishCollect(false)
return
}
await finishDistribute(false)
}
</script>
<style src="@/styles/pages/page2.css"></style>
<style src="@/styles/pages/page3.css"></style>
<style scoped>
.m-error-detail {
margin-top: 6px;
font-size: 10px;
color: #dc3545;
font-weight: 600;
max-width: 320px;
word-break: break-all;
}
.m-progress-circle .fill {
stroke-dasharray: 283;
transition: stroke-dashoffset 0.45s ease;
}
</style>