Files
examples/capabilities/demo-id-card-reader/src/App.vue
T

105 lines
11 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { CoreAPI } from '@cutos/core'
import { DeviceCapabilityIdCardReader, type IdCardReadEvent, type DeviceInfo } from '@cutos/device-id-card-reader'
import CutosTopbar from '@/components/CutosTopbar.vue'
import { loadConfig } from '@/utils/config'
type EventKind = 'data' | 'status' | 'error'
interface EventItem { id: number; time: string; kind: EventKind; value: unknown }
interface ReaderClient {
init(): Promise<unknown>; connect(): Promise<{ connected: boolean }>; disconnect(): Promise<{ connected: boolean }>
readDeviceInfo(): Promise<DeviceInfo>; startRead(params?: { image?: boolean }): Promise<{ started: boolean }>
stopRead(): Promise<{ stopped: boolean }>; onData(listener: (data: IdCardReadEvent) => void): () => void
onStatus(listener: (value: unknown) => void): () => void; onError(listener: (value: unknown) => void): () => void
dispose(): boolean
}
class MockReader implements ReaderClient {
private listeners = { data: new Set<(v: any) => void>(), status: new Set<(v: any) => void>(), error: new Set<(v: any) => void>() }
async init() { return true }
async connect() { this.emit('status', { status: 'online', updatedAt: Date.now() }); return { connected: true } }
async disconnect() { this.emit('status', { status: 'offline', updatedAt: Date.now() }); return { connected: false } }
async readDeviceInfo() { return { manufacturer: 'GHC', model: 'GHC825 Mock', version: '0.1.0', deviceType: 'id-card-reader', SAMID: 'MOCK-001' } }
async startRead(params: { image?: boolean } = {}) {
this.emit('status', { status: 'reading', updatedAt: Date.now() })
window.setTimeout(() => this.emit('data', { type: 'read-card', values: { code: '440101199001011234', name: 'CUTOS 示例', sex: '男', birthday: '19900101', address: '深圳市南山区', nation: '汉', department: '深圳市公安局', startDate: '20200101', endDate: '20300101', certType: '居民身份证', ...(params.image ? { base64BMPData: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMzAiIGhlaWdodD0iMTY2Ij48cmVjdCB3aWR0aD0iMTMwIiBoZWlnaHQ9IjE2NiIgZmlsbD0iI2U4ZjdmNCIvPjx0ZXh0IHg9IjY1IiB5PSI4MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzEzOGI4MCIgZm9udC1zaXplPSIxMiI+TW9jayBwaG90bzwvdGV4dD48L3N2Zz4=' } : {}) } }), 500)
return { started: true }
}
async stopRead() { this.emit('status', { status: 'online', updatedAt: Date.now() }); return { stopped: true } }
onData(fn: (v: IdCardReadEvent) => void) { return this.listen('data', fn) }
onStatus(fn: (v: unknown) => void) { return this.listen('status', fn) }
onError(fn: (v: unknown) => void) { return this.listen('error', fn) }
dispose() { Object.values(this.listeners).forEach(v => v.clear()); return true }
private listen(kind: EventKind, fn: (v: any) => void) { this.listeners[kind].add(fn); return () => this.listeners[kind].delete(fn) }
private emit(kind: EventKind, value: unknown) { this.listeners[kind].forEach(fn => fn(value)) }
}
const title = ref('ID Card Reader')
const lwaName = ref(''); const lwaVersion = ref(''); const coreVersion = ref(''); const host = ref('localhost')
const runtimeConnected = ref(false); const initialized = ref(false); const connected = ref(false); const reading = ref(false)
const readerState = ref<'idle' | 'reading' | 'cooldown'>('idle')
const busy = ref(''); const errorText = ref(''); const deviceInfo = ref<DeviceInfo | null>(null)
const connectResult = ref<unknown>(null)
const includeImage = ref(false)
const card = ref<IdCardReadEvent['values'] | null>(null); const events = ref<EventItem[]>([])
const statusEvents = computed(() => events.value.filter(e => e.kind === 'status'))
const errorEvents = computed(() => events.value.filter(e => e.kind === 'error'))
let device: ReaderClient | null = null; let unsubscribers: Array<() => void> = []
let lastStatusAt = 0
function format(value: unknown) { return value == null ? 'No data' : typeof value === 'string' ? value : JSON.stringify(value, null, 2) }
function addEvent(kind: EventKind, value: unknown) { events.value.unshift({ id: Date.now() + Math.random(), time: new Date().toLocaleTimeString(), kind, value }); events.value = events.value.slice(0, 30) }
async function run(name: string, action: () => Promise<void>) { busy.value = name; errorText.value = ''; try { await action() } catch (e) { errorText.value = e instanceof Error ? e.message : String(e); addEvent('error', errorText.value) } finally { busy.value = '' } }
async function connect() { await run('connect', async () => { connectResult.value = await device!.connect(); connected.value = Boolean(connectResult.value && typeof connectResult.value === 'object' && 'connected' in connectResult.value && connectResult.value.connected) }) }
async function disconnect() { await run('disconnect', async () => { await device!.disconnect(); connected.value = false; reading.value = false; readerState.value = 'idle' }) }
async function startRead() { await run('startRead', async () => { reading.value = Boolean((await device!.startRead({ image: includeImage.value })).started); if (reading.value) readerState.value = 'reading' }) }
async function stopRead() { await run('stopRead', async () => { await device!.stopRead(); reading.value = false; readerState.value = 'idle' }) }
function clearCard() { card.value = null }
function handleStatus(value: unknown) {
addEvent('status', value)
const payload = value && typeof value === 'object' ? value as { status?: unknown; updatedAt?: unknown } : {}
const updatedAt = typeof payload.updatedAt === 'number' ? payload.updatedAt : Date.now()
if (updatedAt < lastStatusAt) return
lastStatusAt = updatedAt
const status = typeof payload.status === 'string' ? payload.status : ''
if (status === 'online') { connected.value = true; reading.value = false; readerState.value = 'idle' }
if (status === 'offline') { connected.value = false; reading.value = false; readerState.value = 'idle' }
if (status === 'reading' || status === 'cooldown') { reading.value = true; readerState.value = status }
}
onMounted(async () => {
try {
const config = await loadConfig(); title.value = String(config.params.title || title.value); lwaName.value = config.name; lwaVersion.value = config.version
host.value = import.meta.env.VITE_CUTOS_BROKER_URL || String(config.params.host || 'localhost')
await CoreAPI.init(host.value); runtimeConnected.value = CoreAPI.connected(); coreVersion.value = CoreAPI.getVersion()
device = host.value === 'mock' ? new MockReader() : new DeviceCapabilityIdCardReader(); await device.init()
unsubscribers = [
device.onData(data => { card.value = data.values; addEvent('data', data) }),
device.onStatus(handleStatus),
device.onError(value => addEvent('error', value))
]
initialized.value = true; await connect(); if (connected.value) { deviceInfo.value = await device.readDeviceInfo(); await startRead() }
} catch (e) { errorText.value = e instanceof Error ? e.message : String(e) }
})
onUnmounted(() => { unsubscribers.forEach(fn => fn()); device?.dispose() })
</script>
<template>
<div class="app-shell">
<CutosTopbar :title="title" subtitle="Device Capability · CUTOS 4.0" :core-version="coreVersion" :lwa-name="lwaName" :lwa-version="lwaVersion" :connected="runtimeConnected" />
<main>
<section v-if="errorText" class="error-banner"><strong>Operation failed</strong><span>{{ errorText }}</span></section>
<section class="section-heading"><div><span class="eyebrow">{{ host }}</span><h2>ID Card Reader</h2></div><div class="dependency-tags"><span class="provider-tag">Capability: <strong>@cutos/device-id-card-reader 4.0.0</strong></span><span class="provider-tag">Provider: <strong>device-id-card-reader-provider-ghc825 0.2.1</strong></span></div></section>
<section class="result-grid">
<article class="result-card"><div class="result-header method-header"><div><h3>Connection</h3><code>connect() / disconnect()</code></div><span class="state-tag" :class="{ online: connected }">{{ connected ? 'online' : 'offline' }}</span></div><div class="card-body"><div class="button-row"><button class="button primary" :disabled="!initialized || !!busy || connected" @click="connect">Connect</button><button class="button secondary" :disabled="!connected || !!busy" @click="disconnect">Disconnect</button></div><pre>{{ format(connectResult) }}</pre></div></article>
<article class="result-card"><div class="result-header"><h3>Device information</h3><code>readDeviceInfo()</code></div><pre>{{ format(deviceInfo) }}</pre></article>
<article class="result-card"><div class="result-header method-header"><div><h3>Reader</h3><code>startRead({ image: {{ includeImage }} }) / stopRead()</code></div><span class="state-tag" :class="{ online: reading }">{{ readerState }}</span></div><div class="card-body"><div class="button-row"><button class="button primary" :disabled="!connected || !!busy || reading" @click="startRead">Start read</button><button class="button secondary" :disabled="!reading || !!busy" @click="stopRead">Stop</button><label class="image-switch"><input v-model="includeImage" type="checkbox" :disabled="reading || !!busy"><span>Read portrait</span></label></div><p class="hint">Keep the ID card steadily on the reader for at least 2 seconds. Portrait reading is optional.</p></div></article>
<article class="result-card identity-card"><div class="result-header method-header"><div><h3>ID card result</h3><code>onData()</code></div><button v-if="card" class="text-button" @click="clearCard">Clear</button></div><div v-if="card" class="identity"><div class="portrait"><img v-if="card.base64BMPData" :src="card.base64BMPData" alt="ID portrait"><span v-else>No image</span></div><dl><template v-for="(value, key) in card" :key="key"><template v-if="key !== 'base64BMPData'"><dt>{{ key }}</dt><dd>{{ value }}</dd></template></template></dl></div><p v-else class="empty-state">Place an ID card on the reader</p></article>
<article class="result-card event-card"><div class="result-header"><h3>Status</h3><code>onStatus()</code></div><div class="event-list"><div v-for="event in statusEvents" :key="event.id" class="event-row"><time>{{ event.time }}</time><pre>{{ format(event.value) }}</pre></div><p v-if="!statusEvents.length" class="empty-state">Waiting for status</p></div></article>
<article class="result-card event-card"><div class="result-header"><h3>Errors</h3><code>onError()</code></div><div class="event-list"><div v-for="event in errorEvents" :key="event.id" class="event-row error-event"><time>{{ event.time }}</time><pre>{{ format(event.value) }}</pre></div><p v-if="!errorEvents.length" class="empty-state">No errors</p></div></article>
</section>
</main>
</div>
</template>