449 lines
16 KiB
TypeScript
449 lines
16 KiB
TypeScript
import axios from 'axios'
|
|
import api from './index'
|
|
import { resolveApiBaseUrl } from '@/utils/authMode'
|
|
import type {
|
|
ApiResponse,
|
|
CreateAssetLabelPrintJobPayload,
|
|
CreateAssetLabelPrintJobResult,
|
|
PrintAgentReleaseInfo,
|
|
PrintDriverLatestInfo,
|
|
PrinterDriverRelease,
|
|
PrintJob,
|
|
PrintJobListData,
|
|
PrintJobListQuery,
|
|
PrintJobPayload,
|
|
PrintLabelProfile,
|
|
PrintTerminal,
|
|
PrintTerminalReadiness,
|
|
PrintTerminalPairCode,
|
|
PrintTerminalPrinter,
|
|
PrintTerminalStatus,
|
|
UpdatePrintTerminalStatusPayload
|
|
} from '@/types'
|
|
|
|
const apiBaseUrl = resolveApiBaseUrl(import.meta.env.VITE_API_BASE_URL)
|
|
const downloadBaseUrl = apiBaseUrl.replace(/\/api\/?$/, '')
|
|
|
|
const getAuthHeaders = () => {
|
|
const token = localStorage.getItem('token')
|
|
return token ? { Authorization: `Bearer ${token}` } : undefined
|
|
}
|
|
|
|
const buildDownloadUrl = (path: string) => `${downloadBaseUrl}${path}`
|
|
|
|
const toRecord = (value: unknown): Record<string, any> => {
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
return value as Record<string, any>
|
|
}
|
|
return {}
|
|
}
|
|
|
|
const toNullableString = (value: unknown) => {
|
|
if (typeof value === 'string') {
|
|
const trimmed = value.trim()
|
|
return trimmed ? trimmed : null
|
|
}
|
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
return String(value)
|
|
}
|
|
return null
|
|
}
|
|
|
|
const toNumber = (value: unknown, fallback = 0) => {
|
|
const numberValue = Number(value)
|
|
return Number.isFinite(numberValue) ? numberValue : fallback
|
|
}
|
|
|
|
const toBoolean = (value: unknown) => {
|
|
if (typeof value === 'boolean') return value
|
|
if (typeof value === 'number') return value !== 0
|
|
if (typeof value === 'string') {
|
|
return ['1', 'true', 'yes', 'y'].includes(value.toLowerCase())
|
|
}
|
|
return false
|
|
}
|
|
|
|
const parseJsonObject = <T extends Record<string, any>>(value: unknown): T | null => {
|
|
if (!value) return null
|
|
if (typeof value === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(value)
|
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as T) : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? (value as T) : null
|
|
}
|
|
|
|
const parseJsonArray = <T>(value: unknown): T[] => {
|
|
if (Array.isArray(value)) {
|
|
return value as T[]
|
|
}
|
|
if (typeof value === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(value)
|
|
return Array.isArray(parsed) ? (parsed as T[]) : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
return []
|
|
}
|
|
|
|
const unwrapPayload = <T>(value: unknown): T => {
|
|
const record = toRecord(value)
|
|
if (Object.prototype.hasOwnProperty.call(record, 'code') && Object.prototype.hasOwnProperty.call(record, 'data')) {
|
|
return record.data as T
|
|
}
|
|
return value as T
|
|
}
|
|
|
|
const normalizePrinterSnapshot = (value: unknown): PrintTerminalPrinter[] =>
|
|
parseJsonArray<PrintTerminalPrinter>(value).map((item) => {
|
|
const record = toRecord(item)
|
|
return {
|
|
name: toNullableString(record.name) || '未命名打印机',
|
|
driver_name: toNullableString(record.driver_name),
|
|
port_name: toNullableString(record.port_name),
|
|
is_default: toBoolean(record.is_default),
|
|
is_available: toBoolean(record.is_available)
|
|
}
|
|
})
|
|
|
|
const normalizeLabelProfile = (value: unknown): PrintLabelProfile | null => {
|
|
const record = parseJsonObject<Record<string, any>>(value)
|
|
if (!record) return null
|
|
|
|
return {
|
|
paper_size: toNullableString(record.paper_size),
|
|
width_mm:
|
|
typeof record.width_mm === 'undefined' || record.width_mm === null ? null : toNumber(record.width_mm),
|
|
height_mm:
|
|
typeof record.height_mm === 'undefined' || record.height_mm === null ? null : toNumber(record.height_mm),
|
|
orientation: toNullableString(record.orientation),
|
|
scale_mode: toNullableString(record.scale_mode) || toNullableString(record.scaling),
|
|
scaling: toNullableString(record.scaling) || toNullableString(record.scale_mode),
|
|
configured: typeof record.configured === 'undefined' ? null : toBoolean(record.configured),
|
|
is_ready: typeof record.is_ready === 'undefined' ? null : toBoolean(record.is_ready),
|
|
status: toNullableString(record.status),
|
|
profile_status: toNullableString(record.profile_status),
|
|
message: toNullableString(record.message)
|
|
}
|
|
}
|
|
|
|
const normalizeReadiness = (value: unknown): PrintTerminalReadiness | null => {
|
|
const record = parseJsonObject<Record<string, any>>(value)
|
|
if (!record) return null
|
|
|
|
return {
|
|
is_ready: toBoolean(record.is_ready),
|
|
reason: toNullableString(record.reason),
|
|
message: toNullableString(record.message)
|
|
}
|
|
}
|
|
|
|
const normalizePrintTerminal = (value: unknown): PrintTerminal => {
|
|
const record = toRecord(value)
|
|
const printerSnapshot = normalizePrinterSnapshot(record.printer_snapshot ?? record.printer_snapshot_json)
|
|
const labelProfile = normalizeLabelProfile(record.label_profile ?? record.label_profile_json)
|
|
const readiness = normalizeReadiness(record.readiness)
|
|
const recentFailedJob = parseJsonObject<Record<string, any>>(record.recent_failed_job)
|
|
|
|
return {
|
|
id: toNumber(record.id),
|
|
name: toNullableString(record.name) || toNullableString(record.machine_name) || `终端 ${toNumber(record.id)}`,
|
|
platform: toNullableString(record.platform),
|
|
machine_name: toNullableString(record.machine_name),
|
|
status: toNullableString(record.status) || 'offline',
|
|
is_default: toBoolean(record.is_default),
|
|
last_seen_at: toNullableString(record.last_seen_at),
|
|
selected_printer_name: toNullableString(record.selected_printer_name),
|
|
printer_snapshot_json: (record.printer_snapshot_json ?? record.printer_snapshot) || null,
|
|
printer_snapshot: printerSnapshot,
|
|
label_profile_json: (record.label_profile_json ?? record.label_profile) || null,
|
|
label_profile: labelProfile,
|
|
readiness,
|
|
paired_at: toNullableString(record.paired_at),
|
|
created_at: toNullableString(record.created_at),
|
|
updated_at: toNullableString(record.updated_at),
|
|
latest_error_message:
|
|
toNullableString(record.latest_error_message) ||
|
|
toNullableString(record.recent_error_message) ||
|
|
toNullableString(recentFailedJob?.error_message),
|
|
current_error_message: toNullableString(record.current_error_message),
|
|
recent_failed_job: recentFailedJob
|
|
? {
|
|
id: toNumber(recentFailedJob.id),
|
|
error_message: toNullableString(recentFailedJob.error_message),
|
|
created_at: toNullableString(recentFailedJob.created_at)
|
|
}
|
|
: null
|
|
}
|
|
}
|
|
|
|
const normalizePrintPayload = (value: unknown): PrintJobPayload | null => {
|
|
const record = parseJsonObject<PrintJobPayload>(value)
|
|
return record || null
|
|
}
|
|
|
|
const normalizePrintJob = (value: unknown): PrintJob => {
|
|
const record = toRecord(value)
|
|
const payload = normalizePrintPayload(record.payload ?? record.payload_json)
|
|
|
|
return {
|
|
id: toNumber(record.id),
|
|
job_type: (toNullableString(record.job_type) || 'asset_label') as PrintJob['job_type'],
|
|
status: (toNullableString(record.status) || 'queued') as PrintJob['status'],
|
|
target_terminal_id:
|
|
typeof record.target_terminal_id === 'undefined' ? null : toNumber(record.target_terminal_id, 0),
|
|
target_terminal_name:
|
|
toNullableString(record.target_terminal_name) ||
|
|
toNullableString(record.target_terminal?.name),
|
|
claimed_by_terminal_id:
|
|
typeof record.claimed_by_terminal_id === 'undefined'
|
|
? null
|
|
: toNumber(record.claimed_by_terminal_id, 0),
|
|
claimed_by_terminal_name:
|
|
toNullableString(record.claimed_by_terminal_name) ||
|
|
toNullableString(record.claimed_by_terminal?.name),
|
|
requested_by:
|
|
typeof record.requested_by === 'undefined' || record.requested_by === null
|
|
? null
|
|
: (record.requested_by as number | string),
|
|
requested_by_name:
|
|
toNullableString(record.requested_by_name) ||
|
|
toNullableString(record.requested_user_name) ||
|
|
toNullableString(record.requested_by_user?.name) ||
|
|
toNullableString(record.requested_by?.name),
|
|
payload_json: (record.payload_json ?? record.payload) || null,
|
|
payload,
|
|
error_message: toNullableString(record.error_message),
|
|
attempt_count: toNumber(record.attempt_count, 0),
|
|
claimed_at: toNullableString(record.claimed_at),
|
|
started_at: toNullableString(record.started_at),
|
|
completed_at: toNullableString(record.completed_at),
|
|
created_at: toNullableString(record.created_at),
|
|
updated_at: toNullableString(record.updated_at)
|
|
}
|
|
}
|
|
|
|
const normalizeCollection = <T>(
|
|
value: unknown,
|
|
collectionKeys: string[],
|
|
mapper: (item: unknown) => T
|
|
): T[] => {
|
|
if (Array.isArray(value)) {
|
|
return value.map(mapper)
|
|
}
|
|
|
|
const record = toRecord(value)
|
|
const collection = collectionKeys
|
|
.map((key) => record[key])
|
|
.find((candidate) => Array.isArray(candidate))
|
|
|
|
return Array.isArray(collection) ? collection.map(mapper) : []
|
|
}
|
|
|
|
const normalizePrintJobsData = (value: unknown): PrintJobListData => {
|
|
const record = toRecord(value)
|
|
const list = normalizeCollection(value, ['list', 'items', 'jobs'], normalizePrintJob)
|
|
|
|
return {
|
|
list,
|
|
total: toNumber(record.total, list.length),
|
|
page: toNumber(record.page, 1),
|
|
pageSize: toNumber(record.pageSize ?? record.page_size, list.length || 10)
|
|
}
|
|
}
|
|
|
|
const normalizeReleaseNotes = (value: unknown): string | string[] | null => {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => String(item))
|
|
}
|
|
return toNullableString(value)
|
|
}
|
|
|
|
const normalizePrintAgentRelease = (value: unknown): PrintAgentReleaseInfo => {
|
|
const record = toRecord(value)
|
|
return {
|
|
version: toNullableString(record.version) || '未命名版本',
|
|
published_at: toNullableString(record.published_at),
|
|
filename: toNullableString(record.filename),
|
|
notes: normalizeReleaseNotes(record.notes),
|
|
download_url: toNullableString(record.download_url)
|
|
}
|
|
}
|
|
|
|
const normalizeDriverLatestInfo = (value: unknown): PrintDriverLatestInfo | null => {
|
|
const record = toRecord(value)
|
|
const officialUrl =
|
|
toNullableString(record.official_url) ||
|
|
toNullableString(record.source_url) ||
|
|
toNullableString(record.url)
|
|
|
|
if (!officialUrl) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
title: toNullableString(record.title) || '芯烨官方最新版驱动',
|
|
official_url: officialUrl,
|
|
checked_at: toNullableString(record.checked_at) || toNullableString(record.last_checked_at),
|
|
notes: normalizeReleaseNotes(record.notes),
|
|
source_type: toNullableString(record.source_type) || 'official_latest'
|
|
}
|
|
}
|
|
|
|
const normalizeDriverRelease = (value: unknown): PrinterDriverRelease => {
|
|
const record = toRecord(value)
|
|
return {
|
|
version: toNullableString(record.version) || '未命名版本',
|
|
published_at: toNullableString(record.published_at),
|
|
filename: toNullableString(record.filename),
|
|
notes: normalizeReleaseNotes(record.notes),
|
|
download_url: toNullableString(record.download_url),
|
|
source_type: toNullableString(record.source_type),
|
|
source_url: toNullableString(record.source_url),
|
|
source_path: toNullableString(record.source_path)
|
|
}
|
|
}
|
|
|
|
const extractFileName = (headerValue?: string | null) => {
|
|
if (!headerValue) return null
|
|
|
|
const utf8Match = headerValue.match(/filename\*=UTF-8''([^;]+)/i)
|
|
if (utf8Match?.[1]) {
|
|
return decodeURIComponent(utf8Match[1])
|
|
}
|
|
|
|
const plainMatch = headerValue.match(/filename="?([^";]+)"?/i)
|
|
return plainMatch?.[1] || null
|
|
}
|
|
|
|
export const getPrintTerminals = async () => {
|
|
const response = await api.get<ApiResponse<unknown>>('/print-terminals')
|
|
return normalizeCollection(response.data.data, ['list', 'items', 'terminals'], normalizePrintTerminal)
|
|
}
|
|
|
|
export const getPrintTerminalReadiness = async (): Promise<PrintTerminal | null> => {
|
|
const response = await api.get<ApiResponse<unknown>>('/print-terminals/readiness')
|
|
const data = toRecord(response.data.data)
|
|
const rawTerminal = toRecord(data.default_terminal ?? data.terminal ?? data)
|
|
const terminalId = toNumber(rawTerminal.id ?? rawTerminal.terminal_id, 0)
|
|
|
|
if (!terminalId) {
|
|
return null
|
|
}
|
|
|
|
const readinessValue = rawTerminal.readiness ?? data.readiness ?? {
|
|
is_ready: rawTerminal.is_ready ?? data.is_ready,
|
|
reason: rawTerminal.readiness_reason ?? data.readiness_reason,
|
|
message: rawTerminal.readiness_message ?? data.readiness_message
|
|
}
|
|
|
|
return normalizePrintTerminal({
|
|
...rawTerminal,
|
|
id: terminalId,
|
|
name: rawTerminal.name ?? rawTerminal.terminal_name,
|
|
is_default: true,
|
|
selected_printer_name: rawTerminal.selected_printer_name ?? rawTerminal.printer_name,
|
|
readiness: readinessValue
|
|
})
|
|
}
|
|
|
|
export const createAssetLabelPrintJob = async (
|
|
payload: CreateAssetLabelPrintJobPayload
|
|
): Promise<CreateAssetLabelPrintJobResult> => {
|
|
const response = await api.post<ApiResponse<unknown>>('/print-jobs/asset-labels', payload, {
|
|
timeout: 120000
|
|
})
|
|
return {
|
|
job: normalizePrintJob(response.data.data),
|
|
queue_notice: toNullableString(response.data.message)
|
|
}
|
|
}
|
|
|
|
export const getPrintJobs = async (params?: PrintJobListQuery) => {
|
|
const response = await api.get<ApiResponse<unknown>>('/print-jobs', { params })
|
|
return normalizePrintJobsData(response.data.data)
|
|
}
|
|
|
|
export const cancelQueuedPrintJob = async (id: number) => {
|
|
const response = await api.patch<ApiResponse<unknown>>(`/print-jobs/${id}/cancel`)
|
|
return normalizePrintJob(response.data.data)
|
|
}
|
|
|
|
export const setDefaultPrintTerminal = async (id: number) => {
|
|
const response = await api.patch<ApiResponse<unknown>>(`/print-terminals/${id}/default`)
|
|
return normalizePrintTerminal(response.data.data)
|
|
}
|
|
|
|
export const updatePrintTerminalStatus = async (id: number, status: PrintTerminalStatus) => {
|
|
const payload: UpdatePrintTerminalStatusPayload = { status }
|
|
const response = await api.patch<ApiResponse<unknown>>(`/print-terminals/${id}/status`, payload)
|
|
return normalizePrintTerminal(response.data.data)
|
|
}
|
|
|
|
export const createPrintTerminalPairCode = async () => {
|
|
const response = await api.post<ApiResponse<unknown>>('/print-terminals/pair-codes')
|
|
const record = toRecord(response.data.data)
|
|
return {
|
|
code: toNullableString(record.code) || '',
|
|
expires_at: toNullableString(record.expires_at) || '',
|
|
created_at: toNullableString(record.created_at),
|
|
used_at: toNullableString(record.used_at),
|
|
used_by_terminal_id:
|
|
typeof record.used_by_terminal_id === 'undefined'
|
|
? null
|
|
: toNumber(record.used_by_terminal_id)
|
|
} as PrintTerminalPairCode
|
|
}
|
|
|
|
export const getPrintAgentReleases = async () => {
|
|
const response = await axios.get(buildDownloadUrl('/downloads/print-agent/releases'), {
|
|
withCredentials: true,
|
|
headers: getAuthHeaders()
|
|
})
|
|
const payload = unwrapPayload<unknown>(response.data)
|
|
return normalizeCollection(payload, ['list', 'items', 'releases'], normalizePrintAgentRelease)
|
|
}
|
|
|
|
export const getLatestXPrinterDriver = async () => {
|
|
const response = await axios.get(buildDownloadUrl('/downloads/printer-driver/xprinter/latest'), {
|
|
withCredentials: true,
|
|
headers: getAuthHeaders()
|
|
})
|
|
return normalizeDriverLatestInfo(unwrapPayload<unknown>(response.data))
|
|
}
|
|
|
|
export const getArchivedXPrinterDrivers = async () => {
|
|
const response = await axios.get(buildDownloadUrl('/downloads/printer-driver/xprinter/archive'), {
|
|
withCredentials: true,
|
|
headers: getAuthHeaders()
|
|
})
|
|
const payload = unwrapPayload<unknown>(response.data)
|
|
return normalizeCollection(payload, ['list', 'items', 'releases'], normalizeDriverRelease)
|
|
}
|
|
|
|
export const buildPrintAgentLatestDownloadUrl = () => {
|
|
return buildDownloadUrl('/downloads/print-agent/latest')
|
|
}
|
|
|
|
export const buildArchivedXPrinterDriverDownloadUrl = (version: string) => {
|
|
return buildDownloadUrl(`/downloads/printer-driver/xprinter/archive/${encodeURIComponent(version)}`)
|
|
}
|
|
|
|
export const downloadPrintResource = async (url: string) => {
|
|
const response = await axios.get(url, {
|
|
responseType: 'blob',
|
|
withCredentials: true,
|
|
headers: getAuthHeaders()
|
|
})
|
|
|
|
return {
|
|
blob: response.data as Blob,
|
|
fileName: extractFileName(response.headers['content-disposition']) || url.split('/').pop() || 'download'
|
|
}
|
|
}
|