Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1b73ee3d3 | |||
| 6b135eaf7a |
@@ -0,0 +1,78 @@
|
|||||||
|
// 临时自测:验证 networkPath.ts 的纯函数行为
|
||||||
|
// 跑法:node scripts/network-path-selftest.mjs
|
||||||
|
import {
|
||||||
|
isNetworkPath,
|
||||||
|
extractHostName,
|
||||||
|
buildNetworkUrl,
|
||||||
|
buildNetInfo
|
||||||
|
} from '../src/renderer/src/utils/networkPath.ts'
|
||||||
|
|
||||||
|
const cases = []
|
||||||
|
function eq(name, actual, expected) {
|
||||||
|
// 两边按字面字符串比较(不走 JSON.stringify 避免转义歧义)
|
||||||
|
const ok = actual === expected
|
||||||
|
cases.push({ name, ok, actual, expected })
|
||||||
|
if (!ok) {
|
||||||
|
console.error(`FAIL ${name}`)
|
||||||
|
console.error(` expected: ${JSON.stringify(expected)}`)
|
||||||
|
console.error(` actual: ${JSON.stringify(actual)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isNetworkPath
|
||||||
|
eq('isNetworkPath \\host', isNetworkPath('\\\\192.168.1.100\\share'), true)
|
||||||
|
eq('isNetworkPath //host', isNetworkPath('//nas/share'), true)
|
||||||
|
eq('isNetworkPath D:\\a (应 false)', isNetworkPath('D:\\data'), false)
|
||||||
|
eq('isNetworkPath empty', isNetworkPath(''), false)
|
||||||
|
|
||||||
|
// extractHostName
|
||||||
|
const unc = '\\\\192.168.1.100\\share\\a.pdf' // 实际 \\192.168.1.100\share\a.pdf
|
||||||
|
eq('extractHostName \\host\\share', extractHostName(unc), '192.168.1.100')
|
||||||
|
eq('extractHostName //host/share', extractHostName('//nas/share'), 'nas')
|
||||||
|
eq('extractHostName local', extractHostName('D:\\data'), '')
|
||||||
|
eq('extractHostName empty', extractHostName(''), '')
|
||||||
|
|
||||||
|
// buildNetworkUrl
|
||||||
|
// 期望: \\192.168.1.100 -> 字面 '\\\\192.168.1.100'
|
||||||
|
eq('buildNetworkUrl bare', buildNetworkUrl('192.168.1.100', ''), '\\\\192.168.1.100')
|
||||||
|
// 期望: \\192.168.1.100\share -> 字面 '\\\\192.168.1.100\\share'
|
||||||
|
eq('buildNetworkUrl share', buildNetworkUrl('192.168.1.100', 'share'), '\\\\192.168.1.100\\share')
|
||||||
|
eq('buildNetworkUrl slashes stripped', buildNetworkUrl('host', '/share/'), '\\\\host\\share')
|
||||||
|
|
||||||
|
// buildNetInfo
|
||||||
|
// 期望产物是 JSON 文本(JSON 字符串里 \\ 表示 1 个 \ 字符)
|
||||||
|
// 反序列化后 host_name 值是 2 个 \ 字符 -> JSON 文本中需要 4 个 \ 字符
|
||||||
|
// 4 个 \ 字符 = JS 字面 '\\\\\\\\' (8 个 \)
|
||||||
|
const stored = new Map([['192.168.1.100', { userName: 'admin', password: 'pass', lastUsed: '' }]])
|
||||||
|
const r1 = buildNetInfo(
|
||||||
|
[{ hostName: '192.168.1.100' }],
|
||||||
|
(h) => stored.get(h) || null
|
||||||
|
)
|
||||||
|
eq('buildNetInfo single', r1, '[{"host_name":"\\\\\\\\192.168.1.100","user_name":"admin","password":"pass"}]')
|
||||||
|
|
||||||
|
// dedup
|
||||||
|
const r2 = buildNetInfo(
|
||||||
|
[
|
||||||
|
{ hostName: '192.168.1.100', userName: 'a', password: 'p' },
|
||||||
|
{ hostName: '192.168.1.100', userName: 'b', password: 'q' }
|
||||||
|
],
|
||||||
|
() => null
|
||||||
|
)
|
||||||
|
eq('buildNetInfo dedup', r2, '[{"host_name":"\\\\\\\\192.168.1.100","user_name":"a","password":"p"}]')
|
||||||
|
|
||||||
|
// missing throws
|
||||||
|
let threw = false
|
||||||
|
try {
|
||||||
|
buildNetInfo([{ hostName: 'h1' }], () => null)
|
||||||
|
} catch (e) {
|
||||||
|
threw = e.message.includes('缺少网络凭据')
|
||||||
|
}
|
||||||
|
eq('buildNetInfo missing throws', threw, true)
|
||||||
|
|
||||||
|
// empty -> empty string
|
||||||
|
eq('buildNetInfo empty list', buildNetInfo([], () => null), '')
|
||||||
|
|
||||||
|
const pass = cases.filter((c) => c.ok).length
|
||||||
|
const fail = cases.length - pass
|
||||||
|
console.log(`PASS ${pass} / ${cases.length}`)
|
||||||
|
if (fail > 0) process.exit(1)
|
||||||
@@ -104,8 +104,7 @@ export function registerIpcHandlers(): void {
|
|||||||
const snapshot: PrinterStatusSnapshot = {
|
const snapshot: PrinterStatusSnapshot = {
|
||||||
ribbonType: cached?.ribbonType ?? '—',
|
ribbonType: cached?.ribbonType ?? '—',
|
||||||
statusText: parsed.statusText,
|
statusText: parsed.statusText,
|
||||||
serialNo: cached?.serialNo ?? '—',
|
serialNo: cached?.serialNo ?? '—'
|
||||||
printedCount: cached?.printedCount ?? 0
|
|
||||||
}
|
}
|
||||||
configStore.set('lastPrinterStatus', snapshot)
|
configStore.set('lastPrinterStatus', snapshot)
|
||||||
return ok({ statusText: parsed.statusText, statusCode: code })
|
return ok({ statusText: parsed.statusText, statusCode: code })
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
>状态: <b :class="statusTone">{{ status.statusText }}</b></span
|
>状态: <b :class="statusTone">{{ status.statusText }}</b></span
|
||||||
>
|
>
|
||||||
<span>序列号: <b>{{ status.serialNo }}</b></span>
|
<span>序列号: <b>{{ status.serialNo }}</b></span>
|
||||||
<span>已发行: <b>{{ status.printedCount }}</b></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-header__actions-slot">
|
<div class="c-header__actions-slot">
|
||||||
@@ -26,7 +25,7 @@
|
|||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { refreshPrinterHeader } from '@/composables/usePrinterStatus'
|
import { refreshLiveStatus } from '@/composables/usePrinterStatus'
|
||||||
|
|
||||||
defineProps<{ mode?: string }>()
|
defineProps<{ mode?: string }>()
|
||||||
|
|
||||||
@@ -35,12 +34,12 @@ const appStore = useAppStore()
|
|||||||
const status = computed(() => configStore.printer)
|
const status = computed(() => configStore.printer)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (appStore.initialized) void refreshPrinterHeader(configStore)
|
if (appStore.initialized) void refreshLiveStatus()
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusTone = computed(() => {
|
const statusTone = computed(() => {
|
||||||
const t = status.value.statusText
|
const t = status.value.statusText
|
||||||
if (t.includes('未初始化') || t.includes('未连接')) return 'c-status-warn'
|
if (t.includes('未初始化') || t.includes('未连接') || t.includes('失败')) return 'c-status-warn'
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<transition name="npd-fade">
|
||||||
|
<div v-if="visible" class="npd-mask" @mousedown.self="onCancel">
|
||||||
|
<div class="npd-dialog" role="dialog" aria-modal="true" aria-labelledby="npd-title">
|
||||||
|
<div class="npd-header">
|
||||||
|
<span id="npd-title" class="npd-title">添加网络位置</span>
|
||||||
|
<button type="button" class="npd-close" aria-label="关闭" @click="onCancel">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="npd-body">
|
||||||
|
<p class="npd-hint">系统将按此 UNC 路径访问网络共享,请确认主机可访问且账号有效。</p>
|
||||||
|
|
||||||
|
<label class="npd-field">
|
||||||
|
<span class="npd-label">主机(IP 或主机名)<span class="npd-req">*</span></span>
|
||||||
|
<input
|
||||||
|
v-model.trim="host"
|
||||||
|
type="text"
|
||||||
|
class="c-input"
|
||||||
|
placeholder="例如 192.168.1.100 或 nas-server"
|
||||||
|
:class="{ 'is-invalid': touched && !hostValid }"
|
||||||
|
@blur="touched = true"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="npd-field">
|
||||||
|
<span class="npd-label">共享名(可选)</span>
|
||||||
|
<input
|
||||||
|
v-model.trim="share"
|
||||||
|
type="text"
|
||||||
|
class="c-input"
|
||||||
|
placeholder="例如 share,留空表示只挂载到根"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="npd-field">
|
||||||
|
<span class="npd-label">用户名<span class="npd-req">*</span></span>
|
||||||
|
<input
|
||||||
|
v-model.trim="userName"
|
||||||
|
type="text"
|
||||||
|
class="c-input"
|
||||||
|
placeholder="例如 admin"
|
||||||
|
:class="{ 'is-invalid': touched && !userNameValid }"
|
||||||
|
@blur="touched = true"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="npd-field">
|
||||||
|
<span class="npd-label">密码<span class="npd-req">*</span></span>
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
class="c-input"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
:class="{ 'is-invalid': touched && !passwordValid }"
|
||||||
|
@blur="touched = true"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div v-if="previewUrl" class="npd-preview">
|
||||||
|
<span class="npd-preview-label">将添加为</span>
|
||||||
|
<code class="npd-preview-path">{{ previewUrl }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="touched && errorText" class="npd-error">{{ errorText }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="npd-footer">
|
||||||
|
<button type="button" class="c-button-cs" @click="onCancel">取消</button>
|
||||||
|
<button type="button" class="c-button-cs npd-primary" :disabled="!canConfirm" @click="onConfirm">
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { buildNetworkUrl } from '@/utils/networkPath'
|
||||||
|
|
||||||
|
const props = defineProps<{ visible: boolean }>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:visible': [boolean]
|
||||||
|
confirm: [{ path: string; hostName: string; userName: string; password: string }]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const host = ref('')
|
||||||
|
const share = ref('')
|
||||||
|
const userName = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const touched = ref(false)
|
||||||
|
|
||||||
|
const hostRe = /^[A-Za-z0-9_.-]+$/
|
||||||
|
|
||||||
|
const hostValid = computed(() => hostRe.test(host.value))
|
||||||
|
const userNameValid = computed(() => userName.value.length > 0)
|
||||||
|
const passwordValid = computed(() => password.value.length > 0)
|
||||||
|
const canConfirm = computed(() => hostValid.value && userNameValid.value && passwordValid.value)
|
||||||
|
|
||||||
|
const previewUrl = computed(() => (hostValid.value ? buildNetworkUrl(host.value, share.value) : ''))
|
||||||
|
|
||||||
|
const errorText = computed(() => {
|
||||||
|
if (!hostValid.value) return '主机名/IP 不合法'
|
||||||
|
if (!userNameValid.value) return '请输入用户名'
|
||||||
|
if (!passwordValid.value) return '请输入密码'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function reset(): void {
|
||||||
|
host.value = ''
|
||||||
|
share.value = ''
|
||||||
|
userName.value = ''
|
||||||
|
password.value = ''
|
||||||
|
touched.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel(): void {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConfirm(): void {
|
||||||
|
touched.value = true
|
||||||
|
if (!canConfirm.value) return
|
||||||
|
const path = buildNetworkUrl(host.value, share.value)
|
||||||
|
emit('confirm', {
|
||||||
|
path,
|
||||||
|
hostName: host.value,
|
||||||
|
userName: userName.value,
|
||||||
|
password: password.value
|
||||||
|
})
|
||||||
|
reset()
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(v) => {
|
||||||
|
if (v) {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.npd-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9000;
|
||||||
|
}
|
||||||
|
.npd-dialog {
|
||||||
|
width: 460px;
|
||||||
|
max-width: 92vw;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.npd-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
.npd-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.npd-close {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #909399;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.npd-close:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.npd-body {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.npd-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.npd-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.npd-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.npd-req {
|
||||||
|
color: #f56c6c;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
.is-invalid {
|
||||||
|
border-color: #f56c6c !important;
|
||||||
|
}
|
||||||
|
.npd-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.npd-preview-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.npd-preview-path {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #409eff;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.npd-error {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.npd-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
.npd-primary {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
.npd-primary[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.npd-fade-enter-active,
|
||||||
|
.npd-fade-leave-active {
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
.npd-fade-enter-from,
|
||||||
|
.npd-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { notify } from '@/composables/useNotify'
|
import { notify } from '@/composables/useNotify'
|
||||||
import { applyPrinterPayload, refreshPrinterFullInfo } from '@/composables/usePrinterStatus'
|
import { refreshPrinterAfterInit } from '@/composables/usePrinterStatus'
|
||||||
import { configGet, dllInit, dllRejectAvailable } from '@/api/cardsoon'
|
import { configGet, dllInit, dllRejectAvailable } from '@/api/cardsoon'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
@@ -43,7 +43,7 @@ export function useAppBootstrap(): void {
|
|||||||
appStore.setInitialized(true)
|
appStore.setInitialized(true)
|
||||||
placeholderStatus(configStore, '就绪')
|
placeholderStatus(configStore, '就绪')
|
||||||
await syncRejectApi()
|
await syncRejectApi()
|
||||||
window.setTimeout(() => void refreshPrinterFullInfo(configStore), 1500)
|
window.setTimeout(() => void refreshPrinterAfterInit(), 1500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export function useAppBootstrap(): void {
|
|||||||
if (initMeta?.warning) notify.warning(initMeta.warning)
|
if (initMeta?.warning) notify.warning(initMeta.warning)
|
||||||
placeholderStatus(configStore, initMeta?.warning ? '未连接打印机' : '就绪')
|
placeholderStatus(configStore, initMeta?.warning ? '未连接打印机' : '就绪')
|
||||||
await syncRejectApi()
|
await syncRejectApi()
|
||||||
window.setTimeout(() => void refreshPrinterFullInfo(configStore), 1500)
|
window.setTimeout(() => void refreshPrinterAfterInit(), 1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,68 +1,41 @@
|
|||||||
import { dllPrinterInfo, dllPrinterStatus, parsePrinterInfo } from '@/api/cardsoon'
|
import { dllPrinterInfo, dllPrinterStatus, parsePrinterInfo } from '@/api/cardsoon'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import type { PrinterStatusDisplay } from '@/types/printer'
|
|
||||||
|
|
||||||
function isFullPrinterPayload(data: Record<string, unknown>): boolean {
|
/** SAPI_PrinterCheckstatus:仅刷新 Header 状态文案 */
|
||||||
return (
|
export async function refreshLiveStatus(): Promise<void> {
|
||||||
data.snapshot != null ||
|
const store = useConfigStore()
|
||||||
data.printerList != null ||
|
|
||||||
data.serial_no != null ||
|
|
||||||
data.SerialNo != null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyPrinterPayload(
|
|
||||||
configStore: ReturnType<typeof useConfigStore>,
|
|
||||||
data: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
const snapshot = data.snapshot as PrinterStatusDisplay | undefined
|
|
||||||
if (snapshot) {
|
|
||||||
configStore.setPrinter(snapshot)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isFullPrinterPayload(data)) {
|
|
||||||
configStore.setPrinter(parsePrinterInfo(data))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (typeof data.statusText === 'string') {
|
|
||||||
configStore.setPrinter({
|
|
||||||
...configStore.printer,
|
|
||||||
statusText: data.statusText
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (data.fromCache) {
|
|
||||||
configStore.setPrinter({
|
|
||||||
ribbonType: String(data.ribbonType ?? configStore.printer.ribbonType),
|
|
||||||
statusText: String(data.statusText ?? configStore.printer.statusText),
|
|
||||||
serialNo: String(data.serialNo ?? configStore.printer.serialNo),
|
|
||||||
printedCount: Number(data.printedCount ?? configStore.printer.printedCount)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function refreshPrinterHeader(
|
|
||||||
configStore: ReturnType<typeof useConfigStore>
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
const r = await dllPrinterStatus()
|
const r = await dllPrinterStatus()
|
||||||
if (r.ok && r.data) {
|
if (r.ok && r.data?.statusText) {
|
||||||
applyPrinterPayload(configStore, r.data as Record<string, unknown>)
|
store.setPrinter({ ...store.printer, statusText: r.data.statusText })
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* 无打印机时不阻塞 */
|
/* 无打印机时不阻塞 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshPrinterFullInfo(
|
/** GetPrinterInfoEx:仅刷新色带、序列号(启动时一次) */
|
||||||
configStore: ReturnType<typeof useConfigStore>
|
export async function refreshPrinterInfo(): Promise<void> {
|
||||||
): Promise<void> {
|
const store = useConfigStore()
|
||||||
try {
|
try {
|
||||||
const info = await dllPrinterInfo()
|
const info = await dllPrinterInfo()
|
||||||
if (info.ok && info.data) {
|
if (!info.ok || !info.data) return
|
||||||
applyPrinterPayload(configStore, info.data)
|
const data = info.data as Record<string, unknown>
|
||||||
}
|
const parsed = data.snapshot
|
||||||
|
? (data.snapshot as typeof store.printer)
|
||||||
|
: parsePrinterInfo(data)
|
||||||
|
store.setPrinter({
|
||||||
|
...store.printer,
|
||||||
|
ribbonType: parsed.ribbonType,
|
||||||
|
serialNo: parsed.serialNo
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
/* 无打印机时不阻塞 */
|
/* 无打印机时不阻塞 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 启动后:先拉静态信息,再查实时状态 */
|
||||||
|
export async function refreshPrinterAfterInit(): Promise<void> {
|
||||||
|
await refreshPrinterInfo()
|
||||||
|
await refreshLiveStatus()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { configGet } from '@/api/cardsoon'
|
import { configGet } from '@/api/cardsoon'
|
||||||
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import './styles/design-base.css'
|
import './styles/design-base.css'
|
||||||
@@ -31,4 +32,7 @@ void (async () => {
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
|
useNetworkAuthStore().loadFromStorage()
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export interface PathListItem {
|
|||||||
path: string
|
path: string
|
||||||
meta: string
|
meta: string
|
||||||
sizeBytes: number
|
sizeBytes: number
|
||||||
|
isNetwork?: boolean
|
||||||
|
hostName?: string
|
||||||
|
userName?: string
|
||||||
|
password?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DistributeFormState {
|
export interface DistributeFormState {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// TODO: 后续用 electron safeStorage 加密
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { StoredCredential } from '@/types/network'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'networkCredentials'
|
||||||
|
|
||||||
|
export const useNetworkAuthStore = defineStore('networkAuth', {
|
||||||
|
state: () => ({
|
||||||
|
credentials: {} as Record<string, StoredCredential>
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
hasCredentials: (state) => Object.keys(state.credentials).length > 0
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
loadFromStorage(): void {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (!raw) return
|
||||||
|
const parsed = JSON.parse(raw) as Record<string, StoredCredential>
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
this.credentials = { ...this.credentials, ...parsed }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[networkAuth] loadFromStorage 失败,已忽略', e)
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
} catch {
|
||||||
|
// localStorage 不可用时静默忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCredential(hostName: string, userName: string, password: string): void {
|
||||||
|
const host = (hostName || '').trim()
|
||||||
|
if (!host) return
|
||||||
|
this.credentials[host] = {
|
||||||
|
userName: userName || '',
|
||||||
|
password: password || '',
|
||||||
|
lastUsed: new Date().toISOString()
|
||||||
|
}
|
||||||
|
this.persist()
|
||||||
|
},
|
||||||
|
getCredential(hostName: string): StoredCredential | null {
|
||||||
|
const host = (hostName || '').trim()
|
||||||
|
if (!host) return null
|
||||||
|
return this.credentials[host] || null
|
||||||
|
},
|
||||||
|
removeCredential(hostName: string): void {
|
||||||
|
const host = (hostName || '').trim()
|
||||||
|
if (!host) return
|
||||||
|
if (delete this.credentials[host]) {
|
||||||
|
this.persist()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearAll(): void {
|
||||||
|
this.credentials = {}
|
||||||
|
this.persist()
|
||||||
|
},
|
||||||
|
persist(): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.credentials))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[networkAuth] 持久化失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/** 提交到后端的网络凭据结构(JSON 字符串里的一项) */
|
||||||
|
export interface NetworkCredential {
|
||||||
|
host_name: string
|
||||||
|
user_name: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 客户端缓存的凭据(以 host 为 key) */
|
||||||
|
export interface StoredCredential {
|
||||||
|
userName: string
|
||||||
|
password: string
|
||||||
|
lastUsed: string
|
||||||
|
}
|
||||||
@@ -2,12 +2,10 @@ export interface PrinterStatusDisplay {
|
|||||||
ribbonType: string
|
ribbonType: string
|
||||||
statusText: string
|
statusText: string
|
||||||
serialNo: string
|
serialNo: string
|
||||||
printedCount: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPrinterStatus: PrinterStatusDisplay = {
|
export const defaultPrinterStatus: PrinterStatusDisplay = {
|
||||||
ribbonType: '—',
|
ribbonType: '—',
|
||||||
statusText: '—',
|
statusText: '—',
|
||||||
serialNo: '—',
|
serialNo: '—'
|
||||||
printedCount: 0
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { DistributeFormState } from '@/stores/distributeForm'
|
import type { DistributeFormState } from '@/stores/distributeForm'
|
||||||
import { cleanPathPattern } from '@shared/path-pattern'
|
import { cleanPathPattern } from '@shared/path-pattern'
|
||||||
import { resolveJobTasks } from '@/utils/validateJobConfig'
|
import { resolveJobTasks } from '@/utils/validateJobConfig'
|
||||||
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
|
import { buildNetInfo } from '@/utils/networkPath'
|
||||||
|
|
||||||
export interface BuildJobOptions {
|
export interface BuildJobOptions {
|
||||||
taskId: string
|
taskId: string
|
||||||
@@ -57,5 +59,12 @@ export function buildJobConfig(
|
|||||||
if (form.generateZip) body.is_generate_zip = true
|
if (form.generateZip) body.is_generate_zip = true
|
||||||
if (form.failPrintLabel) body.is_printer_record_logo = true
|
if (form.failPrintLabel) body.is_printer_record_logo = true
|
||||||
|
|
||||||
|
const networkItems = form.pathList.filter((x) => x.isNetwork)
|
||||||
|
if (networkItems.length > 0) {
|
||||||
|
const netStore = useNetworkAuthStore()
|
||||||
|
const netInfo = buildNetInfo(networkItems, (host) => netStore.getCredential(host))
|
||||||
|
if (netInfo) body.net_info = netInfo
|
||||||
|
}
|
||||||
|
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ async function buildJobJson(form: DistributeFormState): Promise<{ ok: true; json
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
return { ok: true, json: JSON.stringify(buildJobConfig(form, { taskId, udfFile })) }
|
return { ok: true, json: JSON.stringify(buildJobConfig(form, { taskId, udfFile })) }
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, message: e instanceof Error ? e.message : String(e) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDistributeJob(
|
export async function createDistributeJob(
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { NetworkCredential, StoredCredential } from '@/types/network'
|
||||||
|
|
||||||
|
export function isNetworkPath(p: string): boolean {
|
||||||
|
if (!p) return false
|
||||||
|
return p.startsWith('\\\\') || p.startsWith('//')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractHostName(p: string): string {
|
||||||
|
if (!p) return ''
|
||||||
|
if (p.startsWith('\\\\')) {
|
||||||
|
const part = p.substring(2).split('\\')[0]
|
||||||
|
return part || ''
|
||||||
|
}
|
||||||
|
if (p.startsWith('//')) {
|
||||||
|
const part = p.substring(2).split('/')[0]
|
||||||
|
return part || ''
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildNetworkUrl(host: string, share: string): string {
|
||||||
|
const h = host.trim()
|
||||||
|
const s = share.trim().replace(/^[\\/]+/, '').replace(/[\\/]+$/, '')
|
||||||
|
return s ? `\\\\${h}\\${s}` : `\\\\${h}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildNetInfo(
|
||||||
|
networkItems: { hostName?: string; userName?: string; password?: string }[],
|
||||||
|
getStored: (host: string) => StoredCredential | null
|
||||||
|
): string {
|
||||||
|
const creds: NetworkCredential[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const it of networkItems) {
|
||||||
|
const host = (it.hostName || '').trim()
|
||||||
|
if (!host || seen.has(host)) continue
|
||||||
|
seen.add(host)
|
||||||
|
const stored = getStored(host)
|
||||||
|
const user_name = (it.userName || stored?.userName || '').trim()
|
||||||
|
const password = it.password || stored?.password || ''
|
||||||
|
if (!user_name || !password) {
|
||||||
|
throw new Error(`缺少网络凭据: ${host}`)
|
||||||
|
}
|
||||||
|
creds.push({
|
||||||
|
host_name: `\\\\${host}`,
|
||||||
|
user_name,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return creds.length > 0 ? JSON.stringify(creds) : ''
|
||||||
|
}
|
||||||
@@ -12,8 +12,11 @@ export async function validateJobPreflight(form: DistributeFormState): Promise<s
|
|||||||
|
|
||||||
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
||||||
if (hasCopy) {
|
if (hasCopy) {
|
||||||
const paths = form.pathList.map((x) => x.path)
|
// 网络项在主进程 fs.existsSync 必返 false,跳过其存在性 / 总大小校验
|
||||||
const ex = await fsPathExists(paths)
|
const localItems = form.pathList.filter((x) => !x.isNetwork)
|
||||||
|
const localPaths = localItems.map((x) => x.path)
|
||||||
|
if (localPaths.length > 0) {
|
||||||
|
const ex = await fsPathExists(localPaths)
|
||||||
if (ex.ok && ex.data?.missing.length) {
|
if (ex.ok && ex.data?.missing.length) {
|
||||||
return `路径不存在: ${ex.data.missing.join(', ')}`
|
return `路径不存在: ${ex.data.missing.join(', ')}`
|
||||||
}
|
}
|
||||||
@@ -21,6 +24,7 @@ export async function validateJobPreflight(form: DistributeFormState): Promise<s
|
|||||||
return '拷贝路径下没有可拷贝的文件'
|
return '拷贝路径下没有可拷贝的文件'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (hasPrint) {
|
if (hasPrint) {
|
||||||
const soon = form.templateFile.trim()
|
const soon = form.templateFile.trim()
|
||||||
const ex = await fsPathExists([soon])
|
const ex = await fsPathExists([soon])
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
<button type="button" class="c-button-cs" @click="addPath">
|
<button type="button" class="c-button-cs" @click="addPath">
|
||||||
添加路径
|
添加路径
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="c-button-cs" @click="networkDialogVisible = true">
|
||||||
|
添加网络位置
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-path-hint">
|
<div class="m-path-hint">
|
||||||
@@ -68,7 +71,10 @@
|
|||||||
<div class="c-panel__body">
|
<div class="c-panel__body">
|
||||||
<div v-for="(item, idx) in formStore.pathList" :key="idx" class="c-path-item">
|
<div v-for="(item, idx) in formStore.pathList" :key="idx" class="c-path-item">
|
||||||
<div class="c-path-item__info">
|
<div class="c-path-item__info">
|
||||||
<div class="c-path-item__name">{{ item.path }}</div>
|
<div class="c-path-item__name">
|
||||||
|
{{ item.path }}
|
||||||
|
<span v-if="item.isNetwork" class="c-path-item__tag">网络</span>
|
||||||
|
</div>
|
||||||
<div class="c-path-item__meta">{{ item.meta }}</div>
|
<div class="c-path-item__meta">{{ item.meta }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="c-path-item__delete" @click="removePath(idx)">
|
<button type="button" class="c-path-item__delete" @click="removePath(idx)">
|
||||||
@@ -141,11 +147,12 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
|
<NetworkPathDialog v-model:visible="networkDialogVisible" @confirm="onNetworkConfirm" />
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { notify, notifyRequireInit } from '@/composables/useNotify'
|
import { notify, notifyRequireInit } from '@/composables/useNotify'
|
||||||
import AppShell from '@/layouts/AppShell.vue'
|
import AppShell from '@/layouts/AppShell.vue'
|
||||||
@@ -154,11 +161,13 @@ import AppFooter from '@/components/AppFooter.vue'
|
|||||||
import NavButton from '@/components/NavButton.vue'
|
import NavButton from '@/components/NavButton.vue'
|
||||||
import AppIcon from '@/components/AppIcon.vue'
|
import AppIcon from '@/components/AppIcon.vue'
|
||||||
import AppSelect from '@/components/AppSelect.vue'
|
import AppSelect from '@/components/AppSelect.vue'
|
||||||
|
import NetworkPathDialog from '@/components/NetworkPathDialog.vue'
|
||||||
import { COPY_TYPE_OPTIONS, FORMAT_TYPE_OPTIONS } from '@/constants/selectOptions'
|
import { COPY_TYPE_OPTIONS, FORMAT_TYPE_OPTIONS } from '@/constants/selectOptions'
|
||||||
import { CARD_CAPACITY_BYTES, CARD_CAPACITY_GB } from '@/constants/cardCapacity'
|
import { CARD_CAPACITY_BYTES, CARD_CAPACITY_GB } from '@/constants/cardCapacity'
|
||||||
import { useDistributeFormStore } from '@/stores/distributeForm'
|
import { useDistributeFormStore } from '@/stores/distributeForm'
|
||||||
import { useJobStore } from '@/stores/job'
|
import { useJobStore } from '@/stores/job'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
import { validateJobPreflight } from '@/utils/validateJobPreflight'
|
import { validateJobPreflight } from '@/utils/validateJobPreflight'
|
||||||
import { createDistributeJob } from '@/utils/createDistributeJob'
|
import { createDistributeJob } from '@/utils/createDistributeJob'
|
||||||
import { formatBytesAsGb, formatBytesCompact } from '@/utils/formatBytes'
|
import { formatBytesAsGb, formatBytesCompact } from '@/utils/formatBytes'
|
||||||
@@ -176,6 +185,9 @@ const router = useRouter()
|
|||||||
const formStore = useDistributeFormStore()
|
const formStore = useDistributeFormStore()
|
||||||
const jobStore = useJobStore()
|
const jobStore = useJobStore()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const netStore = useNetworkAuthStore()
|
||||||
|
|
||||||
|
const networkDialogVisible = ref(false)
|
||||||
|
|
||||||
const canUse = computed(() => appStore.initialized)
|
const canUse = computed(() => appStore.initialized)
|
||||||
const canSubmit = computed(
|
const canSubmit = computed(
|
||||||
@@ -296,6 +308,26 @@ function removePath(idx: number): void {
|
|||||||
formStore.pathList.splice(idx, 1)
|
formStore.pathList.splice(idx, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onNetworkConfirm(payload: {
|
||||||
|
path: string
|
||||||
|
hostName: string
|
||||||
|
userName: string
|
||||||
|
password: string
|
||||||
|
}): void {
|
||||||
|
netStore.setCredential(payload.hostName, payload.userName, payload.password)
|
||||||
|
formStore.pathList.push({
|
||||||
|
path: payload.path,
|
||||||
|
meta: '网络位置 · 待提交',
|
||||||
|
sizeBytes: 0,
|
||||||
|
isNetwork: true,
|
||||||
|
hostName: payload.hostName,
|
||||||
|
userName: payload.userName,
|
||||||
|
password: payload.password
|
||||||
|
})
|
||||||
|
networkDialogVisible.value = false
|
||||||
|
notify.info(`已添加网络位置: ${payload.path}`)
|
||||||
|
}
|
||||||
|
|
||||||
async function pickTemplate(): Promise<void> {
|
async function pickTemplate(): Promise<void> {
|
||||||
const r = await dialogOpenSoon()
|
const r = await dialogOpenSoon()
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -361,6 +393,18 @@ async function onSubmit(): Promise<void> {
|
|||||||
<style src="@/styles/pages/page4.css"></style>
|
<style src="@/styles/pages/page4.css"></style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.c-path-item__tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: #409eff;
|
||||||
|
border-radius: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.c-card-small--slot {
|
.c-card-small--slot {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { notify } from '@/composables/useNotify'
|
import { notify } from '@/composables/useNotify'
|
||||||
|
import { refreshLiveStatus } from '@/composables/usePrinterStatus'
|
||||||
import AppShell from '@/layouts/AppShell.vue'
|
import AppShell from '@/layouts/AppShell.vue'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
import AppFooter from '@/components/AppFooter.vue'
|
||||||
@@ -259,6 +260,7 @@ async function enterWaitPhase(next: 'completed' | 'failed', errorText = ''): Pro
|
|||||||
appStore.setMode(sessionMode)
|
appStore.setMode(sessionMode)
|
||||||
lastCardPosition = -1
|
lastCardPosition = -1
|
||||||
await startCardPositionWatch(sessionMode)
|
await startCardPositionWatch(sessionMode)
|
||||||
|
void refreshLiveStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enterFailedPhase(errorText = ''): Promise<void> {
|
async function enterFailedPhase(errorText = ''): Promise<void> {
|
||||||
@@ -321,6 +323,7 @@ async function resubmitTask(): Promise<void> {
|
|||||||
usbAwaitNewCycle = true
|
usbAwaitNewCycle = true
|
||||||
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
||||||
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
||||||
|
void refreshLiveStatus()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +352,7 @@ async function resubmitTask(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
|
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
|
||||||
|
void refreshLiveStatus()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await backToWait(String(e))
|
await backToWait(String(e))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -460,6 +464,7 @@ onMounted(async () => {
|
|||||||
setProgress(0)
|
setProgress(0)
|
||||||
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
||||||
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
||||||
|
void refreshLiveStatus()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,6 +481,7 @@ onMounted(async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
|
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
|
||||||
|
void refreshLiveStatus()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { notify, notifyRequireInit } from '@/composables/useNotify'
|
import { notify, notifyRequireInit } from '@/composables/useNotify'
|
||||||
import { refreshPrinterHeader } from '@/composables/usePrinterStatus'
|
import { refreshLiveStatus } from '@/composables/usePrinterStatus'
|
||||||
import AppShell from '@/layouts/AppShell.vue'
|
import AppShell from '@/layouts/AppShell.vue'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
import AppFooter from '@/components/AppFooter.vue'
|
||||||
@@ -77,7 +77,7 @@ async function onReset(): Promise<void> {
|
|||||||
const r = await dllPrinterReset()
|
const r = await dllPrinterReset()
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
notify.success('已发送重置指令')
|
notify.success('已发送重置指令')
|
||||||
await refreshPrinterHeader(configStore)
|
await refreshLiveStatus()
|
||||||
} else notify.error(r.message || '重置失败')
|
} else notify.error(r.message || '重置失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +88,10 @@ async function onReject(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const r = await dllPrinterReject()
|
const r = await dllPrinterReject()
|
||||||
if (r.ok) notify.success('已废弃卡片')
|
if (r.ok) {
|
||||||
else notify.error(r.message || '操作失败')
|
notify.success('已废弃卡片')
|
||||||
|
await refreshLiveStatus()
|
||||||
|
} else notify.error(r.message || '操作失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTemplate(): Promise<void> {
|
async function onTemplate(): Promise<void> {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export interface PrinterStatusSnapshot {
|
|||||||
ribbonType: string
|
ribbonType: string
|
||||||
statusText: string
|
statusText: string
|
||||||
serialNo: string
|
serialNo: string
|
||||||
printedCount: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRINTER_STATUS_MAP: Record<string, string> = {
|
const PRINTER_STATUS_MAP: Record<string, string> = {
|
||||||
@@ -30,6 +29,7 @@ function normalizeStatus(raw: unknown): string {
|
|||||||
return PRINTER_STATUS_MAP[s] ?? s
|
return PRINTER_STATUS_MAP[s] ?? s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** SAPI_PrinterCheckstatus 返回值 → 展示文案 */
|
||||||
export function statusTextFromCheckstatus(code: number): {
|
export function statusTextFromCheckstatus(code: number): {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
statusText: string
|
statusText: string
|
||||||
@@ -59,36 +59,18 @@ function snapshotFromRecord(row: Record<string, unknown>): PrinterStatusSnapshot
|
|||||||
'PrinterSerial',
|
'PrinterSerial',
|
||||||
'PrinterName'
|
'PrinterName'
|
||||||
])
|
])
|
||||||
const statusRaw = pickFirst(row, [
|
|
||||||
'printer_status',
|
|
||||||
'PrinterStatus',
|
|
||||||
'PrinterType',
|
|
||||||
'Status'
|
|
||||||
])
|
|
||||||
const ribbon = pickFirst(row, ['ribbon_type', 'RibbonType', 'RibbonAmount'])
|
const ribbon = pickFirst(row, ['ribbon_type', 'RibbonType', 'RibbonAmount'])
|
||||||
const printed = pickFirst(row, ['printed_count', 'PrintedCount', 'PrintCount', 'printedCount'])
|
|
||||||
|
|
||||||
let statusText = normalizeStatus(statusRaw)
|
|
||||||
if (statusText === '—') {
|
|
||||||
const remain = row.RibbonRemain ?? row.RemainCount
|
|
||||||
const capacity = row.RibbonCapacity ?? row.Capacity ?? row.MaxCount
|
|
||||||
if (remain != null && capacity != null) {
|
|
||||||
statusText = `${remain}/${capacity}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ribbonType: String(ribbon ?? '—'),
|
ribbonType: String(ribbon ?? '—'),
|
||||||
statusText,
|
statusText: '—',
|
||||||
serialNo: String(serial ?? '—'),
|
serialNo: String(serial ?? '—')
|
||||||
printedCount: Number(printed ?? 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePrinterInfoFromDll(json: Record<string, unknown>): PrinterStatusSnapshot {
|
export function parsePrinterInfoFromDll(json: Record<string, unknown>): PrinterStatusSnapshot {
|
||||||
const flatSerial = pickFirst(json, ['serial_no', 'SerialNo', 'serialNo', 'szPrinterSerial'])
|
const flatSerial = pickFirst(json, ['serial_no', 'SerialNo', 'serialNo', 'szPrinterSerial'])
|
||||||
const flatStatus = pickFirst(json, ['printer_status', 'PrinterStatus'])
|
if (flatSerial != null) {
|
||||||
if (flatSerial != null || flatStatus != null) {
|
|
||||||
return snapshotFromRecord(json)
|
return snapshotFromRecord(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user