Blame view

美国版/Food Labeling Management App UniApp/src/utils/print/hydrateTemplateImagesForPrint.ts 6.82 KB
b04bc3a5   杨鑫   打印日志 重新打印
1
2
3
4
5
6
7
  /**
   * TSC/Android 位图打印:`BitmapFactory.decodeFile` 无法直接读 http(s) 或「仅相对路径」;
   * 打印前把需网络的图片拉到本地临时路径,重打/预览落库里的 /picture/ URL 才能出图。
   */
  import { resolveMediaUrlForApp, storedValueLooksLikeImagePath } from '../resolveMediaUrl'
  import type { SystemLabelTemplate, SystemTemplateElementBase } from './types/printer'
  
58d2e61c   杨鑫   最新代码
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
  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')
  }
  
b04bc3a5   杨鑫   打印日志 重新打印
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
  /** 与 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
58d2e61c   杨鑫   最新代码
77
    if (s.startsWith('_doc/') || s.startsWith('_www/') || /^[A-Za-z]:[\\/]/.test(s)) return false
b04bc3a5   杨鑫   打印日志 重新打印
78
    if (/^https?:\/\//i.test(s)) return true
58d2e61c   杨鑫   最新代码
79
80
81
82
83
84
    if (
      s.startsWith('/picture/')
      || s.startsWith('/static/')
      || s.startsWith('picture/')
      || s.startsWith('static/')
    ) return true
b04bc3a5   杨鑫   打印日志 重新打印
85
86
87
88
89
90
91
92
93
94
    return false
  }
  
  function downloadUrlToTempFile (url: string): Promise<string | null> {
    return new Promise((resolve) => {
      if (!url) {
        resolve(null)
        return
      }
      try {
58d2e61c   杨鑫   最新代码
95
96
97
98
99
100
        let done = false
        const timer = setTimeout(() => {
          if (done) return
          done = true
          resolve(null)
        }, 6000)
b04bc3a5   杨鑫   打印日志 重新打印
101
102
103
104
        uni.downloadFile({
          url,
          header: downloadAuthHeaders(),
          success: (res) => {
58d2e61c   杨鑫   最新代码
105
106
107
            if (done) return
            done = true
            clearTimeout(timer)
b04bc3a5   杨鑫   打印日志 重新打印
108
109
110
            if (res.statusCode === 200 && res.tempFilePath) resolve(res.tempFilePath)
            else resolve(null)
          },
58d2e61c   杨鑫   最新代码
111
112
113
114
115
116
          fail: () => {
            if (done) return
            done = true
            clearTimeout(timer)
            resolve(null)
          },
b04bc3a5   杨鑫   打印日志 重新打印
117
118
119
120
121
122
123
        })
      } catch {
        resolve(null)
      }
    })
  }
  
58d2e61c   杨鑫   最新代码
124
125
126
127
  async function resolveToLocalPathIfNeeded (
    raw: string,
    debugMeta: Record<string, unknown>,
  ): Promise<string | null> {
b04bc3a5   杨鑫   打印日志 重新打印
128
    const trimmed = String(raw || '').trim()
58d2e61c   杨鑫   最新代码
129
130
131
132
133
134
135
136
    if (!trimmed) {
      logHydrateDebug('skip', { ...debugMeta, reason: 'empty-source' })
      return null
    }
    if (!needsDownloadForDecode(trimmed)) {
      logHydrateDebug('no-need-download', { ...debugMeta, raw: trimmed })
      return null
    }
b04bc3a5   杨鑫   打印日志 重新打印
137
    const url = resolveMediaUrlForApp(trimmed)
58d2e61c   杨鑫   最新代码
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
    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
b04bc3a5   杨鑫   打印日志 重新打印
173
174
175
176
177
  }
  
  async function hydrateElement (el: SystemTemplateElementBase): Promise<SystemTemplateElementBase> {
    const type = String(el.type || '').toUpperCase()
    const cfg = { ...(el.config || {}) } as Record<string, unknown>
58d2e61c   杨鑫   最新代码
178
179
180
181
    const debugMeta = {
      elementId: String(el.id || ''),
      type,
    }
b04bc3a5   杨鑫   打印日志 重新打印
182
183
  
    if (type === 'IMAGE' || type === 'LOGO') {
58d2e61c   杨鑫   最新代码
184
185
      const raw = cfgStr(cfg, ['src', 'url', 'data', 'Src', 'Url', 'Data'])
      const local = await resolveToLocalPathIfNeeded(raw, debugMeta)
b04bc3a5   杨鑫   打印日志 重新打印
186
187
188
      if (!local) return el
      return {
        ...el,
58d2e61c   杨鑫   最新代码
189
        config: { ...cfg, src: local, url: local, data: local, Src: local, Url: local, Data: local },
b04bc3a5   杨鑫   打印日志 重新打印
190
191
192
193
194
      }
    }
  
    if (type === 'QRCODE') {
      const raw = cfgStr(cfg, ['data', 'Data'])
58d2e61c   杨鑫   最新代码
195
196
197
198
199
      if (!raw || !storedValueLooksLikeImagePath(raw)) {
        logHydrateDebug('skip', { ...debugMeta, raw, reason: 'qrcode-data-not-image-path' })
        return el
      }
      const local = await resolveToLocalPathIfNeeded(raw, debugMeta)
b04bc3a5   杨鑫   打印日志 重新打印
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
      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 }
  }