refactor(ui): Rediseño completo de UI con Nuxt UI 4
- Nuevo layout responsivo mobile-first con tabs inferiores - Sidebar colapsable en desktop con cola de impresión - Sistema de templates reutilizables con localStorage - Soporte Dark/Light mode con UColorModeButton - Composables usePrintQueue y useTemplates para estado global - Componentes modulares: CommandBuilder, QuickActions, PrintQueue, QueueItem - Navegación por tabs: Constructor | Cola | Templates
This commit is contained in:
36
server/api/print/cut.post.ts
Normal file
36
server/api/print/cut.post.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Endpoint para cortar papel
|
||||
import { buildFromOperations } from '~/server/utils/eposBuilder'
|
||||
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '~/server/utils/printer'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const config = useRuntimeConfig()
|
||||
const body = await readBody(event)
|
||||
|
||||
const { type = 'feed' } = body as { type?: string }
|
||||
|
||||
const inner = buildFromOperations([{ op: 'cut', type }])
|
||||
const soap = buildSoapEnvelope(inner)
|
||||
|
||||
const result = await sendToPrinter(
|
||||
soap,
|
||||
config.printerHost,
|
||||
config.printerDeviceId,
|
||||
parseInt(config.printerTimeoutMs)
|
||||
)
|
||||
|
||||
const { success, code } = parsePrinterResponse(result.data)
|
||||
|
||||
return {
|
||||
ok: success,
|
||||
httpStatus: result.status,
|
||||
code,
|
||||
raw: result.data
|
||||
}
|
||||
} catch (err: any) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err.message
|
||||
}
|
||||
}
|
||||
})
|
||||
120
server/api/print/image.post.ts
Normal file
120
server/api/print/image.post.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// Endpoint para imprimir imágenes usando Jimp
|
||||
import Jimp from 'jimp'
|
||||
import { EposMessageBuilder } from '~/server/utils/eposBuilder'
|
||||
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '~/server/utils/printer'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const config = useRuntimeConfig()
|
||||
const body = await readBody(event)
|
||||
|
||||
const {
|
||||
path,
|
||||
width,
|
||||
threshold = 128,
|
||||
mode = 'mono'
|
||||
} = body as {
|
||||
path?: string
|
||||
width?: number
|
||||
threshold?: number
|
||||
mode?: string
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'path required'
|
||||
}
|
||||
}
|
||||
|
||||
// Leer y procesar imagen
|
||||
const img = await Jimp.read(path)
|
||||
let targetWidth = width || img.bitmap.width
|
||||
if (targetWidth <= 0) targetWidth = img.bitmap.width
|
||||
|
||||
const scale = targetWidth / img.bitmap.width
|
||||
const targetHeight = Math.max(1, Math.round(img.bitmap.height * scale))
|
||||
|
||||
img.resize(targetWidth, targetHeight, Jimp.RESIZE_BILINEAR)
|
||||
img.grayscale()
|
||||
|
||||
// Empaquetar bits MSB first por byte
|
||||
const bytesPerRow = Math.ceil(targetWidth / 8)
|
||||
const out = Buffer.alloc(bytesPerRow * targetHeight)
|
||||
let outIdx = 0
|
||||
|
||||
for (let y = 0; y < targetHeight; y++) {
|
||||
let byte = 0
|
||||
let bit = 7
|
||||
let rowBytes = 0
|
||||
|
||||
for (let x = 0; x < targetWidth; x++) {
|
||||
const idx = (y * targetWidth + x) * 4
|
||||
const rgba = img.bitmap.data
|
||||
const r = rgba[idx]
|
||||
const g = rgba[idx + 1]
|
||||
const b = rgba[idx + 2]
|
||||
const lum = 0.299 * r + 0.587 * g + 0.114 * b
|
||||
const isBlack = lum < threshold
|
||||
|
||||
if (isBlack) byte |= (1 << bit)
|
||||
bit--
|
||||
|
||||
if (bit < 0) {
|
||||
out[outIdx++] = byte
|
||||
rowBytes++
|
||||
byte = 0
|
||||
bit = 7
|
||||
}
|
||||
}
|
||||
|
||||
// Rellenar bits restantes
|
||||
if (bit !== 7) {
|
||||
out[outIdx++] = byte
|
||||
rowBytes++
|
||||
}
|
||||
|
||||
// Rellenar a bytes completos si es necesario
|
||||
while (rowBytes < bytesPerRow) {
|
||||
out[outIdx++] = 0
|
||||
rowBytes++
|
||||
}
|
||||
}
|
||||
|
||||
const base64 = out.toString('base64')
|
||||
|
||||
// Construir mensaje ePOS
|
||||
const builder = new EposMessageBuilder()
|
||||
builder.imageRaw({
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
mode,
|
||||
base64
|
||||
})
|
||||
|
||||
const soap = buildSoapEnvelope(builder.build())
|
||||
|
||||
const result = await sendToPrinter(
|
||||
soap,
|
||||
config.printerHost,
|
||||
config.printerDeviceId,
|
||||
parseInt(config.printerTimeoutMs)
|
||||
)
|
||||
|
||||
const { success, code } = parsePrinterResponse(result.data)
|
||||
|
||||
return {
|
||||
ok: success,
|
||||
httpStatus: result.status,
|
||||
code,
|
||||
raw: result.data,
|
||||
width: targetWidth,
|
||||
height: targetHeight
|
||||
}
|
||||
} catch (err: any) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err.message
|
||||
}
|
||||
}
|
||||
})
|
||||
54
server/api/print/index.post.ts
Normal file
54
server/api/print/index.post.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// Endpoint genérico de impresión que acepta una lista de operaciones
|
||||
import { buildFromOperations, type Operation } from '~/server/utils/eposBuilder'
|
||||
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '~/server/utils/printer'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const config = useRuntimeConfig()
|
||||
const body = await readBody(event)
|
||||
|
||||
const {
|
||||
operations = [],
|
||||
dryRun = false
|
||||
} = body as {
|
||||
operations?: Operation[]
|
||||
dryRun?: boolean
|
||||
}
|
||||
|
||||
// Construir el XML interior con las operaciones
|
||||
const inner = buildFromOperations(operations)
|
||||
const soap = buildSoapEnvelope(inner)
|
||||
|
||||
// Si es dryRun, devolver solo el XML sin enviar a la impresora
|
||||
if (dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
soap
|
||||
}
|
||||
}
|
||||
|
||||
// Enviar a la impresora
|
||||
const result = await sendToPrinter(
|
||||
soap,
|
||||
config.printerHost,
|
||||
config.printerDeviceId,
|
||||
parseInt(config.printerTimeoutMs)
|
||||
)
|
||||
|
||||
// Parsear la respuesta
|
||||
const { success, code } = parsePrinterResponse(result.data)
|
||||
|
||||
return {
|
||||
ok: success,
|
||||
httpStatus: result.status,
|
||||
code,
|
||||
raw: result.data
|
||||
}
|
||||
} catch (err: any) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err.message
|
||||
}
|
||||
}
|
||||
})
|
||||
39
server/api/print/pulse.post.ts
Normal file
39
server/api/print/pulse.post.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Endpoint para abrir cajón de dinero
|
||||
import { buildFromOperations } from '~/server/utils/eposBuilder'
|
||||
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '~/server/utils/printer'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const config = useRuntimeConfig()
|
||||
const body = await readBody(event)
|
||||
|
||||
const { drawer, time } = body as {
|
||||
drawer?: string
|
||||
time?: string
|
||||
}
|
||||
|
||||
const inner = buildFromOperations([{ op: 'pulse', drawer, time }])
|
||||
const soap = buildSoapEnvelope(inner)
|
||||
|
||||
const result = await sendToPrinter(
|
||||
soap,
|
||||
config.printerHost,
|
||||
config.printerDeviceId,
|
||||
parseInt(config.printerTimeoutMs)
|
||||
)
|
||||
|
||||
const { success, code } = parsePrinterResponse(result.data)
|
||||
|
||||
return {
|
||||
ok: success,
|
||||
httpStatus: result.status,
|
||||
code,
|
||||
raw: result.data
|
||||
}
|
||||
} catch (err: any) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err.message
|
||||
}
|
||||
}
|
||||
})
|
||||
86
server/api/print/text.post.ts
Normal file
86
server/api/print/text.post.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// Endpoint de conveniencia para imprimir texto con opciones
|
||||
import { buildFromOperations, type Operation } from '~/server/utils/eposBuilder'
|
||||
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '~/server/utils/printer'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const config = useRuntimeConfig()
|
||||
const body = await readBody(event)
|
||||
|
||||
const {
|
||||
text = '',
|
||||
options = {}
|
||||
} = body as {
|
||||
text?: string
|
||||
options?: {
|
||||
align?: string
|
||||
font?: string
|
||||
size?: { width?: number, height?: number }
|
||||
style?: {
|
||||
reverse?: boolean
|
||||
ul?: boolean
|
||||
em?: boolean
|
||||
color?: string
|
||||
}
|
||||
feedLines?: number
|
||||
cut?: string
|
||||
}
|
||||
}
|
||||
|
||||
// Construir operaciones
|
||||
const ops: Operation[] = []
|
||||
|
||||
if (options.align) {
|
||||
ops.push({ op: 'textAlign', align: options.align })
|
||||
}
|
||||
if (options.font) {
|
||||
ops.push({ op: 'textFont', font: options.font })
|
||||
}
|
||||
if (options.size) {
|
||||
ops.push({
|
||||
op: 'textSize',
|
||||
width: options.size.width,
|
||||
height: options.size.height
|
||||
})
|
||||
}
|
||||
if (options.style) {
|
||||
ops.push({ op: 'textStyle', ...options.style })
|
||||
}
|
||||
|
||||
// Agregar el texto
|
||||
ops.push({ op: 'text', value: text })
|
||||
|
||||
// Opciones post-texto
|
||||
if (options.feedLines) {
|
||||
ops.push({ op: 'feedLine', line: options.feedLines })
|
||||
}
|
||||
if (options.cut) {
|
||||
ops.push({ op: 'cut', type: options.cut })
|
||||
}
|
||||
|
||||
// Construir SOAP y enviar
|
||||
const inner = buildFromOperations(ops)
|
||||
const soap = buildSoapEnvelope(inner)
|
||||
|
||||
const result = await sendToPrinter(
|
||||
soap,
|
||||
config.printerHost,
|
||||
config.printerDeviceId,
|
||||
parseInt(config.printerTimeoutMs)
|
||||
)
|
||||
|
||||
const { success, code } = parsePrinterResponse(result.data)
|
||||
|
||||
return {
|
||||
ok: success,
|
||||
httpStatus: result.status,
|
||||
code,
|
||||
raw: result.data
|
||||
}
|
||||
} catch (err: any) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err.message
|
||||
}
|
||||
}
|
||||
})
|
||||
286
server/utils/eposBuilder.ts
Normal file
286
server/utils/eposBuilder.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
// Server-side minimal ePOS-Print XML builder for Epson printers
|
||||
// Focused on printer-only tags. Builds the inner message for <epos-print>.
|
||||
|
||||
// Tipos para las operaciones
|
||||
export interface Operation {
|
||||
op: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface TextStyleOptions {
|
||||
reverse?: boolean
|
||||
ul?: boolean
|
||||
em?: boolean
|
||||
color?: string
|
||||
}
|
||||
|
||||
export interface PulseOptions {
|
||||
drawer?: string
|
||||
time?: string
|
||||
}
|
||||
|
||||
export interface BarcodeOptions {
|
||||
type?: string
|
||||
hri?: string
|
||||
font?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface QRCodeOptions {
|
||||
model?: string
|
||||
level?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
function escapeXml(str: string | number): string {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
export class EposMessageBuilder {
|
||||
private parts: string[] = []
|
||||
|
||||
// Text and styles
|
||||
text(content: string): this {
|
||||
this.parts.push(`<text>${escapeXml(content)}</text>`)
|
||||
return this
|
||||
}
|
||||
|
||||
textLang(lang: string): this {
|
||||
this.parts.push(`<text lang="${lang}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
textAlign(align: string): this {
|
||||
this.parts.push(`<text align="${align}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
textRotate(rotate: boolean): this {
|
||||
this.parts.push(`<text rotate="${rotate ? 'true' : 'false'}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
textLineSpace(linespc: number): this {
|
||||
this.parts.push(`<text linespc="${linespc}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
textFont(font: string): this {
|
||||
this.parts.push(`<text font="${font}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
textSmooth(smooth: boolean): this {
|
||||
this.parts.push(`<text smooth="${smooth ? 'true' : 'false'}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
textDouble(dw?: boolean, dh?: boolean): this {
|
||||
const attrs = [
|
||||
dw !== undefined ? `dw="${dw ? 'true' : 'false'}"` : '',
|
||||
dh !== undefined ? `dh="${dh ? 'true' : 'false'}"` : ''
|
||||
].filter(Boolean).join(' ')
|
||||
this.parts.push(`<text ${attrs}/>`.trim())
|
||||
return this
|
||||
}
|
||||
|
||||
textSize(width?: number, height?: number): this {
|
||||
const attrs = [
|
||||
width !== undefined ? `width="${width}"` : '',
|
||||
height !== undefined ? `height="${height}"` : ''
|
||||
].filter(Boolean).join(' ')
|
||||
this.parts.push(`<text ${attrs}/>`.trim())
|
||||
return this
|
||||
}
|
||||
|
||||
textStyle(options: TextStyleOptions = {}): this {
|
||||
const { reverse, ul, em, color } = options
|
||||
const attrs: string[] = []
|
||||
if (reverse !== undefined) attrs.push(`reverse="${reverse ? 'true' : 'false'}"`)
|
||||
if (ul !== undefined) attrs.push(`ul="${ul ? 'true' : 'false'}"`)
|
||||
if (em !== undefined) attrs.push(`em="${em ? 'true' : 'false'}"`)
|
||||
if (color !== undefined) attrs.push(`color="${color}"`)
|
||||
this.parts.push(`<text ${attrs.join(' ')}/>`.trim())
|
||||
return this
|
||||
}
|
||||
|
||||
textPosition(x: number): this {
|
||||
this.parts.push(`<text x="${x}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
textVPosition(y: number): this {
|
||||
this.parts.push(`<text y="${y}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
// Feed
|
||||
feed(): this {
|
||||
this.parts.push('<feed/>')
|
||||
return this
|
||||
}
|
||||
|
||||
feedUnit(unit: number): this {
|
||||
this.parts.push(`<feed unit="${unit}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
feedLine(line: number): this {
|
||||
this.parts.push(`<feed line="${line}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
feedPosition(pos: number): this {
|
||||
this.parts.push(`<feed pos="${pos}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
// Cut
|
||||
cut(type: string): this {
|
||||
this.parts.push(`<cut type="${type}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
// Drawer
|
||||
pulse(options: PulseOptions = {}): this {
|
||||
const { drawer = 'drawer_1', time = 'pulse_200' } = options
|
||||
this.parts.push(`<pulse drawer="${drawer}" time="${time}"/>`)
|
||||
return this
|
||||
}
|
||||
|
||||
// Barcode and symbols
|
||||
barcode(data: string, options: BarcodeOptions = {}): this {
|
||||
const { type, hri, font, width, height } = options
|
||||
const attrs: string[] = []
|
||||
if (type) attrs.push(`type="${type}"`)
|
||||
if (hri) attrs.push(`hri="${hri}"`)
|
||||
if (font) attrs.push(`font="${font}"`)
|
||||
if (width) attrs.push(`width="${width}"`)
|
||||
if (height) attrs.push(`height="${height}"`)
|
||||
this.parts.push(`<barcode ${attrs.join(' ')}>${escapeXml(data)}</barcode>`.trim())
|
||||
return this
|
||||
}
|
||||
|
||||
qrcode(data: string, options: QRCodeOptions = {}): this {
|
||||
const { model = 'qrcode_model_2', level = 'level_m', size } = options
|
||||
const attrs = [`type="${model}"`, `level="${level}"`]
|
||||
if (size) attrs.push(`size="${size}"`)
|
||||
this.parts.push(`<symbol ${attrs.join(' ')}>${escapeXml(data)}</symbol>`)
|
||||
return this
|
||||
}
|
||||
|
||||
// Image
|
||||
imageRaw(options: {
|
||||
width: number
|
||||
height: number
|
||||
mode?: string
|
||||
base64: string
|
||||
}): this {
|
||||
const { width, height, mode = 'mono', base64 } = options
|
||||
this.parts.push(`<image width="${width}" height="${height}" mode="${mode}">${base64}</image>`)
|
||||
return this
|
||||
}
|
||||
|
||||
build(): string {
|
||||
return this.parts.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// Accepts a list of operations: [{ op: 'text', args: {...} | [] }, ...]
|
||||
export function buildFromOperations(ops: Operation[]): string {
|
||||
const b = new EposMessageBuilder()
|
||||
|
||||
for (const item of ops || []) {
|
||||
const { op } = item
|
||||
if (!op) continue
|
||||
|
||||
switch (op) {
|
||||
case 'text':
|
||||
b.text(item.value ?? '')
|
||||
break
|
||||
case 'textLang':
|
||||
b.textLang(item.lang)
|
||||
break
|
||||
case 'textAlign':
|
||||
b.textAlign(item.align)
|
||||
break
|
||||
case 'textRotate':
|
||||
b.textRotate(!!item.rotate)
|
||||
break
|
||||
case 'textLineSpace':
|
||||
b.textLineSpace(item.linespc)
|
||||
break
|
||||
case 'textFont':
|
||||
b.textFont(item.font)
|
||||
break
|
||||
case 'textSmooth':
|
||||
b.textSmooth(!!item.smooth)
|
||||
break
|
||||
case 'textDouble':
|
||||
b.textDouble(item.dw, item.dh)
|
||||
break
|
||||
case 'textSize':
|
||||
b.textSize(item.width, item.height)
|
||||
break
|
||||
case 'textStyle':
|
||||
b.textStyle({
|
||||
reverse: item.reverse,
|
||||
ul: item.ul,
|
||||
em: item.em,
|
||||
color: item.color
|
||||
})
|
||||
break
|
||||
case 'textPosition':
|
||||
b.textPosition(item.x)
|
||||
break
|
||||
case 'textVPosition':
|
||||
b.textVPosition(item.y)
|
||||
break
|
||||
case 'feed':
|
||||
b.feed()
|
||||
break
|
||||
case 'feedUnit':
|
||||
b.feedUnit(item.unit)
|
||||
break
|
||||
case 'feedLine':
|
||||
b.feedLine(item.line)
|
||||
break
|
||||
case 'feedPosition':
|
||||
b.feedPosition(item.pos)
|
||||
break
|
||||
case 'cut':
|
||||
b.cut(item.type || 'feed')
|
||||
break
|
||||
case 'pulse':
|
||||
b.pulse({ drawer: item.drawer, time: item.time })
|
||||
break
|
||||
case 'barcode':
|
||||
b.barcode(item.data || '', {
|
||||
type: item.type,
|
||||
hri: item.hri,
|
||||
font: item.font,
|
||||
width: item.width,
|
||||
height: item.height
|
||||
})
|
||||
break
|
||||
case 'qrcode':
|
||||
b.qrcode(item.data || '', {
|
||||
model: item.model,
|
||||
level: item.level,
|
||||
size: item.size
|
||||
})
|
||||
break
|
||||
default:
|
||||
// ignore unknown for now
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return b.build()
|
||||
}
|
||||
62
server/utils/printer.ts
Normal file
62
server/utils/printer.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Utilidades para comunicación con impresoras Epson ePOS
|
||||
import axios from 'axios'
|
||||
import https from 'https'
|
||||
|
||||
export interface PrinterResponse {
|
||||
status: number
|
||||
headers: any
|
||||
data: string
|
||||
}
|
||||
|
||||
export function buildSoapEnvelope(inner: string): string {
|
||||
return (
|
||||
'<?xml version="1.0" encoding="utf-8"?>' +
|
||||
'<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">' +
|
||||
'<s:Body>' +
|
||||
'<epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print">' +
|
||||
inner +
|
||||
'</epos-print>' +
|
||||
'</s:Body>' +
|
||||
'</s:Envelope>'
|
||||
)
|
||||
}
|
||||
|
||||
export async function sendToPrinter(
|
||||
xml: string,
|
||||
printerHost: string,
|
||||
printerDeviceId: string,
|
||||
printerTimeoutMs: number
|
||||
): Promise<PrinterResponse> {
|
||||
const url = `https://${printerHost}/cgi-bin/epos/service.cgi?devid=${encodeURIComponent(printerDeviceId)}&timeout=${printerTimeoutMs}`
|
||||
|
||||
// Agente HTTPS que acepta certificados auto-firmados
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
|
||||
const res = await axios.post(url, xml, {
|
||||
headers: {
|
||||
'Content-Type': 'text/xml; charset=utf-8'
|
||||
},
|
||||
httpsAgent,
|
||||
timeout: printerTimeoutMs + 5000,
|
||||
validateStatus: () => true, // Aceptar cualquier status code
|
||||
})
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
data: typeof res.data === 'string' ? res.data : String(res.data)
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePrinterResponse(responseData: string): {
|
||||
success: boolean
|
||||
code: string
|
||||
} {
|
||||
const success = /success\s*=\s*"true"/.test(responseData)
|
||||
const codeMatch = responseData.match(/code="([^"]*)"/)
|
||||
const code = codeMatch ? codeMatch[1] : ''
|
||||
|
||||
return { success, code }
|
||||
}
|
||||
Reference in New Issue
Block a user