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'
}
}