Add CUTOS capability and core API examples

This commit is contained in:
yankun
2026-07-26 14:40:42 +08:00
commit 7cc505a5e2
61 changed files with 5136 additions and 0 deletions
@@ -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
}
}
+9
View File
@@ -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
}
}
}
})
@@ -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,14 @@
# CUTOS Receipt Printer Demo
End-to-end LWA for `@cutos/device-receipt-printer` and the MS-MA90 Provider.
The Demo covers USB/serial connection, device information, styled text, QR code, paper feed, cutting, status, and Device events. Text and QR printing explicitly call `feed({ dots: 240 })` after printing so the result advances to the paper outlet. The standalone Feed action also defaults to 240 dots. Development uses CUTOS Mock through `.env.development`; deployed builds use CUTOS Runtime and the declared Provider dependency.
The card grid is responsive: portrait displays use two columns, landscape displays use four columns, and narrow displays use one column.
```sh
npm install
npx cutos lwa build
npx cutos lwa upload
npx cutos lwa publish --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>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
{
"name": "demo-receipt-printer",
"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-receipt-printer": "^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-receipt-printer",
"version": "0.1.3",
"description": "CUTOS 4.0 Receipt Printer end-to-end demo",
"params": {
"title": "CUTOS Receipt Printer",
"host": "localhost"
},
"drvDependencies": {
"device-receipt-printer-provider-ms-ma90": "0.1.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,140 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { CoreAPI } from '@cutos/core'
import {
DeviceCapabilityReceiptPrinter,
type DeviceInfo,
type GetStatusResult
} from '@cutos/device-receipt-printer'
import { loadConfig } from '@/utils/config'
import cutosLogo from '@/assets/cutos.png'
type EventKind = 'data' | 'status' | 'error'
type Alignment = 'left' | 'center' | 'right'
interface EventItem { id: number; time: string; kind: EventKind; value: unknown }
interface ReceiptPrinterClient {
init(): Promise<unknown>
connect(params?: { connection?: 'usb' | 'serial'; port?: string; baudRate?: 9600 | 38400 | 115200 }): Promise<{ connected: boolean }>
disconnect(): Promise<{ connected: boolean }>
readDeviceInfo(): Promise<DeviceInfo>
printText(params: { text: string; alignment?: Alignment; width?: number; height?: number; bold?: boolean }): Promise<{ success: boolean }>
printQrCode(params: { content: string; size?: number; leftMargin?: number }): Promise<{ success: boolean }>
feed(params?: { dots?: number }): Promise<{ success: boolean }>
cut(params?: { mode?: 'full' | 'partial' }): Promise<{ success: boolean }>
getStatus(): Promise<GetStatusResult>
onData(fn: (value: unknown) => void): () => void
onStatus(fn: (value: unknown) => void): () => void
onError(fn: (value: unknown) => void): () => void
dispose(): boolean
}
class MockReceiptPrinter implements ReceiptPrinterClient {
private listeners = { data: new Set<(v: any) => void>(), status: new Set<(v: any) => void>(), error: new Set<(v: any) => void>() }
async init() { return 'Mock receipt printer initialized' }
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: 'MASUNG', model: 'MS-MA90 Mock', version: '0.1.0', deviceType: 'receipt-printer', sdkVersion: '2.2.2.10' } }
async printText() { return this.complete('printText') }
async printQrCode() { return this.complete('printQrCode') }
async feed() { return this.complete('feed') }
async cut() { return this.complete('cut') }
async getStatus() { return { code: 0, state: 'ready' as const, ready: true, message: 'Printer is ready.' } }
onData(fn: (v: unknown) => 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(value => value.clear()); return true }
private async complete(operation: string) { this.emit('data', { type: 'operation-complete', operation, success: true, completedAt: Date.now() }); return { success: 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('CUTOS Receipt Printer')
const host = ref('localhost')
const coreVersion = ref('')
const lwaName = ref('')
const lwaVersion = ref('')
const runtimeConnected = ref(false)
const initialized = ref(false)
const connected = ref(false)
const busy = ref('')
const errorText = ref('')
const deviceInfo = ref<DeviceInfo | null>(null)
const result = ref<unknown>(null)
const printerStatus = ref<GetStatusResult | null>(null)
const connection = ref<'usb' | 'serial'>('usb')
const port = ref('')
const baudRate = ref<9600 | 38400 | 115200>(115200)
const text = ref('CUTOS 4.0\nMS-MA90 receipt printer')
const alignment = ref<Alignment>('left')
const bold = ref(false)
const width = ref(1)
const height = ref(1)
const qrContent = ref('https://www.cut-os.com')
const qrSize = ref(6)
const qrLeftMargin = ref(0)
const feedDots = ref(240)
const cutMode = ref<'full' | 'partial'>('partial')
const events = ref<EventItem[]>([])
const dataEvents = computed(() => events.value.filter(event => event.kind === 'data'))
const statusEvents = computed(() => events.value.filter(event => event.kind === 'status'))
const errorEvents = computed(() => events.value.filter(event => event.kind === 'error'))
let device: ReceiptPrinterClient | null = null
let unsubscribers: Array<() => void> = []
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, 40) }
async function run(name: string, action: () => Promise<void>) { busy.value = name; errorText.value = ''; try { await action() } catch (error) { errorText.value = error instanceof Error ? error.message : String(error); addEvent('error', errorText.value) } finally { busy.value = '' } }
async function connect() { await run('connect', async () => { const params = connection.value === 'serial' ? { connection: connection.value, port: port.value, baudRate: baudRate.value } : { connection: connection.value, ...(port.value ? { port: port.value } : {}) }; connected.value = (await device!.connect(params)).connected; if (connected.value) { deviceInfo.value = await device!.readDeviceInfo(); printerStatus.value = await device!.getStatus() } }) }
async function disconnect() { await run('disconnect', async () => { await device!.disconnect(); connected.value = false }) }
async function printText() { await run('printText', async () => { const printed = await device!.printText({ text: text.value, alignment: alignment.value, bold: bold.value, width: width.value, height: height.value }); const fed = await device!.feed({ dots: 240 }); result.value = { printed, fed, feedDots: 240 } }) }
async function printQrCode() { await run('printQrCode', async () => { const printed = await device!.printQrCode({ content: qrContent.value, size: qrSize.value, leftMargin: qrLeftMargin.value }); const fed = await device!.feed({ dots: 240 }); result.value = { printed, fed, feedDots: 240 } }) }
async function feed() { await run('feed', async () => { result.value = await device!.feed({ dots: feedDots.value }) }) }
async function cut() { await run('cut', async () => { result.value = await device!.cut({ mode: cutMode.value }) }) }
async function refreshStatus() { await run('getStatus', async () => { printerStatus.value = await device!.getStatus() }) }
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 MockReceiptPrinter() : new DeviceCapabilityReceiptPrinter()
await device.init()
unsubscribers = [
device.onData(value => addEvent('data', value)),
device.onStatus(value => { addEvent('status', value); const state = value && typeof value === 'object' && 'status' in value ? value.status : ''; if (state === 'online') connected.value = true; if (state === 'offline') connected.value = false }),
device.onError(value => addEvent('error', value))
]
initialized.value = true
} catch (error) { errorText.value = error instanceof Error ? error.message : String(error) }
})
onUnmounted(() => { unsubscribers.forEach(unsubscribe => unsubscribe()); device?.dispose() })
</script>
<template>
<div class="app-shell">
<header class="topbar">
<div class="brand"><img :src="cutosLogo" alt="CUTOS"><div><h1>{{ title }}</h1><p>Device Capability · CUTOS 4.0</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: runtimeConnected }"><i></i>{{ runtimeConnected ? 'Connected' : 'Disconnected' }}</span></div>
</header>
<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>Receipt Printer · MS-MA90</h2></div><div class="dependency-tags"><span class="provider-tag">Capability: <strong>@cutos/device-receipt-printer 4.0.0</strong></span><span class="provider-tag">Provider: <strong>device-receipt-printer-provider-ms-ma90 0.1.0</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 form"><label>Connection<select v-model="connection"><option value="usb">USB</option><option value="serial">Serial</option></select></label><label>{{ connection === 'usb' ? 'USB port (optional)' : 'COM port' }}<input v-model="port" :placeholder="connection === 'usb' ? 'Auto detect' : 'COM1'"></label><label v-if="connection === 'serial'">Baud rate<select v-model="baudRate"><option :value="9600">9600</option><option :value="38400">38400</option><option :value="115200">115200</option></select></label><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></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"><h3>Text</h3><code>printText()</code></div><div class="card-body form"><label>Text<textarea v-model="text" rows="3"></textarea></label><div class="inline-fields"><label>Align<select v-model="alignment"><option>left</option><option>center</option><option>right</option></select></label><label>Width<input v-model.number="width" type="number" min="1" max="8"></label><label>Height<input v-model.number="height" type="number" min="1" max="8"></label></div><label class="check"><input v-model="bold" type="checkbox">Bold</label><button class="button primary" :disabled="!connected || !!busy || !text" @click="printText">Print text</button></div></article>
<article class="result-card"><div class="result-header"><h3>QR code</h3><code>printQrCode()</code></div><div class="card-body form"><label>Content<input v-model="qrContent"></label><div class="inline-fields"><label>Size<input v-model.number="qrSize" type="number" min="1" max="8"></label><label>Left margin (mm)<input v-model.number="qrLeftMargin" type="number" min="0" max="27"></label></div><button class="button primary" :disabled="!connected || !!busy || !qrContent" @click="printQrCode">Print QR code</button></div></article>
<article class="result-card"><div class="result-header"><h3>Paper</h3><code>feed() / cut()</code></div><div class="card-body form"><label>Feed dots<input v-model.number="feedDots" type="number" min="0" max="250"></label><button class="button secondary" :disabled="!connected || !!busy" @click="feed">Feed paper</button><label>Cut mode<select v-model="cutMode"><option value="partial">Partial</option><option value="full">Full</option></select></label><button class="button primary" :disabled="!connected || !!busy" @click="cut">Cut paper</button></div></article>
<article class="result-card"><div class="result-header"><h3>Printer status</h3><code>getStatus()</code></div><div class="card-body"><button class="button secondary" :disabled="!connected || !!busy" @click="refreshStatus">Refresh</button><pre>{{ format(printerStatus) }}</pre></div></article>
<article class="result-card event-card"><div class="result-header"><h3>Operations</h3><code>onData()</code></div><div class="event-list"><div v-for="event in dataEvents" :key="event.id" class="event-row"><time>{{ event.time }}</time><pre>{{ format(event.value) }}</pre></div><p v-if="!dataEvents.length" class="empty-state">Waiting for operations</p></div></article>
<article class="result-card event-card"><div class="result-header"><h3>Status / Errors</h3><code>onStatus() / onError()</code></div><div class="event-list"><div v-for="event in [...statusEvents, ...errorEvents]" :key="event.id" class="event-row" :class="{ 'error-event': event.kind === 'error' }"><time>{{ event.time }}</time><pre>{{ format(event.value) }}</pre></div><p v-if="!statusEvents.length && !errorEvents.length" class="empty-state">Waiting for status</p></div></article>
</section>
<section class="last-result"><strong>Last result</strong><pre>{{ format(result) }}</pre></section>
</main>
</div>
</template>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
@@ -0,0 +1,10 @@
@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,input{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.48}.app-shell{min-height:100vh;background:#eef1f3}
.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}.brand img{width:42px;height:42px;object-fit:contain}.brand h1{margin:0;font-size:20px}.brand p{margin:4px 0 0;font-size:12px;color:#aeb8bf}.status-cluster,.dependency-tags,.button-row{display:flex;align-items:center;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}
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:225px;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}
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{height:210px}.card-body>pre{height:110px;margin-top:12px;padding:10px;background:#f7f9fa;border:1px solid #e0e5e8}.form{display:grid;gap:12px}.form label{display:grid;gap:6px;color:#5d6870;font-size:12px}.form input,.form select,.form textarea{width:100%;min-height:36px;padding:7px 9px;border:1px solid #bfc8cd;border-radius:4px;background:#fff}.form textarea{resize:vertical}.form .check{display:flex;align-items:center;grid-template-columns:auto 1fr}.form .check input{width:auto;min-height:auto}.inline-fields{display:grid;grid-template-columns:2fr 1fr 1fr;gap:8px}.last-result{display:grid;grid-template-columns:auto minmax(0,1fr);gap:15px;align-items:start;margin-top:12px;padding:12px 15px;background:#fff;border:1px solid #d8dee2;border-radius:6px}.last-result strong{padding-top:4px;font-size:12px}.last-result pre{padding:4px 0}
.event-card{height:360px;display:grid;grid-template-rows:auto minmax(0,1fr)}.event-list{min-height:0;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}.empty-state{margin:0;padding:24px 15px;color:#77838b;font-size:12px}
@media(orientation:landscape) and (min-width:1000px){.result-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media(max-width:640px){.topbar,.section-heading{align-items:flex-start;flex-direction:column}.result-grid{grid-template-columns:1fr}.inline-fields{grid-template-columns:1fr}.last-result{grid-template-columns:1fr}}
.file-name{color:#77838b;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -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
}
}
+9
View File
@@ -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
}
}
}
})
+1
View File
@@ -0,0 +1 @@
VITE_CUTOS_BROKER_URL=mock
+1
View File
@@ -0,0 +1 @@
VITE_CUTOS_BROKER_URL=localhost
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
dist-ssr
release
*.local
*.log
.DS_Store
+10
View File
@@ -0,0 +1,10 @@
{
"printWidth": 200,
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"trailingComma": "none",
"plugins": ["prettier-plugin-tailwindcss"]
}
+34
View File
@@ -0,0 +1,34 @@
# CUTOS 4.0 Core API Demo
The basic CUTOS 4.0 LWA demo. It depends only on `@cutos/core` and does not require an additional Capability SDK or Provider.
The demo covers the same foundation as the CUTOS 3.x `demo-core-api`, updated for the Promise-first 4.0 API:
- Runtime version and connection state
- Platform, runtime config, box, and device information
- Network notifications
- Logger levels
- Volume read/write
- Automatic HTTP proxy configuration and a ping request for verifying cross-origin access
## Install
```bash
npm install
```
## Development
```bash
npm run dev
```
Development uses CUTOS Mock by default. To connect to a CUTOS Node, set `VITE_CUTOS_BROKER_URL` to its host.
## Build And Package
```bash
npm run build
```
The packaged `.lwa` file is generated in `release/`.
+12
View File
@@ -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 Core API Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
{
"name": "demo-core-api",
"private": true,
"version": "4.0.1",
"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.6",
"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"
}
}
+7
View File
@@ -0,0 +1,7 @@
export default {
plugins: {
'postcss-import': {},
tailwindcss: {},
autoprefixer: {}
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "demo-core-api",
"version": "4.0.2",
"description": "CUTOS 4.0 Core API demo",
"params": {
"title": "CUTOS Core API",
"host": "localhost"
},
"drvDependencies": {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+340
View File
@@ -0,0 +1,340 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { CoreAPI } from '@cutos/core'
import CutosTopbar from '@/components/CutosTopbar.vue'
import { loadConfig } from '@/utils/config'
type ApiKey = 'platform' | 'runtimeConfig' | 'box' | 'device'
type LogLevel = 'info' | 'warning' | 'error' | 'debug'
interface RuntimeResult {
key: ApiKey
method: string
label: string
value: unknown
error: string
loading: boolean
}
interface NotificationMessage {
event?: string
msg?: unknown
}
interface ActivityItem {
id: number
time: string
kind: string
message: string
}
const title = ref('CUTOS Core API')
const lwaName = ref('')
const lwaVersion = ref('')
const coreVersion = ref('')
const host = ref('localhost')
const connected = ref(false)
const initializing = ref(true)
const initError = ref('')
const volume = ref(50)
const volumeResult = ref('')
const proxyResult = ref<unknown>(null)
const proxyLoading = ref(false)
const proxyCompleted = ref(false)
const pingResult = ref<unknown>(null)
const pingLoading = ref(false)
const activities = ref<ActivityItem[]>([])
const results = reactive<RuntimeResult[]>([
{ key: 'platform', method: 'CoreAPI.getPlatform()', label: 'Platform', value: null, error: '', loading: true },
{ key: 'runtimeConfig', method: 'CoreAPI.getConfig()', label: 'Runtime config', value: null, error: '', loading: true },
{ key: 'box', method: 'CoreAPI.getBoxInfo()', label: 'Box', value: null, error: '', loading: true },
{ key: 'device', method: 'CoreAPI.getDeviceInfo()', label: 'Device', value: null, error: '', loading: true }
])
const statusLabel = computed(() => {
if (initializing.value) return 'Connecting'
if (connected.value) return 'Connected'
return 'Disconnected'
})
function format(value: unknown) {
if (value === null || value === undefined) return 'No data'
if (typeof value === 'string') return value
return JSON.stringify(value, null, 2)
}
function errorMessage(error: unknown) {
return error instanceof Error ? error.message : String(error)
}
function addActivity(kind: string, message: string) {
activities.value.unshift({
id: Date.now() + Math.random(),
time: new Date().toLocaleTimeString(),
kind,
message
})
activities.value = activities.value.slice(0, 8)
}
async function loadResult(key: ApiKey, operation: () => Promise<unknown>) {
const item = results.find((result) => result.key === key)
if (!item) return
item.loading = true
item.error = ''
try {
item.value = await operation()
} catch (error) {
item.error = errorMessage(error)
} finally {
item.loading = false
}
}
async function refreshRuntime() {
await Promise.all([
loadResult('platform', () => CoreAPI.getPlatform()),
loadResult('runtimeConfig', () => CoreAPI.getConfig()),
loadResult('box', () => CoreAPI.getBoxInfo()),
loadResult('device', () => CoreAPI.getDeviceInfo())
])
addActivity('runtime', 'Runtime information refreshed')
}
async function readVolume() {
try {
const current = await CoreAPI.getVolume<number>()
volume.value = Number(current)
} catch (error) {
volumeResult.value = errorMessage(error)
}
}
async function applyVolume() {
volumeResult.value = ''
try {
const result = await CoreAPI.setVolume(volume.value)
volumeResult.value = format(result)
addActivity('volume', `Volume set to ${volume.value}`)
} catch (error) {
volumeResult.value = errorMessage(error)
}
}
function writeLog(level: LogLevel) {
const logger = CoreAPI.getLogger()
const content = `Core API demo ${level} message at ${new Date().toISOString()}`
try {
logger[level]('demo-core-api', content)
addActivity(`log.${level}`, content)
} catch (error) {
addActivity(`log.${level}.error`, errorMessage(error))
}
}
async function configureProxy() {
proxyLoading.value = true
proxyCompleted.value = false
proxyResult.value = null
try {
proxyResult.value = await CoreAPI.setHttpProxy('api', 'https://www.cut-os.com')
addActivity('proxy', 'HTTP proxy configured')
} catch (error) {
proxyResult.value = { error: errorMessage(error) }
} finally {
proxyLoading.value = false
proxyCompleted.value = true
}
}
async function pingProxy() {
pingLoading.value = true
pingResult.value = null
try {
const response = await fetch('http://localhost:3000/proxy/api/rest/sv/system/runtime/ping')
const contentType = response.headers.get('content-type') || ''
const data = contentType.includes('application/json')
? await response.json()
: await response.text()
pingResult.value = {
ok: response.ok,
status: response.status,
statusText: response.statusText,
data
}
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`.trim())
}
addActivity('proxy.ping', 'HTTP proxy ping succeeded')
} catch (error) {
const message = errorMessage(error)
pingResult.value = { error: message, response: pingResult.value }
addActivity('proxy.ping.error', message)
} finally {
pingLoading.value = false
}
}
const notification = CoreAPI.getNotification()
onMounted(async () => {
const config = await loadConfig()
title.value = config.params.title
lwaName.value = config.name
lwaVersion.value = config.version
host.value = import.meta.env.VITE_CUTOS_BROKER_URL || config.params.host
try {
await CoreAPI.init(host.value)
connected.value = CoreAPI.connected()
coreVersion.value = CoreAPI.getVersion()
addActivity('core', `Core initialized at ${host.value}`)
notification.register((data) => {
const message = data as NotificationMessage
if (message.event === 'networkConnection') {
connected.value = Boolean(message.msg)
}
addActivity(`notification.${message.event || 'event'}`, format(message.msg))
})
await Promise.all([refreshRuntime(), readVolume(), configureProxy()])
} catch (error) {
connected.value = false
initError.value = errorMessage(error)
} finally {
initializing.value = false
}
})
onUnmounted(() => notification.unregister())
</script>
<template>
<div class="app-shell">
<CutosTopbar
:title="title"
subtitle="Core API · CUTOS 4.0"
:core-version="coreVersion"
:lwa-name="lwaName"
:lwa-version="lwaVersion"
:connected="connected"
:status-label="statusLabel"
/>
<main>
<section v-if="initError" class="error-banner">
<strong>Core initialization failed</strong>
<span>{{ initError }}</span>
</section>
<section class="runtime-section">
<div class="section-heading">
<div>
<span class="eyebrow">{{ host }}</span>
<h2>Runtime snapshot</h2>
</div>
<button type="button" class="button secondary" :disabled="!connected" @click="refreshRuntime">
Refresh
</button>
</div>
<div class="result-grid">
<article v-for="item in results" :key="item.key" class="result-card">
<div class="result-header">
<h3>{{ item.label }}</h3>
<code>{{ item.method }}</code>
</div>
<pre v-if="item.loading" class="pending">Loading</pre>
<pre v-else-if="item.error" class="result-error">{{ item.error }}</pre>
<pre v-else>{{ format(item.value) }}</pre>
</article>
<article class="result-card activity-card">
<div class="result-header activity-header">
<div>
<h3>Activity</h3>
<code>CoreAPI.getNotification()</code>
</div>
<button v-if="activities.length" type="button" class="text-button" @click="activities = []">Clear</button>
</div>
<div v-if="activities.length" class="activity-list">
<div v-for="item in activities" :key="item.id" class="activity-row">
<time>{{ item.time }}</time>
<code>{{ item.kind }}</code>
<span>{{ item.message }}</span>
</div>
</div>
<p v-else class="empty-state">No activity</p>
</article>
</div>
</section>
<section class="controls-section">
<div class="control-panel volume-panel">
<div class="panel-heading card-style-heading">
<div>
<h2>Audio</h2>
<span class="eyebrow">CoreAPI.getVolume() / setVolume()</span>
</div>
<strong>{{ volume }}</strong>
</div>
<input v-model.number="volume" type="range" min="0" max="100" step="1" :disabled="!connected" />
<div class="panel-actions">
<button type="button" class="button primary" :disabled="!connected" @click="applyVolume">Apply volume</button>
<span class="inline-result">{{ volumeResult }}</span>
</div>
</div>
<div class="control-panel logger-panel">
<div class="panel-heading card-style-heading">
<div>
<h2>Logger</h2>
<span class="eyebrow">CoreAPI.getLogger()</span>
</div>
</div>
<div class="button-row">
<button type="button" class="button secondary" :disabled="!connected" @click="writeLog('info')">Info</button>
<button type="button" class="button secondary" :disabled="!connected" @click="writeLog('warning')">Warning</button>
<button type="button" class="button danger" :disabled="!connected" @click="writeLog('error')">Error</button>
<button type="button" class="button secondary" :disabled="!connected" @click="writeLog('debug')">Debug</button>
</div>
</div>
<div class="control-panel proxy-panel">
<div class="panel-heading card-style-heading">
<div>
<h2>HTTP proxy</h2>
<span class="eyebrow">CoreAPI.setHttpProxy()</span>
</div>
</div>
<div class="proxy-columns">
<div class="proxy-column">
<div class="proxy-target"><code>api https://www.cut-os.com</code></div>
<pre v-if="proxyLoading" class="compact-result pending">Configuring</pre>
<pre v-else-if="proxyCompleted" class="compact-result">{{ format(proxyResult) }}</pre>
</div>
<div class="proxy-column ping-column">
<div class="ping-actions">
<button
type="button"
class="button secondary"
:disabled="!connected || proxyLoading || pingLoading"
@click="pingProxy"
>
{{ pingLoading ? 'Pinging…' : 'Ping' }}
</button>
<p class="ping-hint">Verify access through <code>/proxy/api/</code></p>
</div>
<pre v-if="pingResult" class="compact-result">{{ format(pingResult) }}</pre>
</div>
</div>
</div>
</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>
+5
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
+100
View File
@@ -0,0 +1,100 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, "Segoe UI", Arial, sans-serif;
color: #20262c;
background: #eef1f3;
font-synthesis: none;
text-rendering: optimizeLegibility;
letter-spacing: 0;
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; min-height: 100vh; }
button, input { font: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: 0.48; }
.app-shell { min-height: 100vh; background: #eef1f3; }
main { width: 100%; margin: 0 auto; padding: 28px clamp(18px, 2vw, 36px) 32px; }
section + section { margin-top: 34px; }
.error-banner { display: flex; gap: 12px; padding: 12px 16px; color: #7e2420; background: #fff0ef; border-left: 4px solid #d54d47; }
.section-heading, .panel-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
.section-heading { margin-bottom: 15px; }
.section-heading h2, .panel-heading h2 { margin: 3px 0 0; font-size: 18px; line-height: 1.2; }
.card-style-heading h2 { margin: 0 0 6px; }
.eyebrow { display: block; color: #64717a; font-family: "Cascadia Code", Consolas, monospace; font-size: 11px; }
.result-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.result-card { min-width: 0; min-height: 224px; 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; }
pre { margin: 0; padding: 15px; font: 12px/1.55 "Cascadia Code", Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
.result-card pre { max-height: 220px; overflow: auto; }
.pending { color: #75828a; }
.result-error { color: #b43b35; }
.activity-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.activity-card { grid-column: span 2; }
.activity-header h3 { margin-bottom: 6px; }
.controls-section { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid #cbd3d8; border-bottom: 1px solid #cbd3d8; }
.control-panel { min-width: 0; padding: 22px; background: #f7f9fa; }
.control-panel + .control-panel { border-left: 1px solid #cbd3d8; }
.panel-heading strong { font-size: 28px; color: #138b80; }
.volume-panel input[type="range"] { width: 100%; margin: 28px 0 20px; accent-color: #138b80; }
.panel-actions { min-height: 34px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.inline-result { max-width: 100%; color: #64717a; font: 11px/1.4 "Cascadia Code", Consolas, monospace; overflow-wrap: anywhere; }
.button-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 25px; }
.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; }
.button.danger { color: #a12d28; background: #fff; border-color: #dba7a4; }
.proxy-panel { grid-column: span 2; }
.proxy-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
.proxy-column { min-width: 0; }
.ping-column { min-width: 0; }
.ping-actions { min-height: 44px; display: flex; align-items: center; gap: 10px; }
.proxy-target { padding: 9px 10px; background: #fff; border: 1px solid #d8dee2; overflow-wrap: anywhere; }
.proxy-target code { font-size: 11px; }
.compact-result { width: 100%; margin-top: 10px; padding: 9px 10px; background: #fff; border: 1px solid #d8dee2; max-height: 82px; overflow: auto; }
.ping-column .compact-result { margin-top: 10px; }
.ping-hint { margin: 0; color: #64717a; font-size: 11px; line-height: 1.5; }
.ping-hint code { overflow-wrap: anywhere; }
.text-button { padding: 4px 0; border: 0; color: #138b80; background: transparent; font-size: 12px; }
.activity-list { max-height: 157px; overflow: auto; }
.activity-row { display: grid; grid-template-columns: 82px 240px minmax(0, 1fr); gap: 12px; padding: 10px 13px; font-size: 12px; border-bottom: 1px solid #edf0f2; }
.activity-row:last-child { border-bottom: 0; }
.activity-row time { color: #77838b; font-variant-numeric: tabular-nums; }
.activity-row code { color: #087b71; overflow-wrap: anywhere; }
.activity-row span { min-width: 0; overflow-wrap: anywhere; }
.empty-state { margin: 0; padding: 24px 15px; color: #77838b; font-size: 13px; }
@media (min-width: 1400px) and (min-height: 800px) {
main { min-height: calc(100vh - 82px); display: flex; flex-direction: column; }
.runtime-section { flex: 1; display: flex; flex-direction: column; }
.result-grid { flex: 1; grid-template-rows: repeat(2, minmax(0, 1fr)); }
.result-card { min-height: 0; }
.controls-section { min-height: 220px; }
.control-panel { display: flex; flex-direction: column; justify-content: center; }
}
@media (max-width: 980px) {
.result-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.activity-card { grid-column: auto; }
.controls-section { grid-template-columns: 1fr; }
.proxy-panel { grid-column: auto; }
.control-panel + .control-panel { border-left: 0; border-top: 1px solid #cbd3d8; }
}
@media (max-width: 640px) {
.result-grid { grid-template-columns: 1fr; }
.result-card { min-height: 190px; }
.activity-row { grid-template-columns: 70px minmax(0, 1fr); }
.activity-row span { grid-column: 1 / -1; }
.proxy-columns { grid-template-columns: 1fr; }
}
@@ -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
}
}
+9
View File
@@ -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: []
}
+22
View File
@@ -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"]
}
+48
View File
@@ -0,0 +1,48 @@
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: {
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
}
}
}
})