Hanye官网
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

localize-images.mjs 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. #!/usr/bin/env node
  2. /**
  3. * 图片本地化处理脚本
  4. * 在静态站点生成后运行,将API返回的图片下载到本地
  5. */
  6. import fs from 'fs'
  7. import path from 'path'
  8. import { createHash } from 'crypto'
  9. import { fileURLToPath } from 'url'
  10. // 获取当前文件的目录
  11. const __filename = fileURLToPath(import.meta.url)
  12. const __dirname = path.dirname(__filename)
  13. const PROJECT_ROOT = path.resolve(__dirname, '..')
  14. /**
  15. * 下载并本地化图片
  16. * @param imageUrl 原始图片URL
  17. * @param basePath 保存图片的基础路径
  18. * @returns 本地化后的图片路径
  19. */
  20. async function localizeImage(imageUrl, basePath = 'public/images/remote') {
  21. // 检查URL是否有效
  22. if (!imageUrl || !imageUrl.startsWith('http')) {
  23. return imageUrl
  24. }
  25. try {
  26. // 创建保存目录
  27. const fullBasePath = path.resolve(PROJECT_ROOT, basePath)
  28. if (!fs.existsSync(fullBasePath)) {
  29. fs.mkdirSync(fullBasePath, { recursive: true })
  30. }
  31. // 生成唯一文件名(使用URL的MD5哈希)
  32. const urlHash = createHash('md5').update(imageUrl).digest('hex')
  33. const fileExt = path.extname(new URL(imageUrl).pathname) || '.jpg'
  34. const fileName = `${urlHash}${fileExt}`
  35. const filePath = path.join(fullBasePath, fileName)
  36. // 如果文件已存在,直接返回路径
  37. if (fs.existsSync(filePath)) {
  38. return `/images/remote/${fileName}`
  39. }
  40. // 下载图片
  41. const response = await fetch(imageUrl)
  42. if (!response.ok) {
  43. throw new Error(`下载图片失败: ${response.status} ${response.statusText}`)
  44. }
  45. const buffer = Buffer.from(await response.arrayBuffer())
  46. fs.writeFileSync(filePath, buffer)
  47. console.log(`✅ 下载图片成功: ${imageUrl} -> /images/remote/${fileName}`)
  48. // 返回本地URL(相对于public目录)
  49. return `/images/remote/${fileName}`
  50. } catch (error) {
  51. console.error(`❌ 本地化图片失败 ${imageUrl}:`, error)
  52. return imageUrl // 失败时返回原始URL
  53. }
  54. }
  55. /**
  56. * 递归处理对象中的所有图片URL
  57. * @param data 需要处理的数据对象或数组
  58. * @param imageFields 指定哪些字段包含图片URL
  59. * @returns 处理后的数据
  60. */
  61. async function localizeImages(
  62. data,
  63. imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src']
  64. ) {
  65. if (!data) return data
  66. // 处理数组
  67. if (Array.isArray(data)) {
  68. return Promise.all(data.map(item => localizeImages(item, imageFields)))
  69. }
  70. // 处理对象
  71. if (typeof data === 'object') {
  72. const result = { ...data }
  73. // 处理所有键
  74. for (const [key, value] of Object.entries(result)) {
  75. // 如果是图片字段且值是字符串,本地化图片
  76. if (imageFields.includes(key) && typeof value === 'string') {
  77. result[key] = await localizeImage(value)
  78. }
  79. // 递归处理嵌套对象或数组
  80. else if (typeof value === 'object' && value !== null) {
  81. result[key] = await localizeImages(value, imageFields)
  82. }
  83. }
  84. return result
  85. }
  86. return data
  87. }
  88. /**
  89. * 从JSON文件提取图片URL并下载到本地
  90. * @param jsonFilePath JSON文件路径
  91. * @param imageFields 包含图片URL的字段名称数组
  92. */
  93. async function extractAndDownloadImages(
  94. jsonFilePath,
  95. imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src']
  96. ) {
  97. // 检查文件是否存在
  98. if (!fs.existsSync(jsonFilePath)) {
  99. console.error(`文件不存在: ${jsonFilePath}`)
  100. return
  101. }
  102. try {
  103. // 读取JSON文件
  104. const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8'))
  105. // 递归查找所有图片URL
  106. const imageUrls = new Set()
  107. // 递归函数查找所有图片URL
  108. function findImageUrls(obj) {
  109. if (!obj) return
  110. if (Array.isArray(obj)) {
  111. obj.forEach(item => findImageUrls(item))
  112. return
  113. }
  114. if (typeof obj === 'object') {
  115. for (const [key, value] of Object.entries(obj)) {
  116. if (imageFields.includes(key) && typeof value === 'string' && value.startsWith('http')) {
  117. imageUrls.add(value)
  118. } else if (typeof value === 'object' && value !== null) {
  119. findImageUrls(value)
  120. }
  121. }
  122. }
  123. }
  124. findImageUrls(jsonData)
  125. // 下载所有图片
  126. console.log(`📋 在 ${jsonFilePath} 中找到 ${imageUrls.size} 个图片URL`)
  127. let downloadedCount = 0
  128. for (const url of imageUrls) {
  129. try {
  130. const localUrl = await localizeImage(url)
  131. if (localUrl !== url) {
  132. downloadedCount++
  133. }
  134. } catch (error) {
  135. console.error(`❌ 下载失败 ${url}:`, error)
  136. }
  137. }
  138. console.log(`📊 成功下载 ${downloadedCount}/${imageUrls.size} 个图片`)
  139. // 将本地化的图片URL写回到JSON文件
  140. try {
  141. const localizedData = await localizeImages(jsonData)
  142. fs.writeFileSync(jsonFilePath, JSON.stringify(localizedData, null, 2))
  143. console.log(`💾 已更新图片URL至本地路径: ${jsonFilePath}`)
  144. } catch (error) {
  145. console.error(`❌ 无法更新JSON文件 ${jsonFilePath}:`, error)
  146. }
  147. } catch (error) {
  148. console.error(`❌ 处理文件 ${jsonFilePath} 时出错:`, error)
  149. }
  150. }
  151. /**
  152. * 处理API响应文件中的图片
  153. * @param outputDir 输出目录路径(通常是.output目录)
  154. */
  155. async function processApiResponseImages(outputDir = '.output') {
  156. console.log('🚀 开始处理API响应文件中的图片...')
  157. // 检查输出目录是否存在
  158. if (!fs.existsSync(outputDir)) {
  159. console.error(`❌ 输出目录不存在: ${outputDir}`)
  160. return
  161. }
  162. const serverDir = path.join(outputDir, 'server/api')
  163. if (!fs.existsSync(serverDir)) {
  164. console.error(`❌ 服务器API目录不存在: ${serverDir}`)
  165. return
  166. }
  167. console.log(`🔍 扫描API响应文件: ${serverDir}`)
  168. // 递归函数查找所有JSON文件
  169. async function processDirectory(dir) {
  170. const entries = fs.readdirSync(dir, { withFileTypes: true })
  171. for (const entry of entries) {
  172. const fullPath = path.join(dir, entry.name)
  173. if (entry.isDirectory()) {
  174. await processDirectory(fullPath)
  175. } else if (entry.name.endsWith('.json')) {
  176. console.log(`📄 处理JSON文件: ${fullPath}`)
  177. await extractAndDownloadImages(fullPath)
  178. }
  179. }
  180. }
  181. await processDirectory(serverDir)
  182. console.log('✅ 所有API响应文件处理完成')
  183. }
  184. // 确保脚本能单独运行
  185. async function main() {
  186. console.log('===== 🖼️ 开始图片本地化处理 =====')
  187. try {
  188. // 默认的输出目录是.output
  189. const outputDir = process.argv[2] || '.output'
  190. await processApiResponseImages(outputDir)
  191. console.log('===== ✅ 图片本地化处理完成 =====')
  192. process.exit(0)
  193. } catch (error) {
  194. console.error('❌ 图片本地化处理失败:', error)
  195. process.exit(1)
  196. }
  197. }
  198. main()