Add CUTOS capability and core API examples
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VITE_CUTOS_BROKER_URL=mock
|
||||
@@ -0,0 +1 @@
|
||||
VITE_CUTOS_BROKER_URL=localhost
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
release
|
||||
*.local
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 200,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"trailingComma": "none",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# demo-id-card-reader
|
||||
|
||||
CUTOS 4.0 end-to-end ID Card Reader LWA using `@cutos/device-id-card-reader` and `device-id-card-reader-provider-ghc825`.
|
||||
|
||||
The LWA connects automatically and starts continuous reading. A successful card is displayed immediately, the reader enters a five-second `cooldown`, and then returns automatically to `reading`. Use Stop to enter `idle`. Portrait reading is optional; keep the card steadily on the reader for at least two seconds.
|
||||
|
||||
Set `params.host` to `mock` in `public/config.json` for browser-only UI development.
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev
|
||||
npx cutos lwa build
|
||||
npx cutos lwa upload
|
||||
npx cutos lwa publish demo-id-card-reader --device <device-id>
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CUTOS LWA</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "demo-id-card-reader",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "cutos lwa build",
|
||||
"build:app": "vue-tsc --noEmit && vite build",
|
||||
"package": "cutos lwa package",
|
||||
"validate": "cutos lwa validate",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cutos/core": "^4.0.8",
|
||||
"@cutos/device-id-card-reader": "^4.0.0",
|
||||
"vue": "^3.4.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.40",
|
||||
"postcss-import": "^16.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.18.0",
|
||||
"vite": "^5.3.4",
|
||||
"vue-tsc": "^2.0.26"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "demo-id-card-reader",
|
||||
"version": "0.3.0",
|
||||
"description": "CUTOS 4.0 ID Card Reader end-to-end demo",
|
||||
"params": {
|
||||
"title": "CUTOS ID Card Reader",
|
||||
"host": "localhost"
|
||||
},
|
||||
"drvDependencies": {
|
||||
"device-id-card-reader-provider-ghc825": "0.2.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,104 @@
|
||||
<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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import cutosLogo from '@/assets/cutos.png'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
coreVersion?: string
|
||||
lwaName?: string
|
||||
lwaVersion?: string
|
||||
connected: boolean
|
||||
statusLabel?: string
|
||||
}>(), {
|
||||
subtitle: 'CUTOS 4.0 LWA',
|
||||
coreVersion: '',
|
||||
lwaName: '',
|
||||
lwaVersion: '',
|
||||
statusLabel: ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<img :src="cutosLogo" alt="CUTOS" />
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-cluster">
|
||||
<span class="version-tag">Core {{ coreVersion || '—' }}</span>
|
||||
<span class="version-tag">LWA {{ lwaName }} {{ lwaVersion || '—' }}</span>
|
||||
<span class="connection" :class="{ online: connected }">
|
||||
<i></i>{{ statusLabel || (connected ? 'Connected' : 'Disconnected') }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.topbar {
|
||||
min-height: 82px;
|
||||
padding: 14px clamp(18px, 4vw, 52px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
color: #f8fafb;
|
||||
background: #20262c;
|
||||
border-bottom: 4px solid #16a394;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 15px; min-width: 0; }
|
||||
.brand img { width: 42px; height: 42px; object-fit: contain; }
|
||||
.brand h1 { margin: 0; font-size: 20px; line-height: 1.2; font-weight: 700; }
|
||||
.brand p { margin: 4px 0 0; font-size: 12px; color: #aeb8bf; }
|
||||
.status-cluster { display: flex; align-items: center; justify-content: flex-end; gap: 9px; flex-wrap: wrap; }
|
||||
.version-tag, .connection {
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
border: 1px solid #4b555d;
|
||||
border-radius: 4px;
|
||||
background: #2b3339;
|
||||
}
|
||||
.connection i { width: 8px; height: 8px; margin-right: 7px; border-radius: 50%; background: #e06b65; }
|
||||
.connection.online i { background: #42c6a8; box-shadow: 0 0 0 3px rgba(66, 198, 168, 0.14); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.topbar { align-items: flex-start; flex-direction: column; gap: 12px; }
|
||||
.status-cluster { justify-content: flex-start; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
cutos: string
|
||||
lwa: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-[8px]">
|
||||
<div class="text-[14px] font-semibold uppercase tracking-[0.12em] text-slate-500">{{ title }}</div>
|
||||
<div class="my-[20px] text-[33px] font-semibold">Hello World!</div>
|
||||
<div>
|
||||
<span>We are using CUTOS {{ cutos }}. </span>
|
||||
<span>LWA version {{ lwa }}.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,22 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
:root { font-family: Inter,"Segoe UI",Arial,sans-serif; color:#20262c; background:#eef1f3; font-synthesis:none; }
|
||||
* { box-sizing:border-box; } body { margin:0; min-width:320px; min-height:100vh; } button { font:inherit; cursor:pointer; } button:disabled { cursor:not-allowed; opacity:.48; }
|
||||
.app-shell { min-height:100vh; background:#eef1f3; }
|
||||
.dependency-tags,.button-row { display:flex; align-items:center; gap:9px; flex-wrap:wrap; }
|
||||
main { width:100%; padding:28px clamp(18px,2vw,36px) 32px; } .error-banner { display:flex; gap:12px; margin-bottom:20px; padding:12px 16px; color:#7e2420; background:#fff0ef; border-left:4px solid #d54d47; }
|
||||
.section-heading { display:flex; align-items:flex-end; justify-content:space-between; gap:16px; margin-bottom:15px; } .section-heading h2 { margin:3px 0 0; font-size:18px; } .eyebrow { color:#64717a; font:11px "Cascadia Code",Consolas,monospace; }
|
||||
.dependency-tags { justify-content:flex-end; } .provider-tag { padding:6px 9px; color:#5d6870; background:#f8fafb; border:1px solid #cbd3d8; border-radius:4px; font:11px "Cascadia Code",Consolas,monospace; } .provider-tag strong { color:#253038; }
|
||||
.result-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; } .result-card { min-width:0; min-height:220px; background:#fff; border:1px solid #d8dee2; border-radius:6px; overflow:hidden; }
|
||||
.result-header { min-height:66px; padding:13px 15px; border-bottom:1px solid #e4e8eb; background:#f8fafb; } .result-header h3 { margin:0 0 6px; font-size:14px; } .result-header code { display:block; color:#68757d; font-size:11px; overflow-wrap:anywhere; }
|
||||
.method-header { display:flex; align-items:center; justify-content:space-between; gap:14px; } .state-tag { padding:5px 8px; color:#8b3c37; background:#fff0ef; border-radius:3px; font:10px "Cascadia Code",Consolas,monospace; } .state-tag.online { color:#087b71; background:#e8f7f4; }
|
||||
.card-body { padding:15px; } .button { min-height:34px; padding:7px 13px; border:1px solid transparent; border-radius:4px; font-weight:650; font-size:12px; } .button.primary { color:#fff; background:#138b80; border-color:#138b80; } .button.secondary { color:#2d373d; background:#fff; border-color:#bfc8cd; } .hint,.empty-state { margin:15px 0 0; color:#77838b; font-size:12px; }
|
||||
.text-button { padding:5px 0; border:0; color:#138b80; background:transparent; font-size:12px; }
|
||||
.image-switch { min-height:34px; display:inline-flex; align-items:center; gap:7px; margin-left:auto; color:#46535b; font-size:12px; cursor:pointer; } .image-switch input { width:15px; height:15px; margin:0; accent-color:#138b80; } .image-switch:has(input:disabled) { cursor:not-allowed; opacity:.48; }
|
||||
pre { margin:0; padding:15px; font:12px/1.55 "Cascadia Code",Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; overflow:auto; } .result-card>pre { max-height:260px; }
|
||||
.identity-card { min-height:330px; } .identity { display:grid; grid-template-columns:130px minmax(0,1fr); gap:20px; padding:16px; } .portrait { width:130px; height:166px; display:grid; place-items:center; color:#87939a; background:#f2f5f6; border:1px solid #dce2e5; } .portrait img { width:100%; height:100%; object-fit:cover; }
|
||||
dl { display:grid; grid-template-columns:90px minmax(0,1fr); gap:0; margin:0; font-size:12px; } dt,dd { margin:0; padding:7px 8px; border-bottom:1px solid #edf0f2; } dt { color:#68757d; } dd { font-family:"Cascadia Code",Consolas,monospace; overflow-wrap:anywhere; }
|
||||
.event-card { min-height:330px; display:grid; grid-template-rows:auto minmax(0,1fr); } .event-list { min-height:0; max-height:300px; overflow:auto; } .event-row { display:grid; grid-template-columns:78px minmax(0,1fr); gap:10px; padding:8px 13px; border-bottom:1px solid #edf0f2; } .event-row time { padding-top:4px; color:#77838b; font-size:11px; } .event-row pre { padding:4px 0; } .error-event pre { color:#b43b35; } .event-list .empty-state,.identity-card>.empty-state { padding:10px 15px; }
|
||||
@media(max-width:980px){.result-grid{grid-template-columns:repeat(2,minmax(0,1fr));}}
|
||||
@media(max-width:640px){.section-heading{align-items:flex-start;flex-direction:column}.result-grid{grid-template-columns:1fr}.identity-card{grid-column:auto}.identity{grid-template-columns:1fr}.portrait{width:110px;height:140px}}
|
||||
@@ -0,0 +1,66 @@
|
||||
export interface LwaConfig {
|
||||
name: string
|
||||
version: string
|
||||
description?: string
|
||||
params: {
|
||||
title: string
|
||||
host: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
drvDependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
const defaultConfig: LwaConfig = {
|
||||
name: 'cutos-lwa',
|
||||
version: '0.0.0',
|
||||
params: {
|
||||
title: 'CUTOS LWA',
|
||||
host: 'localhost'
|
||||
}
|
||||
}
|
||||
|
||||
export let config: LwaConfig = { ...defaultConfig, params: { ...defaultConfig.params } }
|
||||
|
||||
function parse(str?: string) {
|
||||
if (!str || !str.length) return {}
|
||||
|
||||
return str.split('&').reduce<Record<string, string>>((acc, row) => {
|
||||
const [key, value] = row.split('=').map(decodeURIComponent)
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function mergeQueryParams(config: LwaConfig) {
|
||||
if (typeof window === 'undefined') {
|
||||
return config
|
||||
}
|
||||
|
||||
const query = location.href.split('?')[1]
|
||||
const { params } = parse(query) as { params?: string }
|
||||
|
||||
try {
|
||||
if (params) config.params = { ...config.params, ...JSON.parse(params) }
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export async function loadConfig(): Promise<LwaConfig> {
|
||||
try {
|
||||
const configUrl =
|
||||
typeof window === 'undefined' ? 'config.json' : new URL('config.json', window.location.href).toString()
|
||||
const response = await fetch(configUrl, { cache: 'no-cache' })
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load config.json: ${response.status}`)
|
||||
}
|
||||
config = mergeQueryParams((await response.json()) as LwaConfig)
|
||||
return config
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
config = mergeQueryParams({ ...defaultConfig, params: { ...defaultConfig.params } })
|
||||
return config
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_CUTOS_BROKER_URL?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
publicDir: 'public',
|
||||
build: {
|
||||
target: ['chrome74']
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
// '/proxy': 'http://192.168.1.30'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
dedupe: ['@cutos/core'],
|
||||
alias: {
|
||||
'@': '/src'
|
||||
}
|
||||
},
|
||||
define: {},
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
include: [
|
||||
/\.[tj]sx?$/,
|
||||
/\.vue$/,
|
||||
/\.vue\?vue/,
|
||||
/\.md$/
|
||||
],
|
||||
imports: ['vue'],
|
||||
vueTemplate: true,
|
||||
cache: true,
|
||||
dts: false
|
||||
})
|
||||
],
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
define: {
|
||||
global: 'globalThis'
|
||||
},
|
||||
target: 'es2015',
|
||||
supported: {
|
||||
bigint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user