59e51671
“wangming”
1
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
|
/**
* TSC/Android 位图打印:`BitmapFactory.decodeFile` 无法直接读 http(s) 或「仅相对路径」;
* 打印前把需网络的图片拉到本地临时路径,重打/预览落库里的 /picture/ URL 才能出图。
*/
import { resolveMediaUrlForApp, storedValueLooksLikeImagePath } from '../resolveMediaUrl'
import type { SystemLabelTemplate, SystemTemplateElementBase } from './types/printer'
type HydrateDebugStage = 'skip' | 'download-success' | 'download-failed' | 'no-need-download'
type HydrateDebugRecord = {
stage: HydrateDebugStage
payload: Record<string, unknown>
}
const hydrateDebugRecords: HydrateDebugRecord[] = []
const MAX_HYDRATE_DEBUG_RECORDS = 40
function logHydrateDebug (
stage: HydrateDebugStage,
payload: Record<string, unknown>,
): void {
hydrateDebugRecords.push({ stage, payload: { ...payload } })
if (hydrateDebugRecords.length > MAX_HYDRATE_DEBUG_RECORDS) {
hydrateDebugRecords.splice(0, hydrateDebugRecords.length - MAX_HYDRATE_DEBUG_RECORDS)
}
try {
console.log('[print-image-hydrate]', stage, JSON.stringify(payload))
} catch {
console.log('[print-image-hydrate]', stage, payload)
}
}
export function resetHydrateImageDebugRecords (): void {
hydrateDebugRecords.length = 0
}
export function getHydrateImageDebugReport (): string {
if (!hydrateDebugRecords.length) return 'No hydrate image records.'
const lines: string[] = []
hydrateDebugRecords.forEach((r, i) => {
const p = r.payload || {}
lines.push(`#${i + 1} ${r.stage}`)
if (p.elementId != null && p.elementId !== '') lines.push(`id=${String(p.elementId)}`)
if (p.type != null && p.type !== '') lines.push(`type=${String(p.type)}`)
if (p.reason != null && p.reason !== '') lines.push(`reason=${String(p.reason)}`)
if (p.raw != null && p.raw !== '') lines.push(`raw=${String(p.raw)}`)
if (p.resolvedUrl != null && p.resolvedUrl !== '') lines.push(`resolved=${String(p.resolvedUrl)}`)
if (p.tempFilePath != null && p.tempFilePath !== '') lines.push(`temp=${String(p.tempFilePath)}`)
lines.push('---')
})
return lines.join('\n')
}
/** 与 usAppApiRequest 一致:静态图 /picture/ 常需登录态,无头下载会 401 → 解码失败、纸面空白 */
function downloadAuthHeaders (): Record<string, string> {
const h: Record<string, string> = {}
try {
const token = uni.getStorageSync('access_token')
if (token) h.Authorization = `Bearer ${token}`
} catch (_) {}
return h
}
function cfgStr(config: Record<string, unknown>, keys: string[]): string {
for (const k of keys) {
const v = config[k]
if (v != null && String(v).trim() !== '') return String(v).trim()
}
return ''
}
/** 需先下载再 decodeFile 的地址(非 data:、非已是本地 file) */
function needsDownloadForDecode (raw: string): boolean {
const s = String(raw || '').trim()
if (!s) return false
if (s.startsWith('data:')) return false
if (s.startsWith('file://')) return false
if (s.startsWith('_doc/') || s.startsWith('_www/') || /^[A-Za-z]:[\\/]/.test(s)) return false
if (/^https?:\/\//i.test(s)) return true
if (
s.startsWith('/picture/')
|| s.startsWith('/static/')
|| s.startsWith('picture/')
|| s.startsWith('static/')
) return true
return false
}
function downloadUrlToTempFile (url: string): Promise<string | null> {
return new Promise((resolve) => {
if (!url) {
resolve(null)
return
}
try {
let done = false
const timer = setTimeout(() => {
if (done) return
done = true
resolve(null)
}, 6000)
uni.downloadFile({
url,
header: downloadAuthHeaders(),
success: (res) => {
if (done) return
done = true
clearTimeout(timer)
if (res.statusCode === 200 && res.tempFilePath) resolve(res.tempFilePath)
else resolve(null)
},
fail: () => {
if (done) return
done = true
clearTimeout(timer)
resolve(null)
},
})
} catch {
resolve(null)
}
})
}
async function resolveToLocalPathIfNeeded (
raw: string,
debugMeta: Record<string, unknown>,
): Promise<string | null> {
const trimmed = String(raw || '').trim()
if (!trimmed) {
logHydrateDebug('skip', { ...debugMeta, reason: 'empty-source' })
return null
}
if (!needsDownloadForDecode(trimmed)) {
logHydrateDebug('no-need-download', { ...debugMeta, raw: trimmed })
return null
}
const url = resolveMediaUrlForApp(trimmed)
if (!url) {
logHydrateDebug('download-failed', {
...debugMeta,
raw: trimmed,
reason: 'resolveMediaUrlForApp-empty',
})
return null
}
const local = await downloadUrlToTempFile(url)
if (local) {
let finalLocal = local
// #ifdef APP-PLUS
try {
const plusAny = (globalThis as any)?.plus
const converted = plusAny?.io?.convertLocalFileSystemURL?.(local)
if (converted && typeof converted === 'string') {
finalLocal = converted
}
} catch (_) {}
// #endif
logHydrateDebug('download-success', {
...debugMeta,
raw: trimmed,
resolvedUrl: url,
tempFilePath: finalLocal,
})
return finalLocal
}
logHydrateDebug('download-failed', {
...debugMeta,
raw: trimmed,
resolvedUrl: url,
reason: 'downloadFile-null',
})
return null
}
async function hydrateElement (el: SystemTemplateElementBase): Promise<SystemTemplateElementBase> {
const type = String(el.type || '').toUpperCase()
const cfg = { ...(el.config || {}) } as Record<string, unknown>
const debugMeta = {
elementId: String(el.id || ''),
type,
}
if (type === 'IMAGE' || type === 'LOGO') {
const raw = cfgStr(cfg, ['src', 'url', 'data', 'Src', 'Url', 'Data'])
const local = await resolveToLocalPathIfNeeded(raw, debugMeta)
if (!local) return el
return {
...el,
config: { ...cfg, src: local, url: local, data: local, Src: local, Url: local, Data: local },
}
}
if (type === 'QRCODE') {
const raw = cfgStr(cfg, ['data', 'Data'])
if (!raw || !storedValueLooksLikeImagePath(raw)) {
logHydrateDebug('skip', { ...debugMeta, raw, reason: 'qrcode-data-not-image-path' })
return el
}
const local = await resolveToLocalPathIfNeeded(raw, debugMeta)
if (!local) return el
return {
...el,
config: { ...cfg, data: local, Data: local, src: local, url: local },
}
}
return el
}
/**
* 返回新模板对象;无元素或无需下载时可能与原引用相同(未改元素时仍返回浅拷贝以统一调用方)。
*/
export async function hydrateSystemTemplateImagesForPrint (
tmpl: SystemLabelTemplate
): Promise<SystemLabelTemplate> {
const elements = tmpl.elements || []
if (elements.length === 0) return tmpl
const next = await Promise.all(elements.map((el) => hydrateElement(el)))
return { ...tmpl, elements: next }
}
|