@zzz0350/ozon-listing-workbench 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1060) hide show
  1. package/README.md +10 -10
  2. package/current.txt +1 -1
  3. package/package.json +4 -4
  4. package/start.ps1 +1 -1
  5. package/update-manifest.json +9 -1056
  6. package/versions/1.0.0/OzonWorkbench.exe +0 -0
  7. package/versions/1.0.0/_internal/OzonStudio.exe +0 -0
  8. package/versions/1.0.0/_internal/THIRD_PARTY_NOTICES.txt +24011 -0
  9. package/versions/1.0.0/_internal/WebView2Loader.dll +0 -0
  10. package/versions/0.5.0/OzonWorkbench.exe +0 -0
  11. package/versions/0.5.0/_internal/81d243bd2c585b0f4821__mypyc.cp314-win_amd64.pyd +0 -0
  12. package/versions/0.5.0/_internal/PIL/_avif.cp314-win_amd64.pyd +0 -0
  13. package/versions/0.5.0/_internal/PIL/_imaging.cp314-win_amd64.pyd +0 -0
  14. package/versions/0.5.0/_internal/PIL/_imagingcms.cp314-win_amd64.pyd +0 -0
  15. package/versions/0.5.0/_internal/PIL/_imagingft.cp314-win_amd64.pyd +0 -0
  16. package/versions/0.5.0/_internal/PIL/_imagingmath.cp314-win_amd64.pyd +0 -0
  17. package/versions/0.5.0/_internal/PIL/_imagingtk.cp314-win_amd64.pyd +0 -0
  18. package/versions/0.5.0/_internal/PIL/_webp.cp314-win_amd64.pyd +0 -0
  19. package/versions/0.5.0/_internal/VCRUNTIME140.dll +0 -0
  20. package/versions/0.5.0/_internal/VCRUNTIME140_1.dll +0 -0
  21. package/versions/0.5.0/_internal/_asyncio.pyd +0 -0
  22. package/versions/0.5.0/_internal/_brotli.cp314-win_amd64.pyd +0 -0
  23. package/versions/0.5.0/_internal/_bz2.pyd +0 -0
  24. package/versions/0.5.0/_internal/_cffi_backend.cp314-win_amd64.pyd +0 -0
  25. package/versions/0.5.0/_internal/_ctypes.pyd +0 -0
  26. package/versions/0.5.0/_internal/_decimal.pyd +0 -0
  27. package/versions/0.5.0/_internal/_elementtree.pyd +0 -0
  28. package/versions/0.5.0/_internal/_hashlib.pyd +0 -0
  29. package/versions/0.5.0/_internal/_lzma.pyd +0 -0
  30. package/versions/0.5.0/_internal/_multiprocessing.pyd +0 -0
  31. package/versions/0.5.0/_internal/_overlapped.pyd +0 -0
  32. package/versions/0.5.0/_internal/_queue.pyd +0 -0
  33. package/versions/0.5.0/_internal/_socket.pyd +0 -0
  34. package/versions/0.5.0/_internal/_sqlite3.pyd +0 -0
  35. package/versions/0.5.0/_internal/_ssl.pyd +0 -0
  36. package/versions/0.5.0/_internal/_tcl_data/auto.tcl +0 -649
  37. package/versions/0.5.0/_internal/_tcl_data/clock.tcl +0 -4553
  38. package/versions/0.5.0/_internal/_tcl_data/encoding/ascii.enc +0 -20
  39. package/versions/0.5.0/_internal/_tcl_data/encoding/big5.enc +0 -1516
  40. package/versions/0.5.0/_internal/_tcl_data/encoding/cns11643.enc +0 -1584
  41. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1250.enc +0 -20
  42. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1251.enc +0 -20
  43. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1252.enc +0 -20
  44. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1253.enc +0 -20
  45. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1254.enc +0 -20
  46. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1255.enc +0 -20
  47. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1256.enc +0 -20
  48. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1257.enc +0 -20
  49. package/versions/0.5.0/_internal/_tcl_data/encoding/cp1258.enc +0 -20
  50. package/versions/0.5.0/_internal/_tcl_data/encoding/cp437.enc +0 -20
  51. package/versions/0.5.0/_internal/_tcl_data/encoding/cp737.enc +0 -20
  52. package/versions/0.5.0/_internal/_tcl_data/encoding/cp775.enc +0 -20
  53. package/versions/0.5.0/_internal/_tcl_data/encoding/cp850.enc +0 -20
  54. package/versions/0.5.0/_internal/_tcl_data/encoding/cp852.enc +0 -20
  55. package/versions/0.5.0/_internal/_tcl_data/encoding/cp855.enc +0 -20
  56. package/versions/0.5.0/_internal/_tcl_data/encoding/cp857.enc +0 -20
  57. package/versions/0.5.0/_internal/_tcl_data/encoding/cp860.enc +0 -20
  58. package/versions/0.5.0/_internal/_tcl_data/encoding/cp861.enc +0 -20
  59. package/versions/0.5.0/_internal/_tcl_data/encoding/cp862.enc +0 -20
  60. package/versions/0.5.0/_internal/_tcl_data/encoding/cp863.enc +0 -20
  61. package/versions/0.5.0/_internal/_tcl_data/encoding/cp864.enc +0 -20
  62. package/versions/0.5.0/_internal/_tcl_data/encoding/cp865.enc +0 -20
  63. package/versions/0.5.0/_internal/_tcl_data/encoding/cp866.enc +0 -20
  64. package/versions/0.5.0/_internal/_tcl_data/encoding/cp869.enc +0 -20
  65. package/versions/0.5.0/_internal/_tcl_data/encoding/cp874.enc +0 -20
  66. package/versions/0.5.0/_internal/_tcl_data/encoding/cp932.enc +0 -801
  67. package/versions/0.5.0/_internal/_tcl_data/encoding/cp936.enc +0 -2162
  68. package/versions/0.5.0/_internal/_tcl_data/encoding/cp949.enc +0 -2128
  69. package/versions/0.5.0/_internal/_tcl_data/encoding/cp950.enc +0 -1499
  70. package/versions/0.5.0/_internal/_tcl_data/encoding/dingbats.enc +0 -20
  71. package/versions/0.5.0/_internal/_tcl_data/encoding/ebcdic.enc +0 -19
  72. package/versions/0.5.0/_internal/_tcl_data/encoding/euc-cn.enc +0 -1397
  73. package/versions/0.5.0/_internal/_tcl_data/encoding/euc-jp.enc +0 -1353
  74. package/versions/0.5.0/_internal/_tcl_data/encoding/euc-kr.enc +0 -1533
  75. package/versions/0.5.0/_internal/_tcl_data/encoding/gb12345.enc +0 -1414
  76. package/versions/0.5.0/_internal/_tcl_data/encoding/gb1988.enc +0 -20
  77. package/versions/0.5.0/_internal/_tcl_data/encoding/gb2312-raw.enc +0 -1380
  78. package/versions/0.5.0/_internal/_tcl_data/encoding/gb2312.enc +0 -1397
  79. package/versions/0.5.0/_internal/_tcl_data/encoding/iso2022-jp.enc +0 -12
  80. package/versions/0.5.0/_internal/_tcl_data/encoding/iso2022-kr.enc +0 -7
  81. package/versions/0.5.0/_internal/_tcl_data/encoding/iso2022.enc +0 -14
  82. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-1.enc +0 -20
  83. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-10.enc +0 -20
  84. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-11.enc +0 -20
  85. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-13.enc +0 -20
  86. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-14.enc +0 -20
  87. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-15.enc +0 -20
  88. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-16.enc +0 -20
  89. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-2.enc +0 -20
  90. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-3.enc +0 -20
  91. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-4.enc +0 -20
  92. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-5.enc +0 -20
  93. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-6.enc +0 -20
  94. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-7.enc +0 -20
  95. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-8.enc +0 -20
  96. package/versions/0.5.0/_internal/_tcl_data/encoding/iso8859-9.enc +0 -20
  97. package/versions/0.5.0/_internal/_tcl_data/encoding/jis0201.enc +0 -20
  98. package/versions/0.5.0/_internal/_tcl_data/encoding/jis0208.enc +0 -1319
  99. package/versions/0.5.0/_internal/_tcl_data/encoding/jis0212.enc +0 -1159
  100. package/versions/0.5.0/_internal/_tcl_data/encoding/koi8-r.enc +0 -20
  101. package/versions/0.5.0/_internal/_tcl_data/encoding/koi8-ru.enc +0 -20
  102. package/versions/0.5.0/_internal/_tcl_data/encoding/koi8-t.enc +0 -20
  103. package/versions/0.5.0/_internal/_tcl_data/encoding/koi8-u.enc +0 -20
  104. package/versions/0.5.0/_internal/_tcl_data/encoding/ksc5601.enc +0 -1516
  105. package/versions/0.5.0/_internal/_tcl_data/encoding/macCentEuro.enc +0 -20
  106. package/versions/0.5.0/_internal/_tcl_data/encoding/macCroatian.enc +0 -20
  107. package/versions/0.5.0/_internal/_tcl_data/encoding/macCyrillic.enc +0 -20
  108. package/versions/0.5.0/_internal/_tcl_data/encoding/macDingbats.enc +0 -20
  109. package/versions/0.5.0/_internal/_tcl_data/encoding/macGreek.enc +0 -20
  110. package/versions/0.5.0/_internal/_tcl_data/encoding/macIceland.enc +0 -20
  111. package/versions/0.5.0/_internal/_tcl_data/encoding/macJapan.enc +0 -785
  112. package/versions/0.5.0/_internal/_tcl_data/encoding/macRoman.enc +0 -20
  113. package/versions/0.5.0/_internal/_tcl_data/encoding/macRomania.enc +0 -20
  114. package/versions/0.5.0/_internal/_tcl_data/encoding/macThai.enc +0 -20
  115. package/versions/0.5.0/_internal/_tcl_data/encoding/macTurkish.enc +0 -20
  116. package/versions/0.5.0/_internal/_tcl_data/encoding/macUkraine.enc +0 -20
  117. package/versions/0.5.0/_internal/_tcl_data/encoding/shiftjis.enc +0 -690
  118. package/versions/0.5.0/_internal/_tcl_data/encoding/symbol.enc +0 -20
  119. package/versions/0.5.0/_internal/_tcl_data/encoding/tis-620.enc +0 -20
  120. package/versions/0.5.0/_internal/_tcl_data/history.tcl +0 -335
  121. package/versions/0.5.0/_internal/_tcl_data/http1.0/http.tcl +0 -377
  122. package/versions/0.5.0/_internal/_tcl_data/http1.0/pkgIndex.tcl +0 -11
  123. package/versions/0.5.0/_internal/_tcl_data/init.tcl +0 -828
  124. package/versions/0.5.0/_internal/_tcl_data/msgs/af.msg +0 -49
  125. package/versions/0.5.0/_internal/_tcl_data/msgs/af_za.msg +0 -6
  126. package/versions/0.5.0/_internal/_tcl_data/msgs/ar.msg +0 -54
  127. package/versions/0.5.0/_internal/_tcl_data/msgs/ar_in.msg +0 -6
  128. package/versions/0.5.0/_internal/_tcl_data/msgs/ar_jo.msg +0 -39
  129. package/versions/0.5.0/_internal/_tcl_data/msgs/ar_lb.msg +0 -39
  130. package/versions/0.5.0/_internal/_tcl_data/msgs/ar_sy.msg +0 -39
  131. package/versions/0.5.0/_internal/_tcl_data/msgs/be.msg +0 -52
  132. package/versions/0.5.0/_internal/_tcl_data/msgs/bg.msg +0 -52
  133. package/versions/0.5.0/_internal/_tcl_data/msgs/bn.msg +0 -49
  134. package/versions/0.5.0/_internal/_tcl_data/msgs/bn_in.msg +0 -6
  135. package/versions/0.5.0/_internal/_tcl_data/msgs/ca.msg +0 -50
  136. package/versions/0.5.0/_internal/_tcl_data/msgs/cs.msg +0 -54
  137. package/versions/0.5.0/_internal/_tcl_data/msgs/da.msg +0 -52
  138. package/versions/0.5.0/_internal/_tcl_data/msgs/de.msg +0 -54
  139. package/versions/0.5.0/_internal/_tcl_data/msgs/de_at.msg +0 -35
  140. package/versions/0.5.0/_internal/_tcl_data/msgs/de_be.msg +0 -53
  141. package/versions/0.5.0/_internal/_tcl_data/msgs/el.msg +0 -52
  142. package/versions/0.5.0/_internal/_tcl_data/msgs/en_au.msg +0 -7
  143. package/versions/0.5.0/_internal/_tcl_data/msgs/en_be.msg +0 -7
  144. package/versions/0.5.0/_internal/_tcl_data/msgs/en_bw.msg +0 -6
  145. package/versions/0.5.0/_internal/_tcl_data/msgs/en_ca.msg +0 -7
  146. package/versions/0.5.0/_internal/_tcl_data/msgs/en_gb.msg +0 -7
  147. package/versions/0.5.0/_internal/_tcl_data/msgs/en_hk.msg +0 -8
  148. package/versions/0.5.0/_internal/_tcl_data/msgs/en_ie.msg +0 -7
  149. package/versions/0.5.0/_internal/_tcl_data/msgs/en_in.msg +0 -8
  150. package/versions/0.5.0/_internal/_tcl_data/msgs/en_nz.msg +0 -7
  151. package/versions/0.5.0/_internal/_tcl_data/msgs/en_ph.msg +0 -8
  152. package/versions/0.5.0/_internal/_tcl_data/msgs/en_sg.msg +0 -6
  153. package/versions/0.5.0/_internal/_tcl_data/msgs/en_za.msg +0 -6
  154. package/versions/0.5.0/_internal/_tcl_data/msgs/en_zw.msg +0 -6
  155. package/versions/0.5.0/_internal/_tcl_data/msgs/eo.msg +0 -54
  156. package/versions/0.5.0/_internal/_tcl_data/msgs/es.msg +0 -52
  157. package/versions/0.5.0/_internal/_tcl_data/msgs/es_ar.msg +0 -6
  158. package/versions/0.5.0/_internal/_tcl_data/msgs/es_bo.msg +0 -6
  159. package/versions/0.5.0/_internal/_tcl_data/msgs/es_cl.msg +0 -6
  160. package/versions/0.5.0/_internal/_tcl_data/msgs/es_co.msg +0 -6
  161. package/versions/0.5.0/_internal/_tcl_data/msgs/es_cr.msg +0 -6
  162. package/versions/0.5.0/_internal/_tcl_data/msgs/es_do.msg +0 -6
  163. package/versions/0.5.0/_internal/_tcl_data/msgs/es_ec.msg +0 -6
  164. package/versions/0.5.0/_internal/_tcl_data/msgs/es_gt.msg +0 -6
  165. package/versions/0.5.0/_internal/_tcl_data/msgs/es_hn.msg +0 -6
  166. package/versions/0.5.0/_internal/_tcl_data/msgs/es_mx.msg +0 -6
  167. package/versions/0.5.0/_internal/_tcl_data/msgs/es_ni.msg +0 -6
  168. package/versions/0.5.0/_internal/_tcl_data/msgs/es_pa.msg +0 -6
  169. package/versions/0.5.0/_internal/_tcl_data/msgs/es_pe.msg +0 -6
  170. package/versions/0.5.0/_internal/_tcl_data/msgs/es_pr.msg +0 -6
  171. package/versions/0.5.0/_internal/_tcl_data/msgs/es_py.msg +0 -6
  172. package/versions/0.5.0/_internal/_tcl_data/msgs/es_sv.msg +0 -6
  173. package/versions/0.5.0/_internal/_tcl_data/msgs/es_uy.msg +0 -6
  174. package/versions/0.5.0/_internal/_tcl_data/msgs/es_ve.msg +0 -6
  175. package/versions/0.5.0/_internal/_tcl_data/msgs/et.msg +0 -52
  176. package/versions/0.5.0/_internal/_tcl_data/msgs/eu.msg +0 -47
  177. package/versions/0.5.0/_internal/_tcl_data/msgs/eu_es.msg +0 -7
  178. package/versions/0.5.0/_internal/_tcl_data/msgs/fa.msg +0 -47
  179. package/versions/0.5.0/_internal/_tcl_data/msgs/fa_in.msg +0 -52
  180. package/versions/0.5.0/_internal/_tcl_data/msgs/fa_ir.msg +0 -9
  181. package/versions/0.5.0/_internal/_tcl_data/msgs/fi.msg +0 -50
  182. package/versions/0.5.0/_internal/_tcl_data/msgs/fo.msg +0 -47
  183. package/versions/0.5.0/_internal/_tcl_data/msgs/fo_fo.msg +0 -7
  184. package/versions/0.5.0/_internal/_tcl_data/msgs/fr.msg +0 -52
  185. package/versions/0.5.0/_internal/_tcl_data/msgs/fr_be.msg +0 -7
  186. package/versions/0.5.0/_internal/_tcl_data/msgs/fr_ca.msg +0 -7
  187. package/versions/0.5.0/_internal/_tcl_data/msgs/fr_ch.msg +0 -7
  188. package/versions/0.5.0/_internal/_tcl_data/msgs/ga.msg +0 -47
  189. package/versions/0.5.0/_internal/_tcl_data/msgs/ga_ie.msg +0 -7
  190. package/versions/0.5.0/_internal/_tcl_data/msgs/gl.msg +0 -47
  191. package/versions/0.5.0/_internal/_tcl_data/msgs/gl_es.msg +0 -6
  192. package/versions/0.5.0/_internal/_tcl_data/msgs/gv.msg +0 -47
  193. package/versions/0.5.0/_internal/_tcl_data/msgs/gv_gb.msg +0 -6
  194. package/versions/0.5.0/_internal/_tcl_data/msgs/he.msg +0 -52
  195. package/versions/0.5.0/_internal/_tcl_data/msgs/hi.msg +0 -39
  196. package/versions/0.5.0/_internal/_tcl_data/msgs/hi_in.msg +0 -6
  197. package/versions/0.5.0/_internal/_tcl_data/msgs/hr.msg +0 -50
  198. package/versions/0.5.0/_internal/_tcl_data/msgs/hu.msg +0 -54
  199. package/versions/0.5.0/_internal/_tcl_data/msgs/id.msg +0 -47
  200. package/versions/0.5.0/_internal/_tcl_data/msgs/id_id.msg +0 -6
  201. package/versions/0.5.0/_internal/_tcl_data/msgs/is.msg +0 -50
  202. package/versions/0.5.0/_internal/_tcl_data/msgs/it.msg +0 -54
  203. package/versions/0.5.0/_internal/_tcl_data/msgs/it_ch.msg +0 -6
  204. package/versions/0.5.0/_internal/_tcl_data/msgs/ja.msg +0 -44
  205. package/versions/0.5.0/_internal/_tcl_data/msgs/kl.msg +0 -47
  206. package/versions/0.5.0/_internal/_tcl_data/msgs/kl_gl.msg +0 -7
  207. package/versions/0.5.0/_internal/_tcl_data/msgs/ko.msg +0 -55
  208. package/versions/0.5.0/_internal/_tcl_data/msgs/ko_kr.msg +0 -8
  209. package/versions/0.5.0/_internal/_tcl_data/msgs/kok.msg +0 -39
  210. package/versions/0.5.0/_internal/_tcl_data/msgs/kok_in.msg +0 -6
  211. package/versions/0.5.0/_internal/_tcl_data/msgs/kw.msg +0 -47
  212. package/versions/0.5.0/_internal/_tcl_data/msgs/kw_gb.msg +0 -6
  213. package/versions/0.5.0/_internal/_tcl_data/msgs/lt.msg +0 -52
  214. package/versions/0.5.0/_internal/_tcl_data/msgs/lv.msg +0 -52
  215. package/versions/0.5.0/_internal/_tcl_data/msgs/mk.msg +0 -52
  216. package/versions/0.5.0/_internal/_tcl_data/msgs/mr.msg +0 -39
  217. package/versions/0.5.0/_internal/_tcl_data/msgs/mr_in.msg +0 -6
  218. package/versions/0.5.0/_internal/_tcl_data/msgs/ms.msg +0 -47
  219. package/versions/0.5.0/_internal/_tcl_data/msgs/ms_my.msg +0 -6
  220. package/versions/0.5.0/_internal/_tcl_data/msgs/mt.msg +0 -27
  221. package/versions/0.5.0/_internal/_tcl_data/msgs/nb.msg +0 -52
  222. package/versions/0.5.0/_internal/_tcl_data/msgs/nl.msg +0 -50
  223. package/versions/0.5.0/_internal/_tcl_data/msgs/nl_be.msg +0 -7
  224. package/versions/0.5.0/_internal/_tcl_data/msgs/nn.msg +0 -52
  225. package/versions/0.5.0/_internal/_tcl_data/msgs/pl.msg +0 -52
  226. package/versions/0.5.0/_internal/_tcl_data/msgs/pt.msg +0 -50
  227. package/versions/0.5.0/_internal/_tcl_data/msgs/pt_br.msg +0 -7
  228. package/versions/0.5.0/_internal/_tcl_data/msgs/ro.msg +0 -52
  229. package/versions/0.5.0/_internal/_tcl_data/msgs/ru.msg +0 -52
  230. package/versions/0.5.0/_internal/_tcl_data/msgs/ru_ua.msg +0 -6
  231. package/versions/0.5.0/_internal/_tcl_data/msgs/sh.msg +0 -52
  232. package/versions/0.5.0/_internal/_tcl_data/msgs/sk.msg +0 -52
  233. package/versions/0.5.0/_internal/_tcl_data/msgs/sl.msg +0 -52
  234. package/versions/0.5.0/_internal/_tcl_data/msgs/sq.msg +0 -54
  235. package/versions/0.5.0/_internal/_tcl_data/msgs/sr.msg +0 -52
  236. package/versions/0.5.0/_internal/_tcl_data/msgs/sv.msg +0 -52
  237. package/versions/0.5.0/_internal/_tcl_data/msgs/sw.msg +0 -49
  238. package/versions/0.5.0/_internal/_tcl_data/msgs/ta.msg +0 -39
  239. package/versions/0.5.0/_internal/_tcl_data/msgs/ta_in.msg +0 -6
  240. package/versions/0.5.0/_internal/_tcl_data/msgs/te.msg +0 -47
  241. package/versions/0.5.0/_internal/_tcl_data/msgs/te_in.msg +0 -8
  242. package/versions/0.5.0/_internal/_tcl_data/msgs/th.msg +0 -54
  243. package/versions/0.5.0/_internal/_tcl_data/msgs/tr.msg +0 -50
  244. package/versions/0.5.0/_internal/_tcl_data/msgs/uk.msg +0 -52
  245. package/versions/0.5.0/_internal/_tcl_data/msgs/vi.msg +0 -50
  246. package/versions/0.5.0/_internal/_tcl_data/msgs/zh.msg +0 -55
  247. package/versions/0.5.0/_internal/_tcl_data/msgs/zh_cn.msg +0 -7
  248. package/versions/0.5.0/_internal/_tcl_data/msgs/zh_hk.msg +0 -28
  249. package/versions/0.5.0/_internal/_tcl_data/msgs/zh_sg.msg +0 -8
  250. package/versions/0.5.0/_internal/_tcl_data/msgs/zh_tw.msg +0 -8
  251. package/versions/0.5.0/_internal/_tcl_data/opt0.4/optparse.tcl +0 -1072
  252. package/versions/0.5.0/_internal/_tcl_data/opt0.4/pkgIndex.tcl +0 -12
  253. package/versions/0.5.0/_internal/_tcl_data/package.tcl +0 -751
  254. package/versions/0.5.0/_internal/_tcl_data/parray.tcl +0 -28
  255. package/versions/0.5.0/_internal/_tcl_data/safe.tcl +0 -1293
  256. package/versions/0.5.0/_internal/_tcl_data/tclIndex +0 -85
  257. package/versions/0.5.0/_internal/_tcl_data/tm.tcl +0 -380
  258. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Abidjan +0 -6
  259. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Accra +0 -5
  260. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Addis_Ababa +0 -5
  261. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Algiers +0 -39
  262. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Asmara +0 -5
  263. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Asmera +0 -5
  264. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Bamako +0 -5
  265. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Bangui +0 -5
  266. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Banjul +0 -5
  267. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Bissau +0 -7
  268. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Blantyre +0 -5
  269. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Brazzaville +0 -5
  270. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Bujumbura +0 -5
  271. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Cairo +0 -286
  272. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Casablanca +0 -202
  273. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Ceuta +0 -259
  274. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Conakry +0 -5
  275. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Dakar +0 -5
  276. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Dar_es_Salaam +0 -5
  277. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Djibouti +0 -5
  278. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Douala +0 -5
  279. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/El_Aaiun +0 -191
  280. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Freetown +0 -5
  281. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Gaborone +0 -5
  282. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Harare +0 -5
  283. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Johannesburg +0 -11
  284. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Juba +0 -40
  285. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Kampala +0 -5
  286. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Khartoum +0 -40
  287. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Kigali +0 -5
  288. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Kinshasa +0 -5
  289. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Lagos +0 -9
  290. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Libreville +0 -5
  291. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Lome +0 -5
  292. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Luanda +0 -5
  293. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Lubumbashi +0 -5
  294. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Lusaka +0 -5
  295. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Malabo +0 -5
  296. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Maputo +0 -6
  297. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Maseru +0 -5
  298. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Mbabane +0 -5
  299. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Mogadishu +0 -5
  300. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Monrovia +0 -8
  301. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Nairobi +0 -10
  302. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Ndjamena +0 -8
  303. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Niamey +0 -5
  304. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Nouakchott +0 -5
  305. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Ouagadougou +0 -5
  306. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Porto-Novo +0 -5
  307. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Sao_Tome +0 -9
  308. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Timbuktu +0 -5
  309. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Tripoli +0 -34
  310. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Tunis +0 -39
  311. package/versions/0.5.0/_internal/_tcl_data/tzdata/Africa/Windhoek +0 -58
  312. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Adak +0 -276
  313. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Anchorage +0 -275
  314. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Anguilla +0 -5
  315. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Antigua +0 -5
  316. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Araguaina +0 -60
  317. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Buenos_Aires +0 -67
  318. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Catamarca +0 -68
  319. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/ComodRivadavia +0 -5
  320. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Cordoba +0 -67
  321. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Jujuy +0 -67
  322. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/La_Rioja +0 -69
  323. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Mendoza +0 -68
  324. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Rio_Gallegos +0 -68
  325. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Salta +0 -66
  326. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/San_Juan +0 -69
  327. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/San_Luis +0 -68
  328. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Tucuman +0 -69
  329. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Argentina/Ushuaia +0 -68
  330. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Aruba +0 -5
  331. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Asuncion +0 -259
  332. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Atikokan +0 -5
  333. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Atka +0 -5
  334. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Bahia +0 -68
  335. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Bahia_Banderas +0 -67
  336. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Barbados +0 -22
  337. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Belem +0 -35
  338. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Belize +0 -103
  339. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Blanc-Sablon +0 -5
  340. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Boa_Vista +0 -40
  341. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Bogota +0 -9
  342. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Boise +0 -281
  343. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Buenos_Aires +0 -5
  344. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Cambridge_Bay +0 -266
  345. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Campo_Grande +0 -96
  346. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Cancun +0 -48
  347. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Caracas +0 -10
  348. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Catamarca +0 -5
  349. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Cayenne +0 -7
  350. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Cayman +0 -5
  351. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Chicago +0 -369
  352. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Chihuahua +0 -67
  353. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Ciudad_Juarez +0 -223
  354. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Coral_Harbour +0 -5
  355. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Cordoba +0 -5
  356. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Costa_Rica +0 -15
  357. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Creston +0 -5
  358. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Cuiaba +0 -96
  359. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Curacao +0 -5
  360. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Danmarkshavn +0 -39
  361. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Dawson +0 -100
  362. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Dawson_Creek +0 -64
  363. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Denver +0 -291
  364. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Detroit +0 -275
  365. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Dominica +0 -5
  366. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Edmonton +0 -280
  367. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Eirunepe +0 -41
  368. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/El_Salvador +0 -10
  369. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Ensenada +0 -5
  370. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Fort_Nelson +0 -151
  371. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Fort_Wayne +0 -5
  372. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Fortaleza +0 -48
  373. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Glace_Bay +0 -273
  374. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Godthab +0 -5
  375. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Goose_Bay +0 -338
  376. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Grand_Turk +0 -245
  377. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Grenada +0 -5
  378. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Guadeloupe +0 -5
  379. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Guatemala +0 -14
  380. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Guayaquil +0 -9
  381. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Guyana +0 -9
  382. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Halifax +0 -361
  383. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Havana +0 -285
  384. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Hermosillo +0 -20
  385. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indiana/Indianapolis +0 -234
  386. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indiana/Knox +0 -285
  387. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indiana/Marengo +0 -236
  388. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indiana/Petersburg +0 -247
  389. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indiana/Tell_City +0 -230
  390. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indiana/Vevay +0 -213
  391. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indiana/Vincennes +0 -234
  392. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indiana/Winamac +0 -240
  393. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Indianapolis +0 -5
  394. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Inuvik +0 -263
  395. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Iqaluit +0 -264
  396. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Jamaica +0 -29
  397. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Jujuy +0 -5
  398. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Juneau +0 -276
  399. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Kentucky/Louisville +0 -311
  400. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Kentucky/Monticello +0 -279
  401. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Knox_IN +0 -5
  402. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Kralendijk +0 -5
  403. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/La_Paz +0 -8
  404. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Lima +0 -16
  405. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Los_Angeles +0 -317
  406. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Louisville +0 -5
  407. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Lower_Princes +0 -5
  408. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Maceio +0 -52
  409. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Managua +0 -21
  410. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Manaus +0 -39
  411. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Marigot +0 -5
  412. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Martinique +0 -9
  413. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Matamoros +0 -219
  414. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Mazatlan +0 -67
  415. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Mendoza +0 -5
  416. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Menominee +0 -274
  417. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Merida +0 -62
  418. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Metlakatla +0 -213
  419. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Mexico_City +0 -74
  420. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Miquelon +0 -234
  421. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Moncton +0 -342
  422. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Monterrey +0 -69
  423. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Montevideo +0 -96
  424. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Montreal +0 -5
  425. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Montserrat +0 -5
  426. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Nassau +0 -5
  427. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/New_York +0 -369
  428. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Nipigon +0 -5
  429. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Nome +0 -276
  430. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Noronha +0 -48
  431. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/North_Dakota/Beulah +0 -279
  432. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/North_Dakota/Center +0 -279
  433. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/North_Dakota/New_Salem +0 -279
  434. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Nuuk +0 -246
  435. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Ojinaga +0 -223
  436. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Panama +0 -7
  437. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Pangnirtung +0 -5
  438. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Paramaribo +0 -9
  439. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Phoenix +0 -17
  440. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Port-au-Prince +0 -215
  441. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Port_of_Spain +0 -5
  442. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Porto_Acre +0 -5
  443. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Porto_Velho +0 -35
  444. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Puerto_Rico +0 -10
  445. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Punta_Arenas +0 -123
  446. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Rainy_River +0 -5
  447. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Rankin_Inlet +0 -262
  448. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Recife +0 -48
  449. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Regina +0 -58
  450. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Resolute +0 -262
  451. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Rio_Branco +0 -37
  452. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Rosario +0 -5
  453. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Santa_Isabel +0 -5
  454. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Santarem +0 -36
  455. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Santiago +0 -289
  456. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Santo_Domingo +0 -21
  457. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Sao_Paulo +0 -97
  458. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Scoresbysund +0 -246
  459. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Shiprock +0 -5
  460. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Sitka +0 -275
  461. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/St_Barthelemy +0 -5
  462. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/St_Johns +0 -372
  463. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/St_Kitts +0 -5
  464. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/St_Lucia +0 -5
  465. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/St_Thomas +0 -5
  466. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/St_Vincent +0 -5
  467. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Swift_Current +0 -29
  468. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Tegucigalpa +0 -12
  469. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Thule +0 -224
  470. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Thunder_Bay +0 -5
  471. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Tijuana +0 -291
  472. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Toronto +0 -365
  473. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Tortola +0 -5
  474. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Vancouver +0 -320
  475. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Virgin +0 -5
  476. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Whitehorse +0 -100
  477. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Winnipeg +0 -316
  478. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Yakutat +0 -276
  479. package/versions/0.5.0/_internal/_tcl_data/tzdata/America/Yellowknife +0 -5
  480. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Casey +0 -22
  481. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Davis +0 -12
  482. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/DumontDUrville +0 -5
  483. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Macquarie +0 -276
  484. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Mawson +0 -7
  485. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/McMurdo +0 -5
  486. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Palmer +0 -87
  487. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Rothera +0 -6
  488. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/South_Pole +0 -5
  489. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Syowa +0 -5
  490. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Troll +0 -196
  491. package/versions/0.5.0/_internal/_tcl_data/tzdata/Antarctica/Vostok +0 -9
  492. package/versions/0.5.0/_internal/_tcl_data/tzdata/Arctic/Longyearbyen +0 -5
  493. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Aden +0 -5
  494. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Almaty +0 -58
  495. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Amman +0 -92
  496. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Anadyr +0 -72
  497. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Aqtau +0 -58
  498. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Aqtobe +0 -58
  499. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Ashgabat +0 -31
  500. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Ashkhabad +0 -5
  501. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Atyrau +0 -58
  502. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Baghdad +0 -59
  503. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Bahrain +0 -5
  504. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Baku +0 -74
  505. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Bangkok +0 -7
  506. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Barnaul +0 -73
  507. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Beirut +0 -270
  508. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Bishkek +0 -58
  509. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Brunei +0 -5
  510. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Calcutta +0 -5
  511. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Chita +0 -72
  512. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Choibalsan +0 -5
  513. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Chongqing +0 -5
  514. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Chungking +0 -5
  515. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Colombo +0 -13
  516. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Dacca +0 -5
  517. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Damascus +0 -126
  518. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Dhaka +0 -13
  519. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Dili +0 -9
  520. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Dubai +0 -6
  521. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Dushanbe +0 -29
  522. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Famagusta +0 -256
  523. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Gaza +0 -344
  524. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Harbin +0 -5
  525. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Hebron +0 -343
  526. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Ho_Chi_Minh +0 -14
  527. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Hong_Kong +0 -75
  528. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Hovd +0 -55
  529. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Irkutsk +0 -72
  530. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Istanbul +0 -5
  531. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Jakarta +0 -13
  532. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Jayapura +0 -8
  533. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Jerusalem +0 -278
  534. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Kabul +0 -7
  535. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Kamchatka +0 -71
  536. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Karachi +0 -16
  537. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Kashgar +0 -5
  538. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Kathmandu +0 -7
  539. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Katmandu +0 -5
  540. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Khandyga +0 -73
  541. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Kolkata +0 -12
  542. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Krasnoyarsk +0 -71
  543. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Kuala_Lumpur +0 -5
  544. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Kuching +0 -23
  545. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Kuwait +0 -5
  546. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Macao +0 -5
  547. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Macau +0 -76
  548. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Magadan +0 -72
  549. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Makassar +0 -9
  550. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Manila +0 -15
  551. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Muscat +0 -5
  552. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Nicosia +0 -257
  553. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Novokuznetsk +0 -71
  554. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Novosibirsk +0 -73
  555. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Omsk +0 -71
  556. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Oral +0 -58
  557. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Phnom_Penh +0 -5
  558. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Pontianak +0 -13
  559. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Pyongyang +0 -10
  560. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Qatar +0 -7
  561. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Qostanay +0 -59
  562. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Qyzylorda +0 -58
  563. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Rangoon +0 -5
  564. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Riyadh +0 -6
  565. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Saigon +0 -5
  566. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Sakhalin +0 -73
  567. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Samarkand +0 -31
  568. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Seoul +0 -34
  569. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Shanghai +0 -34
  570. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Singapore +0 -13
  571. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Srednekolymsk +0 -71
  572. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Taipei +0 -46
  573. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Tashkent +0 -31
  574. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Tbilisi +0 -60
  575. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Tehran +0 -76
  576. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Tel_Aviv +0 -5
  577. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Thimbu +0 -5
  578. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Thimphu +0 -7
  579. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Tokyo +0 -14
  580. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Tomsk +0 -73
  581. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Ujung_Pandang +0 -5
  582. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Ulaanbaatar +0 -55
  583. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Ulan_Bator +0 -5
  584. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Urumqi +0 -6
  585. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Ust-Nera +0 -71
  586. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Vientiane +0 -5
  587. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Vladivostok +0 -71
  588. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Yakutsk +0 -71
  589. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Yangon +0 -9
  590. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Yekaterinburg +0 -72
  591. package/versions/0.5.0/_internal/_tcl_data/tzdata/Asia/Yerevan +0 -70
  592. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Azores +0 -346
  593. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Bermuda +0 -286
  594. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Canary +0 -247
  595. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Cape_Verde +0 -9
  596. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Faeroe +0 -5
  597. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Faroe +0 -245
  598. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Jan_Mayen +0 -5
  599. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Madeira +0 -344
  600. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Reykjavik +0 -5
  601. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/South_Georgia +0 -6
  602. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/St_Helena +0 -5
  603. package/versions/0.5.0/_internal/_tcl_data/tzdata/Atlantic/Stanley +0 -75
  604. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/ACT +0 -5
  605. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Adelaide +0 -273
  606. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Brisbane +0 -23
  607. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Broken_Hill +0 -275
  608. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Canberra +0 -5
  609. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Currie +0 -5
  610. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Darwin +0 -15
  611. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Eucla +0 -25
  612. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Hobart +0 -285
  613. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/LHI +0 -5
  614. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Lindeman +0 -28
  615. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Lord_Howe +0 -245
  616. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Melbourne +0 -272
  617. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/NSW +0 -5
  618. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/North +0 -5
  619. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Perth +0 -25
  620. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Queensland +0 -5
  621. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/South +0 -5
  622. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Sydney +0 -272
  623. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Tasmania +0 -5
  624. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Victoria +0 -5
  625. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/West +0 -5
  626. package/versions/0.5.0/_internal/_tcl_data/tzdata/Australia/Yancowinna +0 -5
  627. package/versions/0.5.0/_internal/_tcl_data/tzdata/Brazil/Acre +0 -5
  628. package/versions/0.5.0/_internal/_tcl_data/tzdata/Brazil/DeNoronha +0 -5
  629. package/versions/0.5.0/_internal/_tcl_data/tzdata/Brazil/East +0 -5
  630. package/versions/0.5.0/_internal/_tcl_data/tzdata/Brazil/West +0 -5
  631. package/versions/0.5.0/_internal/_tcl_data/tzdata/CET +0 -5
  632. package/versions/0.5.0/_internal/_tcl_data/tzdata/CST6CDT +0 -5
  633. package/versions/0.5.0/_internal/_tcl_data/tzdata/Canada/Atlantic +0 -5
  634. package/versions/0.5.0/_internal/_tcl_data/tzdata/Canada/Central +0 -5
  635. package/versions/0.5.0/_internal/_tcl_data/tzdata/Canada/Eastern +0 -5
  636. package/versions/0.5.0/_internal/_tcl_data/tzdata/Canada/Mountain +0 -5
  637. package/versions/0.5.0/_internal/_tcl_data/tzdata/Canada/Newfoundland +0 -5
  638. package/versions/0.5.0/_internal/_tcl_data/tzdata/Canada/Pacific +0 -5
  639. package/versions/0.5.0/_internal/_tcl_data/tzdata/Canada/Saskatchewan +0 -5
  640. package/versions/0.5.0/_internal/_tcl_data/tzdata/Canada/Yukon +0 -5
  641. package/versions/0.5.0/_internal/_tcl_data/tzdata/Chile/Continental +0 -5
  642. package/versions/0.5.0/_internal/_tcl_data/tzdata/Chile/EasterIsland +0 -5
  643. package/versions/0.5.0/_internal/_tcl_data/tzdata/Cuba +0 -5
  644. package/versions/0.5.0/_internal/_tcl_data/tzdata/EET +0 -5
  645. package/versions/0.5.0/_internal/_tcl_data/tzdata/EST +0 -5
  646. package/versions/0.5.0/_internal/_tcl_data/tzdata/EST5EDT +0 -5
  647. package/versions/0.5.0/_internal/_tcl_data/tzdata/Egypt +0 -5
  648. package/versions/0.5.0/_internal/_tcl_data/tzdata/Eire +0 -5
  649. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT +0 -5
  650. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+0 +0 -5
  651. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+1 +0 -5
  652. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+10 +0 -5
  653. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+11 +0 -5
  654. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+12 +0 -5
  655. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+2 +0 -5
  656. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+3 +0 -5
  657. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+4 +0 -5
  658. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+5 +0 -5
  659. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+6 +0 -5
  660. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+7 +0 -5
  661. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+8 +0 -5
  662. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT+9 +0 -5
  663. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-0 +0 -5
  664. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-1 +0 -5
  665. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-10 +0 -5
  666. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-11 +0 -5
  667. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-12 +0 -5
  668. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-13 +0 -5
  669. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-14 +0 -5
  670. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-2 +0 -5
  671. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-3 +0 -5
  672. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-4 +0 -5
  673. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-5 +0 -5
  674. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-6 +0 -5
  675. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-7 +0 -5
  676. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-8 +0 -5
  677. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT-9 +0 -5
  678. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/GMT0 +0 -5
  679. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/Greenwich +0 -5
  680. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/UCT +0 -5
  681. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/UTC +0 -5
  682. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/Universal +0 -5
  683. package/versions/0.5.0/_internal/_tcl_data/tzdata/Etc/Zulu +0 -5
  684. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Amsterdam +0 -5
  685. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Andorra +0 -237
  686. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Astrakhan +0 -71
  687. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Athens +0 -268
  688. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Belfast +0 -5
  689. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Belgrade +0 -250
  690. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Berlin +0 -274
  691. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Bratislava +0 -5
  692. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Brussels +0 -316
  693. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Bucharest +0 -268
  694. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Budapest +0 -283
  695. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Busingen +0 -5
  696. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Chisinau +0 -272
  697. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Copenhagen +0 -5
  698. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Dublin +0 -358
  699. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Gibraltar +0 -328
  700. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Guernsey +0 -5
  701. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Helsinki +0 -248
  702. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Isle_of_Man +0 -5
  703. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Istanbul +0 -125
  704. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Jersey +0 -5
  705. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Kaliningrad +0 -86
  706. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Kiev +0 -5
  707. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Kirov +0 -70
  708. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Kyiv +0 -251
  709. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Lisbon +0 -356
  710. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Ljubljana +0 -5
  711. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/London +0 -372
  712. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Luxembourg +0 -5
  713. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Madrid +0 -292
  714. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Malta +0 -299
  715. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Mariehamn +0 -5
  716. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Minsk +0 -75
  717. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Monaco +0 -5
  718. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Moscow +0 -83
  719. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Nicosia +0 -5
  720. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Oslo +0 -5
  721. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Paris +0 -314
  722. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Podgorica +0 -5
  723. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Prague +0 -275
  724. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Riga +0 -258
  725. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Rome +0 -302
  726. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Samara +0 -73
  727. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/San_Marino +0 -5
  728. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Sarajevo +0 -5
  729. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Saratov +0 -71
  730. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Simferopol +0 -82
  731. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Skopje +0 -5
  732. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Sofia +0 -258
  733. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Stockholm +0 -5
  734. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Tallinn +0 -254
  735. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Tirane +0 -263
  736. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Tiraspol +0 -5
  737. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Ulyanovsk +0 -73
  738. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Uzhgorod +0 -5
  739. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Vaduz +0 -5
  740. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Vatican +0 -5
  741. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Vienna +0 -271
  742. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Vilnius +0 -252
  743. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Volgograd +0 -73
  744. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Warsaw +0 -296
  745. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Zagreb +0 -5
  746. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Zaporozhye +0 -5
  747. package/versions/0.5.0/_internal/_tcl_data/tzdata/Europe/Zurich +0 -250
  748. package/versions/0.5.0/_internal/_tcl_data/tzdata/GB +0 -5
  749. package/versions/0.5.0/_internal/_tcl_data/tzdata/GB-Eire +0 -5
  750. package/versions/0.5.0/_internal/_tcl_data/tzdata/GMT +0 -5
  751. package/versions/0.5.0/_internal/_tcl_data/tzdata/GMT+0 +0 -5
  752. package/versions/0.5.0/_internal/_tcl_data/tzdata/GMT-0 +0 -5
  753. package/versions/0.5.0/_internal/_tcl_data/tzdata/GMT0 +0 -5
  754. package/versions/0.5.0/_internal/_tcl_data/tzdata/Greenwich +0 -5
  755. package/versions/0.5.0/_internal/_tcl_data/tzdata/HST +0 -5
  756. package/versions/0.5.0/_internal/_tcl_data/tzdata/Hongkong +0 -5
  757. package/versions/0.5.0/_internal/_tcl_data/tzdata/Iceland +0 -5
  758. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Antananarivo +0 -5
  759. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Chagos +0 -7
  760. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Christmas +0 -5
  761. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Cocos +0 -5
  762. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Comoro +0 -5
  763. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Kerguelen +0 -5
  764. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Mahe +0 -5
  765. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Maldives +0 -7
  766. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Mauritius +0 -10
  767. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Mayotte +0 -5
  768. package/versions/0.5.0/_internal/_tcl_data/tzdata/Indian/Reunion +0 -5
  769. package/versions/0.5.0/_internal/_tcl_data/tzdata/Iran +0 -5
  770. package/versions/0.5.0/_internal/_tcl_data/tzdata/Israel +0 -5
  771. package/versions/0.5.0/_internal/_tcl_data/tzdata/Jamaica +0 -5
  772. package/versions/0.5.0/_internal/_tcl_data/tzdata/Japan +0 -5
  773. package/versions/0.5.0/_internal/_tcl_data/tzdata/Kwajalein +0 -5
  774. package/versions/0.5.0/_internal/_tcl_data/tzdata/Libya +0 -5
  775. package/versions/0.5.0/_internal/_tcl_data/tzdata/MET +0 -5
  776. package/versions/0.5.0/_internal/_tcl_data/tzdata/MST +0 -5
  777. package/versions/0.5.0/_internal/_tcl_data/tzdata/MST7MDT +0 -5
  778. package/versions/0.5.0/_internal/_tcl_data/tzdata/Mexico/BajaNorte +0 -5
  779. package/versions/0.5.0/_internal/_tcl_data/tzdata/Mexico/BajaSur +0 -5
  780. package/versions/0.5.0/_internal/_tcl_data/tzdata/Mexico/General +0 -5
  781. package/versions/0.5.0/_internal/_tcl_data/tzdata/NZ +0 -5
  782. package/versions/0.5.0/_internal/_tcl_data/tzdata/NZ-CHAT +0 -5
  783. package/versions/0.5.0/_internal/_tcl_data/tzdata/Navajo +0 -5
  784. package/versions/0.5.0/_internal/_tcl_data/tzdata/PRC +0 -5
  785. package/versions/0.5.0/_internal/_tcl_data/tzdata/PST8PDT +0 -5
  786. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Apia +0 -31
  787. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Auckland +0 -285
  788. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Bougainville +0 -10
  789. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Chatham +0 -258
  790. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Chuuk +0 -5
  791. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Easter +0 -268
  792. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Efate +0 -28
  793. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Enderbury +0 -5
  794. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Fakaofo +0 -7
  795. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Fiji +0 -34
  796. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Funafuti +0 -5
  797. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Galapagos +0 -9
  798. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Gambier +0 -6
  799. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Guadalcanal +0 -6
  800. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Guam +0 -26
  801. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Honolulu +0 -12
  802. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Johnston +0 -5
  803. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Kanton +0 -8
  804. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Kiritimati +0 -8
  805. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Kosrae +0 -14
  806. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Kwajalein +0 -11
  807. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Majuro +0 -5
  808. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Marquesas +0 -6
  809. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Midway +0 -5
  810. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Nauru +0 -9
  811. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Niue +0 -7
  812. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Norfolk +0 -172
  813. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Noumea +0 -12
  814. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Pago_Pago +0 -7
  815. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Palau +0 -7
  816. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Pitcairn +0 -7
  817. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Pohnpei +0 -5
  818. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Ponape +0 -5
  819. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Port_Moresby +0 -7
  820. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Rarotonga +0 -33
  821. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Saipan +0 -5
  822. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Samoa +0 -5
  823. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Tahiti +0 -6
  824. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Tarawa +0 -6
  825. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Tongatapu +0 -16
  826. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Truk +0 -5
  827. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Wake +0 -5
  828. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Wallis +0 -5
  829. package/versions/0.5.0/_internal/_tcl_data/tzdata/Pacific/Yap +0 -5
  830. package/versions/0.5.0/_internal/_tcl_data/tzdata/Poland +0 -5
  831. package/versions/0.5.0/_internal/_tcl_data/tzdata/Portugal +0 -5
  832. package/versions/0.5.0/_internal/_tcl_data/tzdata/ROC +0 -5
  833. package/versions/0.5.0/_internal/_tcl_data/tzdata/ROK +0 -5
  834. package/versions/0.5.0/_internal/_tcl_data/tzdata/Singapore +0 -5
  835. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/AST4 +0 -5
  836. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/AST4ADT +0 -5
  837. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/CST6 +0 -5
  838. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/CST6CDT +0 -5
  839. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/EST5 +0 -5
  840. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/EST5EDT +0 -5
  841. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/HST10 +0 -5
  842. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/MST7 +0 -5
  843. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/MST7MDT +0 -5
  844. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/PST8 +0 -5
  845. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/PST8PDT +0 -5
  846. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/YST9 +0 -5
  847. package/versions/0.5.0/_internal/_tcl_data/tzdata/SystemV/YST9YDT +0 -5
  848. package/versions/0.5.0/_internal/_tcl_data/tzdata/Turkey +0 -5
  849. package/versions/0.5.0/_internal/_tcl_data/tzdata/UCT +0 -5
  850. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Alaska +0 -5
  851. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Aleutian +0 -5
  852. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Arizona +0 -5
  853. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Central +0 -5
  854. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/East-Indiana +0 -5
  855. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Eastern +0 -5
  856. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Hawaii +0 -5
  857. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Indiana-Starke +0 -5
  858. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Michigan +0 -5
  859. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Mountain +0 -5
  860. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Pacific +0 -5
  861. package/versions/0.5.0/_internal/_tcl_data/tzdata/US/Samoa +0 -5
  862. package/versions/0.5.0/_internal/_tcl_data/tzdata/UTC +0 -5
  863. package/versions/0.5.0/_internal/_tcl_data/tzdata/Universal +0 -5
  864. package/versions/0.5.0/_internal/_tcl_data/tzdata/W-SU +0 -5
  865. package/versions/0.5.0/_internal/_tcl_data/tzdata/WET +0 -5
  866. package/versions/0.5.0/_internal/_tcl_data/tzdata/Zulu +0 -5
  867. package/versions/0.5.0/_internal/_tcl_data/word.tcl +0 -154
  868. package/versions/0.5.0/_internal/_tk_data/bgerror.tcl +0 -272
  869. package/versions/0.5.0/_internal/_tk_data/button.tcl +0 -782
  870. package/versions/0.5.0/_internal/_tk_data/choosedir.tcl +0 -310
  871. package/versions/0.5.0/_internal/_tk_data/clrpick.tcl +0 -694
  872. package/versions/0.5.0/_internal/_tk_data/comdlg.tcl +0 -322
  873. package/versions/0.5.0/_internal/_tk_data/console.tcl +0 -1154
  874. package/versions/0.5.0/_internal/_tk_data/dialog.tcl +0 -175
  875. package/versions/0.5.0/_internal/_tk_data/entry.tcl +0 -699
  876. package/versions/0.5.0/_internal/_tk_data/focus.tcl +0 -178
  877. package/versions/0.5.0/_internal/_tk_data/fontchooser.tcl +0 -515
  878. package/versions/0.5.0/_internal/_tk_data/iconlist.tcl +0 -715
  879. package/versions/0.5.0/_internal/_tk_data/icons.tcl +0 -153
  880. package/versions/0.5.0/_internal/_tk_data/images/README +0 -7
  881. package/versions/0.5.0/_internal/_tk_data/images/logo.eps +0 -2091
  882. package/versions/0.5.0/_internal/_tk_data/images/logo100.gif +0 -0
  883. package/versions/0.5.0/_internal/_tk_data/images/logo64.gif +0 -0
  884. package/versions/0.5.0/_internal/_tk_data/images/logoLarge.gif +0 -0
  885. package/versions/0.5.0/_internal/_tk_data/images/logoMed.gif +0 -0
  886. package/versions/0.5.0/_internal/_tk_data/images/pwrdLogo.eps +0 -1897
  887. package/versions/0.5.0/_internal/_tk_data/images/pwrdLogo100.gif +0 -0
  888. package/versions/0.5.0/_internal/_tk_data/images/pwrdLogo150.gif +0 -0
  889. package/versions/0.5.0/_internal/_tk_data/images/pwrdLogo175.gif +0 -0
  890. package/versions/0.5.0/_internal/_tk_data/images/pwrdLogo200.gif +0 -0
  891. package/versions/0.5.0/_internal/_tk_data/images/pwrdLogo75.gif +0 -0
  892. package/versions/0.5.0/_internal/_tk_data/images/tai-ku.gif +0 -0
  893. package/versions/0.5.0/_internal/_tk_data/license.terms +0 -40
  894. package/versions/0.5.0/_internal/_tk_data/listbox.tcl +0 -560
  895. package/versions/0.5.0/_internal/_tk_data/megawidget.tcl +0 -297
  896. package/versions/0.5.0/_internal/_tk_data/menu.tcl +0 -1387
  897. package/versions/0.5.0/_internal/_tk_data/mkpsenc.tcl +0 -1488
  898. package/versions/0.5.0/_internal/_tk_data/msgbox.tcl +0 -427
  899. package/versions/0.5.0/_internal/_tk_data/msgs/cs.msg +0 -77
  900. package/versions/0.5.0/_internal/_tk_data/msgs/da.msg +0 -78
  901. package/versions/0.5.0/_internal/_tk_data/msgs/de.msg +0 -91
  902. package/versions/0.5.0/_internal/_tk_data/msgs/el.msg +0 -86
  903. package/versions/0.5.0/_internal/_tk_data/msgs/en.msg +0 -91
  904. package/versions/0.5.0/_internal/_tk_data/msgs/en_gb.msg +0 -3
  905. package/versions/0.5.0/_internal/_tk_data/msgs/eo.msg +0 -75
  906. package/versions/0.5.0/_internal/_tk_data/msgs/es.msg +0 -76
  907. package/versions/0.5.0/_internal/_tk_data/msgs/fi.msg +0 -89
  908. package/versions/0.5.0/_internal/_tk_data/msgs/fr.msg +0 -72
  909. package/versions/0.5.0/_internal/_tk_data/msgs/hu.msg +0 -78
  910. package/versions/0.5.0/_internal/_tk_data/msgs/it.msg +0 -73
  911. package/versions/0.5.0/_internal/_tk_data/msgs/nl.msg +0 -91
  912. package/versions/0.5.0/_internal/_tk_data/msgs/pl.msg +0 -91
  913. package/versions/0.5.0/_internal/_tk_data/msgs/pt.msg +0 -74
  914. package/versions/0.5.0/_internal/_tk_data/msgs/ru.msg +0 -93
  915. package/versions/0.5.0/_internal/_tk_data/msgs/sv.msg +0 -76
  916. package/versions/0.5.0/_internal/_tk_data/msgs/zh_cn.msg +0 -92
  917. package/versions/0.5.0/_internal/_tk_data/obsolete.tcl +0 -178
  918. package/versions/0.5.0/_internal/_tk_data/optMenu.tcl +0 -43
  919. package/versions/0.5.0/_internal/_tk_data/palette.tcl +0 -244
  920. package/versions/0.5.0/_internal/_tk_data/panedwindow.tcl +0 -194
  921. package/versions/0.5.0/_internal/_tk_data/pkgIndex.tcl +0 -7
  922. package/versions/0.5.0/_internal/_tk_data/safetk.tcl +0 -262
  923. package/versions/0.5.0/_internal/_tk_data/scale.tcl +0 -307
  924. package/versions/0.5.0/_internal/_tk_data/scrlbar.tcl +0 -456
  925. package/versions/0.5.0/_internal/_tk_data/spinbox.tcl +0 -594
  926. package/versions/0.5.0/_internal/_tk_data/tclIndex +0 -253
  927. package/versions/0.5.0/_internal/_tk_data/tearoff.tcl +0 -157
  928. package/versions/0.5.0/_internal/_tk_data/text.tcl +0 -1242
  929. package/versions/0.5.0/_internal/_tk_data/tk.tcl +0 -708
  930. package/versions/0.5.0/_internal/_tk_data/tkfbox.tcl +0 -1241
  931. package/versions/0.5.0/_internal/_tk_data/ttk/altTheme.tcl +0 -141
  932. package/versions/0.5.0/_internal/_tk_data/ttk/aquaTheme.tcl +0 -118
  933. package/versions/0.5.0/_internal/_tk_data/ttk/button.tcl +0 -83
  934. package/versions/0.5.0/_internal/_tk_data/ttk/clamTheme.tcl +0 -175
  935. package/versions/0.5.0/_internal/_tk_data/ttk/classicTheme.tcl +0 -130
  936. package/versions/0.5.0/_internal/_tk_data/ttk/combobox.tcl +0 -447
  937. package/versions/0.5.0/_internal/_tk_data/ttk/cursors.tcl +0 -205
  938. package/versions/0.5.0/_internal/_tk_data/ttk/defaults.tcl +0 -152
  939. package/versions/0.5.0/_internal/_tk_data/ttk/entry.tcl +0 -641
  940. package/versions/0.5.0/_internal/_tk_data/ttk/fonts.tcl +0 -154
  941. package/versions/0.5.0/_internal/_tk_data/ttk/menubutton.tcl +0 -233
  942. package/versions/0.5.0/_internal/_tk_data/ttk/notebook.tcl +0 -201
  943. package/versions/0.5.0/_internal/_tk_data/ttk/panedwindow.tcl +0 -89
  944. package/versions/0.5.0/_internal/_tk_data/ttk/progress.tcl +0 -49
  945. package/versions/0.5.0/_internal/_tk_data/ttk/scale.tcl +0 -94
  946. package/versions/0.5.0/_internal/_tk_data/ttk/scrollbar.tcl +0 -126
  947. package/versions/0.5.0/_internal/_tk_data/ttk/sizegrip.tcl +0 -102
  948. package/versions/0.5.0/_internal/_tk_data/ttk/spinbox.tcl +0 -192
  949. package/versions/0.5.0/_internal/_tk_data/ttk/treeview.tcl +0 -383
  950. package/versions/0.5.0/_internal/_tk_data/ttk/ttk.tcl +0 -191
  951. package/versions/0.5.0/_internal/_tk_data/ttk/utils.tcl +0 -337
  952. package/versions/0.5.0/_internal/_tk_data/ttk/vistaTheme.tcl +0 -255
  953. package/versions/0.5.0/_internal/_tk_data/ttk/winTheme.tcl +0 -112
  954. package/versions/0.5.0/_internal/_tk_data/ttk/xpTheme.tcl +0 -93
  955. package/versions/0.5.0/_internal/_tk_data/unsupported.tcl +0 -269
  956. package/versions/0.5.0/_internal/_tk_data/xmfbox.tcl +0 -989
  957. package/versions/0.5.0/_internal/_tkinter.pyd +0 -0
  958. package/versions/0.5.0/_internal/_uuid.pyd +0 -0
  959. package/versions/0.5.0/_internal/_wmi.pyd +0 -0
  960. package/versions/0.5.0/_internal/_zstd.pyd +0 -0
  961. package/versions/0.5.0/_internal/api/server.json +0 -1
  962. package/versions/0.5.0/_internal/api/web/category-templates.js +0 -299
  963. package/versions/0.5.0/_internal/api/web/favicon.ico +0 -0
  964. package/versions/0.5.0/_internal/api/web/index.html +0 -95
  965. package/versions/0.5.0/_internal/api/web/workbench.css +0 -346
  966. package/versions/0.5.0/_internal/api/web/workbench.js +0 -2167
  967. package/versions/0.5.0/_internal/api-ms-win-core-console-l1-1-0.dll +0 -0
  968. package/versions/0.5.0/_internal/api-ms-win-core-datetime-l1-1-0.dll +0 -0
  969. package/versions/0.5.0/_internal/api-ms-win-core-debug-l1-1-0.dll +0 -0
  970. package/versions/0.5.0/_internal/api-ms-win-core-errorhandling-l1-1-0.dll +0 -0
  971. package/versions/0.5.0/_internal/api-ms-win-core-fibers-l1-1-0.dll +0 -0
  972. package/versions/0.5.0/_internal/api-ms-win-core-fibers-l1-1-1.dll +0 -0
  973. package/versions/0.5.0/_internal/api-ms-win-core-file-l1-1-0.dll +0 -0
  974. package/versions/0.5.0/_internal/api-ms-win-core-file-l1-2-0.dll +0 -0
  975. package/versions/0.5.0/_internal/api-ms-win-core-file-l2-1-0.dll +0 -0
  976. package/versions/0.5.0/_internal/api-ms-win-core-handle-l1-1-0.dll +0 -0
  977. package/versions/0.5.0/_internal/api-ms-win-core-heap-l1-1-0.dll +0 -0
  978. package/versions/0.5.0/_internal/api-ms-win-core-interlocked-l1-1-0.dll +0 -0
  979. package/versions/0.5.0/_internal/api-ms-win-core-kernel32-legacy-l1-1-1.dll +0 -0
  980. package/versions/0.5.0/_internal/api-ms-win-core-libraryloader-l1-1-0.dll +0 -0
  981. package/versions/0.5.0/_internal/api-ms-win-core-localization-l1-2-0.dll +0 -0
  982. package/versions/0.5.0/_internal/api-ms-win-core-memory-l1-1-0.dll +0 -0
  983. package/versions/0.5.0/_internal/api-ms-win-core-namedpipe-l1-1-0.dll +0 -0
  984. package/versions/0.5.0/_internal/api-ms-win-core-processenvironment-l1-1-0.dll +0 -0
  985. package/versions/0.5.0/_internal/api-ms-win-core-processthreads-l1-1-0.dll +0 -0
  986. package/versions/0.5.0/_internal/api-ms-win-core-processthreads-l1-1-1.dll +0 -0
  987. package/versions/0.5.0/_internal/api-ms-win-core-profile-l1-1-0.dll +0 -0
  988. package/versions/0.5.0/_internal/api-ms-win-core-rtlsupport-l1-1-0.dll +0 -0
  989. package/versions/0.5.0/_internal/api-ms-win-core-string-l1-1-0.dll +0 -0
  990. package/versions/0.5.0/_internal/api-ms-win-core-synch-l1-1-0.dll +0 -0
  991. package/versions/0.5.0/_internal/api-ms-win-core-synch-l1-2-0.dll +0 -0
  992. package/versions/0.5.0/_internal/api-ms-win-core-sysinfo-l1-1-0.dll +0 -0
  993. package/versions/0.5.0/_internal/api-ms-win-core-sysinfo-l1-2-0.dll +0 -0
  994. package/versions/0.5.0/_internal/api-ms-win-core-timezone-l1-1-0.dll +0 -0
  995. package/versions/0.5.0/_internal/api-ms-win-core-util-l1-1-0.dll +0 -0
  996. package/versions/0.5.0/_internal/api-ms-win-crt-conio-l1-1-0.dll +0 -0
  997. package/versions/0.5.0/_internal/api-ms-win-crt-convert-l1-1-0.dll +0 -0
  998. package/versions/0.5.0/_internal/api-ms-win-crt-environment-l1-1-0.dll +0 -0
  999. package/versions/0.5.0/_internal/api-ms-win-crt-filesystem-l1-1-0.dll +0 -0
  1000. package/versions/0.5.0/_internal/api-ms-win-crt-heap-l1-1-0.dll +0 -0
  1001. package/versions/0.5.0/_internal/api-ms-win-crt-locale-l1-1-0.dll +0 -0
  1002. package/versions/0.5.0/_internal/api-ms-win-crt-math-l1-1-0.dll +0 -0
  1003. package/versions/0.5.0/_internal/api-ms-win-crt-process-l1-1-0.dll +0 -0
  1004. package/versions/0.5.0/_internal/api-ms-win-crt-runtime-l1-1-0.dll +0 -0
  1005. package/versions/0.5.0/_internal/api-ms-win-crt-stdio-l1-1-0.dll +0 -0
  1006. package/versions/0.5.0/_internal/api-ms-win-crt-string-l1-1-0.dll +0 -0
  1007. package/versions/0.5.0/_internal/api-ms-win-crt-time-l1-1-0.dll +0 -0
  1008. package/versions/0.5.0/_internal/api-ms-win-crt-utility-l1-1-0.dll +0 -0
  1009. package/versions/0.5.0/_internal/attrs-26.1.0.dist-info/INSTALLER +0 -1
  1010. package/versions/0.5.0/_internal/attrs-26.1.0.dist-info/METADATA +0 -199
  1011. package/versions/0.5.0/_internal/attrs-26.1.0.dist-info/RECORD +0 -55
  1012. package/versions/0.5.0/_internal/attrs-26.1.0.dist-info/WHEEL +0 -4
  1013. package/versions/0.5.0/_internal/attrs-26.1.0.dist-info/licenses/LICENSE +0 -21
  1014. package/versions/0.5.0/_internal/base_library.zip +0 -0
  1015. package/versions/0.5.0/_internal/certifi/cacert.pem +0 -3892
  1016. package/versions/0.5.0/_internal/certifi/py.typed +0 -0
  1017. package/versions/0.5.0/_internal/charset_normalizer/cd.cp314-win_amd64.pyd +0 -0
  1018. package/versions/0.5.0/_internal/charset_normalizer/md.cp314-win_amd64.pyd +0 -0
  1019. package/versions/0.5.0/_internal/click-8.4.2.dist-info/INSTALLER +0 -1
  1020. package/versions/0.5.0/_internal/click-8.4.2.dist-info/METADATA +0 -84
  1021. package/versions/0.5.0/_internal/click-8.4.2.dist-info/RECORD +0 -40
  1022. package/versions/0.5.0/_internal/click-8.4.2.dist-info/WHEEL +0 -4
  1023. package/versions/0.5.0/_internal/click-8.4.2.dist-info/licenses/LICENSE.txt +0 -28
  1024. package/versions/0.5.0/_internal/libcrypto-3.dll +0 -0
  1025. package/versions/0.5.0/_internal/libffi-8.dll +0 -0
  1026. package/versions/0.5.0/_internal/libssl-3.dll +0 -0
  1027. package/versions/0.5.0/_internal/lxml/_elementpath.cp314-win_amd64.pyd +0 -0
  1028. package/versions/0.5.0/_internal/lxml/builder.cp314-win_amd64.pyd +0 -0
  1029. package/versions/0.5.0/_internal/lxml/etree.cp314-win_amd64.pyd +0 -0
  1030. package/versions/0.5.0/_internal/lxml/html/_difflib.cp314-win_amd64.pyd +0 -0
  1031. package/versions/0.5.0/_internal/lxml/html/diff.cp314-win_amd64.pyd +0 -0
  1032. package/versions/0.5.0/_internal/lxml/isoschematron/resources/rng/iso-schematron.rng +0 -709
  1033. package/versions/0.5.0/_internal/lxml/isoschematron/resources/xsl/RNG2Schtrn.xsl +0 -75
  1034. package/versions/0.5.0/_internal/lxml/isoschematron/resources/xsl/XSD2Schtrn.xsl +0 -77
  1035. package/versions/0.5.0/_internal/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_abstract_expand.xsl +0 -313
  1036. package/versions/0.5.0/_internal/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_dsdl_include.xsl +0 -1160
  1037. package/versions/0.5.0/_internal/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_schematron_message.xsl +0 -55
  1038. package/versions/0.5.0/_internal/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_schematron_skeleton_for_xslt1.xsl +0 -1796
  1039. package/versions/0.5.0/_internal/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_svrl_for_xslt1.xsl +0 -588
  1040. package/versions/0.5.0/_internal/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/readme.txt +0 -84
  1041. package/versions/0.5.0/_internal/lxml/objectify.cp314-win_amd64.pyd +0 -0
  1042. package/versions/0.5.0/_internal/lxml/sax.cp314-win_amd64.pyd +0 -0
  1043. package/versions/0.5.0/_internal/pyexpat.pyd +0 -0
  1044. package/versions/0.5.0/_internal/python3.dll +0 -0
  1045. package/versions/0.5.0/_internal/python314.dll +0 -0
  1046. package/versions/0.5.0/_internal/select.pyd +0 -0
  1047. package/versions/0.5.0/_internal/setuptools/_vendor/jaraco/text/Lorem ipsum.txt +0 -2
  1048. package/versions/0.5.0/_internal/sqlite3.dll +0 -0
  1049. package/versions/0.5.0/_internal/tcl8/8.4/platform/shell-1.1.4.tm +0 -241
  1050. package/versions/0.5.0/_internal/tcl8/8.4/platform-1.0.19.tm +0 -450
  1051. package/versions/0.5.0/_internal/tcl8/8.5/msgcat-1.6.1.tm +0 -1210
  1052. package/versions/0.5.0/_internal/tcl8/8.5/tcltest-2.5.8.tm +0 -3588
  1053. package/versions/0.5.0/_internal/tcl8/8.6/http-2.9.8.tm +0 -3603
  1054. package/versions/0.5.0/_internal/tcl86t.dll +0 -0
  1055. package/versions/0.5.0/_internal/tk86t.dll +0 -0
  1056. package/versions/0.5.0/_internal/ucrtbase.dll +0 -0
  1057. package/versions/0.5.0/_internal/unicodedata.pyd +0 -0
  1058. package/versions/0.5.0/_internal/zlib1.dll +0 -0
  1059. package/versions/0.5.0/_internal/zstandard/_cffi.cp314-win_amd64.pyd +0 -0
  1060. package/versions/0.5.0/_internal/zstandard/backend_c.cp314-win_amd64.pyd +0 -0
@@ -1,2167 +0,0 @@
1
- 'use strict';
2
-
3
- const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, char => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
4
- const pathLines = value => String(value ?? '').split(/\r?\n/).map(path => path.trim()).filter(Boolean);
5
- const moveItem = (values, index, offset) => {
6
- const result = [...values], target = index + offset;
7
- if (index >= 0 && index < result.length && target >= 0 && target < result.length) [result[index], result[target]] = [result[target], result[index]];
8
- return result;
9
- };
10
- const reorderMedia = (draft, from, to) => {
11
- const family = {primary:'image',images:'image',cover:'video',videos:'video'};
12
- const valid = slot => slot && family[slot.kind] && (!['images','videos'].includes(slot.kind) || (Number.isInteger(slot.index) && slot.index >= 0 && slot.index < draft[slot.kind].length));
13
- if (!draft || !valid(from) || !valid(to) || family[from.kind] !== family[to.kind]) return draft;
14
- if (from.kind === to.kind && (!['images','videos'].includes(from.kind) || from.index === to.index)) return draft;
15
- if (['primary','cover'].includes(from.kind) && !draft[from.kind === 'primary' ? 'primary_image' : 'video_cover']) return draft;
16
- const result = structuredClone(draft);
17
- if (from.kind === to.kind) {
18
- const [entry] = result[from.kind].splice(from.index,1);
19
- result[to.kind].splice(to.index,0,entry);
20
- } else {
21
- const kind = family[from.kind] === 'image' ? 'images' : 'videos';
22
- const field = kind === 'images' ? 'primary_image' : 'video_cover';
23
- const index = from.kind === kind ? from.index : to.index;
24
- const previous = result[field];
25
- result[field] = kind === 'images' ? result.images[index] : result.videos[index].path;
26
- if (previous) {
27
- if (kind === 'images') result.images[index] = previous;
28
- else result.videos[index].path = previous;
29
- } else if (kind === 'images') result.images.splice(index,1);
30
- }
31
- return result;
32
- };
33
- const reconcileCardOrder = (ids, saved) => {
34
- const current = new Set(ids), seen = new Set();
35
- return [...(Array.isArray(saved) ? saved : []),...ids].filter(id => typeof id === 'string' && current.has(id) && !seen.has(id) && seen.add(id));
36
- };
37
- const moveCardOrder = (ids, from, to) => {
38
- const result = [...ids], a = result.indexOf(from), b = result.indexOf(to);
39
- if (a >= 0 && b >= 0 && a !== b) {result.splice(a,1);result.splice(b,0,from);}
40
- return result;
41
- };
42
- const bytes = value => {
43
- if (value === null || value === undefined || !Number.isFinite(Number(value))) return '未查询';
44
- const size = Number(value);
45
- return size >= 1073741824 ? `${(size / 1073741824).toFixed(2)} GB` : size >= 1048576 ? `${(size / 1048576).toFixed(2)} MB` : `${Math.ceil(size / 1024)} KB`;
46
- };
47
- const safePublicUrl = value => {
48
- try { const url = new URL(value); return url.protocol === 'https:' && !url.username && !url.password ? url.href : ''; } catch { return ''; }
49
- };
50
- const sameScope = (a, b) => JSON.stringify([...a].map(String).sort()) === JSON.stringify([...b].map(String).sort());
51
- const itemSummary = (result, label) => {
52
- if (!Array.isArray(result?.items)) return `${label}任务已返回,详情见任务日志`;
53
- const counts = result.items.reduce((sum,item) => {
54
- if (item.skipped === true) sum.skipped++;
55
- else if (item.ok === true || (!Object.hasOwn(item,'ok') && item.path)) sum.success++;
56
- else if (item.ok === false) sum.failed++;
57
- else sum.unknown++;
58
- return sum;
59
- },{success:0,skipped:0,failed:0,unknown:0});
60
- return `${label}:成功 ${counts.success}、跳过 ${counts.skipped}、失败 ${counts.failed}${counts.unknown ? `、结果待核实 ${counts.unknown}` : ''}`;
61
- };
62
- const UI_VERSION = '0.6.0';
63
- const mediaSignature = (draft={}) => JSON.stringify([draft.primary_image || '',draft.images || [],(draft.videos || []).map(video=>[video.path,video.name || '']),draft.video_cover || '']);
64
- const uploadEntries = paths => [...new Set(paths)].map(path=>({path,state:'waiting',size:null,error:''}));
65
- const mediaCategories = {'listing-images':'上架图片','model-samples':'型号样本',videos:'商品视频','video-covers':'视频封面'};
66
- const filterLibrary = (items,filter={}) => {
67
- const query=(filter.query || '').toLowerCase(), min=Number(filter.minMB || 0)*1048576, max=filter.maxMB ? Number(filter.maxMB)*1048576 : Infinity;
68
- const after=filter.after ? new Date(`${filter.after}T00:00:00`).getTime()/1000 : -Infinity;
69
- const before=filter.before ? new Date(`${filter.before}T23:59:59.999`).getTime()/1000 : Infinity;
70
- const result=items.filter(item=>`${item.name} ${(item.source_names || []).join(' ')} ${item.note || ''}`.toLowerCase().includes(query)
71
- && (!filter.type || filter.type==='all' || (/\.(mp4|mov)$/i.test(item.name)?'video':'image')===filter.type)
72
- && (!filter.category || filter.category==='all' || (filter.category==='unclassified' ? !item.categories?.length : item.categories?.includes(filter.category)))
73
- && (!filter.reference || filter.reference==='all' || (item.references?.length?'known':'unknown')===filter.reference)
74
- && item.size>=min && item.size<=max && (!filter.after && !filter.before || Number.isFinite(item.mtime) && item.mtime>=after && item.mtime<=before));
75
- const sorts={'size-desc':(a,b)=>b.size-a.size,'size-asc':(a,b)=>a.size-b.size,'date-desc':(a,b)=>(b.mtime || 0)-(a.mtime || 0),'date-asc':(a,b)=>(a.mtime || 0)-(b.mtime || 0),'name':(a,b)=>a.name.localeCompare(b.name,'zh-CN',{numeric:true})};
76
- return filter.sort==='manual' ? result : result.sort(sorts[filter.sort] || sorts['name']);
77
- };
78
- const filterQueue = (groups,query='',status='all',plan=null) => groups.filter(item=>{
79
- const ready=Boolean(item.primary_image && !item.media_errors?.length);
80
- return `${item.label} ${(item.offer_ids || []).join(' ')}`.toLowerCase().includes(query.toLowerCase())
81
- && (status==='all' || status==='ready' && ready || status==='missing' && !ready || status==='prepared' && plan?.scope?.map(String).includes(String(item.id)));
82
- });
83
- const phoneModelKey = value => String(value).toUpperCase().replaceAll('+','PLUS').replace(/[^A-Z0-9]/g,'');
84
- const scopeQueueGroups = (groups,scope='all',catalog={}) => {
85
- if(scope==='all')return groups;
86
- const popular={IMD:new Set(catalog?.popular?.IMD || []),JT:new Set(catalog?.popular?.JT || [])};
87
- return groups.flatMap(group=>{
88
- const offer_ids=group.offer_ids.filter(offer=>{
89
- const match=/^(IMD|JIETI|XINJIETI)-(.+)-J\d+-.+$/.exec(offer);
90
- const key=match && phoneModelKey(match[2]);
91
- return match && popular[match[1]==='IMD'?'IMD':'JT'].has(key);
92
- });
93
- return offer_ids.length?[{...group,offer_ids}]:[];
94
- });
95
- };
96
- const queueModelCoverage = (groups,catalog={}) => {
97
- const patterns=new Map();
98
- for(const group of groups)for(const offer of group.offer_ids){
99
- const match=/^(IMD|JIETI|XINJIETI)-(.+)-J\d+-(RUZSHG[A-Z0-9]+)$/i.exec(offer);
100
- if(!match)continue;
101
- const family=match[1].toUpperCase()==='IMD'?'IMD':'JT',pattern=match[3].toUpperCase(),id=`${family}:${pattern}`;
102
- if(!patterns.has(id))patterns.set(id,{family,pattern,found:new Set()});
103
- patterns.get(id).found.add(phoneModelKey(match[2]));
104
- }
105
- return [...patterns.values()].map(({family,pattern,found})=>{
106
- const popular=new Set(catalog?.popular?.[family] || []);
107
- const models=(catalog?.models?.[family] || []).filter(row=>popular.has(phoneModelKey(row.sku_model)) && !found.has(phoneModelKey(row.sku_model))).map(row=>row.sku_model);
108
- return {pattern,models};
109
- }).filter(row=>row.models.length);
110
- };
111
- const pageItems = (items,page=1,size=30) => {
112
- const pages=Math.max(1,Math.ceil(items.length/size)), current=Math.min(pages,Math.max(1,Number(page) || 1));
113
- return {items:items.slice((current-1)*size,current*size),page:current,pages,total:items.length};
114
- };
115
- const QUEUE_COLUMNS = [['name','商品标题'],['sku','SKU 数'],['media','媒体清单'],['old_price','划线价格'],['price','折扣价'],['stock','库存预设'],['color','颜色'],['preflight','预检']];
116
- const normalizeQueueView = (value={}) => ({
117
- search:typeof value.search==='string'?value.search.slice(0,120):'',
118
- filter:['all','missing','ready','prepared'].includes(value.filter)?value.filter:'all',
119
- size:[30,60,100,500].includes(value.size)?value.size:30,
120
- density:value.density==='compact'?'compact':'comfortable',
121
- columns_version:3,
122
- columns:Array.isArray(value.columns)?[...new Set([...value.columns.filter(column=>QUEUE_COLUMNS.some(([key])=>key===column)),...([2,3].includes(value.columns_version)?[]:['old_price','price','stock','color']),...(value.columns_version===3?[]:['name'])])]:QUEUE_COLUMNS.map(([key])=>key)
123
- });
124
- const listingStats = groups => ({groups:groups.length, skus:groups.reduce((sum,item)=>sum+item.offer_ids.length,0),ready:groups.filter(item=>item.primary_image&&!item.media_errors?.length).length});
125
- const flipFrames = (from,to) => from.width>0 && from.height>0 && to.width>0 && to.height>0 ? [{transform:`translate(${from.left-to.left}px,${from.top-to.top}px) scale(${from.width/to.width},${from.height/to.height})`,transformOrigin:'top left'},{transform:'none',transformOrigin:'top left'}] : [];
126
- const zoomImageAt = (view,factor,x=0,y=0) => {
127
- const scale=Math.min(8,Math.max(.1,view.scale*factor)), ratio=scale/view.scale;
128
- return {...view,scale,x:x-(x-view.x)*ratio,y:y-(y-view.y)*ratio};
129
- };
130
- const statusText = value => ({started:'已开始',queued:'排队中',waiting:'待处理',running:'运行中',uploading:'传输中',checking:'检查中',done:'已完成',failed:'失败',pending:'待核实',uncertain:'结果待核实',accepted:'已受理',imported:'已导入',submitting:'提交中',prepared:'已预览',matched:'匹配',mismatch:'不一致',approved:'审核通过',declined:'审核拒绝',rejected:'已拒绝',created:'已建立',processed:'已处理',processing:'处理中',new:'待处理',skipped:'已跳过',rolled_back:'已恢复',success:'成功',error:'错误',blocked:'已阻止',not_submitted:'未提交',pending_verification:'待核实'})[value] || (/^[\x00-\x7F]+$/.test(String(value || '')) ? '状态待核实' : value || '待核实');
131
- const fieldName = (key,names={}) => ({offer_id:'货号',name:'商品标题',price:'售价',old_price:'划线价',vat:'税率',currency_code:'币种',weight:'包装重量(克)',width:'包装宽度(毫米)',height:'包装高度(毫米)',depth:'包装长度(毫米)',dimension_unit:'尺寸单位',weight_unit:'重量单位',description_category_id:'类目编号',type_id:'商品类型编号',primary_image:'主图',images:'附图',color_image:'颜色样本',barcode:'条形码',stock:'库存数量',warehouse_id:'仓库',attributes:'商品属性',media:'图片与视频',video_names:'视频名称',compatible_models:'适用于的型号',rich_content:'富文本',title:'商品标题',description:'描述',package:'包装清单',country:'原产国',brand:'品牌'})[key] || (/^(?:媒体)?属性\s*(\d+)\s*\/\s*\d+/.test(key) ? names[String(key).match(/属性\s*(\d+)/)[1]] || '商品属性(名称待核对)' : /^attribute:\d+$/.test(key) ? names[key.split(':')[1]] || '商品属性(名称待核对)' : key);
132
- const productFields = record => {
133
- const value = raw => Array.isArray(raw) ? raw.map(v=>typeof v==='object'?v.value ?? v.dictionary_value_id ?? JSON.stringify(v):v).join(';') : raw;
134
- const fields=Object.entries(record.item || {}).filter(([key])=>!['attributes','complex_attributes'].includes(key)).map(([key,v])=>({name:fieldName(key),value:value(v),section:'商品信息'}));
135
- const append = (attrs,section) => attrs.forEach(a=>fields.push({name:record.attribute_names?.[String(a.id)] || '商品属性(名称待核对)',value:value(a.values),section}));
136
- append(record.raw_attributes || record.item?.attributes || [],'商品属性');
137
- (record.raw_complex || []).forEach(attrs=>append(attrs,({100004:'适用于的型号',100001:'商品视频',100002:'视频封面',8788:'附件'})[attrs[0]?.complex_id] || '组合属性'));
138
- return fields;
139
- };
140
- if (typeof module !== 'undefined') module.exports = {statusText,productFields,escapeHtml, pathLines, moveItem, reorderMedia, bytes, safePublicUrl, sameScope, itemSummary, listingStats, flipFrames, zoomImageAt,mediaSignature,filterLibrary,filterQueue,scopeQueueGroups,queueModelCoverage,pageItems,uploadEntries,normalizeQueueView,reconcileCardOrder,moveCardOrder};
141
-
142
- if (typeof document !== 'undefined') {
143
- const $ = selector => document.querySelector(selector), e = escapeHtml;
144
- const labels = {generation:'生成表格',queue:'商品队列',library:'服务器媒体库',history:'上架记录',settings:'配置中心',copy:'编辑文案',manual:'手动建表',media:'编辑媒体',autoMedia:'自动关联媒体',book:'表格商品',product:'商品字段',file:'文件详情',upload:'上传媒体',queueAdd:'加入已有表格',record:'货号任务详情',preflight:'预检详情',task:'任务详情',manifest:'导入媒体清单',configImport:'导入配置',commerce:'经营参数',commercePlan:'价格与库存提交',phoneModels:'手机型号目录'};
145
- const ui = {page:'queue', data:null, group:null, design:null, record:0, file:null, designs:new Set(), files:new Set(), search:'', media:null,mediaBase:null,mediaUndo:null,mediaTyping:null,modelMaterial:'IMD',modelFilter:'popular',modelPage:1,uploadCategory:'listing-images',libraryFilter:{category:'all',type:'all',reference:'all',sort:'manual'},libraryPage:1,queueFilter:'all',queuePage:1,pageSize:30,storeSearch:'',uploadItems:[],uploading:false, manual:{file_type:'2',source_path:'',new_number:'',title:'',description:''}, manualPaths:[], manualPreview:null, copy:null, upload:'', addBooks:'', importText:'', models:[], lastResult:null, lastJob:null, active:false};
146
- const cloudMode = window.OZON_CLOUD === true;
147
- const categoryModule=window.CategoryTemplates || null;
148
- ui.categoryTemplates=categoryModule?categoryModule.createState(''):null;
149
- Object.assign(labels,{categoryCreate:'用类目模板新建'});
150
- Object.assign(ui,{businessSelected:new Set(),businessRow:null,businessDraft:null,businessChanged:false,warehouses:[],warehouseClient:'',commerceOperations:[],queueDetail:null,queueSearch:'',queueScroll:[0,0],queueDensity:'comfortable',queueColumns:QUEUE_COLUMNS.map(([key])=>key),queueSaved:null,queueSavedKey:null});
151
- Object.assign(ui,{qualityDocuments:[],qualityId:null,qualityLoaded:false,qualityDirty:false,qualityScope:null});
152
- Object.assign(ui,{taskLogOpen:false,taskLogLevel:'INFO',jobReads:new Map(),historySearch:'',historyFilter:'all',historyPage:1,historySize:30,stateReadAt:null,stateReadError:'',historyReadError:'',historyReadJobId:'',recordMediaOpen:false});
153
- try{const level=window.localStorage?.getItem('ozon.taskLogLevel');if(['INFO','WARN','ERROR'].includes(level))ui.taskLogLevel=level;}catch{}
154
- let logDrag=null,logPosition=null,logClickBlockedUntil=0;
155
- const isAdmin = () => !cloudMode || ui.session?.user?.role === 'admin';
156
- const isSuperAdmin = () => !cloudMode || isAdmin() && ui.session?.user?.super_admin === true;
157
- const roleText = user => user?.super_admin ? '超级管理员' : user?.role==='admin' ? '管理员' : '普通人员';
158
- const fileUrl = url => `${cloudMode ? '/ozon' : ''}${url}${cloudMode && ui.workspace ? `${url.includes('?') ? '&' : '?'}workspace=${encodeURIComponent(ui.workspace)}` : ''}`;
159
- Object.assign(labels,{team:'团队任务',users:'人员管理',workspaces:'工作区',account:'我的账户'});
160
- let toastTimer, renderedPage, mediaDrag, mediaAutoScroll, imagePan, imageSource, imageScroll;
161
- let cardDrag=null,cardPointerBlocked=false,cardClickBlockedUntil=0,cardOrdersKey=null,cardOrders={},cardVisual=null,cardLanding=null;
162
- let teamTimer=null,teamController=null,teamLoading=false,teamEpoch=0,teamPageHidden=false;
163
- let sessionEpoch=0,navigationEpoch=0,stateEpoch=0,inputEpoch=0,cancelConfirmation=null,sessionRefresh=null;
164
- const operations=new Map(),observedJobs=new Map(),readCache=new Map(),jobRequests=new Map();
165
- const cachedActions=new Set(['category_template.list','category_template.inspect','manual.templates','queue.book']);
166
- const submissionActions=new Set(['listing.submit','commerce.submit']);
167
- const configActions=new Set(['config.save','config.import']);
168
- const staleView = () => Object.assign(new Error('会话、工作区或页面已变化,已忽略旧响应'),{code:'STALE_VIEW'});
169
- const scopeLease = () => {const epoch=sessionEpoch,session=ui.session,workspace=ui.workspace;return ()=>!cloudMode || epoch===sessionEpoch && session===ui.session && workspace===ui.workspace && !ui.logoutPending;};
170
- const pageLease = () => {const current=scopeLease(),page=ui.page,navigation=navigationEpoch,input=inputEpoch;return ()=>current() && page===ui.page && navigation===navigationEpoch && input===inputEpoch;};
171
- async function inView(operation) {const current=pageLease(),result=await operation();if(!current())throw staleView();return result;}
172
- const viewAction = (action,payload) => inView(()=>act(action,payload));
173
- const viewPick = kind => inView(()=>pick(kind));
174
- let imageClickBlockedUntil=0,imageGallery=[],imageIndex=0;
175
- const imageView={scale:1,x:0,y:0,angle:0};
176
- function updateImageView() {
177
- $('#image-viewer-image').style.transform=`translate(-50%,-50%) translate(${imageView.x}px,${imageView.y}px) scale(${imageView.scale}) rotate(${imageView.angle}deg)`;
178
- $('#image-viewer-scale').textContent=`${Math.round(imageView.scale*100)}%`;
179
- }
180
- function openImageView(image) {
181
- if(!image.closest('#page') || Date.now()<imageClickBlockedUntil)return;
182
- const dialog=$('#image-viewer');
183
- if(dialog.open)return;
184
- imageSource=image;imagePan=null;
185
- const gallery=image.closest('[data-gallery]') || image.closest('#page');
186
- imageGallery=Array.from(gallery?.querySelectorAll?.('img') || [image]).filter(item=>item.src);
187
- imageIndex=Math.max(0,imageGallery.indexOf(image));
188
- showImageAt(imageIndex);
189
- imageScroll=document.body.style.overflow;document.body.style.overflow='hidden';dialog.showModal();
190
- }
191
- function showImageAt(index) {
192
- if(index<0 || index>=imageGallery.length)return;
193
- imageIndex=index;imagePan=null;imageStage.classList.remove('panning');
194
- const image=imageGallery[index];
195
- Object.assign(imageView,{scale:1,x:0,y:0,angle:0});
196
- const full=$('#image-viewer-image');
197
- full.src=image.dataset?.fullSrc || image.currentSrc || image.src;full.alt=image.alt || '图片预览';full.title=image.title || '';
198
- $('#image-viewer-status').textContent='正在读取图片…';
199
- $('#image-viewer-counter').textContent=`${index+1} / ${imageGallery.length}`;
200
- $('#image-viewer-counter').hidden=imageGallery.length<2;
201
- for(const [action,disabled] of [['prev',index===0],['next',index===imageGallery.length-1]]){const control=$(`[data-image-view="${action}"]`);control.hidden=imageGallery.length<2;control.disabled=disabled;}
202
- updateImageView();
203
- }
204
- const imageStage=$('#image-viewer-stage');
205
- $('#image-viewer-image').addEventListener('load',()=>{$('#image-viewer-status').textContent='';});
206
- $('#image-viewer-image').addEventListener('error',()=>{$('#image-viewer-status').textContent='图片读取失败,请关闭后重试。';});
207
- $('#image-viewer').addEventListener('close',()=>{
208
- imagePan=null;imageStage.classList.remove('panning');document.body.style.overflow=imageScroll;
209
- if(imageSource?.isConnected)imageSource.focus();
210
- imageSource=null;
211
- });
212
- $('#image-viewer').addEventListener('click',event=>{
213
- const action=event.target.closest('[data-image-view]')?.dataset.imageView;
214
- if(action==='prev' || action==='next'){showImageAt(imageIndex+(action==='next'?1:-1));return;}
215
- if(action==='close' || event.target===imageStage && Date.now()>=imageClickBlockedUntil){$('#image-viewer').close();return;}
216
- if(action==='reset')Object.assign(imageView,{scale:1,x:0,y:0,angle:0});
217
- if(action==='rotate-left' || action==='rotate-right')imageView.angle=(imageView.angle+(action==='rotate-right'?90:-90))%360;
218
- if(action==='zoom-in' || action==='zoom-out')Object.assign(imageView,zoomImageAt(imageView,action==='zoom-in'?1.2:1/1.2));
219
- if(action)updateImageView();
220
- });
221
- imageStage.addEventListener('wheel',event=>{
222
- event.preventDefault();
223
- const box=imageStage.getBoundingClientRect(), delta=event.deltaY*(event.deltaMode===1?16:event.deltaMode===2?box.height:1);
224
- Object.assign(imageView,zoomImageAt(imageView,Math.exp(-delta/500),event.clientX-box.left-box.width/2,event.clientY-box.top-box.height/2));
225
- updateImageView();
226
- },{passive:false});
227
- imageStage.addEventListener('pointerdown',event=>{
228
- if(event.button!==0 || event.target.tagName!=='IMG')return;
229
- event.preventDefault();imagePan={id:event.pointerId,x:event.clientX,y:event.clientY,startX:imageView.x,startY:imageView.y};
230
- imageStage.setPointerCapture(event.pointerId);imageStage.classList.add('panning');
231
- });
232
- imageStage.addEventListener('pointermove',event=>{
233
- if(imagePan?.id!==event.pointerId)return;
234
- const x=event.clientX-imagePan.x,y=event.clientY-imagePan.y;
235
- if(Math.hypot(x,y)<3)return;
236
- imageView.x=imagePan.startX+x;imageView.y=imagePan.startY+y;
237
- imageClickBlockedUntil=Date.now()+250;updateImageView();
238
- });
239
- const endImagePan=event=>{
240
- if(imagePan?.id!==event.pointerId)return;
241
- if(imageStage.hasPointerCapture(event.pointerId))imageStage.releasePointerCapture(event.pointerId);
242
- imageClickBlockedUntil=Date.now()+250;imageStage.classList.remove('panning');imagePan=null;
243
- };
244
- imageStage.addEventListener('pointerup',endImagePan);
245
- imageStage.addEventListener('pointercancel',endImagePan);
246
- const actionRequests={
247
- 'design-folder':'config.save','updates-check':'updates.check','updates-download':'updates.download','design-scan':'design.scan',recognize:'design.recognize','recognize-one':'design.recognize','write-designs':'copy.write','regenerate-designs':'copy.write','copy-check':'copy.preview','copy-write':'copy.write',
248
- 'manual-templates':'manual.templates','manual-check':'manual.preview','manual-write':'manual.write','queue-scan':'queue.scan','queue-add':'queue.add','queue-remove':'queue.remove',
249
- 'auto-read':'media.auto.preview','auto-rename':'media.rename','auto-apply':'media.auto.apply','media-save':'media.save','import-media':'media.import',book:'queue.book','business-open':'queue.book',
250
- 'business-save':'commerce.save','business-warehouses':'commerce.warehouses','business-preview-prices':'commerce.prepare','business-preview-stocks':'commerce.prepare','business-submit':'commerce.submit','business-query':'commerce.query',
251
- prepare:'listing.prepare',submit:'listing.submit',query:'listing.query','quality-list':'listing.quality.list','library-refresh':'library.refresh',upload:'library.upload','upload-retry':'library.upload',
252
- 'file-category':'library.category','file-note':'library.note','file-replace':'library.replace','library-delete':'library.delete_preview','file-delete':'library.delete_preview',
253
- 'config-save':'config.save','import-config':'config.import','business-pattern-save':'config.save','store-select':'stores.select','clear-credential':'secrets.set','store-keys-import':'stores.keys.import',
254
- 'model-catalog':'config.save','model-test':'config.save','seller-test':'config.save','server-test':'config.save','phone-model-import':'models.import','phone-model-popular':'models.popular',
255
- 'category-save':'category_template.save','category-save-as':'category_template.save_as','category-rename':'category_template.rename','category-delete':'category_template.delete','category-inspect':'category_template.inspect','category-preview':'category_template.preview','category-write':'category_template.write','category-catalog-refresh':'category_template.catalog'
256
- };
257
- const button = (action, label, data='', kind='', disabled=false) => {
258
- const conflict=actionRequests[action] && conflictingJob(actionRequests[action]);
259
- return `<button data-action="${e(action)}" ${data} class="${kind}" data-intrinsic-blocked="${Boolean(disabled)}" ${conflict?`data-resource-blocked="true" title="${e(conflictReason(conflict))}"`:''} ${disabled || conflict ? 'disabled' : ''}>${e(label)}</button>`;
260
- };
261
- const badge = (text, kind='muted') => `<span class="status ${kind}">${e(/^[a-z_]+$/.test(String(text)) ? statusText(text) : text)}</span>`;
262
- const heading = (title, detail, action='') => `<div class="page-heading"><div><h1>${e(title)}</h1><p class="sub">${e(detail)}</p></div>${action ? `<div class="page-actions" role="group" aria-label="页面操作">${action}</div>` : ''}</div>`;
263
- const back = (page, text) => `<button class="back" data-page="${e(page)}">返回${e(text)}</button>`;
264
- const empty = (title, detail) => `<div class="empty"><h3>${e(title)}</h3><p>${e(detail)}</p></div>`;
265
- const previewUrl = path => fileUrl(`/api/preview?path=${encodeURIComponent(path)}`);
266
- const imageAttrs = path => `src="${e(fileUrl(`/api/thumbnail?path=${encodeURIComponent(path)}`))}" data-full-src="${e(previewUrl(path))}"`;
267
- const download = (kind, label, path='') => `<a class="button-link" href="${e(fileUrl(`/api/download?kind=${encodeURIComponent(kind)}${path ? `&path=${encodeURIComponent(path)}` : ''}`))}">${e(label)}</a>`;
268
- const libraryUrl = item => cloudMode ? fileUrl(`/api/preview?name=${encodeURIComponent(item.name)}`) : safePublicUrl(item.url);
269
- const displayJson = value => JSON.stringify(value,(key,item)=>['state','status','submit_state','moderate_status','verification'].includes(key) && typeof item==='string'?statusText(item):key==='action'?actionText(item):item,2);
270
- const json = value => `<pre class="log">${e(displayJson(value))}</pre>`;
271
- const group = () => ui.data?.groups.find(item => String(item.id) === String(ui.group));
272
- const queueGroups = () => scopeQueueGroups(ui.data?.groups || [],ui.data?.config?.listing?.model_scope || 'all',ui.data?.phone_models);
273
- const design = () => ui.data?.designs.find(item => String(item.id) === String(ui.design));
274
- const file = () => ui.data?.library.find(item => item.name === ui.file);
275
- const preview = () => ui.data?.previews?.[ui.design] || null;
276
- const resourcesFor = action => Array.isArray(ui.data?.action_resources?.[action])?ui.data.action_resources[action]:['*'];
277
- const resourcesConflict = (left,right) => left.includes('*') || right.includes('*') || left.some(resource=>right.includes(resource));
278
- const taskList = () => {
279
- const saved=Array.isArray(ui.data?.jobs)?ui.data.jobs:Object.values(ui.data?.jobs || {});
280
- return [...new Map([...saved,...(ui.data?.running_jobs || []),...(ui.lastJob?[ui.lastJob]:[]),...observedJobs.values()].map(job=>[job.id || job,job])).values()];
281
- };
282
- const runningJobs = () => taskList().filter(job=>['queued','running'].includes(job.state));
283
- const conflictingJob = action => {
284
- const resources=resourcesFor(action),jobs=[...operations.values(),...runningJobs()];
285
- if((ui.active && !operations.size && !jobs.length) || (ui.data?.busy && !Array.isArray(ui.data.running_jobs) && !jobs.length))jobs.push({resources:['*']});
286
- return jobs.find(job=>resourcesConflict(resources,job.resources || resourcesFor(job.action)));
287
- };
288
- const conflictReason = job => job.state==='uncertain' || job.awaiting_confirmation?`“${actionText(job.action)}”结果待核实,请先核实任务状态,勿重复提交。`:`正在执行“${actionText(job.action)}”,占用相同资源;请等待该任务结束。`;
289
- const busy = (action='*') => Boolean(conflictingJob(action) || action==='*' && ui.uploading);
290
- function setActionDisabled(control,disabled,action=actionRequests[control?.dataset.action]) {
291
- if(!control)return;
292
- control.dataset.intrinsicBlocked=String(Boolean(disabled));
293
- const conflict=action && conflictingJob(action);
294
- control.disabled=Boolean(ui.closed || disabled || conflict);
295
- if(conflict){
296
- if(control.dataset.resourceBlocked!=='true')control.dataset.resourceTitle=control.title || '';
297
- control.dataset.resourceBlocked='true';control.title=conflictReason(conflict);
298
- }else if(control.dataset.resourceBlocked==='true'){
299
- control.title=control.dataset.resourceTitle || '';delete control.dataset.resourceBlocked;delete control.dataset.resourceTitle;
300
- }
301
- }
302
- function syncActionControls() {
303
- document.querySelectorAll('[data-action]').forEach(control=>{
304
- if(actionRequests[control.dataset.action])setActionDisabled(control,control.dataset.intrinsicBlocked===undefined?control.disabled:control.dataset.intrinsicBlocked==='true');
305
- });
306
- for(const [selector,action] of [['#group-all,[data-group-check]','queue.select'],['#phone-model-scope','config.save']])document.querySelectorAll(selector).forEach(control=>setActionDisabled(control,false,action));
307
- }
308
- function invalidateReads(action,result) {
309
- let affected=[];
310
- if(/^category_template\.(save|save_as|rename|delete|import_phone_defaults)$/.test(action))affected=['category_template.list','category_template.inspect'];
311
- else if(action==='stores.select')affected=['queue.book'];
312
- else if(/^(copy\.write|manual\.write|category_template\.write|queue\.(scan|add|remove)|commerce\.save|media\.(rename|import|auto\.apply))$/.test(action))affected=['queue.book','manual.templates'];
313
- else if(action==='config.import' || action==='config.save' && result?.refresh_required!==false)affected=[...cachedActions];
314
- for(const [key,value] of readCache)if(affected.includes(value.action))readCache.delete(key);
315
- }
316
- const knownReferences = item => Array.isArray(item.references) && item.references.length > 0;
317
- const qualityScope = () => JSON.stringify([cloudMode?ui.session?.user?.username:'local',ui.workspace || 'local',ui.data?.config.seller.selected_id || '',ui.data?.config.seller.client_id || '']);
318
- function resetQualityDraft() {Object.assign(ui,{qualityDocuments:[],qualityId:null,qualityLoaded:false,qualityDirty:false,qualityScope:null});}
319
- const qualityReview = code => `${({approved:'审核通过',awaiting_verification:'待审核',verification:'审核中',declined:'审核拒绝'})[code] || '审核状态待核实'}${code?`(${code})`:''}`;
320
- const qualityLabel = item => item?`${item.certificate_name || '未命名文件'} · ${item.certificate_number || '编号未返回'}`:'本批不绑定';
321
- const qualityUnavailable = item => !['approved','awaiting_verification','verification'].includes(item.status_code) || Boolean(item.expire_date && Number.isFinite(Date.parse(item.expire_date)) && Date.parse(item.expire_date)<Date.now());
322
- function qualitySelector() {
323
- const selected=ui.qualityDocuments.find(item=>item.certificate_id===ui.qualityId),ready=Boolean(ui.data.config.seller.client_id && ui.data.credentials.seller_key_set);
324
- return `<div class="quality-choice"><div class="toolbar"><label for="quality-certificate">本批质量文件</label><select id="quality-certificate" aria-describedby="quality-document-status" title="${e(qualityLabel(selected))}" ${!ready?'disabled':''}><option value="" ${ui.qualityId===null?'selected':''}>本批不绑定</option>${ui.qualityDocuments.map(item=>`<option value="${e(item.certificate_id)}" ${item.certificate_id===ui.qualityId?'selected':''} ${qualityUnavailable(item)?'disabled':''}>${e(qualityLabel(item))} · ${e(qualityReview(item.status_code))}${qualityUnavailable(item)?' · 不可选择':''}</option>`).join('')}</select>${button('quality-list','读取质量文件','','',!ready)}</div><p id="quality-document-status" class="hint" role="status">${selected?`${e(qualityLabel(selected))} · ${e(qualityReview(selected.status_code))} · 有效期 ${e(selected.expire_date || '无有效期或未返回')}`:!ready?'先选择并配置目标店铺。':ui.qualityLoaded?`已读取 ${ui.qualityDocuments.length} 份文件;本批不绑定,不保存默认。`:'可读取当前店铺已有文件;每批选择一份,不保存默认。'}${ui.qualityDirty?' · 质量文件已更改,请重新预检。':''}</p></div>`;
325
- }
326
- function qualityResult(item,detail=false) {
327
- if(!item)return '<span class="hint">本批不绑定</span>';
328
- const state=({waiting:'等待商品建立',accepted:'关联请求已受理',linked:'已确认关联',failed:'关联失败',uncertain:'关联结果待核实',partial:'部分关联待核实'})[item.state] || '关联状态待核实';
329
- return `<div class="quality-result">${badge(state,item.state==='linked'?'good':item.state==='failed'?'bad':'muted')}<small>${e(qualityLabel(item))}</small>${(item.product_status_codes || []).map(code=>`<small>关联审核:${e(qualityReview(code))}</small>`).join('')}${detail?`<p class="hint">${e(item.message || '关联与商品导入、审核和可售状态分别核对。')}</p>`:''}</div>`;
330
- }
331
- function storeChoice() {
332
- const stores=ui.data.stores || [], selected=ui.data.config.seller.selected_id || '';
333
- const store=stores.find(item=>item.id===selected);
334
- const label=store?`${store.name} · ${store.ready?'凭据已配置':'缺少凭据'}`:'请选择上架店铺';
335
- return `<div class="form-field full store-selector"><label id="store-choice-label" for="store-choice">目标 Ozon 店铺</label><div class="store-controls"><button id="store-choice" data-action="store-open" class="store-choice" role="combobox" aria-haspopup="listbox" aria-expanded="false" aria-controls="store-options" ${busy('stores.select')?'disabled':''}><span title="${e(label)}">${e(label)}</span><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button><div id="store-popover" class="store-popover" hidden><input id="store-search" type="search" aria-label="搜索店铺" placeholder="搜索店铺名称" autocomplete="off"><div id="store-options" role="listbox" aria-labelledby="store-choice-label">${renderStoreOptions()}</div></div></div><small>${stores.length?`已导入 ${stores.length} 个店铺;切换店铺后需重新预检。`:'在配置中心从文件导入店铺凭据,或填写单店铺凭据。'}</small></div>`;
336
- }
337
- function renderStoreOptions() {
338
- const stores=(ui.data.stores || []).filter(store=>store.name.toLowerCase().includes(ui.storeSearch.toLowerCase())),selected=ui.data.config.seller.selected_id || '';
339
- return `${ui.storeSearch?'':`<button role="option" aria-selected="${!selected}" data-action="store-select" data-id="">暂不选择店铺</button>`}${stores.map(store=>`<button role="option" aria-selected="${store.id===selected}" data-action="store-select" data-id="${e(store.id)}">${e(store.name)}<small>${store.ready?'凭据已配置':'缺少凭据'}</small></button>`).join('')}${stores.length?'':'<p class="hint">没有匹配的店铺</p>'}`;
340
- }
341
- function closeStorePicker() {
342
- const panel=$('#store-popover');if(panel)panel.hidden=true;
343
- $('#store-choice')?.setAttribute('aria-expanded','false');
344
- }
345
- function openStorePicker() {
346
- if(busy('stores.select'))return;
347
- ui.storeSearch='';$('#store-options').innerHTML=renderStoreOptions();$('#store-search').value='';$('#store-popover').hidden=false;$('#store-choice').setAttribute('aria-expanded','true');$('#store-search').focus();
348
- }
349
- const referenceLabel = item => knownReferences(item) ? `${item.references.length} 条关联` : '外部引用待核实';
350
- const toast = (message,duration=5000) => {
351
- clearTimeout(toastTimer); $('#toast').textContent = message; $('#toast').classList.add('visible');
352
- toastTimer = setTimeout(() => $('#toast').classList.remove('visible'), duration);
353
- };
354
- const showError = error => {if(error?.code==='STALE_VIEW')return;const message=error?.message || String(error);if(error?.code==='OPERATION_CONFLICT'){toast(message,2500);return;}const login=cloudMode && !ui.ready && $('#login-status');if(login){login.textContent=message;login.hidden=false;}else{$('#error-message').textContent=message;$('#error').hidden=false;const retry=$('#error-retry'),configPending=error?.code==='CONFIG_SAVE_UNCONFIRMED';retry.hidden=!error?.readOnly && !configPending;retry.dataset.action=configPending?'recover-write':error?.jobId?'retry-job-read':'refresh';retry.dataset.id=error?.jobId || '';retry.textContent=configPending?'核实保存结果':error?.jobId?'重读任务状态':'重新读取列表';}};
355
- const clearError = () => { $('#error').hidden=true;$('#error-message').textContent='';$('#error-retry').hidden=true;const login=$('#login-status');if(login){login.hidden=true;login.textContent='';} };
356
- function bindPreviews() {
357
- document.querySelectorAll('#page img').forEach(image=>{
358
- image.tabIndex=0;image.draggable=false;image.setAttribute('role','button');image.setAttribute('aria-haspopup','dialog');image.setAttribute('aria-label',`${image.alt || '图片'},查看大图`);
359
- image.onerror=()=>{if(image.dataset.recordImage!==undefined){image.alt='图片预览失败,源链接未更改';if(image.nextElementSibling)image.nextElementSibling.hidden=false;return;}if(image.dataset.fullSrc && !image.dataset.fallback){image.dataset.fallback='true';image.src=image.dataset.fullSrc;}};
360
- });
361
- document.querySelectorAll('[data-server-video]').forEach(video=>{
362
- video.onloadedmetadata=()=>{const label=document.querySelector(`[data-duration="${video.dataset.serverVideo}"]`);if(label)label.textContent=Number.isFinite(video.duration)?`${Math.floor(Math.round(video.duration)/60)}:${String(Math.round(video.duration)%60).padStart(2,'0')}`:'时长未知';if(video.paused && video.currentTime===0)video.currentTime=Math.min(.01,video.duration || .01);};
363
- video.onerror=()=>{const label=document.querySelector(`[data-duration="${video.dataset.serverVideo}"]`);if(label)label.textContent='浏览器无法预览此视频';};
364
- });
365
- }
366
- async function request(url, options={}) {
367
- const session=ui.session,workspace=ui.workspace,epoch=sessionEpoch;
368
- const ensureCurrent=()=>{if(cloudMode && (epoch!==sessionEpoch || session!==ui.session || workspace!==ui.workspace))throw staleView();};
369
- const headers={...options.headers};
370
- if(cloudMode){if(ui.workspace)headers['X-Ozon-Workspace']=ui.workspace;if(options.method && options.method!=='GET')headers['X-Ozon-CSRF']=ui.session?.csrf || ui.data?.csrf || '';}
371
- const controller=(!options.method || options.method==='GET' || url==='/api/login') && !options.signal ? new AbortController() : null;
372
- const timer=controller ? setTimeout(()=>controller.abort(),20000) : null;
373
- if(controller)options={...options,signal:controller.signal};
374
- try{
375
- let response;try{response=await fetch(`${cloudMode && url.startsWith('/api/') ? '/ozon' : ''}${url}`, {cache:'no-store',credentials:'same-origin', ...options,headers});}catch(error){ensureCurrent();throw error;}
376
- ensureCurrent();
377
- let data;try{data=await response.json();}catch(error){ensureCurrent();if(error.name==='AbortError')throw error;throw Object.assign(new Error(`服务返回无法读取的响应(HTTP ${response.status}),请刷新或重试`),response.ok?{}:{status:response.status,code:'REQUEST_FAILED'});}
378
- ensureCurrent();
379
- if(cloudMode && response.status===401){showLogin();throw new Error(typeof data.detail==='string'?data.detail:'登录已过期,请重新登录');}
380
- if (!response.ok || data.error) {const message=data.error || (typeof data.detail==='string' ? data.detail : '') || `请求失败(HTTP ${response.status})`;throw Object.assign(new Error(message),{status:response.status,code:message.startsWith('操作冲突:')?'OPERATION_CONFLICT':'REQUEST_FAILED'});}
381
- return data;
382
- }catch(error){if(controller?.signal.aborted){ensureCurrent();throw new Error('读取服务器数据超时,请点击重新连接或重试');}throw error;}finally{if(timer!==null)clearTimeout(timer);}
383
- }
384
- const post = (url,value) => request(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(value)});
385
- function resetWorkspace() {
386
- ui.taskLogOpen=false;$('#task-log-tools').hidden=true;$('#task-log-panel').hidden=true;$('#task-log-content').innerHTML='';$('#task-log-alert').innerHTML='';
387
- ui.queueSelectionDirty=false;ui.recordDetail=null;
388
- operations.clear();observedJobs.clear();readCache.clear();jobRequests.clear();ui.selectedJobId=null;
389
- Object.assign(ui,{jobReads:new Map(),historySearch:'',historyFilter:'all',historyPage:1,historySize:30,stateReadAt:null,stateReadError:'',historyReadError:'',historyReadJobId:'',recordMediaOpen:false});
390
- clearCardDrag();
391
- resetQualityDraft();
392
- if(categoryModule)categoryModule.setScope(ui.categoryTemplates,'');
393
- Object.assign(ui,{data:null,page:'queue',group:null,design:null,file:null,record:0,media:null,mediaBase:null,mediaUndo:null,mediaTyping:null,designs:new Set(),files:new Set(),search:'',upload:'',uploadItems:[],addBooks:'',importText:'',models:[],lastJob:null,lastResult:null,copy:null,autoDraft:null,bookRecords:[],manualPaths:[],manualPreview:null,manual:{file_type:'2',source_path:'',new_number:'',title:'',description:''}});
394
- Object.assign(ui,{businessSelected:new Set(),businessRow:null,businessDraft:null,businessChanged:false,warehouses:[],warehouseClient:'',commerceOperations:[],queueDetail:null,queueSearch:'',queueScroll:[0,0],queueDensity:'comfortable',queueColumns:QUEUE_COLUMNS.map(([key])=>key),queueSaved:null,queueSavedKey:null,queueFilter:'all',queuePage:1,pageSize:30});
395
- renderedPage=undefined;
396
- }
397
- function showLogin() {
398
- sessionEpoch++;navigationEpoch++;stateEpoch++;
399
- cancelConfirmation?.();if($('#image-viewer').open)$('#image-viewer').close();
400
- stopTeamPolling();
401
- ui.session=null;resetWorkspace();ui.active=false;ui.uploading=false;ui.ready=false;ui.logoutPending=false;ui.authLoading=false;
402
- ui.users=null;ui.team=null;ui.serverSettings=null;ui.allWorkspaces=null;$('#navigation-status').hidden=true;
403
- $('#cloud-tools').hidden=true;$('#team-nav').hidden=true;$('#users-nav').hidden=true;
404
- document.body.classList.add('cloud-login','login-enter');$('#server-summary').textContent='登录后读取服务器媒体';$('#version').textContent=`界面 ${UI_VERSION} · 云端工作台`;
405
- $('#connection').textContent='请登录';$('#connection').className='status muted';$('#breadcrumb').textContent='登录工作台';
406
- $('[data-action="shutdown"]').disabled=true;$('[data-action="theme"]').disabled=true;
407
- const theme=window.sessionStorage.getItem('ozon.loginTheme') || document.documentElement.dataset.theme || (window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.dataset.theme=theme;
408
- $('#page').innerHTML=`<div class="login-surface"><div class="login-brand"><span class="brand-mark" aria-hidden="true">o</span><strong>Ozon 工作台</strong><button type="button" data-action="login-theme" aria-label="切换登录页浅色或暗色外观">${theme==='dark'?'浅色外观':'暗色外观'}</button></div><div class="page-heading"><div><h1>登录工作台</h1><p class="sub">使用分配的账户,进入自己的工作区。</p></div></div><div id="login-status" class="error-banner" role="alert" hidden></div><p id="login-loading" class="login-loading" role="status" aria-live="polite" hidden></p><form id="login-form" class="form-grid"><div class="form-field full"><label for="login-username">用户名</label><input id="login-username" name="username" autocomplete="username" required maxlength="32" autofocus></div><div class="form-field full"><label for="login-password">密码</label><input id="login-password" name="password" type="password" autocomplete="current-password" required maxlength="200"></div><div class="full"><button type="submit" class="primary">登录</button><button type="button" class="quiet login-retry" data-action="refresh">重新连接</button></div></form><p class="hint">工作区独立保存 · 可在多个标签页并行处理</p><p class="login-version">界面 ${UI_VERSION}</p></div>`;
409
- $('#login-username').focus();
410
- }
411
- function setLoginLoading(message='') {
412
- ui.authLoading=Boolean(message);const status=$('#login-loading');if(status){status.textContent=message;status.hidden=!message;}
413
- const form=$('#login-form');if(form){form.setAttribute('aria-busy',String(Boolean(message)));form.querySelectorAll?.('input,button[type="submit"]').forEach(input=>{input.disabled=Boolean(message);});const submit=form.querySelector?.('[type="submit"]');if(submit)submit.textContent=message?'正在进入…':'登录';}
414
- }
415
- async function initCloud(knownSession) {
416
- const epoch=sessionEpoch;
417
- setLoginLoading('正在读取账户与工作区…');
418
- try{
419
- const session=knownSession || await request('/api/session');
420
- if(epoch!==sessionEpoch)throw staleView();
421
- if(!session.authenticated){showLogin();return false;}
422
- ui.session=session;
423
- const selected=new URLSearchParams(window.location?.search || '').get('workspace') || window.sessionStorage.getItem('ozon.workspace') || session.workspace_id;
424
- ui.workspace=session.workspaces.find(item=>item.id===selected)?.id || session.workspace_id || session.workspaces[0]?.id;
425
- if(!ui.workspace)throw new Error('账户没有工作区,请联系管理员');
426
- window.sessionStorage.setItem('ozon.workspace',ui.workspace);return true;
427
- }catch(error){if(epoch===sessionEpoch)setLoginLoading();throw error;}
428
- }
429
- async function switchWorkspace(id) {
430
- if(id===ui.workspace)return true;
431
- if(busy()){toast('当前工作区任务运行中,可在新标签页处理其他工作区');$('#workspace-choice').value=ui.workspace;return false;}
432
- if(!ui.session.workspaces.some(item=>item.id===id))throw new Error('工作区不属于当前账户');
433
- if(!await leaveMedia() || !await leaveCategoryDraft()){$('#workspace-choice').value=ui.workspace;return false;}
434
- sessionEpoch++;navigationEpoch++;resetWorkspace();ui.workspace=id;window.sessionStorage.setItem('ozon.workspace',id);window.history?.replaceState(null,'',`/ozon/?workspace=${encodeURIComponent(id)}`);await reload();return true;
435
- }
436
- function openWorkspace(id=ui.workspace) {window.open(`/ozon/?workspace=${encodeURIComponent(id)}`,'_blank','noopener');}
437
- function applyWorkspaces(result) {ui.allWorkspaces=result.workspaces || [];ui.session.workspaces=ui.allWorkspaces.filter(item=>!item.deleted);}
438
- async function refreshWorkspaces() {applyWorkspaces(await request('/api/workspaces?include_deleted=true'));}
439
- function renderWorkspaces() {
440
- const active=ui.session.workspaces,deleted=(ui.allWorkspaces || []).filter(item=>item.deleted);
441
- return heading('管理工作区','按项目或批次分开处理;已有工作区可直接使用,无需每次新建。')+`<div class="workspace-guide"><p>每个账号的配置、素材清单、商品队列、任务与上架记录独立保存;通用型号样本与视频可复用,超级管理员可查看所有账号。</p><p>在新标签页打开不同工作区,可并行处理任务。改名保留原有路径;删除保留文件与历史,可从下方恢复。</p></div><div class="table-wrap workspace-table"><table><thead><tr><th>工作区名称</th><th>状态</th><th>操作</th></tr></thead><tbody>${active.map(item=>{const running=item.busy || item.id===ui.workspace && busy();return `<tr><td><label class="visually-hidden" for="workspace-rename-${e(item.id)}">${e(item.name)} 的新名称</label><input id="workspace-rename-${e(item.id)}" value="${e(item.name)}" maxlength="60" required ${running?'disabled':''}></td><td>${badge(running?'任务运行中':'可使用',running?'':'good')}${item.id===ui.workspace?'<small class="hint">当前工作区</small>':''}</td><td><div class="actions">${button('workspace-open','新标签页',`data-id="${e(item.id)}"`)}${button('workspace-rename','保存名称',`data-id="${e(item.id)}"`,'',running)}${button('workspace-delete','删除',`data-id="${e(item.id)}"`,'danger',running || active.length<=1)}</div></td></tr>`;}).join('')}</tbody></table></div><p class="hint">至少保留一个可用工作区;任务运行时不能改名或删除。</p><details class="workspace-create"><summary>按需新建工作区</summary><div class="panel"><form id="workspace-form" class="form-grid"><div class="form-field"><label for="workspace-name">新工作区名称</label><input id="workspace-name" required maxlength="60" placeholder="例如:今日新款"></div><div class="form-field"><label for="workspace-new-tab">打开方式</label><select id="workspace-new-tab"><option value="tab">在新标签页打开</option>${busy()?'':'<option value="current">在当前标签页打开</option>'}</select></div><div class="full"><button type="submit" class="primary">创建工作区</button></div></form></div></details><details data-workspace-deleted><summary>已删除的工作区 · ${deleted.length} 个</summary>${deleted.length?`<div class="table-wrap"><table><thead><tr><th>工作区</th><th>操作</th></tr></thead><tbody>${deleted.map(item=>`<tr><td>${e(item.name)}</td><td>${button('workspace-restore','恢复工作区',`data-id="${e(item.id)}"`)}</td></tr>`).join('')}</tbody></table></div>`:'<p class="hint">暂无已删除的工作区。</p>'}</details>`;
442
- }
443
- function renderAccount() {
444
- return heading('我的账户',`${ui.session.user.username} · ${roleText(ui.session.user)}`)+`<div class="panel"><form id="password-form" class="form-grid"><div class="form-field full"><label for="current-password">原密码</label><input id="current-password" type="password" autocomplete="current-password" maxlength="200" required></div><div class="form-field"><label for="new-password">新密码</label><input id="new-password" type="password" autocomplete="new-password" minlength="8" maxlength="200" required><small>8–200 个字符。</small></div><div class="form-field"><label for="confirm-password">确认新密码</label><input id="confirm-password" type="password" autocomplete="new-password" minlength="8" maxlength="200" required></div><div class="full"><button type="submit" class="primary">修改自己的密码</button></div></form></div>`;
445
- }
446
- function renderUsers() {
447
- const users=ui.users || [],lastAdmin=users.filter(user=>user.role==='admin'&&!user.disabled).length<=1;
448
- const rows=users.map(user=>{
449
- const protectedUser=user.super_admin || user.username==='zsh',lastActiveAdmin=user.role==='admin'&&!user.disabled&&lastAdmin;
450
- const passwordField=user.username===ui.session.user.username?'<button data-page="account">修改自己的密码</button>':`<div class="field-row"><label class="visually-hidden" for="reset-${e(user.username)}">${e(user.username)} 的新密码</label><input id="reset-${e(user.username)}" type="password" autocomplete="new-password" minlength="8" maxlength="200" placeholder="新的账户密码">${button('user-password','重置密码',`data-username="${e(user.username)}"`)}</div>`;
451
- return `<tr><td>${e(user.username)}</td><td>${roleText(user)}</td><td>${badge(user.disabled?'已禁用':'已启用',user.disabled?'':'good')}</td><td><div class="user-management"><div class="actions">${protectedUser?'<span class="hint">受保护账户</span>':button('user-toggle',user.disabled?'启用':'禁用',`data-username="${e(user.username)}" data-disabled="${!user.disabled}"`,'',lastActiveAdmin)+button('user-delete','删除账户',`data-username="${e(user.username)}"`,'danger',lastActiveAdmin)}</div>${passwordField}</div></td></tr>`;
452
- }).join('');
453
- return heading('人员管理','仅超级管理员可管理账户;创建的管理员或普通人员可直接登录。',button('users-refresh','刷新人员'))+`<div class="panel"><form id="user-form" class="form-grid"><div class="form-field"><label for="new-username">新用户名</label><input id="new-username" autocomplete="off" required maxlength="32" pattern="(?:[A-Za-z0-9_]|-){1,32}" title="1–32 位字母、数字、下划线或横线"><small>1–32 位字母、数字、下划线或横线。</small></div><div class="form-field"><label for="user-role">账户角色</label><select id="user-role"><option value="member">普通人员</option><option value="admin">管理员</option></select></div><div class="form-field full"><label for="user-password">初始密码</label><input id="user-password" type="password" autocomplete="new-password" minlength="8" maxlength="200" required><small>8–200 个字符。创建后直接使用用户名和密码登录。</small></div><div class="full"><button type="submit" class="primary">创建账户</button></div></form></div><div class="table-wrap"><table><thead><tr><th>用户名</th><th>角色</th><th>状态</th><th>管理</th></tr></thead><tbody>${rows || '<tr><td colspan="4">暂无可管理人员。</td></tr>'}</tbody></table></div><p class="hint">zsh 为唯一超级管理员,不能停用或删除。删除其他账户会保留历史,且用户名不能再使用。</p>`;
454
- }
455
- function renderTeam() {
456
- const workspaces=ui.team?.workspaces || [],audit=(ui.team?.audit || []).slice(0,200);
457
- const auditTable=`<details class="team-audit" data-team-detail="audit"><summary>操作审计 · 最近 ${audit.length} 条</summary><div class="table-wrap" data-team-scroll="audit"><table><thead><tr><th>时间</th><th>操作人</th><th>工作区</th><th>动作</th><th>结果</th></tr></thead><tbody>${audit.map(row=>`<tr><td>${e(Number.isFinite(row.at)?new Date(row.at*1000).toLocaleString('zh-CN'):'时间未返回')}</td><td>${e(row.username)}</td><td class="code">${e(row.workspace || '账户 / 服务器')}</td><td>${e(actionText(row.action))}</td><td>${badge(stateText(row.state),row.state==='done'?'good':row.state==='failed'?'bad':'muted')}</td></tr>`).join('') || '<tr><td colspan="5">暂无操作审计记录。</td></tr>'}</tbody></table></div></details>`;
458
- const status=ui.teamError?`自动刷新失败:${ui.teamError},稍后重试`:ui.teamAt?`每约 2 秒读取实际状态 · 最近更新 ${new Date(ui.teamAt).toLocaleTimeString('zh-CN')}`:'正在读取实际团队状态';
459
- return heading('团队任务','查看所有人的工作区状态、任务日志、上架记录与操作审计。',button('team-refresh','刷新团队状态'))+`<p class="hint" id="team-sync" role="status">${e(status)}</p>`+auditTable+(workspaces.length ? workspaces.map(workspace=>{
460
- const key=`${workspace.username}/${workspace.id}`,jobs=Array.isArray(workspace.jobs)?workspace.jobs:Object.values(workspace.jobs || {});
461
- return `<section class="team-workspace"><div class="section-header"><h2>${e(workspace.username)} / ${e(workspace.name || workspace.id)}</h2>${badge(workspace.busy?'任务运行中':'空闲',workspace.busy?'':'good')}</div><p class="hint">店铺:${e(workspace.store_name || '尚未选择')}</p>${jobs.map((job,index)=>{
462
- const jobKey=`${key}/job/${job.id || index}`,files=job.files || [],messages=job.messages || [],latest=messages.at(-1),message=typeof latest==='string'?latest:latest?.message || '';
463
- return `<details data-team-detail="${e(jobKey)}"><summary>${e(actionText(job.action))} · ${e(stateText(job.state))}</summary>${files.length?`<p class="hint">已返回 ${files.length} 个文件事件 · 已完成 ${files.filter(file=>file.state==='done').length} · 失败 ${files.filter(file=>file.state==='failed').length}</p><div class="upload-list" data-team-scroll="${e(jobKey)}/files">${files.map(file=>`<div class="file-progress"><strong title="${e(file.path || file.name)}">${e(String(file.path || file.name || '未命名文件').split(/[\\/]/).at(-1))}</strong>${badge(stateText(file.state),file.state==='done'?'good':file.state==='failed'?'bad':'muted')}${file.error?`<p class="upload-error">${e(file.error)}</p>`:''}</div>`).join('')}</div>`:''}${message?`<p class="hint">最新日志:${e(message)}</p>`:''}<pre class="log" data-team-scroll="${e(jobKey)}/log">${e(messages.map(message=>typeof message==='string'?message:message.message || displayJson(message)).join('\n'))}</pre>${job.error?`<p class="upload-error">${e(job.error)}</p>`:''}</details>`;
464
- }).join('') || empty('暂无任务','此工作区尚无保存的任务。')}<details data-team-detail="${e(key)}/records"><summary>上架记录 · ${(workspace.records || []).length} 条</summary><div class="table-wrap" data-team-scroll="${e(key)}/records"><table><thead><tr><th>货号</th><th>提交</th><th>导入 / 商品状态</th><th>错误</th></tr></thead><tbody>${(workspace.records || []).map(row=>`<tr><td class="code">${e(row.offer_id)}</td><td>${e(stateText(row.submit_state))}</td><td>${e(stateText(row.state || row.status))}</td><td>${e(typeof row.error==='string'?row.error:JSON.stringify(row.errors || []))}</td></tr>`).join('') || '<tr><td colspan="4">暂无上架记录。</td></tr>'}</tbody></table></div></details></section>`;
465
- }).join('') : empty('没有团队工作区记录','点击刷新团队状态读取已保存的任务。'));
466
- }
467
- const actionLabels={'updates.check':'检查软件更新','updates.download':'下载软件更新','design.scan':'读取设计目录','design.recognize':'识别设计图片','copy.preview':'检查文案','copy.write':'生成表格','manual.preview':'预览手动表格','manual.write':'生成手动表格','queue.scan':'读取生成表格','queue.add':'导入已有上架表','queue.remove':'移除队列关联','media.save':'保存媒体清单','media.auto.preview':'预览自动媒体','media.auto.apply':'应用自动媒体','media.rename':'图片编号','media.import':'导入媒体清单','library.upload':'上传媒体','library.refresh':'查询媒体与容量','library.replace':'替换服务器媒体','library.note':'保存媒体备注','library.delete':'删除服务器媒体','listing.existing':'检查店铺已有货号','listing.prepare':'上传与预检','listing.submit':'提交上架','listing.query':'查询上架结果','files.upload':'上传工作区文件','users.create':'创建账户','users.update':'管理账户','server-settings.save':'保存运行限制','config.save':'保存配置','stores.select':'选择店铺','commerce.save':'保存经营参数','commerce.warehouses':'读取仓库','commerce.prepare':'预览经营参数提交','commerce.submit':'提交经营参数','commerce.query':'回读经营参数','model.test':'测试模型连接','seller.test':'测试店铺连接'};
468
- Object.assign(actionLabels,{'workspaces.rename':'重命名工作区','workspaces.delete':'删除工作区','workspaces.restore':'恢复工作区','users.delete':'删除账户','config.import':'导入配置','models.import':'导入手机型号表','models.popular':'修改热门型号','library.category':'保存媒体用途','model.catalog':'读取模型目录','stores.keys.save':'保存店铺凭据','stores.keys.import':'导入店铺凭据','stores.import':'导入店铺目录'});
469
- const actionText=action=>actionLabels[action] || action || '未命名操作';
470
- const stateText=statusText;
471
- const teamVisible=()=>cloudMode && ui.session?.authenticated && isSuperAdmin() && ui.page==='team' && !document.hidden && !teamPageHidden;
472
- function stopTeamPolling() {clearTimeout(teamTimer);teamTimer=null;teamEpoch++;teamController?.abort();}
473
- function syncTeamPolling() {
474
- if(!teamVisible()){stopTeamPolling();return;}
475
- if(!teamLoading && teamTimer===null)teamTimer=setTimeout(()=>{teamTimer=null;return refreshTeam();},2000);
476
- }
477
- async function refreshTeam() {
478
- if(!teamVisible() || teamLoading)return false;
479
- clearTimeout(teamTimer);teamTimer=null;teamLoading=true;const epoch=teamEpoch;teamController=new AbortController();
480
- try{
481
- const result=await request('/api/team',{signal:teamController.signal});
482
- if(epoch!==teamEpoch || !teamVisible())return false;
483
- ui.team=result;ui.teamAt=Date.now();ui.teamError='';render();return true;
484
- }catch(error){if(epoch===teamEpoch && teamVisible()){ui.teamError=error.message || '团队状态读取失败';const status=$('#team-sync');if(status)status.textContent=`自动刷新失败:${ui.teamError},稍后重试`;}}
485
- finally{teamLoading=false;teamController=null;syncTeamPolling();}
486
- }
487
- function refreshSessionOnReturn() {
488
- if(!cloudMode || !ui.ready || ui.closed || ui.logoutPending || !ui.session?.authenticated || document.hidden)return;
489
- if(sessionRefresh)return sessionRefresh;
490
- const epoch=sessionEpoch;
491
- sessionRefresh=request('/api/session').then(saved=>{
492
- if(epoch===sessionEpoch && !saved.authenticated)showLogin();
493
- }).catch(error=>{if(epoch===sessionEpoch)showError(error);}).finally(()=>{sessionRefresh=null;});
494
- return sessionRefresh;
495
- }
496
- document.addEventListener('visibilitychange',()=>{document.body.classList.toggle('page-hidden',document.hidden);syncTeamPolling();refreshSessionOnReturn();});
497
- window.addEventListener('pagehide',()=>{teamPageHidden=true;document.body.classList.add('page-hidden');stopTeamPolling();});
498
- window.addEventListener('pageshow',()=>{teamPageHidden=false;document.body.classList.toggle('page-hidden',document.hidden);syncTeamPolling();refreshSessionOnReturn();});
499
- function checkCloudPermission(action,payload={}) {
500
- if(!cloudMode || isAdmin())return;
501
- if(/^(stores\.(keys\.|import)|server\.test|library\.(replace|note|delete))/.test(action) || action==='picker' && payload.kind==='credentials' || action==='secrets.set' && Object.keys(payload).some(key=>key!=='model_key'))throw new Error('此共享配置或媒体管理操作仅限管理员');
502
- }
503
- async function reload(renderPage=true) {
504
- if(ui.closed || ui.logoutPending)return;
505
- if(cloudMode && !ui.session?.authenticated && !$('#login-form'))showLogin();
506
- if(cloudMode && !ui.session?.authenticated && !await initCloud())return;
507
- const epoch=++stateEpoch,sessionGeneration=sessionEpoch,navigation=navigationEpoch;
508
- if(cloudMode && !ui.ready)setLoginLoading('正在读取配置、素材与任务记录…');
509
- try{
510
- const data=await request('/api/state?summary=1');
511
- let detail=null,recordIndex=ui.record;
512
- if(ui.page==='record' && navigation===navigationEpoch){
513
- const selected=ui.recordDetail || ui.data?.records[ui.record];
514
- const index=data.records.findIndex(row=>row.offer_id===selected?.offer_id && row.batch_key===selected?.batch_key);
515
- if(index<0)throw new Error('此条记录已变化,请返回上架记录重新选择');
516
- const row=data.records[index];
517
- if(row.batch_key)detail=await request(`/api/record?offer_id=${encodeURIComponent(row.offer_id)}&batch_key=${encodeURIComponent(row.batch_key)}`);
518
- recordIndex=index;
519
- }
520
- if(cloudMode && (sessionGeneration!==sessionEpoch || ui.logoutPending))throw staleView();
521
- if(epoch!==stateEpoch)return;
522
- const previousStore=qualityScope();
523
- ui.data = data;ui.queueSelectionDirty=false;ui.stateReadAt=Date.now();ui.stateReadError='';
524
- if(previousStore!==qualityScope())invalidateReads('stores.select');
525
- if(navigation===navigationEpoch){ui.recordDetail=detail;ui.record=recordIndex;}
526
- const jobs=Array.isArray(data.jobs)?data.jobs:Object.values(data.jobs || {});
527
- for(const operation of operations.values()){
528
- if(operation.state!=='uncertain' || !operation.request_id)continue;
529
- const recovered=[...jobs,...(data.running_jobs || [])].find(job=>job.request_id===operation.request_id && job.action===operation.action);
530
- if(!recovered?.id || !['queued','running','done','failed'].includes(recovered.state))continue;
531
- const job={...recovered,...(['queued','running'].includes(recovered.state)?{awaiting_confirmation:true}:{})};
532
- observedJobs.set(job.id,job);ui.lastJob=job;operations.delete(operation);ui.active=operations.size>0;
533
- }
534
- for(const job of [...jobs,...(data.running_jobs || [])]){
535
- const known=observedJobs.get(job.id);
536
- if((known || ui.lastJob?.id===job.id) && (!known || ['queued','running'].includes(known.state) || !['queued','running'].includes(job.state))){const updated=known?.awaiting_confirmation && ['queued','running'].includes(job.state)?{...job,awaiting_confirmation:true}:job;observedJobs.set(job.id,updated);if(ui.lastJob?.id===job.id)ui.lastJob=updated;}
537
- }
538
- if(cloudMode && ui.data.cloud?.workspaces)ui.session.workspaces=ui.data.cloud.workspaces;
539
- if (!ui.lastJob) {const jobs=Array.isArray(ui.data.jobs)?ui.data.jobs:Object.values(ui.data.jobs || {});ui.lastJob=jobs.at(-1) || null;}
540
- ui.designs = new Set([...ui.designs].filter(id => ui.data.designs.some(item => String(item.id) === String(id))));
541
- ui.files = new Set([...ui.files].filter(name => ui.data.library.some(item => item.name === name)));
542
- applyAppearance(); if (renderPage || cloudMode && !ui.ready) render(); else renderChrome();
543
- }catch(error){if(epoch===stateEpoch && sessionGeneration===sessionEpoch && error?.code!=='STALE_VIEW'){ui.stateReadError=error.message || String(error);error.readOnly=true;if(ui.page==='history' && ui.data)render();}throw error;}finally{if(cloudMode && epoch===stateEpoch && sessionGeneration===sessionEpoch && !ui.ready)setLoginLoading();}
544
- }
545
- async function readJob(id,summary=true) {
546
- const key=`${id}:${summary}`;
547
- if(jobRequests.has(key))return jobRequests.get(key);
548
- const current=scopeLease(),previous=ui.jobReads.get(id),controller=new AbortController(),timer=setTimeout(()=>controller.abort(),15000);
549
- const pending=(async()=>{
550
- try{
551
- const job=await request(`/api/job?id=${encodeURIComponent(id)}${summary?'&summary=1':''}`,{signal:controller.signal});
552
- if(!current())throw staleView();
553
- ui.jobReads.set(id,{at:Date.now(),error:''});return job;
554
- }catch(error){
555
- if(!current())throw staleView();
556
- if(controller.signal.aborted)error=new Error('任务状态读取超时,请核实任务状态;后台任务不会自动重发');
557
- if(error?.code!=='STALE_VIEW'){ui.jobReads.set(id,{at:previous?.at,error:error.message || String(error)});error.readOnly=true;error.jobId=id;}
558
- throw error;
559
- }finally{clearTimeout(timer);}
560
- })();
561
- jobRequests.set(key,pending);
562
- try{return await pending;}finally{if(jobRequests.get(key)===pending)jobRequests.delete(key);}
563
- }
564
- async function watchJob(id,onUpdate=()=>{}) {
565
- const current=scopeLease();
566
- for (;;) {
567
- if(!current())throw staleView();
568
- const job = await readJob(id);
569
- if(!current())throw staleView();
570
- if(!job.id)job.id=id;
571
- observedJobs.set(id,job);ui.lastJob=job;onUpdate(job);renderChrome();
572
- if (ui.page === 'task') $('#page').innerHTML = renderJob();
573
- if(ui.uploading && job.action==='library.upload'){
574
- for(const file of job.files || []){
575
- const normalize=path=>String(path).replaceAll('\\','/').toLowerCase();
576
- const item=ui.uploadItems.find(item=>normalize(item.path)===normalize(file.path)) || (job.files.length===1 ? ui.uploadItems.find(item=>['uploading','checking'].includes(item.state)):null);
577
- if(item){const {path,...progress}=file;Object.assign(item,progress);}
578
- }
579
- if(ui.page==='upload'){updateUploads();}
580
- }
581
- if (job.state === 'failed') throw new Error(job.error || '任务失败,请查看任务日志');
582
- if (job.state === 'done') {
583
- if(job.action==='listing.query')return;
584
- if(Object.prototype.hasOwnProperty.call(job,'result'))return job.result;
585
- const complete=await readJob(id,false);
586
- if(!current())throw staleView();
587
- observedJobs.set(id,complete);ui.lastJob=complete;onUpdate(complete);renderChrome();
588
- return complete.result;
589
- }
590
- await new Promise(resolve => setTimeout(resolve, 500));
591
- }
592
- }
593
- const configUnconfirmed = () => Object.assign(new Error('配置保存结果待核实,请点击“核实保存结果”;不会自动重复保存'),{code:'CONFIG_SAVE_UNCONFIRMED'});
594
- async function readConfigReceipt(operation,current=scopeLease()) {
595
- if(!current() || !operations.has(operation))throw staleView();
596
- let status;
597
- try{status=await request(`/api/config-save?request_id=${encodeURIComponent(operation.request_id)}`);}
598
- catch(error){if(!current() || error?.code==='STALE_VIEW')throw staleView();throw configUnconfirmed();}
599
- if(!current() || !operations.has(operation))throw staleView();
600
- if(status.state!=='done' || status.request_id!==operation.request_id || status.action!==operation.action ||
601
- !status.result?.config || typeof status.result.config!=='object' || Array.isArray(status.result.config) || typeof status.result.refresh_required!=='boolean')throw configUnconfirmed();
602
- return status.result;
603
- }
604
- async function recoverConfigSave() {
605
- const operation=[...operations.values()].find(item=>item.state==='uncertain' && configActions.has(item.action));
606
- if(!operation)return false;
607
- const current=scopeLease(),pageCurrent=pageLease(),result=await readConfigReceipt(operation,current);
608
- if(!current())throw staleView();
609
- stateEpoch++;ui.data.config=result.config;if(result.plan_invalidated)ui.data.plan=null;
610
- operation.state='done';operations.delete(operation);ui.active=operations.size>0;
611
- invalidateReads(operation.action,result);
612
- ui.lastResult={action:operation.action,result};
613
- if(ui.lastJob?.request_id===operation.request_id)ui.lastJob={...ui.lastJob,state:'done',error:''};
614
- clearError();applyAppearance();
615
- if(result.refresh_required!==false){
616
- try{await reload(false);}catch(error){if(!current())throw staleView();showError(new Error('配置保存已核实,但页面数据未刷新,请重新读取数据'));}
617
- }
618
- if(!current())throw staleView();if(pageCurrent())render();else renderChrome();
619
- toast(result.superseded?'本次保存已完成,配置随后有更新,请核对当前值':'配置保存已核实');
620
- return true;
621
- }
622
- async function act(action, payload={}) {
623
- checkCloudPermission(action,payload);
624
- const bookScope=action==='queue.book'?qualityScope():null;
625
- const current=scopeLease();if(!current())throw staleView();
626
- if(['media.auto.preview','media.auto.apply'].includes(action) && ui.queueSelectionDirty){
627
- await act('queue.select',{ids:[...ui.data.selected_groups]});
628
- if(!current())throw staleView();
629
- }
630
- const cacheKey=JSON.stringify([action,payload]);
631
- if(cachedActions.has(action) && readCache.has(cacheKey))return structuredClone(readCache.get(cacheKey).result);
632
- const conflict=conflictingJob(action);if(conflict)throw Object.assign(new Error(conflictReason(conflict)),{code:'OPERATION_CONFLICT'});
633
- const operation={action,resources:resourcesFor(action),...(submissionActions.has(action) || configActions.has(action)?{request_id:crypto.randomUUID()}:{})};operations.set(operation,operation);
634
- const readController=action==='queue.book' || configActions.has(action)?new AbortController():null;
635
- const readTimer=readController?setTimeout(()=>readController.abort(),action==='queue.book'?15000:20000):null;
636
- let acknowledged=false;
637
- clearError(); ui.active = true; renderChrome();
638
- try {
639
- let response;
640
- try{
641
- response=await request('/api/action', {method:'POST',headers:{'Content-Type':'application/json','X-Ozon-CSRF':ui.data.csrf},body:JSON.stringify({action,...payload,...(operation.request_id?{request_id:operation.request_id}:{})}),...(readController?{signal:readController.signal}:{})});
642
- }catch(error){
643
- if(!current())throw staleView();
644
- const rejected=error?.status>=400 && error.status<500 && error.status!==408 || error?.code==='REQUEST_FAILED' && error.status>=200 && error.status<400;
645
- if(!configActions.has(action) || rejected)throw error;
646
- toast('保存响应未确认,正在核实结果;不会重复保存');
647
- response={result:await readConfigReceipt(operation,current)};
648
- }finally{if(readTimer!==null)clearTimeout(readTimer);}
649
- if(!response.job_id && !Object.prototype.hasOwnProperty.call(response,'result'))throw new Error('服务未确认操作结果,请先核实任务状态');
650
- acknowledged=true;
651
- if(response.job_id){
652
- operation.id=response.job_id;
653
- const job={...operation,id:response.job_id,state:'queued'};observedJobs.set(job.id,job);ui.lastJob=job;
654
- }
655
- let terminalJob;
656
- const result = response.job_id ? await watchJob(response.job_id,job=>{terminalJob=job;operation.id=job.id;operation.resources=job.resources || operation.resources;}) : response.result;
657
- if(!current() || action==='queue.book' && bookScope!==qualityScope())throw staleView();
658
- ui.lastResult = {action,result};
659
- const planInvalidated=response.job_id?terminalJob?.plan_invalidated:response.plan_invalidated;
660
- invalidateReads(action,result);
661
- if(cachedActions.has(action))readCache.set(cacheKey,{action,result:structuredClone(result)});
662
- if(configActions.has(action) && result?.refresh_required===false){
663
- stateEpoch++;
664
- ui.data.config=result.config;
665
- if(result.plan_invalidated)ui.data.plan=null;
666
- applyAppearance();
667
- }else if(['models.popular','models.import'].includes(action)){
668
- stateEpoch++;
669
- ui.data.phone_models=result;
670
- ui.data.plan=null;
671
- }else if(action==='queue.select' && Array.isArray(result?.ids)){
672
- stateEpoch++;
673
- ui.data.selected_groups=result.ids;
674
- ui.queueSelectionDirty=false;
675
- ui.data.plan=null;
676
- }else if(['listing.quality.list','listing.existing'].includes(action)){
677
- // Current-store documents are a read-only result, independent of the queue snapshot.
678
- }else if(action.startsWith('updates.')){
679
- // Program updates are independent of workspace business state.
680
- }else if(action.startsWith('category_template.') && action!=='category_template.write'){
681
- // Template reads and settings never mutate the current queue or preflight plan.
682
- }else if(['picker','manual.templates','queue.book','media.auto.preview','commerce.warehouses','commerce.query','model.catalog','model.test','seller.test','server.test','library.delete_preview'].includes(action)){
683
- if(planInvalidated)ui.data.plan=null;
684
- }else await reload(false);
685
- if(action==='listing.prepare')ui.qualityDirty=ui.qualityId!==(payload.quality_certificate_id ?? null);
686
- return result;
687
- } catch(error){
688
- if(current()){
689
- invalidateReads(action);
690
- if(operation.id){const job=observedJobs.get(operation.id);if(job && ['queued','running'].includes(job.state))job.awaiting_confirmation=true;}
691
- const rejected=(error?.status>=400 && error.status<500 && error.status!==408 && ['REQUEST_FAILED','OPERATION_CONFLICT'].includes(error?.code)) || configActions.has(action) && error?.code==='REQUEST_FAILED' && error.status>=200 && error.status<400;
692
- if(!acknowledged && (submissionActions.has(action) || configActions.has(action)) && !rejected && error?.code!=='STALE_VIEW'){
693
- operation.state='uncertain';operation.error=configActions.has(action)?'配置保存结果待核实,请点击核实保存结果':'提交结果待核实,请核实任务状态,勿重复提交';ui.lastJob={...operation,id:`request-${operation.request_id}`};
694
- }
695
- }
696
- if(current() && action==='queue.book' && readController?.signal.aborted)throw new Error('读取商品超时,请检查服务连接后重试');throw error;
697
- }
698
- finally { if(readTimer!==null)clearTimeout(readTimer);const owned=operation.state==='uncertain'?false:operations.delete(operation);if(current() || owned && !ui.logoutPending){ui.active=operations.size>0;renderChrome();} }
699
- }
700
- async function resumeJobs() {
701
- const current=scopeLease();
702
- await Promise.allSettled(runningJobs().map(async job=>{
703
- try{await watchJob(job.id);if(!current())throw staleView();invalidateReads(job.action);await reload(false);}
704
- catch(error){if(current())showError(error);}
705
- }));
706
- }
707
- function confirmAction(title, body, accept='确认', discard=false) {
708
- const dialog = $('#confirm-dialog'); $('#confirm-title').textContent = title; $('#confirm-body').innerHTML = body; $('#confirm-accept').textContent = accept;
709
- $('#confirm-discard').hidden=!discard;$('#confirm-cancel').textContent=discard?'留在此页':'取消';
710
- dialog.showModal();
711
- return new Promise(resolve => {
712
- let completed = false;
713
- const finish = accepted => { if (completed) return; completed = true; cancelConfirmation=null;dialog.close(); dialog.removeEventListener('cancel', cancel); $('#confirm-accept').onclick = null; $('#confirm-cancel').onclick = null;$('#confirm-discard').onclick=null; resolve(accepted); };
714
- const cancel = event => {event.preventDefault(); finish(false);};
715
- cancelConfirmation=()=>finish(false);
716
- dialog.addEventListener('cancel', cancel); $('#confirm-accept').onclick = () => finish(true); $('#confirm-cancel').onclick = () => finish(false);
717
- $('#confirm-discard').onclick=()=>finish('discard');
718
- });
719
- }
720
- function applyAppearance() {
721
- document.documentElement.dataset.theme = ui.data.config.ui.theme;
722
- document.body.classList.toggle('theme-b', ui.data.config.ui.theme === 'dark');
723
- document.body.classList.toggle('motion-off', !ui.data.config.ui.motion);
724
- document.body.classList.toggle('motion-on', Boolean(ui.data.config.ui.motion));
725
- }
726
- const motionAllowed = () => ui.data.config.ui.motion && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
727
- async function navigate(page) {
728
- if(categoryModule && page==='settings/commerce')page='settings/categoryTemplates';
729
- clearCardDrag();
730
- const intent=++navigationEpoch,epoch=sessionEpoch,current=()=>intent===navigationEpoch && epoch===sessionEpoch && !ui.logoutPending;
731
- $('#navigation-status').hidden=true;
732
- if(cloudMode && ['users','team'].includes(page) && !isSuperAdmin())throw new Error('此页面仅限超级管理员');
733
- if(cloudMode && !isAdmin() && ['settings/server','settings/seller'].includes(page))throw new Error('此页面仅限管理员');
734
- if(page!==ui.page && ui.page==='media' && mediaDirty() && !await leaveMedia())return;
735
- if(page!==ui.page && ui.page==='settings/categoryTemplates' && !await leaveCategoryDraft())return;
736
- if(!current())return;
737
- try{
738
- if(page==='record'){
739
- const row=ui.data.records[ui.record];ui.recordDetail=null;ui.recordMediaOpen=false;
740
- if(row?.batch_key){
741
- $('#navigation-status').textContent='正在读取此条上架记录…';$('#navigation-status').hidden=false;
742
- const detail=await request(`/api/record?offer_id=${encodeURIComponent(row.offer_id)}&batch_key=${encodeURIComponent(row.batch_key)}`);
743
- if(!current())return;
744
- ui.recordDetail=detail;
745
- }
746
- }
747
- if(['settings/categoryTemplates','categoryCreate'].includes(page)){await loadCategoryTemplates();if(!current())return;}
748
- if(cloudMode && ['users','workspaces','settings/server'].includes(page)){
749
- $('#navigation-status').textContent=`正在读取${labels[page] || '服务器配置'}…`;$('#navigation-status').hidden=false;
750
- const result=await request(page==='users'?'/api/users':page==='workspaces'?'/api/workspaces?include_deleted=true':'/api/server-settings');
751
- if(!current())return;
752
- if(page==='users')ui.users=result.users;else if(page==='workspaces')applyWorkspaces(result);else ui.serverSettings=result;
753
- }
754
- if(!current())return;
755
- const highlight = document.querySelector('nav .selected .nav-highlight');
756
- const before = motionAllowed() && highlight?.getBoundingClientRect();
757
- if(ui.page==='queue'){ui.queueSearch=ui.search;const table=$('.queue-table');if(table)ui.queueScroll=[table.scrollLeft || 0,table.scrollTop || 0];}
758
- ui.page = page; ui.search = page==='queue'?ui.queueSearch:''; render(); window.scrollTo({top:0,behavior:'instant'});
759
- if(cloudMode && page==='team')await refreshTeam();
760
- if(!current())return;
761
- const after = document.querySelector('nav .selected .nav-highlight');
762
- if(before && after?.animate){after.getAnimations().forEach(animation=>animation.cancel());after.animate(flipFrames(before,after.getBoundingClientRect()),{duration:280,easing:'cubic-bezier(.16,1,.3,1)'});}
763
- }catch(error){if(current() && error?.code!=='STALE_VIEW')throw error;}finally{if(current())$('#navigation-status').hidden=true;}
764
- }
765
- function mediaDirty() {return ui.mediaBase!==null && mediaSignature(ui.media)!==ui.mediaBase;}
766
- function updateMediaStatus() {
767
- const status=$('#media-dirty'),undo=$('[data-action="media-undo"]');
768
- if(status){status.textContent=mediaDirty()?'有未保存修改':'已保存';status.className=`status ${mediaDirty()?'':'good'}`;}
769
- if(undo)undo.disabled=!ui.mediaUndo;
770
- }
771
- function changeMedia(draft) {
772
- if(mediaSignature(draft)===mediaSignature(ui.media))return;
773
- ui.mediaUndo=structuredClone(ui.media);ui.mediaTyping=null;ui.media=draft;
774
- }
775
- async function saveMedia() {
776
- const value=structuredClone(collectMedia()),id=group().id;
777
- await act('media.save',{id,primary_image:value.primary_image,images:value.images,videos:value.videos,video_cover:value.video_cover});
778
- if(String(ui.group)===String(id)){
779
- ui.mediaBase=mediaSignature(value);if(!mediaDirty()){ui.mediaUndo=null;ui.mediaTyping=null;}
780
- toast(mediaDirty()?'已保存发送时的清单;后续修改仍需保存':'媒体清单已保存');updateMediaStatus();
781
- }
782
- }
783
- async function leaveMedia() {
784
- if(ui.page!=='media')return true;
785
- collectMedia();if(!mediaDirty())return true;
786
- const choice=await confirmAction('媒体清单尚未保存',`<p>${e(group()?.label || '当前款式')} 的图片、视频或顺序有修改。</p><p>保存后离开,或放弃本次修改。</p>`,'保存并离开',true);
787
- if(choice===true){await saveMedia();return !mediaDirty();}
788
- if(choice==='discard'){ui.media=structuredClone(group());ui.mediaBase=mediaSignature(ui.media);ui.mediaUndo=null;ui.mediaTyping=null;render();}
789
- return Boolean(choice);
790
- }
791
- window.addEventListener('beforeunload',event=>{
792
- if(ui.page==='media' && mediaDirty() || categoryModule && ui.categoryTemplates && (categoryModule.dirty(ui.categoryTemplates) || compatibilityDirty(ui.categoryTemplates))){event.preventDefault();event.returnValue='';}
793
- });
794
- function renderChrome() {
795
- updateCategoryStatus();
796
- if (!ui.data || ui.logoutPending) return;
797
- const visibleGroups=new Set(queueGroups().map(item=>String(item.id)));
798
- if(ui.data.config.listing.model_scope==='popular'){
799
- const previous=ui.data.selected_groups || [],selected=previous.filter(id=>visibleGroups.has(String(id)));
800
- if(selected.length!==previous.length)ui.queueSelectionDirty=true;
801
- ui.data.selected_groups=selected;
802
- }
803
- if(ui.queueDetail!==null && !visibleGroups.has(String(ui.queueDetail)))ui.queueDetail=null;
804
- if(ui.qualityScope!==qualityScope()){
805
- resetQualityDraft();ui.qualityScope=qualityScope();
806
- const plan=ui.data.plan,document=plan?.quality_document,seller=ui.data.config.seller;
807
- if(document && String(plan.client_id)===String(seller.client_id) && String(document.client_id)===String(seller.client_id) && String(plan.store_id)===String(seller.selected_id)){ui.qualityId=document.certificate_id;ui.qualityDocuments=[document];}
808
- }
809
- const updateNotice=$('#update-notice');if(updateNotice){updateNotice.hidden=cloudMode||!ui.data.update_enabled;updateNotice.textContent=ui.updateStatus?.pending_version?'更新已下载':ui.updateStatus?.available?'有新版本':'软件更新';}
810
- $('#version').textContent = `界面 ${UI_VERSION} · 服务 ${ui.data.version}`;
811
- $('#breadcrumb').textContent = ui.page.startsWith('settings/') ? '配置中心 / ' + ({paths:'路径',model:'模型',seller:'Ozon 店铺',server:'服务器与容量',rules:'生成规则',mediaRules:'媒体发布规则',commerce:'经营参数',appearance:'外观'}[ui.page.split('/')[1]] || '') : labels[ui.page] || '工作台';
812
- const configPending=[...operations.values()].some(operation=>operation.state==='uncertain' && configActions.has(operation.action));
813
- $('#connection').textContent = ui.closed ? '正在退出' : configPending ? '保存结果待核实' : busy() ? '任务运行中' : cloudMode ? '云端已连接' : '本地服务已连接';
814
- $('#connection').className = `status ${ui.closed ? 'muted' : busy() ? '' : 'good'}`;
815
- $('[data-action="shutdown"]').disabled=Boolean(ui.closed || !cloudMode && busy());
816
- if(cloudMode){
817
- $('#cloud-tools').hidden=false;$('#team-nav').hidden=!isSuperAdmin();$('#users-nav').hidden=!isSuperAdmin();
818
- $('#workspace-choice').innerHTML=ui.session.workspaces.map(item=>`<option value="${e(item.id)}" ${item.id===ui.workspace?'selected':''}>${e(item.name)}${item.busy?' · 运行中':''}</option>`).join('');
819
- $('#workspace-choice').value=ui.workspace;
820
- $('#workspace-choice').disabled=Boolean(busy());$('#cloud-user').textContent=ui.session.user.username;
821
- $('[data-action="shutdown"]').title='退出登录,后台任务继续运行';$('[data-action="shutdown"]').setAttribute('aria-label','退出登录');
822
- }
823
- $('[data-action="refresh"]').disabled=Boolean(ui.closed);
824
- document.querySelectorAll('.sidebar nav button[data-page]').forEach(item => {
825
- item.disabled=Boolean(ui.closed);
826
- const selected = item.dataset.page === (['copy','manual','categoryCreate'].includes(ui.page) ? 'generation' : ['media','autoMedia','book','product','preflight','manifest','queueAdd','commerce','commercePlan'].includes(ui.page) ? 'queue' : ['file','upload'].includes(ui.page) ? 'library' : ui.page === 'record' ? 'history' : ['configImport','phoneModels'].includes(ui.page) ? 'settings' : ui.page.split('/')[0]);
827
- item.title=labels[item.dataset.page] || item.textContent.trim();item.classList.toggle('selected',selected); if(selected)item.setAttribute('aria-current','page');else item.removeAttribute('aria-current');
828
- });
829
- const themeButton = $('[data-action="theme"]');
830
- themeButton.disabled=Boolean(ui.closed);
831
- themeButton.title = ui.data.config.ui.theme === 'dark' ? '切换到浅色外观' : '切换到暗色外观';
832
- const capacity = ui.data.capacity;
833
- setActionDisabled($('#store-choice'),false,'stores.select');
834
- if($('#quality-certificate'))$('#quality-certificate').disabled=Boolean(!ui.data.config.seller.client_id || !ui.data.credentials.seller_key_set);
835
- setActionDisabled($('[data-action="quality-list"]'),!ui.data.config.seller.client_id || !ui.data.credentials.seller_key_set);
836
- $('#server-summary').innerHTML = capacity ? `服务器空间${badge(capacity.warning ? '达到告警线' : '已查询',capacity.warning ? '' : 'good')}<p>已用 ${bytes(capacity.used)} / ${bytes(capacity.total)}<br>可用 ${bytes(capacity.free)}</p><div class="capacity-track"><div style="transform:scaleX(${Math.min(1,Math.max(0,Number(capacity.used_percent)/100))})"></div></div>` : '服务器容量尚未查询<p>进入媒体库查询实时容量。</p>';
837
- renderTaskbar();
838
- syncActionControls();
839
- }
840
- function renderTaskbar(force=false) {
841
- const visible=Boolean(ui.data && !ui.closed && (!cloudMode || ui.session?.authenticated));
842
- $('#task-log-tools').hidden=!visible;
843
- $('#task-log-panel').hidden=!visible || !ui.taskLogOpen;
844
- $('#task-log-launcher').setAttribute('aria-expanded',String(visible && ui.taskLogOpen));
845
- if(!visible){$('#task-log-content').innerHTML='';$('#task-log-alert').innerHTML='';return;}
846
- const running=runningJobs(),job=running.at(-1) || ui.lastJob || taskList().at(-1);
847
- const uncertain=[...operations.values()].some(operation=>operation.state==='uncertain') || running.some(job=>job.awaiting_confirmation);
848
- const readFailed=job && ui.jobReads.get(job.id)?.error;
849
- $('#task-log-status').textContent=uncertain?'结果待核实':readFailed?'状态读取失败':running.length?`${actionText(job?.action)} · 运行中${running.length>1?`(${running.length})`:''}`:job?.state==='failed'?'任务失败':job?'已结束':'';
850
- const logLabel=`任务日志${$('#task-log-status').textContent?' · '+$('#task-log-status').textContent:''}`;
851
- $('#task-log-launcher').setAttribute('aria-label',logLabel);
852
- $('#task-log-launcher').setAttribute('title',`${logLabel} · 可拖动位置`);
853
- $('#task-log-launcher').dataset.state=uncertain?'uncertain':readFailed?'failed':running.length?'running':job?.state==='failed'?'failed':'idle';
854
- if(logPosition)placeLogLauncher(logPosition.left,logPosition.top);
855
- const configPending=uncertain && [...operations.values()].some(operation=>operation.state==='uncertain' && configActions.has(operation.action));
856
- $('#task-log-alert').innerHTML=uncertain?`<p>${configPending?'配置保存结果待核实,不会自动重复保存。':'提交结果待核实,请先核实任务状态,不会自动重发。'}</p>${button('recover-write',configPending?'核实保存结果':'核实任务状态')}`:'';
857
- if(!ui.taskLogOpen || !force && document.activeElement?.id==='task-log-job')return;
858
- const content=$('#task-log-content'),scroll=content.scrollTop || 0,details=[...content.querySelectorAll?.('details') || []].flatMap((node,index)=>node.open?[index]:[]);
859
- content.innerHTML=renderJob(true);
860
- details.forEach(index=>{const node=content.querySelectorAll?.('details')[index];if(node)node.open=true;});
861
- content.scrollTop=scroll;$('#task-log-level').value=ui.taskLogLevel;
862
- }
863
- function closeTaskLog() {
864
- ui.taskLogOpen=false;renderTaskbar();$('#task-log-launcher').focus();
865
- }
866
- function placeLogLauncher(left,top) {
867
- const launcher=$('#task-log-launcher'),box=launcher.getBoundingClientRect();
868
- const x=Math.min(Math.max(12,left),Math.max(12,window.innerWidth-box.width-12)),y=Math.min(Math.max(12,top),Math.max(12,window.innerHeight-box.height-12));
869
- logPosition={left:x,top:y};Object.assign(launcher.style,{left:`${x}px`,top:`${y}px`,right:'auto',bottom:'auto'});
870
- }
871
- const logLauncher=$('#task-log-launcher');
872
- logLauncher.addEventListener('pointerdown',event=>{
873
- if(event.button!==0)return;
874
- const box=logLauncher.getBoundingClientRect();logDrag={id:event.pointerId,x:event.clientX,y:event.clientY,left:box.left,top:box.top,moved:false};
875
- logLauncher.setPointerCapture?.(event.pointerId);
876
- });
877
- logLauncher.addEventListener('pointermove',event=>{
878
- if(!logDrag || logDrag.id!==event.pointerId)return;
879
- const dx=event.clientX-logDrag.x,dy=event.clientY-logDrag.y;
880
- if(!logDrag.moved && Math.hypot(dx,dy)<6)return;
881
- logDrag.moved=true;event.preventDefault();placeLogLauncher(logDrag.left+dx,logDrag.top+dy);
882
- });
883
- const endLogDrag=event=>{if(!logDrag || logDrag.id!==event.pointerId)return;if(logDrag.moved)logClickBlockedUntil=Date.now()+250;logDrag=null;};
884
- logLauncher.addEventListener('pointerup',endLogDrag);logLauncher.addEventListener('pointercancel',endLogDrag);
885
- logLauncher.addEventListener('click',event=>{if(event.detail && Date.now()<logClickBlockedUntil){event.preventDefault();event.stopImmediatePropagation();}});
886
- window.addEventListener('resize',()=>{if(logPosition)placeLogLauncher(logPosition.left,logPosition.top);});
887
- function queuePreparation() {
888
- const available=queueGroups(),selected=ui.data.selected_groups || [],groups=selected.length?available.filter(item=>selected.map(String).includes(String(item.id))):available,plan=ui.data.plan,seller=ui.data.config.seller;
889
- const prepared=plan && plan.count>0 && sameScope(plan.scope || [],selected) && String(plan.client_id)===String(seller.client_id) && String(plan.store_id || '')===String(seller.selected_id || '') && !ui.qualityDirty;
890
- const ready=groups.length && groups.every(item=>item.primary_image && !item.media_errors?.length);
891
- const level=prepared?3:ready?2:available.length?1:0;
892
- const title=prepared?'上架计划已生成,检查后确认提交':ready?'媒体已指定,下一步上传与预检':available.length?'表格已就绪,下一步关联媒体':ui.data.groups.length?'当前型号范围暂无匹配商品':'从模板表格开始准备商品';
893
- return {level,title};
894
- }
895
- function stepper(preparation=queuePreparation()) {
896
- const {level}=preparation;
897
- return `<div class="stepper">${['生成表格','指定媒体','上传与预检','提交与回读'].map((label,index) => `${index ? `<i class="step-rule ${index <= level ? 'done' : ''}"></i>` : ''}<div class="step ${index < level ? 'done' : index === level ? 'current' : ''}"><b>${index+1}</b>${label}</div>`).join('')}</div>`;
898
- }
899
- function queueValueCell(group,field) {
900
- const rows=group.offer_ids.map(offer=>ui.data.queue_values?.[offer]);
901
- if(!rows.length || rows.some(row=>!row))return `<td data-column="${field}" class="queue-value muted">${rows.some(Boolean)?'部分未读取':'未读取'}</td>`;
902
- const values=[...new Set(rows.flatMap(row=>field==='color'?(row.color?.length?row.color:['未填写']):[row[field]==null?(field==='stock' && row.stock_note?'其他店铺草稿':'未设置'):`${row[field]}${['price','old_price'].includes(field) && row.currency_code?' '+row.currency_code:''}`]))];
903
- const text=values.length>2?`${values.slice(0,2).join(' / ')} …(${values.length} 种)`:values.join(' / ');
904
- const notes=field==='stock'?rows.map(row=>row.stock_note).filter(Boolean):[];
905
- const content=field==='name'?`<span class="queue-product-title">${e(text)}</span><small class="code">${e(group.offer_ids.slice(0,2).join(' / '))}${group.offer_ids.length>2?` …(共 ${group.offer_ids.length} 个货号)`:''}</small>`:e(text);
906
- return `<td data-column="${field}" class="queue-value" title="${e([...values,...notes].join(';'))}">${content}</td>`;
907
- }
908
- function renderQueue() {
909
- loadQueueView();
910
- const selected=ui.data.selected_groups.map(String),view=queueView(),groups=view.items;ui.queuePage=view.page;
911
- const detail=renderQueueDetail(), classes=['queue-table',ui.queueDensity==='compact'?'compact':'',...QUEUE_COLUMNS.map(([key])=>key).filter(column=>!ui.queueColumns.includes(column)).map(column=>'hide-'+column)].join(' ');
912
- const available=queueGroups(),stats=listingStats(available),sourceStats=listingStats(ui.data.groups),next=available.find(item=>!item.primary_image || item.media_errors?.length) || available[0];
913
- const missing=ui.data.config.listing.model_scope==='popular'?queueModelCoverage(ui.data.groups,ui.data.phone_models):[];
914
- const preparation=queuePreparation(),title=preparation.title,blocked=conflictingJob('listing.prepare');
915
- const prepareHint=!selected.length?'先勾选需要上架的商品':!ui.data.credentials.seller_key_set || !ui.data.config.seller.client_id?'先选择凭据完整的店铺':blocked?conflictReason(blocked):'';
916
- return `<div class="page-heading queue-heading"><div><h1>商品队列</h1><p class="sub">准备表格与媒体,选择店铺后预检提交。</p></div><div class="queue-heading-actions"><div class="queue-store-tools">${storeChoice()}${isAdmin()?'<button class="quiet" data-page="settings/seller">配置店铺凭据</button>':''}</div><button data-page="settings/mediaRules">图片发布方式</button>${button('add-books','导入已有上架表','title="把已有 XLSX 上架表导入当前商品队列"')}${ui.data.plan?'<button data-page="preflight">查看预检</button>':''}${button('prepare','上传与预检','aria-describedby="prepare-hint"','primary',!selected.length || !ui.data.credentials.seller_key_set || !ui.data.config.seller.client_id)}</div></div>`
917
- + `<p id="prepare-hint" class="hint prepare-hint" role="status" ${prepareHint?'':'hidden'}>${e(prepareHint)}</p>`
918
- + `<section class="queue-hero"><div class="hero-row"><div><span class="hero-kicker">商品准备流程</span><h2>${title}</h2></div>${next ? button('edit-media',stats.ready === stats.groups ? '查看媒体清单' : '开始关联媒体',`data-id="${e(next.id)}"`,'') : button('add-books','导入第一份上架表','','')}</div>${stepper(preparation)}</section><div class="stats-grid queue-stats"><div class="stat-card"><span>媒体清单</span><strong>${stats.groups}</strong><small>每份对应一个或多个 SKU</small></div><div class="stat-card"><span>待处理 SKU</span><strong>${stats.skus}</strong><small>从实际表格读取</small></div><div class="stat-card"><span>已指定媒体</span><strong>${stats.ready} <small>/ ${stats.groups}</small></strong><small>已填写且清单无本地错误</small></div></div>`
919
- + modelScopeControl() + `<p class="hint" id="queue-scope-status" role="status">上架表共 ${sourceStats.skus} 个 SKU,当前${ui.data.config.listing.model_scope==='popular'?'热门':'全部'}范围 ${stats.skus} 个${sourceStats.skus>stats.skus?`,已隐藏 ${sourceStats.skus-stats.skus} 个未列入当前热门名单的 SKU`:''};自动关联与预检使用当前范围内的勾选商品。</p>${missing.length?`<details class="queue-scope-gaps"><summary>上架表还缺 ${missing.reduce((sum,row)=>sum+row.models.length,0)} 个当前热门 SKU · ${missing.length} 个款式</summary><p class="hint">模板表用于生成,上架表是已生成的商品。热门名单变化不会给旧上架表自动补行;请在“生成表格”重新生成对应款式,确认覆盖时会保留旧表备份。</p><ul>${missing.map(row=>`<li><strong>${e(row.pattern)}</strong>:缺 ${row.models.length} 个型号,${e(row.models.join('、'))}</li>`).join('')}</ul></details>`:''}` + qualitySelector() + queueViewControls() + (ui.data.queue_values_error?`<p class="banner" role="status">本地表格字段暂未读取:${e(ui.data.queue_values_error)}。修复表格后刷新;未知值不会显示成 0。</p>`:'')
920
- + `<div class="toolbar queue-toolbar"><input id="search" type="search" aria-label="搜索款式或货号" placeholder="搜索款式或货号" value="${e(ui.search)}"><select id="queue-filter" aria-label="商品队列状态筛选">${[['all','全部清单'],['missing','媒体待完善'],['ready','媒体已准备'],['prepared','已预检']].map(([value,label])=>`<option value="${value}" ${ui.queueFilter===value?'selected':''}>${label}</option>`).join('')}</select>${button('queue-scan','读取生成目录','', '')}${button('queue-remove','移除选中','', '',!selected.length)}${button('auto-open','自动关联媒体','','',!available.length)}<details class="queue-tools"><summary>导入与导出</summary><div>${button('navigate-manifest','导入媒体清单')}${download('media','导出清单')}</div></details><span class="spacer"></span><span class="summary">已选 ${selected.length} / 共 ${available.length} 份清单</span></div>`
921
- + `<div class="queue-layout ${detail?'with-detail':''}"><section class="queue-list"><div class="table-wrap ${classes}"><table><thead><tr><th><input id="group-all" type="checkbox" aria-label="选择本页全部款式" ${groups.length && groups.every(item => selected.includes(String(item.id))) ? 'checked' : ''} ${busy('queue.select') ? 'disabled' : ''}></th><th>款式 / 生成表格</th><th data-column="name">商品标题</th><th data-column="sku">SKU 数</th><th data-column="media">媒体清单</th><th data-column="old_price">划线价格</th><th data-column="price">折扣价</th><th data-column="stock" title="按本地匹配规则计算的库存预设,待确认提交">库存预设</th><th data-column="color">颜色</th><th data-column="preflight">预检</th><th>操作</th></tr></thead><tbody>${groups.map(item => `<tr class="${String(item.id)===String(ui.queueDetail)?'queue-focused':''}"><td><input type="checkbox" data-group-check="${e(item.id)}" aria-label="选择 ${e(item.label)}" ${selected.includes(String(item.id)) ? 'checked' : ''} ${busy('queue.select') ? 'disabled' : ''}></td><td><div class="queue-row-content">${item.primary_image?`<img ${imageAttrs(item.primary_image)} alt="${e(item.label)} 主图" loading="lazy">`:'<span class="queue-row-placeholder" aria-hidden="true">—</span>'}<button class="queue-row-name" data-action="queue-detail" data-id="${e(item.id)}" aria-label="查看 ${e(item.label)} 连续详情"><span><strong class="code">${e(item.label)}</strong><small>${e((item.source_paths || []).map(path => path.split(/[\\/]/).at(-1)).join('、'))}</small></span></button></div></td>${queueValueCell(item,'name')}<td data-column="sku">${item.offer_ids.length}</td><td data-column="media">${badge(item.media_errors?.length ? `待完善 ${item.media_errors.length} 项` : item.primary_image ? `${item.images.length} 附图 / ${item.videos.length} 视频` : '待指定', item.primary_image && !item.media_errors?.length ? 'good' : '')}</td>${['old_price','price','stock','color'].map(field=>queueValueCell(item,field)).join('')}<td data-column="preflight">${badge(ui.data.plan?.scope?.map(String).includes(String(item.id)) ? '已预检' : '未开始', ui.data.plan?.scope?.map(String).includes(String(item.id)) ? 'good' : 'muted')}</td><td>${button('edit-media','编辑媒体',`data-id="${e(item.id)}"`)} ${button('book','查看商品',`data-id="${e(item.id)}"`)}</td></tr>`).join('') || `<tr><td colspan="11">${ui.data.groups.length?'当前型号范围或筛选条件下没有商品,请调整型号范围、搜索词或状态筛选。':'队列为空,先生成上架表或导入已有 XLSX 上架表。'}</td></tr>`}</tbody></table></div>${pagination('queue',view)}</section>${detail}</div><p class="hint">生成目录:<span class="path-display">${e(ui.data.config.generator.output_dir)}</span> <button class="quiet" data-page="settings/paths">修改路径</button></p>`;
922
- }
923
- function loadQueueView() {
924
- const key=`ozon.queueView:${cloudMode?ui.session?.user?.username || 'guest':'local'}:${ui.workspace || 'local'}`;
925
- if(ui.queueSavedKey===key)return;
926
- ui.queueSavedKey=key;ui.queueSaved=null;
927
- try{const value=JSON.parse(window.localStorage?.getItem(key) || 'null');if(value && typeof value==='object' && !Array.isArray(value))ui.queueSaved=normalizeQueueView(value);}catch{}
928
- }
929
- function queueViewControls() {
930
- return `<div class="queue-viewbar" aria-label="商品队列视图设置"><div class="view-actions">${button('queue-view-save','保存常用视图')}${button('queue-view-load','恢复常用视图','','',!ui.queueSaved)}${button('queue-view-clear','清除常用视图','','quiet',!ui.queueSaved)}</div><span class="spacer"></span>${button('queue-density',ui.queueDensity==='compact'?'舒适行高':'紧凑行高',`aria-pressed="${ui.queueDensity==='compact'}"`)}<details class="queue-columns"><summary>显示列</summary><div>${QUEUE_COLUMNS.map(([key,label])=>`<label class="check"><input type="checkbox" data-queue-column="${key}" ${ui.queueColumns.includes(key)?'checked':''}>${label}</label>`).join('')}</div></details></div>`;
931
- }
932
- function renderQueueDetail() {
933
- const items=filterQueue(queueGroups(),ui.search,ui.queueFilter,ui.data.plan),index=items.findIndex(item=>String(item.id)===String(ui.queueDetail)),item=items[index];
934
- if(!item)return '';
935
- return `<aside class="queue-inspector" aria-label="商品连续详情"><header><strong>款式详情 · ${index+1} / ${items.length}</strong>${button('queue-detail-close','关闭详情',`aria-label="关闭商品连续详情"`)}</header><div class="inspector-body"><div class="inspector-image">${item.primary_image?`<img ${imageAttrs(item.primary_image)} alt="${e(item.label)} 主图">`:'<span>主图尚未指定</span>'}</div><h2 tabindex="-1" id="queue-detail-title">${e(item.label)}</h2><p class="hint">${item.offer_ids.length} 个 SKU · ${item.images.length} 张附图 · ${item.videos.length} 个视频</p><dl class="definition"><dt>媒体准备</dt><dd>${badge(item.media_errors?.length?`待完善 ${item.media_errors.length} 项`:item.primary_image?'已指定':'待指定',item.primary_image&&!item.media_errors?.length?'good':'')}</dd><dt>预检范围</dt><dd>${badge(ui.data.plan?.scope?.map(String).includes(String(item.id))?'已预检':'未预检',ui.data.plan?.scope?.map(String).includes(String(item.id))?'good':'muted')}</dd></dl>${item.media_errors?.length?`<ul class="errors">${item.media_errors.map(error=>`<li>${e(error)}</li>`).join('')}</ul>`:''}<details><summary>SKU 与来源表格</summary><div class="inspector-skus">${item.offer_ids.map(id=>`<span class="code">${e(id)}</span>`).join('')}</div>${(item.source_paths || []).map(path=>`<p class="hint path-display" title="${e(path)}">${e(path)}</p>`).join('')}</details><div class="actions">${button('edit-media','编辑媒体',`data-id="${e(item.id)}"`,'')}${button('book','查看商品字段',`data-id="${e(item.id)}"`,'')}</div></div><footer>${button('queue-detail-step','上一款',`data-offset="-1"`,'',index===0)}${button('queue-detail-step','下一款',`data-offset="1"`,'',index===items.length-1)}</footer></aside>`;
936
- }
937
- function modelScopeControl() {
938
- const scope=ui.data.config.listing.model_scope || 'all';
939
- return `<div class="toolbar model-scope"><label for="phone-model-scope">生成/上架型号</label><select id="phone-model-scope" ${busy('config.save')?'disabled':''}><option value="popular" ${scope==='popular'?'selected':''}>只上热门型号</option><option value="all" ${scope==='all'?'selected':''}>所有型号</option></select><button data-page="phoneModels">管理热门型号</button><span class="hint">按材质匹配;JT 沿用新阶梯优先及转换规则。</span></div>`;
940
- }
941
- function renderPhoneModels() {
942
- const data=ui.data.phone_models || {}, material=ui.modelMaterial;
943
- if(!data.imported)return back('generation','生成表格')+heading('手机型号目录','先导入完整型号表,后续使用工作台保存的数据。')+empty('尚未导入型号表','导入后可设置各材质热门型号。')+(isAdmin()?button('phone-model-import','导入型号表','','primary'):'');
944
- const popular=new Set(data.popular[material] || []), key=value=>String(value).toUpperCase().replaceAll('+','PLUS').replace(/[^A-Z0-9]/g,'');
945
- const rows=(data.models[material] || []).filter(row=>`${row.sku_model} ${row.title_model}`.toLowerCase().includes(ui.search.toLowerCase()) && (ui.modelFilter==='all' || popular.has(key(row.sku_model))===(ui.modelFilter==='popular')));
946
- const flame='<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3c1 4 6 6 6 11a6 6 0 0 1-12 0c0-2 1-4 3-6 0 3 1 4 2 4 1-2 2-5 1-9Z"/></svg>';
947
- const view=pageItems(rows,ui.modelPage,ui.pageSize);ui.modelPage=view.page;
948
- return back('generation','生成表格')+heading('手机型号目录','完整型号表已保存。IMD 单独维护;阶梯和新阶梯共用热门名单,并在同一张上传表中按型号匹配 SKU 前缀。',isAdmin()?button('phone-model-import','重新导入型号表','',''):'')
949
- + `<div class="toolbar"><select id="phone-model-material" aria-label="型号材质">${['IMD','JT'].map(m=>`<option value="${m}" ${m===material?'selected':''}>${{IMD:'IMD',JT:'阶梯/新阶梯'}[m]}</option>`).join('')}</select><select id="phone-model-filter" aria-label="热门型号筛选">${[['popular','热门型号'],['all','全部型号'],['regular','非热门型号']].map(([value,label])=>`<option value="${value}" ${ui.modelFilter===value?'selected':''}>${label}</option>`).join('')}</select><input id="search" type="search" placeholder="搜索 SKU 型号或标题型号" aria-label="搜索手机型号" value="${e(ui.search)}"><span class="summary">${(data.models[material] || []).length} 个型号 · ${popular.size} 个热门 · 筛选 ${rows.length} 个</span></div>`
950
- + `<div class="table-wrap phone-models"><table><thead><tr><th>SKU 型号/颜色名称</th><th>标题型号</th><th class="model-hot-column">热门</th></tr></thead><tbody>${view.items.map(row=>{const hot=popular.has(key(row.sku_model)),label=hot?'移出热门':'添加到热门';return `<tr><td>${e(row.sku_model)}</td><td>${e(row.title_model)}</td><td class="model-hot-column">${isAdmin()?`<button class="model-hot" data-action="phone-model-popular" data-intrinsic-blocked="false" data-material="${material}" data-sku="${e(row.sku_model)}" data-popular="${!hot}" title="${label}" aria-label="${e(row.sku_model)}:${label}" aria-pressed="${hot}" ${busy('models.popular')?'disabled':''}>${flame}</button>`:`<span class="model-hot" role="img" aria-label="${hot?'热门型号':'非热门型号'}" data-hot="${hot}">${flame}</span>`}</td></tr>`;}).join('')}</tbody></table></div>`
951
- + pagination('models',view) + (rows.length?'':empty('没有匹配的型号','调整搜索词后重试。'))
952
- + `<details><summary>导入来源与规则提示</summary><p>${e(data.source?.name)} · 已保存 ${(Object.keys(data.sheet_counts || {})).length} 个子表</p><p>阶梯系列 ${(data.models.JT || []).length} 个可上架型号,共用一份热门名单;同一张上传表可包含 JIETI 与 XINJIETI 前缀,继续应用新阶梯优先和阶梯转新阶梯规则。</p>${(data.warnings || []).map(w=>`<p class="hint">${e(w)}</p>`).join('')}</details>`;
953
- }
954
- function generationFiles(item) {
955
- return `<dl class="definition generation-paths"><dt>本机 XLSX</dt><dd class="path-display">${e(item.target_path)}</dd><dt>参数模板</dt><dd class="path-display">${e(item.template_source || '未找到匹配模板')}</dd><dt>结构底板</dt><dd class="path-display">${e(item.structure_template || '未找到匹配底板')}</dd></dl>`;
956
- }
957
- function renderGeneration() {
958
- const designs = visibleCardItems('designs');
959
- return heading('生成表格','扫描设计图片,确认标题与描述后,按当前型号范围从模板表生成上架表。',`<button data-page="manual">手动建表</button>${categoryModule?'<button data-page="categoryCreate">用类目模板新建</button>':''}`) + modelScopeControl() + `<div class="toolbar"><input id="search" type="search" aria-label="搜索设计" placeholder="搜索设计或文件夹" value="${e(ui.search)}">${cloudMode?'':button('design-folder','选择本地图片目录')}${button('design-scan','扫描设计','','primary')}${button('recognize','识别选中并生成文案','', '',!ui.designs.size)}${button('write-designs','写入选中表格','', '',!ui.designs.size)}${button('regenerate-designs','重新生成选中表格','', '',!ui.designs.size)}<span class="spacer"></span><span class="summary">已选 ${ui.designs.size} / 共 ${ui.data.designs.length} 款</span></div>`
960
- + '<p class="hint">已保存的智能手机壳参数按实际材质自动套用;颜色、型号与文案仍随图案和型号生成,不按颜色拆分模板。</p>'
961
- + (cloudMode?'':'<p class="banner">本机写表使用待上传媒体占位链接,不会连接媒体服务器。请通过工作台上传与预检后提交;含占位链接的 XLSX 尚不可直接上传平台。</p>')
962
- + `<div class="section-header"><label class="check"><input id="design-all" type="checkbox" aria-label="选择筛选中的全部设计" ${designs.length && designs.every(item => ui.designs.has(String(item.id))) ? 'checked' : ''}>选择当前筛选的设计</label><span class="summary">图片仅作设计预览;上架主图由媒体清单指定。</span></div><div class="media-grid design-gallery" data-gallery="designs">${designs.map(item=>`<article class="file-card card-sortable" ${cardSortAttrs('designs',String(item.id))}><div class="file-preview">${item.images.length ? `<img data-motion-image="${e(item.id)}" ${imageAttrs(item.images[0])} alt="设计 ${e(item.code)} 预览" title="${e(item.images[0])}" loading="lazy">` : '<span class="file-preview-label">暂无设计图片</span>'}</div><div class="file-content"><div class="card-heading"><strong>${e(item.code)}</strong><input type="checkbox" data-design-check="${e(item.id)}" aria-label="选择设计 ${e(item.code)}" ${ui.designs.has(String(item.id)) ? 'checked' : ''}></div><p class="path-display">${e(item.folder)}</p><p>${item.images.length} 张设计图片</p><details class="generation-files"><summary>模板与本机 XLSX 路径</summary>${generationFiles(item)}</details>${badge(ui.data.previews[item.id] ? '已有文案预览' : item.status || '待生成',ui.data.previews[item.id] ? 'good' : 'muted')}<div class="actions">${button('copy','编辑文案',`data-id="${e(item.id)}"`)}</div></div></article>`).join('')}</div>${designs.length ? '' : empty(ui.search ? '没有找到设计' : '尚未扫描设计',ui.search ? '调整搜索词后重试。' : '点击“扫描设计”,或在配置中心修改设计图片目录。')}<p class="hint">识别方式:${e(ui.data.config.generator.recognition_mode === 'manifest_only' ? '仅使用本地识别清单' : '模型识别与本地清单回退')} · 并行 ${e(ui.data.config.generator.workers)} · <button class="quiet" data-page="settings/model">修改模型</button></p>`;
963
- }
964
- function renderCopy() {
965
- const item = design(); if (!item) return back('generation','生成表格') + empty('设计已不在扫描结果中','返回后重新扫描。');
966
- const draft = ui.copy || {title:'',description:'',attrs:{}}, current = preview();
967
- const attrFields = [['color_ru','壳体颜色(俄语)'],['frame_color_ru','镜头框颜色(俄语)'],['pattern_ru','图案(俄语)'],['material_ru','材质(俄语)'],['effect_ru','效果(俄语)']];
968
- return back('generation','生成表格') + heading(item.code,'文案先生成预览;修改后重新检查,再写入表格。',button('recognize-one','重新识别','',''))
969
- + `<details class="panel"><summary>本图案经营参数 · ${e(item.code)}</summary><p class="hint">留空继承材质和全局默认值。保存后用于本图案生成。</p>${businessForm('pattern',ui.data.config.commerce?.patterns?.[item.code])}<div class="sub-actions">${button('business-pattern-save','保存本图案参数','','')}</div></details><div class="inline-preview"><section><div class="media-placeholder">${item.images.length ? `<img data-motion-image="${e(item.id)}" ${imageAttrs(item.images[0])} alt="当前设计的第一张图片">` : '没有图片'}</div><p class="hint path-display">${e(item.folder)}</p><div class="thumbnail-list">${item.images.map(path => `<img ${imageAttrs(path)} title="${e(path)}" alt="${e(path.split(/[\\/]/).at(-1))}" loading="lazy">`).join('')}</div><details><summary>识别属性</summary>${attrFields.map(([key,label]) => `<div class="form-field"><label for="attr-${key}">${label}</label><input id="attr-${key}" data-attr="${key}" value="${e(draft.attrs[key] || '')}"></div>`).join('')}<label class="check"><input type="checkbox" id="case-design" ${draft.attrs.has_case_design !== false ? 'checked' : ''}>包含壳体图案</label></details></section><section><div class="form-field"><label for="copy-title">标题 · ${draft.title.length} 字符</label><textarea id="copy-title" rows="3">${e(draft.title)}</textarea></div><div class="form-field"><label for="copy-description">描述 · ${draft.description.length} 字符</label><textarea id="copy-description" rows="15">${e(draft.description)}</textarea></div>${current?.compliance_issues?.length ? `<ul class="errors">${current.compliance_issues.map(issue => `<li>${e(issue)}</li>`).join('')}</ul>` : current ? badge('预览合规检查已返回','good') : '<p class="hint">尚无文案预览;请先识别当前设计,或使用手动建表。</p>'}${generationFiles(item)}<div class="sub-actions">${button('copy-check','检查文案预览','','',!current)}${button('copy-write','确认写表','','primary',!current)}</div></section></div>`;
970
- }
971
- function renderManual() {
972
- const draft = ui.manual, p = ui.manualPreview;
973
- return back('generation','生成表格') + heading('手动建表','选原模板、填写新编号和文案;原模板不会改写。')
974
- + `<div class="panel"><div class="form-grid"><div class="form-field"><label for="manual-type">模板类型</label><div class="field-row"><select id="manual-type">${[['0','阶梯 JT'],['1','IMD J1'],['2','IMD J2'],['8','IMD J8']].map(([value,label]) => `<option value="${value}" ${draft.file_type === value ? 'selected' : ''}>${label}</option>`).join('')}</select>${button('manual-templates','读取模板','','')}</div></div><div class="form-field"><label for="manual-number">新编号</label><input id="manual-number" inputmode="numeric" value="${e(draft.new_number)}"></div><div class="form-field full"><label for="manual-source">原模板绝对路径</label><div class="field-row"><input id="manual-source" value="${e(draft.source_path)}" list="manual-template-list">${button('pick-manual','选择模板','','')}</div><datalist id="manual-template-list">${ui.manualPaths.map(path => `<option value="${e(path)}"></option>`).join('')}</datalist></div><div class="form-field full"><label for="manual-title">标题</label><textarea id="manual-title" rows="3">${e(draft.title)}</textarea></div><div class="form-field full"><label for="manual-description">描述</label><textarea id="manual-description" rows="13">${e(draft.description)}</textarea></div></div>${p ? `<div class="banner">目标表格:<span class="path-display">${e(p.target_path)}</span>${p.target_exists ? '<br>该文件已存在,写表前需确认覆盖。' : ''}</div>${p.compliance_issues?.length ? `<ul class="errors">${p.compliance_issues.map(issue => `<li>${e(issue)}</li>`).join('')}</ul>` : badge('已生成预览','good')}` : ''}<div class="sub-actions">${button('manual-check','预览与检查','','')}${button('manual-write','确认写表','','primary',!p)}</div></div>`;
975
- }
976
- function renderMedia() {
977
- const item = group(); if (!item) return back('queue','商品队列') + empty('该款已移出队列','返回队列重新选择。');
978
- const draft = ui.media;
979
- const visual = (path,video,label) => `<div class="media-visual">${path ? video ? `<video src="${e(previewUrl(path))}" title="${e(path)}" aria-label="${e(label)}" controls preload="metadata" playsinline draggable="false"></video>` : `<img ${imageAttrs(path)} title="${e(path)}" alt="${e(label)}" draggable="false">` : `<span>${e(label)}尚未指定</span>`}</div>`;
980
- const drag = (kind,index,label,path) => `data-media-kind="${kind}" data-media-index="${index}" draggable="${Boolean(path)}" tabindex="0" aria-label="${e(label)},可拖动互换或排序"`;
981
- const arrow = (kind,offset) => `<button class="media-rail-arrow ${offset<0?'prev':'next'}" data-action="media-scroll" data-offset="${offset}" aria-label="向${offset<0?'左':'右'}查看${kind==='images'?'附图':'视频'}" title="向${offset<0?'左':'右'}滑动" hidden><svg viewBox="0 0 24 24" aria-hidden="true"><path d="${offset<0?'m14 6-6 6 6 6':'m10 6 6 6-6 6'}"/></svg></button>`;
982
- const list = (items,kind) => `<div class="media-rail" data-media-rail="${kind}" data-media-group="${e(ui.group)}">${arrow(kind,-1)}<ol class="media-thumbs${kind==='videos'?' video-thumbs':''}" tabindex="0" aria-label="${kind === 'images' ? '附图' : '视频'}排序">${items.map((entry,index) => {
983
- const video=kind==='videos',path=video?entry.path:entry,label=`${video?'视频':'附图'} ${index+1}`;
984
- return `<li class="media-tile" ${drag(kind,index,label,path)}>${visual(path,video,label)}<button class="media-tile-remove" data-action="media-remove" data-kind="${kind}" data-index="${index}" aria-label="移除${label}" title="移除${label}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button><div class="media-tile-actions"><span class="media-order">${index+1}</span></div>${video?`<label class="video-name">视频名称<input data-video-name="${index}" value="${e(entry.name)}"></label>`:''}</li>`;
985
- }).join('') || '<li class="media-empty">尚未指定</li>'}</ol>${arrow(kind,1)}</div>`;
986
- return back('queue','商品队列') + heading(item.label,`${item.offer_ids.length} 个货号共用此媒体清单;按货号拆分可导入清单。`)
987
- + `<div class="panel media-editor" data-gallery="media"><p class="hint">拖动附图调整顺序,拖到左侧主图互换;视频与封面也可互换。悬浮图片或视频查看路径。</p><div class="media-columns"><section><div class="section-header"><h2>主图 · 必选</h2>${button('media-pick','选择主图','data-kind="primary"')}</div><div class="media-tile media-primary" ${drag('primary',0,'主图',draft.primary_image)}>${visual(draft.primary_image,false,'主图')}</div><div class="section-header"><h2>视频封面</h2>${button('media-pick','选择封面','data-kind="cover"')}</div><div class="media-tile media-cover" ${drag('cover',0,'视频封面',draft.video_cover)}>${visual(draft.video_cover,true,'视频封面')}</div></section><section><div class="section-header"><h2>附图 · ${draft.images.length} / 49</h2>${button('media-pick','添加附图','data-kind="images"')}</div>${list(draft.images,'images')}<div class="section-header"><h2>视频 · ${draft.videos.length} / 5</h2>${button('media-pick','添加视频','data-kind="videos"')}</div>${list(draft.videos,'videos')}</section></div><details class="media-path-config"><summary>路径配置与键盘操作</summary><div class="form-grid"><div class="form-field"><label for="primary-path">主图绝对路径</label><input id="primary-path" value="${e(draft.primary_image)}" placeholder="本地图片绝对路径"></div><div class="form-field"><label for="cover-path">视频封面绝对路径</label><input id="cover-path" value="${e(draft.video_cover)}" placeholder="可选,本地视频绝对路径"></div><div class="form-field full"><label for="image-paths">附图绝对路径 · 每行一个</label><textarea id="image-paths" rows="4">${e(draft.images.join('\n'))}</textarea></div><div class="form-field full"><label for="video-paths">添加视频路径 · 每行一个 MP4 / MOV</label><textarea id="video-paths" rows="3"></textarea></div></div><div class="buttons-line">${button('media-path-refresh','更新预览')}${button('media-add-video-paths','加入视频清单')}</div><p class="hint">选中缩略图后,Alt + 方向键调整顺序,Alt + Home 换入主图或封面。视频封面为空时,拖入视频会指定封面并保留视频清单。</p></details><p id="media-feedback" class="hint" role="status" aria-live="polite">调整后请保存;上传时仍检查图片格式和视频时长。</p><div class="sub-actions"><span id="media-dirty" class="status ${mediaDirty()?'':'good'}">${mediaDirty()?'有未保存修改':'已保存'}</span>${button('media-undo','撤销','','',!ui.mediaUndo)}${button('media-save','保存媒体清单','','primary')}</div></div>`;
988
- }
989
- function renderAutoMedia() {
990
- const draft=ui.autoDraft, codes=[...new Set((draft?.rows || []).map(row=>row.code))];
991
- if(!codes.includes(ui.autoCode))ui.autoCode=codes[0] || '';
992
- const rows=(draft?.rows || []).map((row,index)=>({...row,index})).filter(row=>row.code===ui.autoCode), pictures=rows[0]?.images || [];
993
- return back('queue','商品队列')+heading('自动关联图片与视频','按款式目录读取图片,为容易混淆的型号错开主图。')+`<div class="panel"><div class="buttons-line">${button('auto-read','重新读取预览','','primary')}${button('auto-rename','按款式给图片编号','','')}<button data-page="settings/mediaRules">修改视频规则</button></div><p class="hint">同一图案下,型号数字相同但品牌、系列或版本不同的商品错开主图,例如 iPhone 13、13 Pro、Redmi 13。iPhone 13 / 14、iPhone 13 / Redmi 12 可复用;4G / 5G 不作为型号数字。图片按内容去重,逐型号检查后可手动调整。</p>${draft?.errors?.length ? `<ul class="errors">${draft.errors.map(error=>`<li>${e(error)}</li>`).join('')}</ul>` : ''}${draft ? `<div class="form-field"><label for="auto-code">查看款式 · ${codes.length} 款 / ${draft.rows.length} 个 SKU</label><select id="auto-code">${codes.map(code=>`<option ${code===ui.autoCode?'selected':''}>${e(code)}</option>`).join('')}</select></div><div class="auto-pictures" data-gallery="auto">${pictures.map((path,index)=>`<figure><img ${imageAttrs(path)} alt="款式图片 ${index+1}" loading="lazy"><figcaption>${index+1} · ${e(path.split(/[\\/]/).at(-1))}</figcaption></figure>`).join('')}</div><div class="table-wrap"><table><thead><tr><th>型号 / 分组</th><th>主图顺序</th><th>自动视频与封面</th></tr></thead><tbody>${rows.map(row=>{const g=draft.groups[row.index];return `<tr><td><strong>${e(row.model)}</strong><small class="hint">${e(row.family)}</small></td><td><label class="visually-hidden" for="auto-primary-${row.index}">${e(row.model)} 主图</label><select id="auto-primary-${row.index}" data-auto-primary="${row.index}"><option value="">请选择主图</option>${row.images.map((path,i)=>`<option value="${e(path)}" ${path===g.primary_image?'selected':''}>第 ${i+1} 张</option>`).join('')}</select></td><td><span class="path-display">${e(g.videos[0]?.name || '没有匹配视频')}</span><small class="hint">封面:${e((g.video_cover || '').split(/[\\/]/).at(-1) || '未指定')}</small></td></tr>`;}).join('')}</tbody></table></div><div class="sub-actions">${button('auto-apply',`应用 ${draft.groups.length} 个 SKU 的媒体清单`,'','primary',draft.errors.length>0)}</div>` : empty('尚未读取自动媒体','点击读取预览,检查各型号的主图顺序与视频规则。')}</div>`;
994
- }
995
- const businessLabels={price:'售价',old_price:'划线价',vat:'税率(%)',weight:'包装重量(克)',width:'包装宽度(毫米)',height:'包装高度(毫米)',depth:'包装长度(毫米)',brand:'品牌',country:'原产国',package:'包装清单',stock:'库存数量'};
996
- function businessForm(prefix,values={},options={}) {
997
- return `<div class="form-grid business-form">${Object.entries(businessLabels).map(([key,label])=>{
998
- const id=`business-${prefix}-${key}`,value=values?.[key],numeric=!['brand','country','package'].includes(key);
999
- const input=key==='vat'?`<select id="${id}" data-business="${prefix}" data-key="${key}"><option value="">${options.config?'继承参考值':'不修改 / 继承'}</option>${[0,5,7,10,20,22].map(rate=>`<option value="${rate}" ${String(value)===String(rate)?'selected':''}>${rate}%</option>`).join('')}</select>`:`<input id="${id}" data-business="${prefix}" data-key="${key}" type="${numeric?'number':'text'}" ${numeric?`min="${key==='stock'?0:.01}" step="${['price','old_price'].includes(key)?'.01':'1'}"`:''} value="${e(value ?? '')}" placeholder="${options.config?'留空继承':'留空不修改'}">`;
1000
- return `<div class="form-field"><label for="${id}">${label}</label>${input}${key==='stock' && options.stockHint?`<small>${e(options.stockHint)}</small>`:''}${['old_price','stock'].includes(key)?`<label class="check"><input type="checkbox" data-business-clear="${prefix}" data-key="${key}" ${value===null?'checked':''}>${key==='stock'?'不提交库存':'清除划线价'}</label>`:''}</div>`;
1001
- }).join('')}</div>`;
1002
- }
1003
- function collectBusiness(prefix) {
1004
- const result={};document.querySelectorAll(`[data-business="${prefix}"]`).forEach(input=>{if(input.value.trim()!=='')result[input.dataset.key]=input.value.trim();});
1005
- document.querySelectorAll(`[data-business-clear="${prefix}"]`).forEach(input=>{if(input.checked)result[input.dataset.key]=null;});return result;
1006
- }
1007
- function renderBook(detail=false) {
1008
- const item=group();if(!item)return back('queue','商品队列')+empty('该款已移出队列','返回队列重新选择。');
1009
- const rows=ui.bookRecords || [];
1010
- if(detail){const row=rows[ui.record];return back('book','表格商品')+heading(row?.item?.offer_id || '商品字段','显示当前表格中的真实字段名称。',button('business-one','修改经营参数',`data-index="${ui.record}"`,'primary'))+['商品信息','商品属性','适用于的型号','商品视频','视频封面','附件','组合属性'].map(section=>{const fields=productFields(row || {}).filter(field=>field.section===section);return fields.length?`<section class="panel"><h2>${section}</h2><dl class="definition">${fields.map(field=>`<dt>${e(field.name)}</dt><dd class="field-value">${e(field.value ?? '未填写')}</dd>`).join('')}</dl></section>`:'';}).join('');}
1011
- return back('queue','商品队列')+heading(item.label,`共 ${rows.length} 个 SKU。选择后可批量设置,也可逐个修改。`,(item.source_paths || []).map(path=>download('book','下载表格',path)).join(' '))+`<div class="workbook-paths"><span class="hint">实际 XLSX:</span>${(item.source_paths || []).map(path=>`<p class="path-display">${e(path)}</p>`).join('')}</div><div class="toolbar">${button('business-open','经营参数','','primary')}<button data-page="settings/commerce">设置默认值</button><span class="summary">修改保存到实际表格,并保留原文件备份。</span></div><div class="table-wrap"><table><thead><tr><th>货号</th><th>标题</th><th>售价 / 划线价</th><th>本批库存</th><th>操作</th></tr></thead><tbody>${rows.map((record,index)=>`<tr><td class="code">${e(record.item?.offer_id)}</td><td class="title-cell">${e(record.item?.name)}</td><td>${e(record.item?.price)} ${e(record.item?.currency_code)}<small class="hint">${e(record.item?.old_price || '无划线价')}</small></td><td>${record.stock_settings?.stock == null?'未设置':e(record.stock_settings.stock)}</td><td>${button('product','全部字段',`data-index="${index}"`)} ${button('business-one','修改参数',`data-index="${index}"`)}</td></tr>`).join('') || '<tr><td colspan="5">没有读取到商品行。</td></tr>'}</tbody></table></div>`;
1012
- }
1013
- function collectBusinessDraft(){if(ui.page==='commerce')ui.businessDraft={values:collectBusiness('edit'),warehouse:$('#business-warehouse').value};}
1014
- function renderCommerce() {
1015
- const rows=ui.bookRecords || [],one=ui.businessRow==null?null:rows[ui.businessRow],stock=one?.stock_settings || {},baseStock=one?.base_stock_settings ?? stock;
1016
- let values=one?{price:one.item.price,old_price:one.item.old_price ?? null,vat:String(Math.round(Number(one.item.vat)*100)),...Object.fromEntries(['weight','width','height','depth'].map(key=>[key,one.item[key]])),stock:baseStock.stock}:{};
1017
- const fields=one?productFields(one):[];for(const [key,id] of Object.entries({brand:'品牌',country:'原产国',package:'配套'})){const field=fields.find(field=>field.name===id);if(field)values[key]=field.value;}
1018
- if(ui.businessDraft)values=ui.businessDraft.values;
1019
- const cid=ui.data.config.seller.client_id,warehouses=ui.warehouseClient===cid?ui.warehouses:[];
1020
- const selectedWarehouse=ui.businessDraft?.warehouse ?? stock.warehouse_id ?? '';
1021
- const stockHint=one && Object.hasOwn(one,'base_stock_settings')?`当前生效库存:${stock.stock ?? '未设置'};原库存草稿:${baseStock.stock ?? '未设置'}。${one.stock_policy_match?`规则置0:${one.stock_policy_match}。预览与提交仍按规则置0。`:''}`:'';
1022
- return back('book','表格商品')+heading(one?one.item.offer_id:'批量设置经营参数',one?'保存后更新该 SKU 的表格和库存草稿。':'先选择 SKU;只修改填写的字段。')+`<section class="panel">${businessForm('edit',values,{stockHint})}<div class="form-grid"><div class="form-field"><label for="business-warehouse">${e(ui.data.stores?.find(store=>store.id===ui.data.config.seller.selected_id)?.name || '当前店铺')} · 库存仓库</label><select id="business-warehouse"><option value="">${stock.warehouse_id?'保留当前仓库':'暂不选择'}</option>${warehouses.map(row=>`<option value="${row.warehouse_id}" ${String(row.warehouse_id)===String(selectedWarehouse)?'selected':''}>${e(row.name)} · ${e(row.warehouse_id)}</option>`).join('')}</select><small>库存留空不提交,0 表示零库存;仓库须属于当前店铺。</small></div><div class="form-field"><label>仓库信息</label>${button('business-warehouses','读取当前店铺仓库','','')}</div></div><div class="sub-actions">${button('business-save','保存到表格与库存草稿','','primary')}</div></section><section><div class="section-header"><h2>本次范围 · <span id="business-count">${ui.businessSelected.size}</span> 个 SKU</h2><div class="actions">${button('business-all','选择全部')}${button('business-none','清空选择')}</div></div><div class="table-wrap"><table><thead><tr><th>选择</th><th>货号</th><th>售价</th><th>本批库存</th><th>仓库</th></tr></thead><tbody>${rows.map((row,index)=>`<tr><td><input type="checkbox" data-business-offer="${e(row.item.offer_id)}" aria-label="选择 ${e(row.item.offer_id)}" ${ui.businessSelected.has(row.item.offer_id)?'checked':''}></td><td class="code">${e(row.item.offer_id)}</td><td>${e(row.item.price)} ${e(row.item.currency_code)}</td><td>${row.stock_settings?.stock == null?'未设置':e(row.stock_settings.stock)}</td><td>${row.stock_settings?.warehouse_id?e(row.stock_settings.warehouse_id):'未选择'}</td></tr>`).join('')}</tbody></table></div><div class="sub-actions">${button('business-preview-prices','预览价格提交','','')}${button('business-preview-stocks','预览库存提交','','')}${button('business-query','回读已提交结果','','')}</div><p class="hint">提交预览仅适用于店铺中已经建立的商品。先保存当前修改,再进行预览。</p></section>`;
1023
- }
1024
- function renderCommercePlan() {
1025
- const plan=ui.data.commerce_plan,stock=plan?.kind==='stocks';
1026
- const preview=plan?`<section class="panel"><p>${stock?'库存':'价格'} · ${e(plan.rows.length)} 个 SKU · 当前店铺 ${e(plan.client_id)}</p><div class="table-wrap"><table><thead><tr><th>货号</th><th>${stock?'库存数量':'售价'}</th><th>${stock?'仓库':'划线价'}</th>${stock?'':'<th>税率 / 币种</th>'}</tr></thead><tbody>${plan.rows.map(row=>`<tr><td class="code">${e(row.offer_id)}</td><td>${e(stock?row.stock:row.price)}${stock && plan.stock_policy_matches?.[row.offer_id]?` ${badge('规则置0')}<small>${e(plan.stock_policy_matches[row.offer_id])}</small>`:''}</td><td>${e(stock?`${row.warehouse_name} · ${row.warehouse_id}`:row.old_price)}</td>${stock?'':`<td>${Math.round(Number(row.vat)*100)}% · ${e(row.currency_code)}</td>`}</tr>`).join('')}</tbody></table></div><div class="sub-actions">${button('business-submit',`确认提交${stock?'库存':'价格'}`,'','primary')}</div></section>`:empty('暂无提交预览','返回经营参数,选择商品后生成预览。');
1027
- return back('commerce','经营参数')+heading('价格与库存提交','确认所选店铺、SKU 和最终数值。')+preview+`<div class="buttons-line">${button('business-query','回读已提交结果','','')}</div>`+(ui.commerceOperations || []).map(entry=>`<section class="panel"><h2>${entry.kind==='stocks'?'库存':'价格'}提交 · ${entry.rows.length} 个 SKU</h2><p>${badge(entry.state)} · 回读 ${badge(entry.verification || 'pending')}</p><div class="table-wrap"><table><thead><tr><th>货号</th><th>回读核验</th><th>实际值</th></tr></thead><tbody>${(entry.checks || []).map(check=>`<tr><td>${e(check.offer_id)}</td><td>${badge(check.state)}</td><td>${e(typeof check.actual==='object'?Object.entries(check.actual || {}).map(([key,value])=>`${fieldName(key)}:${value ?? '未返回'}`).join(';'):check.actual ?? '未返回')}</td></tr>`).join('')}</tbody></table></div>${entry.batches.map(batch=>`<p>${badge(batch.state)} ${e(batch.message || '')}</p>${(batch.result || []).filter(row=>row.errors?.length || row.updated!==true).map(row=>`<p class="upload-error">${e(row.offer_id)}:${e((row.errors || []).map(error=>error.message || error.description || '平台拒绝此字段').join(';') || '未更新,请回读核实')}</p>`).join('')}`).join('')}</section>`).join('');
1028
- }
1029
- const cardOrderKey = () => `ozon.cardOrder:${encodeURIComponent(cloudMode?ui.session?.user?.username || 'guest':'local')}:${encodeURIComponent(ui.workspace || 'local')}`;
1030
- const cardId = (list,item) => String(list==='designs'?item.id:item.name);
1031
- const cardSortAttrs = (list,id) => `data-card-list="${list}" data-card-id="${e(id)}" draggable="true" tabindex="0" aria-keyshortcuts="Alt+ArrowLeft Alt+ArrowRight Alt+ArrowUp Alt+ArrowDown" title="拖动调整顺序;聚焦卡片后按 Alt + 方向键移动"`;
1032
- function loadCardOrders() {
1033
- const key=cardOrderKey();
1034
- if(cardOrdersKey===key)return;
1035
- clearCardDrag();cardOrdersKey=key;cardOrders={};
1036
- try {
1037
- const saved=JSON.parse(window.localStorage?.getItem(key) || 'null');
1038
- if(saved?.version===1 && !Array.isArray(saved)){
1039
- for(const list of ['designs','library'])if(Array.isArray(saved[list]))cardOrders[list]=saved[list];
1040
- }
1041
- } catch {}
1042
- }
1043
- function displayCardItems(list) {
1044
- loadCardOrders();
1045
- const source=list==='designs'?ui.data.designs:ui.data.library;
1046
- const items=list==='library'?filterLibrary(source,{sort:ui.libraryFilter.sort==='manual'?'name':ui.libraryFilter.sort}):source;
1047
- if(list==='library' && ui.libraryFilter.sort!=='manual')return items;
1048
- const byId=new Map(items.map(item=>[cardId(list,item),item]));
1049
- return reconcileCardOrder([...byId.keys()],cardOrders[list]).map(id=>byId.get(id));
1050
- }
1051
- function visibleCardItems(list) {
1052
- if(list==='library')return libraryView().items;
1053
- return displayCardItems('designs').filter(item=>`${item.code} ${item.folder}`.toLowerCase().includes(ui.search.toLowerCase()));
1054
- }
1055
- function moveDisplayCards(list,from,to) {
1056
- if(!['designs','library'].includes(list) || ui.page!==(list==='designs'?'generation':'library') || from===to)return false;
1057
- const visible=visibleCardItems(list).map(item=>cardId(list,item));
1058
- if(!visible.includes(from) || !visible.includes(to))return false;
1059
- cardOrders[list]=moveCardOrder(displayCardItems(list).map(item=>cardId(list,item)),from,to);
1060
- if(list==='library')ui.libraryFilter.sort='manual';
1061
- let saved=false;
1062
- try {
1063
- if(window.localStorage){window.localStorage.setItem(cardOrdersKey,JSON.stringify({version:1,...cardOrders}));saved=true;}
1064
- } catch {}
1065
- render();toast(saved?'卡片顺序已调整,已保存到当前浏览器':'卡片顺序已调整;浏览器存储未保存,仅本次页面有效');
1066
- return true;
1067
- }
1068
- function releaseCardVisual() {
1069
- const visual=cardVisual;cardVisual=null;
1070
- visual?.entries.forEach(entry=>{entry.node.style.transform=entry.transform;entry.node.classList.remove('card-sort-placeholder','card-sort-moving','card-dragging','card-drop-target');});
1071
- visual?.image.remove();
1072
- return visual;
1073
- }
1074
- function clearCardLanding() {
1075
- if(!cardLanding)return;
1076
- cancelAnimationFrame(cardLanding.frame);clearTimeout(cardLanding.timer);
1077
- cardLanding.node.classList.remove('card-sort-placeholder');cardLanding.overlay.remove();cardLanding=null;
1078
- }
1079
- function clearCardDrag() {
1080
- releaseCardVisual()?.overlay.remove();clearCardLanding();
1081
- if(cardDrag)cardClickBlockedUntil=Date.now()+250;
1082
- cardDrag=null;cardPointerBlocked=false;
1083
- document.querySelectorAll('.card-dragging,.card-drop-target').forEach(card=>card.classList.remove('card-dragging','card-drop-target'));
1084
- }
1085
- function beginCardVisual(source,event) {
1086
- const container=source.parentElement,box=container.getBoundingClientRect(),rect=source.getBoundingClientRect();
1087
- const entries=Array.from(container.querySelectorAll('.card-sortable')).filter(node=>node.dataset.cardList===cardDrag.list).map(node=>{
1088
- const bounds=node.getBoundingClientRect();return {node,id:node.dataset.cardId,x:bounds.left-box.left,y:bounds.top-box.top,width:bounds.width,height:bounds.height,transform:node.style.transform};
1089
- });
1090
- const overlay=document.createElement('div'),clone=source.cloneNode(true);
1091
- overlay.className=`card-sort-overlay ${cardDrag.list==='designs'?'design-gallery':'server-gallery'}`;
1092
- overlay.setAttribute('aria-hidden','true');overlay.setAttribute('inert','');overlay.style.pointerEvents='none';
1093
- Object.assign(overlay.style,{left:`${rect.left}px`,top:`${rect.top}px`,width:`${rect.width}px`,height:`${rect.height}px`,transform:'translate(0px,0px)'});
1094
- clone.classList.remove('card-sortable','card-dragging','card-drop-target','card-sort-placeholder','card-sort-moving');
1095
- [clone,...clone.querySelectorAll('*')].forEach(node=>{
1096
- for(const name of ['id','data-card-list','data-card-id','data-action','data-page','data-design-check','data-file-check','data-motion-image','data-duration','data-server-video'])node.removeAttribute(name);
1097
- node.tabIndex=-1;node.draggable=false;
1098
- });
1099
- clone.style.width='100%';clone.style.height='100%';overlay.append(clone);document.body.append(overlay);
1100
- const image=document.createElement('canvas');image.width=1;image.height=1;
1101
- image.setAttribute('aria-hidden','true');Object.assign(image.style,{position:'fixed',left:'-9999px',pointerEvents:'none'});document.body.append(image);
1102
- event.dataTransfer.setDragImage?.(image,0,0);
1103
- cardVisual={container,box,rect,entries,order:entries.map(entry=>entry.id),overlay,image,targetId:null,offsetX:event.clientX-rect.left,offsetY:event.clientY-rect.top};
1104
- source.classList.add('card-sort-placeholder');
1105
- }
1106
- function validCardVisual() {
1107
- const visual=cardVisual;
1108
- if(!visual || !cardDrag)return false;
1109
- if(cardDrag.key!==cardOrderKey() || ui.page!==(cardDrag.list==='designs'?'generation':'library') || visual.entries.some(entry=>!entry.node.isConnected) || Math.abs(visual.container.getBoundingClientRect().width-visual.box.width)>1){clearCardDrag();return false;}
1110
- const order=visibleCardItems(cardDrag.list).map(item=>cardId(cardDrag.list,item));
1111
- if(order.length!==visual.order.length || order.some((id,index)=>id!==visual.order[index])){clearCardDrag();return false;}
1112
- return true;
1113
- }
1114
- function positionCardOverlay(event) {
1115
- if(cardVisual && Number.isFinite(event.clientX) && Number.isFinite(event.clientY))cardVisual.overlay.style.transform=`translate(${event.clientX-cardVisual.offsetX-cardVisual.rect.left}px,${event.clientY-cardVisual.offsetY-cardVisual.rect.top}px)`;
1116
- }
1117
- function cardTargetAt(event,drop=false) {
1118
- const direct=event.target.closest('[data-card-list]');
1119
- if(!cardVisual)return direct;
1120
- if(!validCardVisual() || !cardTransferMatches(event.dataTransfer,drop))return null;
1121
- if(direct && direct.dataset.cardList!==cardDrag.list || !cardVisual.container.contains(event.target))return null;
1122
- if(!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY))return direct;
1123
- const box=cardVisual.container.getBoundingClientRect();
1124
- // Hit-test the original slots, not cards translated away during the preview.
1125
- return cardVisual.entries.find(entry=>event.clientX>=box.left+entry.x && event.clientX<=box.left+entry.x+entry.width && event.clientY>=box.top+entry.y && event.clientY<=box.top+entry.y+entry.height)?.node || null;
1126
- }
1127
- function previewCardInsertion(target,event) {
1128
- if(!cardVisual)return;positionCardOverlay(event);
1129
- const visual=cardVisual,order=target?moveCardOrder(visual.order,cardDrag.id,target.dataset.cardId):visual.order;
1130
- visual.targetId=target?.dataset.cardId || null;
1131
- visual.entries.forEach(entry=>{
1132
- const slot=visual.entries[order.indexOf(entry.id)],x=slot.x-entry.x,y=slot.y-entry.y;
1133
- entry.node.classList.toggle('card-sort-moving',Boolean(x || y));
1134
- entry.node.style.transform=x || y?`translate(${x}px,${y}px)`:entry.transform;
1135
- entry.node.classList.toggle('card-drop-target',entry.node===target);
1136
- });
1137
- }
1138
- function landCardVisual(visual,list,id) {
1139
- if(!visual)return;
1140
- const node=Array.from(document.querySelectorAll('#page .card-sortable')).find(node=>node.dataset.cardList===list && node.dataset.cardId===id);
1141
- if(!node || !motionAllowed()){visual.overlay.remove();return;}
1142
- const rect=node.getBoundingClientRect(),style=getComputedStyle(visual.overlay),value=style.getPropertyValue('--card-sort-duration').trim();
1143
- const duration=Number.isFinite(parseFloat(value))?parseFloat(value)*(value.endsWith('ms')?1:1000):180;
1144
- if(!duration){visual.overlay.remove();return;}
1145
- node.classList.add('card-sort-placeholder');
1146
- const landing={node,overlay:visual.overlay};cardLanding=landing;
1147
- landing.frame=requestAnimationFrame(()=>{
1148
- if(cardLanding!==landing)return;
1149
- landing.overlay.classList.add('card-sort-moving');
1150
- landing.overlay.style.transform=`translate(${rect.left-visual.rect.left}px,${rect.top-visual.rect.top}px)`;
1151
- landing.timer=setTimeout(()=>{if(cardLanding===landing)clearCardLanding();},duration+32);
1152
- });
1153
- }
1154
- const cardMime = 'application/x-ozon-card';
1155
- function cardTransferMatches(transfer,drop) {
1156
- const types=Array.from(transfer?.types || []);
1157
- if(!cardDrag || !types.includes(cardMime) || types.some(type=>type!==cardMime && type!=='chromium/x-drag-id'))return false;
1158
- if(!drop)return true;
1159
- try {
1160
- const payload=JSON.parse(transfer.getData(cardMime));
1161
- return payload?.list===cardDrag.list && payload.id===cardDrag.id && payload.key===cardDrag.key;
1162
- } catch {return false;}
1163
- }
1164
- function compatibleCard(card,transfer,drop=false) {
1165
- return cardTransferMatches(transfer,drop) && card && cardDrag.key===cardOrderKey() && cardDrag.list===card.dataset.cardList && cardDrag.id!==card.dataset.cardId && ui.page===(cardDrag.list==='designs'?'generation':'library')
1166
- && visibleCardItems(cardDrag.list).some(item=>cardId(cardDrag.list,item)===card.dataset.cardId);
1167
- }
1168
- function libraryView() {return pageItems(filterLibrary(displayCardItems('library'),{...ui.libraryFilter,query:ui.search}),ui.libraryPage,ui.pageSize);}
1169
- function queueView() {return pageItems(filterQueue(queueGroups(),ui.search,ui.queueFilter,ui.data.plan),ui.queuePage,ui.pageSize);}
1170
- function pagination(kind,view) {
1171
- const history=kind==='history',size=history?ui.historySize:ui.pageSize;
1172
- return `<nav class="pagination" aria-label="${history?'上架记录':kind==='library'?'媒体库':kind==='models'?'手机型号':'商品队列'}分页"><span class="summary">${view.total} 项 · ${view.page} / ${view.pages} 页</span><label>每页 <select id="${history?'history-size':'list-size'}" aria-label="每页数量">${(history?[30,60,100]:[30,60,100,500]).map(value=>`<option value="${value}" ${size===value?'selected':''}>${value}</option>`).join('')}</select></label>${button('list-page','上一页',`data-list="${kind}" data-number="${view.page-1}"`,'',view.page<=1)}${button('list-page','下一页',`data-list="${kind}" data-number="${view.page+1}"`,'',view.page>=view.pages)}</nav>`;
1173
- }
1174
- function renderLibrary() {
1175
- const view=libraryView(),items=view.items,capacity=ui.data.capacity,filter=ui.libraryFilter;
1176
- ui.libraryPage=view.page;
1177
- const selectedBytes=ui.data.library.filter(item=>ui.files.has(item.name)).reduce((sum,item)=>sum+item.size,0);
1178
- const choices=(values,current)=>values.map(([value,label])=>`<option value="${value}" ${current===value?'selected':''}>${label}</option>`).join('');
1179
- return heading('服务器媒体库',isAdmin()?'查看本账户与通用媒体;超级管理员可查看全部账户媒体。':'查看自己的上架素材与通用型号样本、视频;其他账户的素材仅超级管理员可查看。',`<button data-page="settings/mediaRules">图片发布方式</button><button data-page="upload" class="primary">上传文件</button>`)
1180
- + `<div class="usage"><div class="usage-number">${capacity?bytes(capacity.free):'未查询'}</div><div class="usage-body"><h2>服务器可用空间</h2><p>${capacity?`已用 ${bytes(capacity.used)} / ${bytes(capacity.total)} · 保留 ${bytes(capacity.reserve_bytes)} · ${Number(capacity.used_percent).toFixed(1)}% 已用`:'点击“查询媒体与容量”读取实时数据。'} ${isAdmin()?'<button class="quiet" data-page="settings/server">修改告警与保留空间</button>':''}</p></div>${capacity?.warning?badge('空间达到告警线',''):''}</div>`
1181
- + `<div class="toolbar"><input id="search" type="search" aria-label="搜索服务器媒体" placeholder="搜索原文件名或备注" value="${e(ui.search)}"><select data-library-filter="category" aria-label="媒体用途分类">${choices([['all','全部用途'],...Object.entries(mediaCategories),['unclassified','待分类']],filter.category || 'all')}</select>${button('library-refresh','查询媒体与容量','','')}${isAdmin()?button('library-delete','删除选中','','danger',!ui.files.size):''}<span class="spacer"></span>${isAdmin()?`<span id="selected-media-summary" class="summary">已选 ${ui.files.size} 个 · ${bytes(selectedBytes)}</span>`:''}</div>`
1182
- + `<details class="filter-disclosure"><summary>筛选与排序${filter.type!=='all' || filter.reference!=='all' || filter.minMB || filter.maxMB || filter.before || filter.after?' · 已启用':''}</summary><div class="filter-fields"><label>文件类型<select data-library-filter="type">${choices([['all','全部'],['image','图片'],['video','视频']],filter.type)}</select></label><label>关联状态<select data-library-filter="reference">${choices([['all','全部'],['known','已知关联'],['unknown','外部引用待核实']],filter.reference)}</select></label><label>排序<select data-library-filter="sort">${choices([['manual','手动排列'],['name','文件名'],['size-desc','最大文件优先'],['size-asc','最小文件优先'],['date-desc','最新文件优先'],['date-asc','最早文件优先']],filter.sort)}</select></label><label>最小大小(MB)<input type="number" min="0" step="0.1" data-library-filter="minMB" value="${e(filter.minMB || '')}"></label><label>最大大小(MB)<input type="number" min="0" step="0.1" data-library-filter="maxMB" value="${e(filter.maxMB || '')}"></label><label>起始日期<input type="date" data-library-filter="after" value="${e(filter.after || '')}"></label><label>结束日期<input type="date" data-library-filter="before" value="${e(filter.before || '')}"></label>${button('library-filter-reset','清除筛选')}</div></details>`
1183
- + `<div class="section-header">${isAdmin()?`<label class="check"><input type="checkbox" id="file-all" aria-label="选择本页全部媒体" ${items.length && items.every(item=>ui.files.has(item.name))?'checked':''}>选择本页全部媒体</label>`:''}<span class="summary">筛选 ${view.total} / 已读取 ${ui.data.library.length} 个 · 删除前核对关联</span></div>`
1184
- + `<div class="media-grid server-gallery" data-gallery="library">${items.map(item=>{const url=libraryUrl(item),video=/\.(mp4|mov)$/i.test(item.name);return `<article class="file-card card-sortable" ${cardSortAttrs('library',item.name)}><div class="file-preview">${url?(video?`<video src="${e(url)}" controls preload="metadata" data-server-video="${e(item.name)}" title="${e(url)}"></video>`:`<img src="${e(fileUrl(`/api/thumbnail?name=${encodeURIComponent(item.name)}`))}" data-full-src="${e(url)}" alt="${e(item.note || item.name)}" title="${e(url)}" loading="lazy">`):'<span class="file-preview-label">公共地址未确认</span>'}</div><div class="file-content">${cloudMode?`<p class="hint">${e(item.shared?'通用素材':item.owners?.length?'所属账号:'+item.owners.join('、'):'历史媒体')}</p>`:''}<p class="hint">${e((item.categories || []).map(c=>mediaCategories[c]).join('、') || '待分类')}</p><div class="card-heading"><strong class="code" title="${e(item.name)}">${e(item.note || item.source_names?.[0] || item.name)}</strong>${isAdmin()?`<input type="checkbox" data-file-check="${e(item.name)}" aria-label="选择 ${e(item.name)}" ${ui.files.has(item.name)?'checked':''} ${knownReferences(item)?'title="有关联,删除前将显示影响"':''}>`:''}</div><p>${bytes(item.size)}${video?` · <span data-duration="${e(item.name)}">时长读取中</span>`:''}</p>${badge(referenceLabel(item),knownReferences(item)?'good':'')}<div class="actions">${button('file',isAdmin()?'详情 / 管理':'查看详情',`data-name="${e(item.name)}"`)}</div></div></article>`;}).join('')}</div>${items.length?'':empty(capacity?ui.search?'没有找到媒体文件':'没有符合条件的媒体':'媒体尚未查询',capacity?'调整搜索词或筛选条件。':'点击“查询媒体与容量”读取服务器实时数据。')}${pagination('library',view)}<p class="hint">列表显示独立缩略图,点击读取原图。${isAdmin()?'外部引用待核实不代表文件可安全删除;替换产生新 URL,原文件与关联保留。':'共享媒体管理由管理员维护。'}</p>`;
1185
- }
1186
- function renderFile() {
1187
- const item = file(); if (!item) return back('library','服务器媒体库') + empty('文件未在当前查询结果中','返回并刷新服务器媒体。');
1188
- const url = libraryUrl(item), video = /\.(mp4|mov)$/i.test(item.name);
1189
- const management=isAdmin()?`<fieldset class="media-purpose"><legend>媒体用途</legend>${Object.entries(mediaCategories).filter(([c])=>(c==='videos')===video).map(([c,label])=>`<label class="check"><input type="checkbox" data-file-category="${c}" ${(item.categories || []).includes(c)?'checked':''}>${label}</label>`).join('')}</fieldset>${button('file-category','保存媒体用途','','')}<div class="form-field"><label for="file-note">管理备注</label><textarea id="file-note" rows="3">${e(item.note || '')}</textarea></div>${button('file-note','保存备注','','primary')}<details><summary>替换为新文件</summary><div class="form-field"><label for="replacement-path">新文件路径</label><div class="field-row"><input id="replacement-path">${button('pick-replacement','选择文件')}</div><small>上传新内容获得新 URL;原文件与商品关联保留。</small></div>${button('file-replace','上传替换文件','','')}</details><div class="sub-actions">${button('file-delete','删除此文件','','danger')}</div>`:'<p class="hint">共享媒体的备注、替换与删除由管理员维护。</p>';
1190
- return back('library','服务器媒体库') + heading('文件详情','仅管理已查询的精确文件,不改写现有公共 URL。')
1191
- + `<div class="inline-preview"><section><div class="file-preview large-preview">${url ? video ? `<video controls preload="metadata" src="${e(url)}" aria-label="服务器视频预览"></video>` : `<img src="${e(url)}" alt="服务器图片预览">` : '公共媒体地址未确认'}</div>${url ? `<p><a href="${e(url)}" target="_blank" rel="noopener">${cloudMode?'在新标签页查看':'打开公共文件'}</a></p>` : ''}</section><section><dl class="definition"><dt>文件名</dt><dd class="code">${e(item.name)}</dd><dt>大小</dt><dd>${bytes(item.size)}</dd><dt>修改时间</dt><dd>${e(Number.isFinite(Number(item.mtime)) ? new Date(Number(item.mtime)*1000).toLocaleString() : item.mtime || '未知')}</dd><dt>关联</dt><dd>${e(referenceLabel(item))}${knownReferences(item) ? `<ul>${item.references.map(ref => `<li>${e(ref)}</li>`).join('')}</ul>` : ''}</dd></dl>${management}</section></div>`;
1192
- }
1193
- const uploadStateLabel = {waiting:'等待上传',uploading:'传输中',checking:'检查公开直链',done:'媒体已就绪',failed:'上传失败'};
1194
- function renderUploadList() {
1195
- return ui.uploadItems.map((item,index)=>`<article class="upload-row"><div class="upload-preview">${/\.(mp4|mov)$/i.test(item.path)?`<video src="${e(previewUrl(item.path))}" preload="metadata" controls title="${e(item.path)}"></video>`:`<img ${imageAttrs(item.path)} alt="待上传图片" title="${e(item.path)}" loading="lazy">`}</div><div><strong title="${e(item.path)}">${e(item.path.split(/[\\/]/).at(-1))}</strong><p>${badge(uploadStateLabel[item.state] || '状态待核实',item.state==='done'?'good':item.state==='failed'?'bad':'muted')} ${item.size!==null?bytes(item.size):''}${item.reused?' · 复用已有文件':''}</p>${item.error?`<p class="upload-error">${e(item.error)}</p>`:''}</div>${item.state==='failed'?button('upload-retry','重试',`data-index="${index}"`,''):''}</article>`).join('') || empty('尚未选择文件','选择图片或视频后显示逐文件上传状态。');
1196
- }
1197
- const uploadSummary=()=>`${ui.uploadItems.length} 个文件 · ${ui.uploadItems.filter(item=>item.state==='done').length} 个就绪`;
1198
- function updateUploads() {
1199
- $('#upload-list').innerHTML=renderUploadList();$('#upload-summary').textContent=uploadSummary();bindPreviews();
1200
- }
1201
- function syncUploadEntries() {
1202
- ui.uploadItems=uploadEntries(pathLines(ui.upload)).map(item=>ui.uploadItems.find(old=>old.path===item.path) || item);
1203
- }
1204
- async function runUploads(paths) {
1205
- if(ui.uploading)throw new Error('当前上传尚未结束');
1206
- const conflict=conflictingJob('library.upload');if(conflict)throw new Error(conflictReason(conflict));
1207
- const current=scopeLease();
1208
- ui.uploading=true;render();
1209
- try {
1210
- for(const path of paths){
1211
- if(!current())throw staleView();
1212
- const item=ui.uploadItems.find(item=>item.path===path);if(!item || item.state==='done')continue;
1213
- Object.assign(item,{state:'uploading',error:''});if(ui.page==='upload'){updateUploads();}
1214
- try{const result=await act('library.upload',{paths:[path],category:/\.(mp4|mov)$/i.test(path)?'videos':ui.uploadCategory});if(!current())throw staleView();const file=result?.files?.[0];if(result?.files?.length!==1 || !safePublicUrl(file?.url) || !Number.isFinite(file?.size))throw new Error('服务未返回该文件的完整上传结果,请查询媒体库');Object.assign(item,{state:'done',error:'',size:file.size,url:file.url});}
1215
- catch(error){if(error?.code==='STALE_VIEW' || !current())throw staleView();item.state='failed';item.error=error.message || String(error);}
1216
- if(ui.page==='upload'){updateUploads();}
1217
- }
1218
- toast(`媒体上传:成功 ${ui.uploadItems.filter(item=>item.state==='done').length},失败 ${ui.uploadItems.filter(item=>item.state==='failed').length}`);
1219
- } finally {if(current()){ui.uploading=false;if(ui.page==='upload')render();else renderChrome();}}
1220
- }
1221
- function renderUpload() {
1222
- const imagekit=ui.data.config.listing.image_host==='imagekit';
1223
- const purpose=imagekit?'':`<div class="form-field"><label for="upload-category">图片用途</label><select id="upload-category" ${ui.uploading?'disabled':''}>${Object.entries(mediaCategories).filter(([c])=>c!=='videos').map(([c,label])=>`<option value="${c}" ${ui.uploadCategory===c?'selected':''}>${label}</option>`).join('')}</select><small>图片按所选用途归类,视频自动进入商品视频分类。</small></div>`;
1224
- return back('library','服务器媒体库') + heading(imagekit?'上传媒体':'上传服务器媒体',imagekit?'图片发布至 ImageKit,视频发布至服务器。':cloudMode?'从浏览器选择图片或视频,先传入自己的工作区,再发布为服务器媒体。':'选择本地文件或输入绝对路径;上传前检查可用容量,上传后检查公共直链。')
1225
- + `<div class="panel">${purpose}<div class="buttons-line">${button('pick-upload-images','选择图片','','')}${button('pick-upload-videos','选择视频','','')}${button('upload','上传待处理文件','','primary',!ui.uploadItems.some(item=>item.state==='waiting'))}<span id="upload-summary" class="summary">${uploadSummary()}</span></div><div id="upload-list" class="upload-list" data-gallery="upload">${renderUploadList()}</div><details class="media-path-config"><summary>路径配置</summary><div class="form-field"><label for="upload-paths">${cloudMode?'自己工作区内的媒体路径':'本地 JPG / PNG / MP4 / MOV 文件'} · 每行一个</label><textarea id="upload-paths" rows="4" ${ui.uploading?'disabled':''}>${e(ui.upload)}</textarea></div></details><p class="hint">失败文件可单独重试;成功媒体不会随重试重复传输。媒体就绪后仍需商品预检与提交。</p></div>`;
1226
- }
1227
-
1228
- function renderQueueAdd() {
1229
- return back('queue','商品队列') + heading('导入已有上架表','将已有 XLSX 上架表中的商品读取到当前商品队列;导入时检查重复货号。此操作不会向 Ozon 提交商品。')
1230
- + `<div class="panel"><div class="form-field"><label for="book-paths">${cloudMode?'工作区内的 XLSX 文件路径':'XLSX 文件绝对路径'} · 每行一个</label><textarea id="book-paths" rows="8">${e(ui.addBooks)}</textarea></div>${button('pick-books','选择 XLSX 上架表','','')}<div class="sub-actions">${button('queue-add','读取并导入商品队列','','primary',!pathLines(ui.addBooks).length)}</div></div>`;
1231
- }
1232
- function renderHistory(detail=false) {
1233
- const verifyBadge = state => badge(({matched:'匹配',mismatch:'不一致',pending:'待核实'})[state] || '待核实',state === 'matched' ? 'good' : state === 'mismatch' ? 'bad' : 'muted');
1234
- const stockResult = row => {
1235
- const item=row.stock_update;
1236
- if(!item)return '';
1237
- const state=item.verification==='matched'?'已回读匹配':({waiting:'待商品建立',accepted:'已提交,待回读',failed:'提交失败',uncertain:'结果待核实'})[item.state] || '待核实';
1238
- return `<p class="hint">本批库存:${badge(state,item.verification==='matched'?'good':item.state==='failed'?'bad':'muted')}<br>期望 ${e(item.stock)} · 回读 ${e(item.actual ?? '未返回')} · ${e(item.warehouse_name)}(${e(item.warehouse_id)})${item.message?`<br>${e(item.message)}`:''}</p>`;
1239
- };
1240
- const priceResult = row => row.price_update ? `<p class="hint">本次价格更新:${badge(row.price_update.verification==='matched'?'已回读匹配':stateText(row.price_update.state),row.price_update.verification==='matched'?'good':'muted')} ${e(row.price_update.message || row.price_update.note || '请回读核实价格;商品导入与价格结果分别确认。')}</p>` : '';
1241
- if (detail) {
1242
- const row=ui.recordDetail || ui.data.records[ui.record] || {}, verify=row.verification || {};
1243
- const value = data => e(data == null ? '未返回' : typeof data === 'object' ? JSON.stringify(data) : data);
1244
- const images=(verify.checks || []).find(check=>check.field==='图片内容与顺序');
1245
- const gallery=(values,label)=>`<section><h3>${label}</h3><div class="record-image-list">${Array.isArray(values)?values.slice(0,50).map((url,index)=>{const safe=safePublicUrl(url);return `<figure>${safe?`<img src="${e(safe)}" alt="${label}第 ${index+1} 张" data-record-image="true" loading="lazy" decoding="async" referrerpolicy="no-referrer" width="104" height="116"><span class="hint" hidden>图片预览失败,请核对源链接。</span>`:'<span class="record-image-placeholder">链接无法安全预览</span>'}<figcaption>${index===0?'主图':`附图 ${index}`}</figcaption></figure>`;}).join('') || '<p class="hint">返回为空</p>':'<p class="hint">未返回图片列表</p>'}</div></section>`;
1246
- const media=images?`<section class="panel record-media"><div class="section-header"><h2>图片内容与顺序</h2>${button('record-media',ui.recordMediaOpen?'收起图片对照':'展开图片对照',`aria-expanded="${ui.recordMediaOpen}" aria-controls="record-image-comparison"`)}</div><p class="hint">手动对照提交图与回读图;地址转换不代表内容相同或不同,预览失败也不修改核验结果。</p>${ui.recordMediaOpen?`<div id="record-image-comparison" class="record-image-comparison">${gallery(images.expected,'本次提交')}${gallery(images.actual,'Ozon 回读')}</div>`:''}</section>`:'';
1247
- return back('history','上架记录') + heading('货号任务详情','按实际提交内容核对;导入、审核和可售状态分别保留。')
1248
- + `<section class="panel"><h2>质量文件关联</h2>${qualityResult(row.quality_document,true)}</section>`
1249
- + priceResult(row) + stockResult(row)
1250
- + `<section class="panel"><h2>${e(row.offer_id || '未选择货号')}</h2><p>字段核验 ${verifyBadge(verify.state)} 属性 ${verifyBadge(verify.attributes)} 媒体 ${verifyBadge(verify.media)}</p><p class="hint">${e(verify.note || '媒体地址转换后,内容和顺序需打开商品卡确认;匹配不代表审核通过或可售。')}</p></section>`
1251
- + media
1252
- + ((verify.checks || []).length ? `<div class="table-wrap record-checks"><table><thead><tr><th>字段</th><th>核验</th><th>提交值</th><th>回读值</th><th>说明</th></tr></thead><tbody>${verify.checks.map(c=>`<tr><td>${e(c.field_name || fieldName(c.field,row.source_row?.attribute_names))}</td><td>${verifyBadge(c.state)}</td><td class="code">${value(c.expected)}</td><td class="code">${value(c.actual)}</td><td>${e(c.note || '')}</td></tr>`).join('')}</tbody></table></div>` : empty('尚无字段核验','查询任务与商品后可查看提交值和回读值。'))
1253
- + `<details><summary>原始回读与账本数据</summary>${json(row)}</details>`;
1254
- }
1255
- const excel = ui.data.xlsx_report ? download('report-xlsx','下载 Excel 报告') : '';
1256
- const flags=row=>({failed:['failed','rejected'].includes(row.state) || ['failed','rejected'].includes(row.submit_state) || Boolean(row.error) || row.quality_document?.state==='failed' || row.stock_update?.state==='failed',mismatch:[row.verification?.state,row.verification?.attributes,row.verification?.media,row.price_update?.verification,row.stock_update?.verification].includes('mismatch'),pending:Boolean(row.stock_update && row.stock_update.verification!=='matched') || !row.readback || ['uncertain','submitting','pending','pending_verification'].includes(row.submit_state) || !['matched','mismatch'].includes(row.verification?.state) || Boolean(row.price_update && !['matched','mismatch'].includes(row.price_update.verification)) || ['waiting','accepted','uncertain','partial'].includes(row.quality_document?.state)});
1257
- const records=ui.data.records.map((item,index)=>({item,index,flags:flags(item)})),query=ui.historySearch.trim().toLowerCase();
1258
- const totals={all:records.length,...Object.fromEntries(['failed','pending','mismatch'].map(key=>[key,records.filter(row=>row.flags[key]).length]))};
1259
- const filtered=records.filter(row=>String(row.item.offer_id || '').toLowerCase().includes(query) && (ui.historyFilter==='all' || row.flags[ui.historyFilter]));
1260
- const view=pageItems(filtered,ui.historyPage,ui.historySize);ui.historyPage=view.page;
1261
- const store=(ui.data.stores || []).find(item=>item.id===ui.data.config.seller.selected_id),error=ui.stateReadError || ui.historyReadError;
1262
- const latestQuery=taskList().filter(job=>job.action==='listing.query').at(-1);
1263
- const context=`当前配置店铺:${store?.name || (ui.data.config.seller.client_id?`Client-Id ${ui.data.config.seller.client_id}`:'未选择')} · 页面更新:${ui.stateReadAt?new Date(ui.stateReadAt).toLocaleTimeString('zh-CN',{hour12:false}):'尚未记录'}${latestQuery?` · 最近查询:${stateText(latestQuery.state)}`:''}`;
1264
- return heading('上架记录','仅展示当前任务账本与实际回读;提交未决时显示“结果待核实”。',button('query','查询任务与商品','','primary'))
1265
- + `<p class="hint history-context">${e(context)}<br>平台状态以最近查询结果为准;每条记录按实际批次核对,账本未记录批次时间时不推算。</p>`
1266
- + (error?`<div class="banner read-error" role="alert"><div><strong>本次读取失败,保留上次结果</strong><p>${e(error)}</p></div>${button(ui.stateReadError?'refresh':'query',ui.stateReadError?'重新读取列表':'重新查询商品')}</div>`:'')
1267
- + `<div class="toolbar history-toolbar"><input id="history-search" type="search" aria-label="搜索上架记录货号" placeholder="搜索货号" value="${e(ui.historySearch)}"><select id="history-filter" aria-label="上架记录状态筛选">${[['all','全部'],['failed','失败'],['pending','待核实'],['mismatch','不一致']].map(([key,label])=>`<option value="${key}" ${ui.historyFilter===key?'selected':''}>${label}(${totals[key]})</option>`).join('')}</select>${button('history-clear','清除筛选','','quiet',!ui.historySearch && ui.historyFilter==='all')}<span class="spacer"></span>${excel}${download('report','下载 JSON 报告')}</div>`
1268
- + `<p class="hint" role="status">筛选出 ${view.total} / 共 ${records.length} 条记录;同一商品可同时存在多类异常。</p><div class="table-wrap history-table"><table><thead><tr><th>货号 / 批次</th><th>提交状态</th><th>导入状态</th><th>商品核验 / 价格</th><th>质量文件 / 库存</th><th>任务编号</th><th>操作</th></tr></thead><tbody>${view.items.map(({item,index}) => `<tr><td class="code">${e(item.offer_id)}<small class="hint">${item.operation==='update'?'更新已有商品':'新建商品'}</small><small class="hint" title="${e(item.batch_key || '')}">批次:${e(item.batch_key?String(item.batch_key).slice(0,12):'未记录')}</small></td><td>${badge(item.submit_state || '未知',item.submit_state === 'uncertain' ? '' : 'muted')}</td><td>${badge(item.state || '未查询',item.state === 'imported' ? 'good' : item.state === 'failed' ? 'bad' : 'muted')}</td><td>${verifyBadge(item.verification?.state)}<small class="hint">${item.readback ? '已取得商品数据' : '尚无商品回读'}</small>${priceResult(item)}</td><td>${qualityResult(item.quality_document)}${stockResult(item)}</td><td class="code">${e(item.task_id || '未取得')}</td><td>${button('record','详情',`data-index="${index}"`)}</td></tr>`).join('') || `<tr><td colspan="7">${records.length?'没有符合条件的记录,请调整货号或状态筛选。':'尚无上架任务。先在商品队列完成预检与确认提交。'}</td></tr>`}</tbody></table></div>${pagination('history',view)}`;
1269
- }
1270
- function listingStocks(plan) {
1271
- const stock=plan.stock_plan;
1272
- if(!stock)return '<p class="hint">旧计划未核对库存,请重新预检。</p>';
1273
- if(!stock.rows.length)return '<p class="hint">本批库存未设置,本次不修改平台库存。</p>';
1274
- const warehouses=[...new Set(stock.rows.map(row=>`${row.warehouse_name}(${row.warehouse_id})`))];
1275
- return `<section class="listing-stocks"><h2>本批库存 · ${stock.rows.length} 个 SKU</h2><p>商品建立后提交以下库存快照;仓库:${warehouses.map(e).join('、')}。${stock.rows.some(row=>row.stock===0)?'<strong>数量 0 表示明确清零。</strong>':''}</p><details><summary>核对每个货号的库存数量与仓库</summary><div class="table-wrap"><table><thead><tr><th>货号</th><th>库存数量</th><th>仓库</th></tr></thead><tbody>${stock.rows.map(row=>`<tr><td class="code">${e(row.offer_id)}</td><td>${e(row.stock)}${row.stock_policy_match?` ${badge('规则置0')}<small>${e(row.stock_policy_match)}</small>`:''}</td><td>${e(row.warehouse_name)} · ${e(row.warehouse_id)}</td></tr>`).join('')}</tbody></table></div></details><p class="hint">上架成功与库存生效分别确认;待商品建立或回读未匹配会显示在上架记录中,不重复创建商品。</p></section>`;
1276
- }
1277
- const listingCounts = plan => `新建 ${e(plan.new_count ?? plan.count)} · 更新 ${e(plan.update_count || 0)} · 跳过 ${e(plan.skipped_count || 0)}`;
1278
- function listingDifferences(plan) {
1279
- const value = item => item == null ? '<span class="hint">未提供</span>' : Array.isArray(item) && item.every(value=>typeof value==='string') ? item.map(e).join('<br>') || '<span class="hint">清空</span>' : typeof item==='object'
1280
- ? `<details><summary>查看 ${Array.isArray(item)?item.length:1} 项内容</summary><pre>${e(JSON.stringify(item,null,2))}</pre></details>` : e(item);
1281
- return Object.entries(plan.changes || {}).map(([offer,rows])=>`<details class="listing-difference"><summary>${e(offer)} · ${rows.length} 项差异</summary>${rows.length?`<div class="table-wrap"><table><thead><tr><th>参数</th><th>店铺当前(确认前)</th><th>本次提交</th></tr></thead><tbody>${rows.map(row=>`<tr><th scope="row">${e(row.label || fieldName(row.field))}</th><td>${value(row.before)}</td><td>${value(row.after)}</td></tr>`).join('')}</tbody></table></div>`:'<p class="hint">回读参数与本次值一致;仍需查询平台导入结果。</p>'}</details>`).join('');
1282
- }
1283
- function renderPreflight() {
1284
- const plan = ui.data.plan;
1285
- if(!plan)return back('queue','商品队列')+heading('上传与预检','先检查已有货号,再按本批选择上传与预检。')+empty('尚无有效上架计划','指定媒体并勾选商品后,在商品队列顶部点击“上传与预检”。');
1286
- const account=plan.account_check || {},remaining=quota=>quota?.limit===-1?'不限':Number.isInteger(quota?.limit)&&Number.isInteger(quota?.usage)?Math.max(0,quota.limit-quota.usage):'未核对';
1287
- const quota=`<dt>店铺币种</dt><dd>${e(account.currency || '未核对')}</dd><dt>每日新建剩余</dt><dd>${e(remaining(account.daily_create))}</dd><dt>每日更新剩余</dt><dd>${e(remaining(account.daily_update))}</dd><dt>商品总量剩余</dt><dd>${e(remaining(account.total))}</dd><dt>每分钟操作额度</dt><dd>${e(account.operation_limits?.limit_type==='RATE_LIMIT_PER_MINUTE'?account.operation_limits.limit:account.operation_limits?.limit_type==='UNSPECIFIED'?'未指定':'未核对')}</dd>`;
1288
- const differences=listingDifferences(plan),skipped=plan.skipped_existing || [];
1289
- const business=(plan.business_summary || []).length?`<h2>本次商品经营参数</h2><div class="table-wrap"><table><thead><tr><th>货号</th><th>售价 / 划线价</th><th>本批库存</th><th>仓库</th></tr></thead><tbody>${plan.business_summary.map(row=>`<tr><td class="code">${e(row.offer_id)}</td><td>${e(row.price)} / ${e(row.old_price || '无划线价')} ${e(row.currency_code)}</td><td>${row.stock_settings?.stock==null?'未设置':e(row.stock_settings.stock)}${row.stock_policy_match?` ${badge('规则置0')}<small>${e(row.stock_policy_match)}</small>`:''}</td><td>${e(row.stock_settings?.warehouse_id || '未选择')}</td></tr>`).join('')}</tbody></table></div>`:'';
1290
- return back('queue','商品队列')+heading('上传与预检','核对本批新建与更新范围、参数差异,确认后才向 Ozon 提交。',button('prepare','重新预检')+button('submit','确认提交','','primary',ui.qualityDirty || !sameScope(plan.scope || [],ui.data.selected_groups)))+`<div class="panel"><dl class="definition"><dt>本批处理</dt><dd><strong>${listingCounts(plan)}</strong> · 实际提交 ${e(plan.count)} 个 SKU</dd><dt>店铺客户端标识</dt><dd>${e(plan.client_id)} · ${e(plan.store_name || '当前店铺')}</dd><dt>预检时间</dt><dd>${e(plan.prepared_at || '未记录')}</dd>${quota}<dt>本批质量文件</dt><dd class="quality-snapshot">${e(qualityLabel(plan.quality_document))}${plan.quality_document?`<small class="hint">${e(qualityReview(plan.quality_document.status_code))} · 有效期 ${e(plan.quality_document.expire_date || '无有效期或未返回')} · 关联不代表审核通过或可售。</small>`:''}</dd><dt>选中款式</dt><dd>${e((plan.scope || []).map(id=>ui.data.groups.find(item=>String(item.id)===String(id))?.label || id).join('、'))}</dd><dt>计划指纹</dt><dd class="code">${e(plan.payload_sha256)}</dd></dl>
1291
- ${differences?`<h2>已有商品参数差异</h2><p class="hint">对比店铺回读与本次提交值;未提供的字段保留。确认后若店铺参数发生变化,会停止并要求重新预检。</p>${differences}`:''}
1292
- ${skipped.length?`<details><summary>本批跳过 ${skipped.length} 个已有商品</summary><ul>${skipped.map(row=>`<li>${e(row.offer_id)} · ${e(row.model || '型号未提供')}</li>`).join('')}</ul></details>`:''}${business}${listingStocks(plan)}
1293
- <p class="hint">新建商品携带表格价格;更新商品的价格在商品请求受理后单独更新并回读。已设置的库存随本次确认提交;未设置的库存不修改平台。</p>
1294
- ${ui.qualityDirty?'<p class="hint" role="status">质量文件已更改,请重新预检;以上仍为上次冻结的文件。</p>':''}${download('plan','下载完整上架计划')}</div>`;
1295
- }
1296
- const jobStage = job => {
1297
- const phase=job.phase || [...(job.events || [])].reverse().find(event=>event.phase || event.stage);
1298
- const latest=job.events?.at(-1) || job.messages?.at(-1);
1299
- return phase?.phase || phase?.stage || (typeof latest==='string'?latest:latest?.message) || '服务尚未返回阶段';
1300
- };
1301
- const jobTime = value => {
1302
- if(value===null || value===undefined || value==='')return NaN;
1303
- return typeof value==='number'?value<1e12?value*1000:value:Date.parse(value);
1304
- };
1305
- const jobDuration = job => {
1306
- const start=jobTime(job.started_at),end=job.finished_at?jobTime(job.finished_at):['queued','running'].includes(job.state)?Date.now():NaN;
1307
- if(!Number.isFinite(start) || !Number.isFinite(end))return '服务未记录';
1308
- const seconds=Math.max(0,Math.floor((end-start)/1000));
1309
- return seconds>=60?`${Math.floor(seconds/60)} 分 ${seconds%60} 秒`:`${seconds} 秒`;
1310
- };
1311
- const eventMessage = event => typeof event==='string'?event:event.message || `${event.phase || event.stage || (event.action?actionText(event.action):'')}${event.state?` · ${stateText(event.state)}`:''}${Number.isFinite(event.completed)?` · ${event.completed}${Number.isFinite(event.total)?` / ${event.total}`:''}${event.unit || ' 项'}`:''}`;
1312
- const eventLevel = event => {const level=String(event?.level || event?.severity || '').toUpperCase();return level==='ERROR' || ['failed','error'].includes(event?.state)?'ERROR':['WARN','WARNING'].includes(level) || event?.state==='uncertain'?'WARN':'INFO';};
1313
- function renderJob(embedded=false) {
1314
- const jobs=taskList(),job=jobs.find(item=>item.id===ui.selectedJobId) || ui.lastJob || jobs.at(-1),files=job?.files || [],events=job?.events?.length?job.events:job?.messages || [];
1315
- const choiceId=embedded?'task-log-job':'job-choice';
1316
- const choice=jobs.length?`<div class="toolbar job-picker"><label for="${choiceId}">查看任务</label><select id="${choiceId}">${[...jobs].reverse().map(item=>{const started=jobTime(item.started_at),label=Number.isFinite(started)?new Date(started).toLocaleString('zh-CN',{hour12:false}):String(item.id || '').slice(0,12);return `<option value="${e(item.id || '')}" title="任务编号:${e(item.id || '未记录')}" ${item===job || item.id && item.id===job?.id?'selected':''}>${e(actionText(item.action))} · ${e(stateText(item.state))}${label?` · ${e(label)}`:''}</option>`;}).join('')}</select></div>`:'';
1317
- const phase=job?.phase || [...events].reverse().find(event=>typeof event==='object' && (event.phase || event.stage));
1318
- const products=job?.action==='listing.query';
1319
- const counts=products?(Number.isFinite(phase?.completed)?`已处理 ${phase.completed}${Number.isFinite(phase.total)?` / ${phase.total}`:''} 个商品`:'服务尚未返回商品计数'):files.length?`已返回 ${files.length} 个文件 · 已完成 ${files.filter(item=>item.state==='done').length} · 失败 ${files.filter(item=>item.state==='failed').length}`:'服务尚未返回逐文件计数';
1320
- const progress=Number.isFinite(phase?.completed)?` · ${phase.completed}${Number.isFinite(phase.total)?` / ${phase.total}`:''}${phase.unit || ' 项'}`:'';
1321
- const next=job?.error?job.action==='listing.submit' || job.action==='commerce.submit'?'先查询并回读任务结果;结果待核实时保留本次任务,不重复提交。':/403|404|图片|媒体|链接|源表/.test(job.error)?'按错误中的货号与字段检查源文件或媒体链接,修复后重新预览;型号小图需使用对应型号样本。':'根据上方错误修正输入或连接,再重新执行此任务。':'';
1322
- const read=job && ui.jobReads.get(job.id),readInfo=read?`<p class="hint job-read-time">最近读取:${read.at?e(new Date(read.at).toLocaleTimeString('zh-CN',{hour12:false})):'尚未成功'}</p>${read.error?`<div class="banner read-error" role="alert"><div><strong>状态读取失败,显示上次结果</strong><p>${e(read.error)}</p></div>${button('retry-job-read','重新读取状态',`data-id="${e(job.id)}"`)}</div>`:['queued','running'].includes(job.state)?button('retry-job-read','重新读取状态',`data-id="${e(job.id)}"`):''}`:'';
1323
- return (embedded?'':back('queue','商品队列')+heading('任务日志','查看当前工作区的任务、真实阶段与处理结果。'))+choice+(job?`<section class="panel job-summary" aria-label="任务概况"><div class="job-title"><h2>${e(actionText(job.action))}</h2>${badge(job.state,job.state==='done'?'good':job.state==='failed'?'bad':'')}</div><dl class="job-overview"><div><dt>当前阶段</dt><dd>${e(jobStage(job))}${products?'':e(progress)}</dd></div><div><dt>耗时</dt><dd>${e(jobDuration(job))}</dd></div><div><dt>${products?'商品':'文件'}进度</dt><dd>${e(counts)}</dd></div></dl>${readInfo}${job.error?`<div class="banner ${job.state==='uncertain'?'':'bad'} job-error"><strong>${job.state==='uncertain'?'结果待核实':'任务失败'}</strong><p>${e(job.error)}</p><p><strong>下一步:</strong>${e(next)}</p></div>`:''}</section>${events.length?`<section class="job-events" aria-label="任务阶段记录"><h2>阶段记录</h2><ol>${events.filter(event=>!embedded || ({INFO:0,WARN:1,ERROR:2})[eventLevel(event)]>=({INFO:0,WARN:1,ERROR:2})[ui.taskLogLevel]).map(event=>`<li data-log-level="${eventLevel(event)}">${embedded?`<b class="log-level">${eventLevel(event)}</b>`:''}${Number.isFinite(jobTime(event?.at))?`<time>${e(new Date(jobTime(event.at)).toLocaleTimeString('zh-CN',{hour12:false}))}</time>`:''}<span>${e(eventMessage(event))}</span></li>`).join('')}</ol></section>`:''}${files.length?`<div class="upload-list">${files.map(item=>`<div class="file-progress"><strong title="${e(item.path)}">${e(String(item.path || '未返回文件名').split(/[\\/]/).at(-1))}</strong>${badge(stateText(item.state),item.state==='done'?'good':item.state==='failed'?'bad':'muted')}<span>${bytes(item.size)}</span>${item.error?`<p class="upload-error">${e(item.error)}</p>`:''}</div>`).join('')}</div>`:''}<details class="job-raw"><summary>原始详细日志与返回数据</summary>${json(job)}</details>`:empty('没有本次工作区任务','执行扫描、识别、上传或查询后可查看真实任务日志。'));
1324
- }
1325
- const configGroups = [['paths','路径','模板表、设计图片、上架表、任务、报告、视频与 SSH 私钥路径'],['model','模型','服务商、模型标识、协议、推理控制与访问密钥'],['seller','Ozon 店铺','本地凭据、文件导入与连接测试'],['server','服务器与容量','SSH、公共媒体地址、保留空间与容量告警'],['commerce','经营参数','全局与材质默认值、价格、税率、包装参数和库存'],['rules','生成规则','识别方式、并行数、图片质量与文案长度'],['mediaRules','媒体发布规则','图片发布方式、自动视频与封面规则'],['appearance','外观','银白浅色 / 炭黑暗色与动画开关']];
1326
- if(categoryModule)configGroups.splice(4,1,['categoryTemplates','类目模板','官方底板、材质参数、库存规则与仓库;仅用于后续新建']);
1327
- if(!cloudMode)configGroups.push(['updates','软件更新','启动时检测新版本,选择下载;工作区数据保留']);
1328
- function renderSettings() {
1329
- return heading('配置中心',cloudMode?'生成参数与自己的工作区路径可修改;共享店铺、服务器与容量由管理员维护。':'所有路径与连接参数均可修改;密钥使用私有文件或会话入口。') + `<div class="settings-list">${configGroups.filter(([key])=>isAdmin() || !['server','seller'].includes(key)).map(([key,title,detail]) => `<button class="settings-item" data-page="settings/${key}"><div><h2>${title}</h2><p>${e(cloudMode && key==='paths'?'模板、设计、输出和任务目录;服务器限制可用路径范围。':cloudMode && key==='seller'?'选择共享店铺;全部已配置店铺均可使用。':detail)}</p></div><span class="settings-arrow" aria-hidden="true">›</span></button>`).join('')}</div><div class="buttons-line">${download('config','导出无密钥配置')}<button data-page="configImport">导入配置</button>${cloudMode?'<button data-page="account">修改自己的密码</button>':''}</div>`;
1330
- }
1331
- function categoryState() {
1332
- if (!categoryModule) throw new Error('类目模板模块未加载,请刷新页面');
1333
- const scope=JSON.stringify([cloudMode?ui.session?.user?.username:'local',ui.workspace || 'local']);
1334
- categoryModule.setScope(ui.categoryTemplates,scope);
1335
- return ui.categoryTemplates;
1336
- }
1337
- async function loadCategoryTemplates() {
1338
- const state=categoryState(),scope=state.scope;
1339
- const result=await act('category_template.list');
1340
- if(state.scope!==scope)throw staleView();
1341
- state.templates=result.templates || [];state.baseWorkbooks=result.base_workbooks || [];
1342
- if(result.catalog)categoryModule.applyCatalog(state,result.catalog);
1343
- if(state.selectedId && !state.templates.some(item=>item.id===state.selectedId)){state.selectedId='';state.generation.preview=null;}
1344
- categoryModule.syncGenerationStyle(state);
1345
- }
1346
- function renderStockRules(config) {
1347
- return `<div class="form-field full"><label for="business-no-stock-patterns">不补库存组合 · 每行一个</label><textarea id="business-no-stock-patterns" data-no-stock-patterns rows="5" placeholder="例如:IMD-IPHONE 11-J2" aria-describedby="business-no-stock-help">${e((config.no_stock_patterns || []).join('\n'))}</textarea><small id="business-no-stock-help">填写完整材质-型号-颜色组合,匹配所有图案。忽略大小写、空白和分隔符;JIETI 与 XINJIETI 独立,型号不扩展到 Pro / Plus。</small></div><p class="hint full">保存规则只影响后续生成的库存草稿与库存预览,不会立即修改平台库存;提交前仍需预览确认。</p>`;
1348
- }
1349
- function renderStockWarehouse(config) {
1350
- return `<div class="form-field"><label for="business-default-warehouse">当前店铺默认库存仓库</label><select id="business-default-warehouse"><option value="">${config.warehouses?.[ui.data.config.seller.client_id]?'沿用已保存仓库':'未设置'}</option>${(ui.warehouseClient===ui.data.config.seller.client_id?ui.warehouses:[]).map(row=>`<option value="${row.warehouse_id}" ${row.warehouse_id===config.warehouses?.[ui.data.config.seller.client_id]?'selected':''}>${e(row.name)} · ${row.warehouse_id}</option>`).join('')}</select><small>默认仓库按店铺分别保存;用于后续生成的库存草稿。</small></div><div class="form-field"><span>仓库信息</span>${button('business-warehouses','读取当前店铺仓库','','')}</div>`;
1351
- }
1352
- function renderCategoryConfig() {
1353
- const config=categoryState().compatibilityDraft || ui.data.config.commerce || {};
1354
- return back('settings','配置中心')+heading('类目模板','读取官方 Ozon Excel 底板,按商品子类目保存参数,可按材质拆分;颜色随图案填写。',`<button data-page="categoryCreate">用模板新建</button>`)+categoryModule.renderEditor(categoryState(),false)+`<section class="panel stock-rules-panel"><h2>不补库存规则</h2>${renderStockRules(config)}<div class="form-grid">${renderStockWarehouse(config)}</div><div class="sub-actions"><span id="stock-rules-status" role="status"></span>${button('config-save','保存库存规则与仓库','','primary')}</div></section>`;
1355
- }
1356
- function renderCategoryCreate() {
1357
- return back('generation','生成表格')+heading('用类目模板新建','适用于非手机壳及其他类目;原手机壳批量生成保留。',`<button data-page="settings/categoryTemplates">管理类目模板</button>`)+categoryModule.renderNew(categoryState(),false);
1358
- }
1359
- function updateCategoryStatus() {
1360
- if(!categoryModule || !['settings/categoryTemplates','categoryCreate'].includes(ui.page))return;
1361
- const state=categoryState(),status=$('#category-dirty');
1362
- if(status)status.textContent=categoryModule.dirty(state)?'有未保存修改':'草稿未修改';
1363
- const stockStatus=$('#stock-rules-status');
1364
- if(stockStatus)stockStatus.textContent=compatibilityDirty(state)?'库存规则有未保存修改,保存后才用于队列预设':`已保存 ${(ui.data.config.commerce.no_stock_patterns || []).length} 条不补库存规则`;
1365
- const write=$('[data-action="category-write"]'),preview=$('[data-action="category-preview"]');
1366
- setActionDisabled(write,!state.generation.preview);
1367
- setActionDisabled(preview,!state.selectedId);
1368
- if(ui.page==='categoryCreate' && !state.generation.preview){const output=$('#category-preview-output'),status=$('#category-preview-status');if(output)output.hidden=true;if(status)status.textContent='请选择模板并预览;草稿或模板变化后需重新预览再写表。';}
1369
- }
1370
- async function saveCategoryTemplate(mode='save') {
1371
- const current=pageLease();
1372
- const state=categoryState();categoryModule.collectEditor(state,document);
1373
- const saved=await categoryModule.persistDraft(state,async draft=>{
1374
- const request=categoryModule.saveRequest(state,draft,mode);
1375
- return (await act(request.action,request.payload)).template;
1376
- },mode==='rename');
1377
- if(!saved)throw staleView();
1378
- toast(categoryModule.dirty(state)?'已保存发送时的模板;后续修改仍需保存':'模板已保存到当前工作区');
1379
- if(current())render();
1380
- }
1381
- async function leaveCategoryDraft() {
1382
- if(!categoryModule || !ui.categoryTemplates)return true;
1383
- const state=categoryState();if(ui.page==='settings/categoryTemplates'){categoryModule.collectEditor(state,document);collectCompatibilityDraft();}
1384
- if(!categoryModule.dirty(state) && !compatibilityDirty(state))return true;
1385
- const current=scopeLease(),choice=await confirmAction('参数草稿尚未保存','<p>类目模板或库存规则有未保存修改。保存后离开,队列库存预设会按已保存规则重新计算;也可放弃本次修改。</p>','保存并离开',true);
1386
- if(!current())return false;
1387
- if(choice===true){
1388
- if(categoryModule.dirty(state))await saveCategoryTemplate();
1389
- if(!current())return false;
1390
- if(compatibilityDirty(state))await saveConfig(state);
1391
- return current() && !categoryModule.dirty(state) && !compatibilityDirty(state);
1392
- }
1393
- if(choice==='discard'){
1394
- const compatibility=JSON.stringify(state.compatibilityDraft);
1395
- if(categoryModule.dirty(state) && !await categoryModule.selectSaved(state,state.editingId,id=>act('category_template.inspect',{id}),true))return false;
1396
- if(!current())return false;
1397
- if(JSON.stringify(state.compatibilityDraft)!==compatibility){if(ui.page==='settings/categoryTemplates')render();return false;}
1398
- state.compatibilityDraft=null;return true;
1399
- }
1400
- return false;
1401
- }
1402
- async function selectCategoryTemplate(id) {
1403
- const current=pageLease();
1404
- const state=categoryState();if(id===state.editingId)return;
1405
- if(!await leaveCategoryDraft())return;
1406
- if(!await categoryModule.selectSaved(state,id,async id=>act('category_template.inspect',{id}),true))throw staleView();
1407
- if(current())render();
1408
- }
1409
- async function inspectCategoryBase() {
1410
- const state=categoryState();categoryModule.collectEditor(state,document);
1411
- const source_path=state.draft.source_path,current=pageLease();
1412
- if(!source_path && !state.editingId)throw new Error('请选择官方 Excel 底板');
1413
- if(state.schema && categoryModule.dirty(state) && !await confirmAction('重新读取底板?','<p>当前属性与经营参数草稿将替换为底板值;模板名称保留。</p>','读取并替换'))return;
1414
- if(!current())return;
1415
- const revision=state.revision,draft=JSON.stringify(state.draft);
1416
- const schema=await act('category_template.inspect',source_path?{source_path}:{id:state.editingId});
1417
- if(!current() || state.revision!==revision || JSON.stringify(state.draft)!==draft)throw staleView();
1418
- categoryModule.applyInspection(state,schema,!source_path);render();
1419
- }
1420
- async function handleCategoryAction(action,target) {
1421
- const state=categoryState();
1422
- collectCompatibilityDraft();
1423
- if(action==='category-refresh'){for(const [key,value] of readCache)if(value.action==='category_template.list')readCache.delete(key);await inView(()=>loadCategoryTemplates());render();}
1424
- else if(action==='category-catalog-refresh'){
1425
- categoryModule.collectEditor(state,document);
1426
- const scope=state.scope,result=await inView(()=>act('category_template.catalog',{refresh:true}));
1427
- if(state.scope!==scope)throw staleView();
1428
- categoryModule.applyCatalog(state,result);
1429
- for(const [key,value] of readCache)if(value.action==='category_template.list')readCache.delete(key);
1430
- toast(`官方目录已读取 ${result.total} 个商品类型并保存在本机`);render();
1431
- }
1432
- else if(['category-catalog-search','category-catalog-prev','category-catalog-next','category-catalog-select'].includes(action)){
1433
- categoryModule.collectEditor(state,document);
1434
- if(action==='category-catalog-prev')state.catalogPageNumber--;
1435
- if(action==='category-catalog-next')state.catalogPageNumber++;
1436
- if(action==='category-catalog-select'){
1437
- const mismatch=categoryModule.selectCatalog(state,$('#category-catalog-choice').value);
1438
- if(mismatch)toast(mismatch);
1439
- }
1440
- render();
1441
- }
1442
- else if(action==='category-pick'){categoryModule.collectEditor(state,document);const paths=await inView(()=>pick('template'));if(paths[0]){state.draft.source_path=paths[0];render();}}
1443
- else if(action==='category-inspect')await inspectCategoryBase();
1444
- else if(['category-style-add','category-style-rename','category-style-remove','category-copy-phone'].includes(action)){
1445
- categoryModule.collectEditor(state,document);
1446
- if(action==='category-style-add')categoryModule.addStyle(state,$('#category-style-new')?.value);
1447
- else if(action==='category-style-rename')categoryModule.renameStyle(state,state.editorStyle,$('#category-style-name')?.value);
1448
- else if(action==='category-style-remove')categoryModule.removeStyle(state,state.editorStyle);
1449
- else categoryModule.importPhoneDefaults(state,ui.data.config.commerce);
1450
- render();
1451
- }
1452
- else if(['category-save','category-save-as','category-rename'].includes(action))await saveCategoryTemplate({'category-save':'save','category-save-as':'save_as','category-rename':'rename'}[action]);
1453
- else if(action==='category-group-add' || action==='category-group-remove'){
1454
- categoryModule.collectEditor(state,document);
1455
- if(action==='category-group-add')state.draft.complex_attributes.push({parent_id:Number(target.dataset.parent),attributes:{}});
1456
- else state.draft.complex_attributes.splice(Number(target.dataset.index),1);
1457
- state.revision++;render();
1458
- } else if(action==='category-preview'){
1459
- categoryModule.collectNew(state,document);
1460
- const payload=categoryModule.previewRequest(state,state.generation),scope=state.scope;
1461
- const result=await viewAction('category_template.preview',payload);
1462
- if(state.scope!==scope || JSON.stringify(payload)!==JSON.stringify(categoryModule.previewRequest(state,state.generation)))throw staleView();
1463
- state.generation.preview=result;render();
1464
- } else if(action==='category-write'){
1465
- categoryModule.collectNew(state,document);const preview=state.generation.preview,current=pageLease();
1466
- if(!preview)throw new Error('请先重新预览本次新建商品');
1467
- if(preview.target_exists && !await confirmAction('覆盖已有表格?',`<p>${e(preview.target_path)}</p><p>只覆盖此目标文件;其他队列商品保留。</p>`,'确认覆盖'))return;
1468
- if(!current() || state.generation.preview!==preview)return;
1469
- const result=await act('category_template.write',{id:preview.id,allow_overwrite:Boolean(preview.target_exists)});
1470
- if(result.ok===false)throw new Error((result.items || []).filter(item=>!item.ok).map(item=>item.error || '写表失败').join(';') || '写表失败,草稿保留');
1471
- if(state.generation.preview===preview)state.generation.preview=null;
1472
- toast(itemSummary(result,'写表'));if(current())render();
1473
- }
1474
- }
1475
- function configField(section,key,label,type='text',hint='',choices=[],picker='') {
1476
- if(!isAdmin() && (['server','storage','seller'].includes(section) || section==='workspace' && key==='identity_file'))return '';
1477
- if(cloudMode && section==='workspace' && key==='identity_file')picker='';
1478
- const value = ui.data.config[section]?.[key] ?? (section==='listing' && key==='image_host'?'server':undefined), id = `cfg-${section}-${key}`;
1479
- const input = type === 'select' ? `<select id="${id}" data-config="${section}.${key}">${choices.map(([option,text]) => `<option value="${e(option)}" ${String(value) === String(option) ? 'selected' : ''}>${e(text)}</option>`).join('')}</select>` : type === 'checkbox' ? `<input id="${id}" type="checkbox" data-config="${section}.${key}" ${value ? 'checked' : ''}>` : `<input id="${id}" data-config="${section}.${key}" type="${type}" value="${e(value ?? '')}" ${type === 'number' ? 'step="any"' : ''}>`;
1480
- return `<div class="form-field"><label for="${id}">${label}</label>${picker ? `<div class="field-row">${input}${button('config-pick','选择',`data-target="${id}" data-kind="${picker}"`)}</div>` : input}${hint ? `<small>${e(hint)}</small>` : ''}</div>`;
1481
- }
1482
- function secretField(kind,label,flag) {return `<div class="form-field"><label for="${kind}-key">${label}</label><input type="password" id="${kind}-key" autocomplete="new-password" placeholder="${flag ? '会话已设置;留空保留,清除请用下方按钮' : '输入后仅保留本次会话'}"><small>不会写入配置文件或导出文件。</small>${button('clear-credential','清除此会话密钥',`data-kind="${kind}"`,'quiet',!flag)}</div>`;}
1483
- function renderUpdates() {
1484
- const value=ui.updateStatus || {},enabled=ui.data.update_enabled;
1485
- return back('settings','配置中心')+heading('软件更新','程序更新保留本地图片、配置和任务记录。')+`<section class="panel"><p>当前版本:${e(ui.data.version)}</p>${value.pending_version?`<p role="status">${e(value.pending_version)} 已下载,下次启动生效。当前任务可继续运行。</p>`:value.available?`<p role="status">发现新版本 ${e(value.latest_version)}</p>`:'<p>启动时自动检查,也可手动检查新版本。</p>'}${value.release_notes?`<pre class="log">${e(value.release_notes)}</pre>`:''}${ui.updateError?`<p role="status">${e(ui.updateError)}</p>`:''}<div class="buttons-line">${button('updates-check','检查新版本','', '',!enabled)}${value.available&&!value.pending_version?button('updates-download','下载更新,下次启动使用','','primary'):''}</div>${enabled?'':'<p>当前为源码运行,请使用本地发行包进行更新。</p>'}</section>`;
1486
- }
1487
- async function checkUpdates(quiet=false) {
1488
- const current=scopeLease();
1489
- try{const value=await act('updates.check');if(!current())return;ui.updateStatus=value;ui.updateError='';if(value.available&&!value.pending_version)toast(`发现新版本 ${value.latest_version},可在软件更新中选择下载`);}
1490
- catch(error){if(!current())return;ui.updateError='检查更新失败,请检查网络后重试';if(!quiet)throw error;}
1491
- finally{if(current()){if(ui.page==='settings/updates')render();else renderChrome();}}
1492
- }
1493
- function renderConfig(category, embedded = false) {
1494
- if(category==='updates'&&!cloudMode)return renderUpdates();
1495
- if(category==='categoryTemplates' || category==='commerce' && categoryModule && !embedded)return renderCategoryConfig();
1496
- const f = configField; let fields='', extra='';
1497
- if(!isAdmin() && ['server','seller'].includes(category))return back('settings','配置中心')+empty('共享店铺与服务器配置由管理员维护','普通人员可在商品队列选择店铺,在媒体库上传与预览。');
1498
- if (category === 'paths') fields = [['generator','template_dir','模板表目录'],['generator','scan_dir','设计图片目录'],['generator','output_dir','上架表目录'],['workspace','task_dir','任务与媒体清单目录'],['workspace','report_dir','查询报告目录'],['workspace','video_dir','视频与封面目录']].map(([section,key,label]) => f(section,key,label,'text','',[],'directory')).join('') + f('workspace','identity_file','SSH 私钥文件路径','text','仅保存路径,不读取或导出私钥内容。',[],'file');
1499
- else if (category === 'model') {
1500
- fields = f('generator','provider_name','服务商名称')+f('generator','api_base_url','模型接口地址')+f('generator','model_id','模型标识','text','可手动填写,或从真实模型列表选择。')+f('generator','openai_api_type','接口协议','select','',[['chat_completions','Chat Completions'],['responses','Responses']])+secretField('model','模型访问密钥',ui.data.credentials.model_key_set)+f('generator','reasoning_control','推理控制方式','select','Effort 与 Budget 只启用所选方式。',[['effort','Reasoning Effort'],['budget','Thinking Budget']])+f('generator','reasoning_effort','Reasoning Effort','select','',[['none','None'],['minimal','Minimal'],['low','Low'],['medium','Medium'],['high','High']])+f('generator','thinking_budget','Thinking Budget','number')+f('generator','temperature','Temperature','number')+f('generator','max_tokens','Max Tokens','number')+f('generator','response_format','Response Format','select','',[['json_object','JSON Object'],['text','Text']])+f('generator','request_timeout','请求超时(秒)','number');
1501
- extra = `<p class="hint">先保存当前参数,再读取模型或测试连接。</p><div class="buttons-line">${button('model-catalog','读取模型列表','','')}${button('model-test','测试模型连接','','')}</div>${ui.models.length ? `<div class="form-field"><label for="model-choice">服务器返回的模型</label><select id="model-choice"><option value="">请选择模型</option>${ui.models.map(item => `<option value="${e(item.id || item.model_id)}">${e(item.name || item.id || item.model_id)}</option>`).join('')}</select></div>` : ''}`;
1502
- } else if (category === 'seller') {
1503
- const keyField=ui.data.config.seller.accounts?.length ? `<div class="form-field"><label for="seller-key">所选店铺访问密钥</label><input type="password" id="seller-key" autocomplete="new-password" placeholder="${ui.data.credentials.seller_key_set ? '本地已保存;留空保留,输入新值替换' : '输入并保存到本地'}"><small>独立存入本地凭据文件;普通配置导出不包含密钥。</small></div>` : secretField('seller','所选店铺访问密钥',ui.data.credentials.seller_key_set);
1504
- fields = storeChoice()+f('seller','client_id','所选店铺客户端标识')+(isAdmin()?keyField:'<p class="hint full">共享店铺与密钥由管理员维护;所有已配置店铺均可用于上架。</p>')+f('listing','omit_placeholder_barcode','省略模板占位条码','checkbox','保留真实条码;此设置影响生成的上架计划。');
1505
- extra = `<div class="buttons-line">${button('seller-test','测试所选店铺连接','','')}</div><details><summary>从文件导入店铺凭据</summary><div class="form-field"><label for="store-key-sheet">凭据文件绝对路径</label><div class="field-row"><input id="store-key-sheet" placeholder="选择 Excel、TXT 或文本文档">${button('store-keys-pick','选择文件','','')}</div></div><div class="buttons-line">${button('store-keys-import','导入店铺凭据','','')}</div><p class="hint">支持 XLSX/XLSM、TXT、CSV/TSV、Markdown、JSON、INI/ENV、CFG/CONF/LOG 和 Word DOCX。读取店铺、客户端标识和访问密钥;已有店铺更新,新店铺加入,全部有效后保存。</p><details><summary>查看文件格式示例</summary><p class="hint">表格使用「店铺」「Seller Client ID」「Seller API Key」三列,也接受 Client ID / API Key。TXT 或 Word 可按以下格式逐店填写:</p><pre>店铺:OZON1
1506
- Client ID:填写对应 Client ID
1507
- API Key:填写对应 API Key</pre><p class="hint">JSON 可使用 stores 数组;INI 可用 [店铺名称] 分节,填写 client_id 和 api_key。旧 XLS/DOC 请另存为 XLSX/DOCX;导入失败保留现有凭据。</p></details></details><details><summary>全部店铺配置状态</summary><div class="table-wrap"><table><thead><tr><th>店铺</th><th>客户端标识</th><th>凭据状态</th></tr></thead><tbody>${(ui.data.stores || []).map(a=>`<tr><td>${e(a.name)}</td><td>${e(a.client_id || '未配置')}</td><td>${badge(a.ready?'已配置':'缺少凭据',a.ready?'good':'')}</td></tr>`).join('')}</tbody></table></div></details>`;
1508
- if(!isAdmin())extra=`<div class="buttons-line">${button('seller-test','测试所选店铺连接','','')}</div>`;
1509
- } else if (category === 'commerce') {
1510
- const config=(categoryModule?categoryState().compatibilityDraft:null) || ui.data.config.commerce || {defaults:{},materials:{}};
1511
- fields=`<section class="full"><h2>全局默认值</h2><p class="hint">留空沿用样表参考值;只影响后续生成。税率填写百分数,包装填写整数。</p>${businessForm('default',config.defaults,{config:'defaults'})}</section>`+['IMD','JIETI','XINJIETI'].map(material=>`<details class="full"><summary>${material} 材质默认值</summary><p class="hint">留空继承全局值;图案设置可覆盖这些值。</p>${businessForm(material,config.materials?.[material],{config:material})}</details>`).join('');
1512
- if(!embedded)fields+=renderStockRules(config);
1513
- fields+=renderStockWarehouse(config);
1514
- extra='<p class="hint">修改已有商品:进入商品队列 → 查看商品 → 经营参数。仓库在所选店铺下读取和选择。</p>';
1515
- } else if (category === 'mediaRules') {
1516
- fields = f('listing','image_host','图片发布方式','select','',[['server','服务器'],['imagekit','ImageKit 图床']])+`<div class="form-field"><label>ImageKit 凭据</label><span>${ui.data.credentials.imagekit_key_set?'已配置':'未配置'}</span></div>`+f('workspace','video_dir','视频与封面目录','text','所选视频也用作视频封面。',[],'directory')+f('listing','video_imd_neutral','IMD · J1 / J2 视频文件名')+f('listing','video_imd_purple','IMD · J8 视频文件名')+f('listing','video_step','JIETI / XINJIETI · 阶梯视频文件名');
1517
- } else if (category === 'server') {
1518
- fields = f('server','host','SSH 主机')+f('server','user','SSH 用户名')+f('server','port','SSH 端口','number')+f('server','remote_dir','独立公共媒体目录','text','必须是独立 public 目录,文件管理仅处理内容指纹媒体。')+f('server','base_url','公开 HTTPS 媒体地址')+f('workspace','identity_file','SSH 私钥文件路径','text','使用已信任的 known_hosts,私钥内容不入配置。',[],'file')+f('storage','reserve_mb','上传后至少保留空间(MB)','number')+f('storage','warning_percent','已用容量告警比例(%)','number');
1519
- extra = `<div class="buttons-line">${button('server-test','测试 SSH 与公开地址','','')}${button('library-refresh','查询容量与媒体','','')}</div>`;
1520
- if(cloudMode){const limits=ui.serverSettings || ui.data.cloud?.limits || {};extra+=`<h2>小组运行限制</h2><form id="server-settings-form" class="form-grid">${[['max_active_jobs','同时运行任务数',3],['upload_max_mb','单次上传上限(MB)',256],['max_upload_files','单次上传文件数',500],['user_workspace_limit','每人工作区上限',8]].map(([key,label,fallback])=>`<div class="form-field"><label for="limit-${key}">${label}</label><input id="limit-${key}" data-server-limit="${key}" type="number" min="1" step="1" required value="${e(limits[key] ?? fallback)}"></div>`).join('')}<div class="full"><button type="submit" class="primary">保存小组运行限制</button></div></form>`;}
1521
- } else if (category === 'rules') fields = f('generator','recognition_mode','识别方式','select','',[['api_fallback','模型识别 / 清单回退'],['manifest_only','仅本地清单']])+f('generator','workers','并行识别数量','number')+f('generator','image_max_side','模型图片最大边长','number')+f('generator','jpeg_quality','模型图片 JPEG 质量','number')+f('generator','title_min','标题最少字符','number')+f('generator','title_max','标题最多字符','number')+f('generator','description_min','描述最少字符','number')+f('generator','description_max','描述最多字符','number')+f('generator','protect_existing','保护已有输出表格','checkbox','普通批量写表跳过已有输出;“重新生成选中表格”会单独确认并备份后覆盖。');
1522
- else if (category === 'appearance') fields = f('ui','theme','界面外观','select','',[['light','银白浅色'],['dark','炭黑暗色']])+f('ui','motion','启用界面动画','checkbox','同时尊重系统的减少动态效果设置。');
1523
- if(embedded)return `<div class="form-grid">${fields}</div><div class="sub-actions">${button('config-save','保存手机壳兼容参数','','primary')}</div>${extra}`;
1524
- return back('settings','配置中心') + heading(configGroups.find(([key]) => key === category)?.[1] || '配置',category==='mediaRules'?'':'保存后由实际生成、上传与查询功能使用。') + `<div class="panel"><div class="form-grid">${fields}</div><div class="sub-actions">${button('config-save','保存配置','','primary')}</div>${extra}${ui.lastResult && ['server.test','seller.test','model.test','model.catalog'].includes(ui.lastResult.action) ? `<details open><summary>最近连接检查结果</summary>${json(ui.lastResult.result)}</details>` : ''}</div>`;
1525
- }
1526
- function renderImport(kind) {
1527
- const config = kind === 'config';
1528
- return back(config ? 'settings' : 'queue',config ? '配置中心' : '商品队列') + heading(config ? '导入配置' : '导入媒体清单',config ? '粘贴或选择此前导出的 JSON;密钥字段和未知配置字段会被拒绝。' : '清单货号必须与当前队列完整相等,可拆分为逐货号媒体组。') + `<div class="panel"><div class="form-field"><label for="import-text">${config ? '配置 JSON' : '媒体清单 JSON'}</label><textarea id="import-text" rows="18">${e(ui.importText)}</textarea></div><label class="button-link" for="import-json">选择 JSON 文件</label><input id="import-json" type="file" accept=".json,application/json" class="visually-hidden"><div class="sub-actions">${button(config ? 'import-config' : 'import-media','检查并导入','','primary')}</div></div>`;
1529
- }
1530
- function render() {
1531
- clearCardDrag();
1532
- if (!ui.data || ui.logoutPending) return;
1533
- const teamDetails=new Map(ui.page==='team'?[...document.querySelectorAll('[data-team-detail]')].map(item=>[item.dataset.teamDetail,item.open]):[]);
1534
- const teamScroll=new Map(ui.page==='team'?[...document.querySelectorAll('[data-team-scroll]')].map(item=>[item.dataset.teamScroll,[item.scrollLeft,item.scrollTop]]):[]);
1535
- const teamWindow=ui.page==='team' && renderedPage==='team' ? {left:window.scrollX || 0,top:window.scrollY || 0,behavior:'instant'}:null;
1536
- const filtersOpen=document.querySelector('.filter-disclosure')?.open;
1537
- const queueDetails=ui.page==='queue' && renderedPage==='queue'?['.queue-columns','.queue-tools'].map(selector=>[selector,document.querySelector(selector)?.open]):[];
1538
- const workspaceDetails=ui.page==='workspaces' && renderedPage==='workspaces' ? ['[data-workspace-deleted]','.workspace-create'].map(selector=>[selector,document.querySelector(selector)?.open]):[];
1539
- const mediaOffsets=new Map([...document.querySelectorAll('.media-rail')].filter(rail=>ui.page==='media' && rail.dataset.mediaGroup===String(ui.group)).map(rail=>[rail.dataset.mediaRail,rail.querySelector('.media-thumbs').scrollLeft]));
1540
- if(ui.page==='queue' && renderedPage==='queue'){const table=$('.queue-table');if(table)ui.queueScroll=[table.scrollLeft || 0,table.scrollTop || 0];}
1541
- stopMediaAutoScroll();
1542
- const changed = renderedPage !== ui.page;
1543
- const previous = new Map(changed && motionAllowed() ? [...document.querySelectorAll('[data-motion-image]')].map(img=>[img.dataset.motionImage,img.getBoundingClientRect()]) : []);
1544
- renderChrome();
1545
- const renderers = {categoryCreate:renderCategoryCreate,phoneModels:renderPhoneModels,queue:renderQueue,generation:renderGeneration,copy:renderCopy,manual:renderManual,media:renderMedia,autoMedia:renderAutoMedia,book:renderBook,product:()=>renderBook(true),commerce:renderCommerce,commercePlan:renderCommercePlan,library:renderLibrary,file:renderFile,upload:renderUpload,queueAdd:renderQueueAdd,history:renderHistory,record:()=>renderHistory(true),preflight:renderPreflight,task:renderJob,settings:renderSettings,manifest:()=>renderImport('media'),configImport:()=>renderImport('config'),...(cloudMode?{workspaces:renderWorkspaces,account:renderAccount,team:isSuperAdmin()?renderTeam:()=>empty('仅限超级管理员','其他账户的数据仅超级管理员可查看。'),users:isSuperAdmin()?renderUsers:()=>empty('仅限超级管理员','人员由超级管理员管理。')}:{})};
1546
- const content = ui.page.startsWith('settings/') ? renderConfig(ui.page.split('/')[1]) : (renderers[ui.page] || renderQueue)();
1547
- $('#page').innerHTML = `<div class="page-content ${changed && motionAllowed() ? 'enter' : ''}">${content}</div>`;
1548
- if(ui.page==='queue'){const table=$('.queue-table');if(table)[table.scrollLeft,table.scrollTop]=ui.queueScroll;}
1549
- queueDetails.forEach(([selector,open])=>{if(open)document.querySelector(selector).open=true;});
1550
- workspaceDetails.forEach(([selector,open])=>{if(open)document.querySelector(selector).open=true;});
1551
- if(ui.page==='team'){
1552
- document.querySelectorAll('[data-team-detail]').forEach(item=>{if(teamDetails.has(item.dataset.teamDetail))item.open=teamDetails.get(item.dataset.teamDetail);});
1553
- document.querySelectorAll('[data-team-scroll]').forEach(item=>{const offset=teamScroll.get(item.dataset.teamScroll);if(offset){item.scrollLeft=offset[0];item.scrollTop=offset[1];}});
1554
- if(teamWindow)window.scrollTo(teamWindow);
1555
- }
1556
- if(ui.page==='library' && filtersOpen)document.querySelector('.filter-disclosure').open=true;
1557
- bindPreviews();
1558
- document.querySelectorAll('.media-rail').forEach(rail=>{
1559
- const strip=rail.querySelector('.media-thumbs');strip.scrollLeft=mediaOffsets.get(rail.dataset.mediaRail) || 0;
1560
- strip.addEventListener('scroll',()=>updateMediaRail(rail),{passive:true});
1561
- rail.addEventListener('dragleave',event=>{if(!rail.contains(event.relatedTarget))stopMediaAutoScroll();});
1562
- updateMediaRail(rail);
1563
- });
1564
- $('#page').classList.toggle('narrow',ui.page.startsWith('settings/') || ['settings','manual','upload','queueAdd','manifest','configImport','preflight'].includes(ui.page));
1565
- updateThinkingFields();updateCategoryStatus();syncActionControls();
1566
- if(previous.size)document.querySelectorAll('[data-motion-image]').forEach(img=>{
1567
- const from=previous.get(img.dataset.motionImage), frames=from && flipFrames(from,img.getBoundingClientRect());
1568
- if(!frames?.length || !img.animate)return;
1569
- const frame=img.parentElement;frame.classList.add('image-transition');
1570
- img.animate(frames,{duration:340,easing:'cubic-bezier(.16,1,.3,1)'}).finished.then(()=>frame.classList.remove('image-transition'),()=>frame.classList.remove('image-transition'));
1571
- });
1572
- renderedPage = ui.page;
1573
- if(cloudMode && !ui.ready){ui.ready=true;ui.authLoading=false;document.body.classList.remove('cloud-login','login-enter');}
1574
- syncTeamPolling();
1575
- }
1576
- function updateThinkingFields() {
1577
- const mode = $('#cfg-generator-reasoning_control')?.value;
1578
- if (mode) {$('#cfg-generator-reasoning_effort').disabled = mode !== 'effort'; $('#cfg-generator-thinking_budget').disabled = mode !== 'budget';}
1579
- }
1580
- function readConfigForm() {
1581
- const config = structuredClone(ui.data.config);
1582
- if(['settings/commerce','settings/categoryTemplates'].includes(ui.page)){
1583
- config.commerce=structuredClone((categoryModule?categoryState().compatibilityDraft:null) || config.commerce || {});
1584
- config.commerce.materials ||= {};config.commerce.patterns ||= {};config.commerce.warehouses ||= {};
1585
- const warehouse=$('#business-default-warehouse')?.value;if(warehouse)config.commerce.warehouses[config.seller.client_id]=Number(warehouse);
1586
- if(document.querySelectorAll('[data-business="default"]').length)config.commerce.defaults=collectBusiness('default');
1587
- for(const material of ['IMD','JIETI','XINJIETI'])if(document.querySelectorAll(`[data-business="${material}"]`).length)config.commerce.materials[material]=collectBusiness(material);
1588
- const rules=document.querySelectorAll('[data-no-stock-patterns]')[0];
1589
- if(rules)config.commerce.no_stock_patterns=rules.value.split(/\r?\n/).map(value=>value.trim()).filter(Boolean);
1590
- }
1591
- document.querySelectorAll('[data-config]').forEach(input => {
1592
- const [section,key] = input.dataset.config.split('.');
1593
- config[section][key] = input.type === 'checkbox' ? input.checked : input.type === 'number' ? Number(input.value) : input.value.trim();
1594
- });
1595
- const account=config.seller.accounts?.find(a=>a.id===config.seller.selected_id);
1596
- if(account && $('#cfg-seller-client_id'))account.client_id=config.seller.client_id;
1597
- return config;
1598
- }
1599
- function collectCompatibilityDraft() {
1600
- if(categoryModule && ui.page==='settings/categoryTemplates')categoryState().compatibilityDraft=readConfigForm().commerce;
1601
- updateCategoryStatus();
1602
- }
1603
- function compatibilityDirty(state) {
1604
- if(!state?.compatibilityDraft)return false;
1605
- // Form controls stringify numeric values and materialize empty inherited overrides.
1606
- const entries=value=>Object.entries(value || {}).sort(([a],[b])=>a.localeCompare(b));
1607
- const values=value=>Object.fromEntries(entries(value).map(([key,item])=>[key,typeof item==='string' && !['brand','country','package'].includes(key) && item.trim() && Number.isFinite(Number(item))?Number(item):item]));
1608
- const layers=value=>Object.fromEntries(entries(value).map(([key,item])=>[key,values(item)]).filter(([,item])=>Object.keys(item).length));
1609
- const signature=value=>JSON.stringify({defaults:values(value?.defaults),materials:layers(value?.materials),patterns:layers(value?.patterns),warehouses:values(value?.warehouses),no_stock_patterns:value?.no_stock_patterns || []});
1610
- return signature(state.compatibilityDraft)!==signature(ui.data?.config?.commerce);
1611
- }
1612
- function collectCopy() {
1613
- if (!ui.copy) ui.copy = {title:'',description:'',attrs:{}};
1614
- ui.copy.title = $('#copy-title')?.value ?? ui.copy.title; ui.copy.description = $('#copy-description')?.value ?? ui.copy.description;
1615
- document.querySelectorAll('[data-attr]').forEach(input => {ui.copy.attrs[input.dataset.attr] = input.value;});
1616
- if ($('#case-design')) ui.copy.attrs.has_case_design = $('#case-design').checked;
1617
- return ui.copy;
1618
- }
1619
- function collectManual() {
1620
- for (const [key,id] of [['file_type','manual-type'],['source_path','manual-source'],['new_number','manual-number'],['title','manual-title'],['description','manual-description']]) if ($(`#${id}`)) ui.manual[key] = $(`#${id}`).value;
1621
- return ui.manual;
1622
- }
1623
- function collectMedia() {
1624
- ui.media.primary_image = $('#primary-path')?.value.trim() ?? ui.media.primary_image;
1625
- ui.media.video_cover = $('#cover-path')?.value.trim() ?? ui.media.video_cover;
1626
- if ($('#image-paths')) ui.media.images = pathLines($('#image-paths').value);
1627
- document.querySelectorAll('[data-video-name]').forEach(input => {ui.media.videos[Number(input.dataset.videoName)].name = input.value;});
1628
- return ui.media;
1629
- }
1630
- const mediaSlot = card => ({kind:card.dataset.mediaKind,index:Number(card.dataset.mediaIndex)});
1631
- function updateMediaRail(rail) {
1632
- const strip=rail.querySelector('.media-thumbs');
1633
- rail.querySelector('[data-offset="-1"]').hidden=strip.scrollLeft<=1;
1634
- rail.querySelector('[data-offset="1"]').hidden=strip.scrollWidth-strip.clientWidth-strip.scrollLeft<=1;
1635
- }
1636
- function stopMediaAutoScroll() {
1637
- if(mediaAutoScroll)cancelAnimationFrame(mediaAutoScroll.frame);
1638
- mediaAutoScroll=null;
1639
- }
1640
- function scrollMediaAtEdge(event) {
1641
- const rail=event.target.closest('.media-rail');
1642
- if(!rail || !mediaDrag || mediaDrag.group!==String(ui.group) || (['primary','images'].includes(mediaDrag.kind)?'images':'videos')!==rail.dataset.mediaRail){stopMediaAutoScroll();return;}
1643
- event.preventDefault();
1644
- const strip=rail.querySelector('.media-thumbs'),rect=strip.getBoundingClientRect();
1645
- const direction=event.clientX<rect.left+40?-1:event.clientX>rect.right-40?1:0;
1646
- if(!direction){stopMediaAutoScroll();return;}
1647
- if(mediaAutoScroll?.strip===strip && mediaAutoScroll.direction===direction)return;
1648
- stopMediaAutoScroll();
1649
- const step=()=>{
1650
- if(!mediaDrag || !strip.isConnected || mediaDrag.group!==String(ui.group)){stopMediaAutoScroll();return;}
1651
- const before=strip.scrollLeft;strip.scrollLeft+=direction*8;updateMediaRail(rail);
1652
- if(strip.scrollLeft===before){stopMediaAutoScroll();return;}
1653
- mediaAutoScroll.frame=requestAnimationFrame(step);
1654
- };
1655
- mediaAutoScroll={strip,direction,frame:requestAnimationFrame(step)};
1656
- }
1657
- window.addEventListener('resize',()=>document.querySelectorAll('.media-rail').forEach(updateMediaRail));
1658
- function clearMediaDrag() {
1659
- stopMediaAutoScroll();
1660
- if(mediaDrag)imageClickBlockedUntil=Date.now()+250;
1661
- mediaDrag=null;
1662
- document.querySelectorAll('.media-tile.dragging,.media-tile.drop-target').forEach(card=>card.classList.remove('dragging','drop-target'));
1663
- }
1664
- function collectMediaForSort() {
1665
- const paths=()=>JSON.stringify([ui.media.primary_image ?? '',ui.media.video_cover ?? '',ui.media.images]);
1666
- const before=paths();collectMedia();
1667
- if(before===paths())return true;
1668
- clearMediaDrag();render();
1669
- $('#media-feedback').textContent='路径已更新预览,请重新拖动或调整顺序。';
1670
- return false;
1671
- }
1672
- function changeMediaPosition(from,to) {
1673
- if(ui.page!=='media')return;
1674
- if(!collectMediaForSort())return;
1675
- const draft=reorderMedia(ui.media,from,to);
1676
- if(draft===ui.media)return;
1677
- changeMedia(draft);render();
1678
- $('#media-feedback').textContent='媒体位置已调整,请保存媒体清单。';
1679
- const card=document.querySelector(`[data-media-kind="${to.kind}"][data-media-index="${to.index}"]`);
1680
- card?.focus?.({preventScroll:true});card?.scrollIntoView?.({block:'nearest',inline:'nearest'});
1681
- }
1682
- async function pick(kind) {
1683
- if(!cloudMode)return (await act('picker',{kind}))?.paths || [];
1684
- const current=scopeLease();
1685
- checkCloudPermission('picker',{kind});const conflict=conflictingJob('files.upload');if(cloudMode && conflict)throw new Error(conflictReason(conflict));
1686
- const input=$('#browser-picker');input.value='';input.multiple=kind!=='file';input.webkitdirectory=kind==='directory';
1687
- input.accept=({images:'.jpg,.jpeg,.png,.webp',videos:'.mp4,.mov',workbooks:'.xlsx,.xlsm',template:'.xlsx,.xlsm',credentials:'.xlsx,.xlsm,.txt,.csv,.tsv,.md,.json,.ini,.env,.cfg,.conf,.log,.docx'})[kind] || '';
1688
- const files=await new Promise(resolve=>{const finish=files=>{input.onchange=null;input.oncancel=null;resolve(files);};input.onchange=()=>finish(Array.from(input.files || []));input.oncancel=()=>finish([]);input.click();});
1689
- if(!current())throw staleView();
1690
- if(!files.length)return [];
1691
- const body=new FormData();body.append('kind',kind);for(const file of files)body.append('files',file,file.webkitRelativePath || file.name);
1692
- const uploadConflict=conflictingJob('files.upload');if(uploadConflict)throw new Error(conflictReason(uploadConflict));
1693
- const operation={action:'files.upload',resources:resourcesFor('files.upload')};operations.set(operation,operation);
1694
- ui.active=true;renderChrome();toast(`正在传送 ${files.length} 个文件到当前工作区`);
1695
- try{const result=await request('/api/upload',{method:'POST',body});if(!current())throw staleView();if(!Array.isArray(result.paths) || !result.paths.length)throw new Error('服务没有返回已上传文件的工作区路径');return result.paths;}
1696
- finally{const owned=operations.delete(operation);if(current() || owned && !ui.logoutPending){ui.active=operations.size>0;renderChrome();}}
1697
- }
1698
- async function saveConfig(categoryDraft=null) {
1699
- const current=pageLease(),scopeCurrent=scopeLease(),page=ui.page,navigation=navigationEpoch;
1700
- const category=categoryDraft || (categoryModule && ui.page==='settings/categoryTemplates'?categoryState():null),compatibilityScope=category?.scope;
1701
- if(category && !categoryDraft && ui.page==='settings/categoryTemplates')categoryModule.collectEditor(category,document);
1702
- const config = categoryDraft?{...structuredClone(ui.data.config),commerce:structuredClone(categoryDraft.compatibilityDraft)}:readConfigForm();
1703
- const model = categoryDraft?'':$('#model-key')?.value, seller = !categoryDraft && isAdmin()?$('#seller-key')?.value:'';
1704
- if(category)category.compatibilityDraft=structuredClone(config.commerce);
1705
- if(!categoryDraft && $('#seller-key') && config.seller.accounts?.length && (seller || config.seller.client_id !== ui.data.config.seller.client_id)) {
1706
- if(!config.seller.selected_id)throw new Error('请先选择要修改凭据的店铺');
1707
- const key=seller || '';
1708
- await act('stores.keys.save',{api_key:key,id:config.seller.selected_id,client_id:config.seller.client_id});
1709
- if(!current())return;
1710
- Object.assign(config.seller,{accounts:structuredClone(ui.data.config.seller.accounts),client_id:ui.data.config.seller.client_id,credential_file:ui.data.config.seller.credential_file});
1711
- }
1712
- if(!isAdmin()){delete config.server;delete config.storage;delete config.seller;delete config.workspace.identity_file;}
1713
- const saved=await act('config.save',{config});
1714
- if(category && category.scope===compatibilityScope && JSON.stringify(category.compatibilityDraft)===JSON.stringify(config.commerce))category.compatibilityDraft=null;
1715
- if(!current()){if(category && scopeCurrent() && page===ui.page && navigation===navigationEpoch)render();return;}
1716
- if (model || seller && !config.seller?.accounts?.length) await act('secrets.set',{...(model ? {model_key:model} : {}),...(seller && !config.seller?.accounts?.length ? {seller_key:seller} : {})});
1717
- if(!current())return;
1718
- if ($('#model-key')) $('#model-key').value = ''; if ($('#seller-key')) $('#seller-key').value = '';
1719
- toast(saved?.superseded?'本次保存已完成,配置随后有更新,请核对当前值':'配置已保存'); render();
1720
- }
1721
- async function overwriteAllowed(ids, manual=false) {
1722
- const values = manual ? [ui.manualPreview] : ids.map(id => ui.data.previews[id]);
1723
- if (values.some(value => !value)) throw new Error('所选设计尚无有效文案预览,请先生成或检查文案');
1724
- const existing = values.filter(value => value.target_exists);
1725
- return !existing.length || await confirmAction('确认覆盖已有表格',`<p>下列输出已存在。确认后覆盖输出表格,原模板保留。</p><ul>${existing.map(value => `<li class="path-display">${e(value.target_path)}</li>`).join('')}</ul>`,'确认覆盖');
1726
- }
1727
- async function writeDesigns(ids,regenerate=false) {
1728
- if (!ids.length) throw new Error('请选择需要写入的设计');
1729
- if (ids.some(id => !ui.data.previews[id])) throw new Error('所选设计尚无有效文案预览,请先生成或检查文案');
1730
- if(regenerate){
1731
- const popular=ui.data.config.listing.model_scope==='popular',catalog=ui.data.phone_models?.[popular?'popular':'models'];
1732
- const range=`${popular?'热门型号':'全部型号'} · IMD ${catalog?.IMD?.length ?? '尚未导入'} 个 · JT ${catalog?.JT?.length ?? '尚未导入'} 个`;
1733
- const body=`<p>本次整份重新生成 ${ids.length} 款,覆盖已有 XLSX,不保留历史备份。原商品行将按新型号范围重建。</p><p><strong>新范围:${e(range)}</strong>,IMD / JT 分别使用各自型号。</p><ul>${ids.map(id=>`<li><strong>${e(ui.data.designs.find(item=>String(item.id)===String(id))?.code || id)}</strong><br><span class="path-display">${e(ui.data.previews[id].target_path)}</span></li>`).join('')}</ul><p>使用已检查的文案预览,不向 Ozon 提交商品。${cloudMode?'':'本机生成的媒体链接为待上传占位,完成工作台上传与预检后才可提交。'}</p>`;
1734
- if(!await confirmAction('重新生成选中表格?',body,'覆盖并重新生成'))return;
1735
- }
1736
- const protectedBatch = !regenerate && ids.length > 1 && ui.data.config.generator.protect_existing;
1737
- const existing = ids.some(id => ui.data.previews[id]?.target_exists);
1738
- if (!regenerate && !protectedBatch && !await overwriteAllowed(ids)) return;
1739
- const result = await viewAction('copy.write',{ids,allow_overwrite:regenerate || existing && !protectedBatch});
1740
- toast(itemSummary(result,'写表')); ui.lastResult = {action:'copy.write',result}; render();
1741
- }
1742
- async function deleteFiles(names) {
1743
- if (!names.length) throw new Error('请选择待删除文件');
1744
- const result = await viewAction('library.delete_preview',{names,allow_referenced:true});
1745
- const referenced=result.files.some(knownReferences);
1746
- const body = `<p>${e(result.warning || '当前工作台之外的引用无法确定,请核实文件未被其他商品或应用使用。')}</p><ul>${result.files.map(item => `<li><span class="code hash-name">${e(item.name)}</span><br>${bytes(item.size)} · ${e(referenceLabel(item))}${knownReferences(item)?`<p class="hint">${e(item.references.slice(0,8).join(';'))}${item.references.length>8?`;另 ${item.references.length-8} 条关联`:''}</p>`:''}</li>`).join('')}</ul><p>本次精确删除 ${result.files.length} 个文件,共 ${bytes(result.bytes)}。</p>`;
1747
- if (await confirmAction('确认删除服务器文件',body,referenced?'已核实关联,确认删除':'已核实引用,确认删除')) {await viewAction('library.delete',{token:result.token,confirm_unknown:true,confirm_referenced:referenced}); ui.files.clear(); toast('删除任务已返回'); await navigate('library');}
1748
- }
1749
- async function handleAction(action, target) {
1750
- if(action==='dismiss-error'){clearError();return;}
1751
- if(action==='recover-write'){if(!await recoverConfigSave())await reload(false);return;}
1752
- if(action.startsWith('category-'))return handleCategoryAction(action,target);
1753
- if(cloudMode && ['users-refresh','user-toggle','user-password','user-delete','team-refresh'].includes(action) && !isSuperAdmin())throw new Error('人员管理仅限超级管理员');
1754
- if(!isAdmin() && ['file-note','pick-replacement','file-replace','library-delete','file-delete','store-keys-pick','store-keys-import','server-test','users-refresh','user-toggle','user-password','team-refresh'].includes(action))throw new Error('此操作仅限管理员');
1755
- switch (action) {
1756
- case 'shutdown': {
1757
- if(cloudMode){if(ui.logoutPending || !busy() && !await leaveMedia())break;sessionEpoch++;navigationEpoch++;stateEpoch++;ui.logoutPending=true;stopTeamPolling();try{await post('/api/logout',{});showLogin();clearError();toast('已退出登录,后台任务继续运行');}catch(error){ui.logoutPending=false;ui.active=operations.size>0;try{await reload(false);void resumeJobs();}catch{}syncTeamPolling();throw error;}break;}
1758
- if(busy()){toast('任务运行中,请等待完成后退出');break;}
1759
- if(!await leaveMedia())break;
1760
- if(!await confirmAction('退出工作台?','<p>停止本地后台服务并释放端口,所有打开的工作台页面都会断开连接。</p><p>已保存的配置、清单和记录保留;其他未保存的表单修改不会保存。</p>','退出工作台'))break;
1761
- clearError();ui.active=true;renderChrome();
1762
- try {
1763
- const response=await request('/api/action',{method:'POST',headers:{'Content-Type':'application/json','X-Ozon-CSRF':ui.data.csrf},body:JSON.stringify({action:'app.shutdown',confirm:true})});
1764
- if(response.result?.stopping!==true)throw new Error('服务没有确认退出请求,请刷新状态后重试');
1765
- ui.closed=true;stopMediaAutoScroll();
1766
- $('#page').innerHTML=empty('退出请求已受理','本地服务正在停止。重新使用时,请双击项目中的“启动工作台.bat”,再打开工作台页面。');
1767
- } finally {ui.active=false;renderChrome();}
1768
- break;
1769
- }
1770
- case 'refresh': readCache.clear();await reload();clearError();if(ui.data)toast(cloudMode?'已读取最新云端状态':'已读取最新本地状态'); break;
1771
- case 'retry-job-read': {
1772
- const id=target.dataset.id;if(!id || !taskList().some(job=>job.id===id))throw new Error('任务已变化,请重新读取列表');
1773
- const current=scopeLease();target.disabled=true;
1774
- try{
1775
- const known=taskList().find(job=>job.id===id),job=await readJob(id);if(!current())throw staleView();if(!job.id)job.id=id;
1776
- if(known.awaiting_confirmation && ['queued','running'].includes(job.state))job.awaiting_confirmation=true;
1777
- observedJobs.set(id,job);if(ui.lastJob?.id===id)ui.lastJob=job;
1778
- if(['done','failed'].includes(job.state)){
1779
- await reload(false);if(!current())throw staleView();
1780
- if(job.action==='listing.query' && ui.historyReadJobId===id){ui.historyReadError='';ui.historyReadJobId='';}
1781
- }
1782
- clearError();renderChrome();if(ui.page==='task')$('#page').innerHTML=renderJob();else if(ui.page==='history')render();
1783
- }
1784
- finally{if(current())target.disabled=false;}break;
1785
- }
1786
- case 'history-clear': ui.historySearch='';ui.historyFilter='all';ui.historyPage=1;render();$('#history-search').focus();break;
1787
- case 'record-media': if(ui.page==='record'){ui.recordMediaOpen=!ui.recordMediaOpen;render();document.querySelector('[data-action="record-media"]')?.focus();}break;
1788
- case 'workspace-open': openWorkspace(target.dataset?.id || $('#workspace-choice').value || ui.workspace);break;
1789
- case 'users-refresh': await navigate('users');break;
1790
- case 'workspace-rename': case 'workspace-delete': case 'workspace-restore': {
1791
- const id=target.dataset.id,item=(ui.allWorkspaces || ui.session.workspaces).find(item=>item.id===id);if(!item)throw new Error('工作区不存在,请刷新列表');
1792
- if(action!=='workspace-restore' && (item.busy || id===ui.workspace && busy()))throw new Error('任务运行中,不能改名或删除工作区');
1793
- if(action==='workspace-delete' && ui.session.workspaces.length<=1)throw new Error('至少保留一个可用工作区');
1794
- let payload={};if(action==='workspace-rename'){const name=document.getElementById(`workspace-rename-${id}`).value.trim();if(!name || name.length>60)throw new Error('工作区名称需要 1–60 个字符');payload={name};}
1795
- if(action==='workspace-delete'){if(!await confirmAction('删除工作区?',`<p>${e(item.name)} 将从可用工作区中移除。文件、任务与上架历史保留,可在“已删除的工作区”中恢复。</p>`,'删除工作区'))break;payload={confirm:true};}
1796
- const epoch=sessionEpoch;if(target.disabled)break;target.disabled=true;
1797
- try{await post(`/api/workspaces/${encodeURIComponent(id)}/${action.split('-')[1]}`,payload);await refreshWorkspaces();
1798
- if(action==='workspace-delete' && id===ui.workspace){await switchWorkspace(ui.session.workspaces[0].id);await navigate('workspaces');}else render();
1799
- toast(action==='workspace-rename'?'工作区名称已保存':action==='workspace-delete'?'工作区已删除,文件与历史保留':'工作区已恢复');
1800
- }finally{if(epoch===sessionEpoch)target.disabled=false;}break;
1801
- }
1802
- case 'team-refresh': await refreshTeam();break;
1803
- case 'login-theme': {const theme=document.documentElement.dataset.theme==='dark'?'light':'dark';document.documentElement.dataset.theme=theme;window.sessionStorage.setItem('ozon.loginTheme',theme);target.textContent=theme==='dark'?'浅色外观':'暗色外观';break;}
1804
- case 'user-toggle': {
1805
- const username=target.dataset.username,disabled=target.dataset.disabled==='true',user=ui.users?.find(user=>user.username===username);
1806
- if(user?.super_admin || username==='zsh')throw new Error('超级管理员不能停用');
1807
- if(disabled && user?.role==='admin' && !user.disabled && ui.users.filter(user=>user.role==='admin'&&!user.disabled).length<=1)throw new Error('必须保留至少一个可用管理员');
1808
- if(!await confirmAction(disabled?'禁用账户?':'启用账户?',`<p>${e(username)}${disabled?' 将无法登录。':' 将可再次登录。'}</p>`,'确认'))break;
1809
- await post(`/api/users/${encodeURIComponent(username)}`,{disabled});ui.users=(await request('/api/users')).users;render();break;
1810
- }
1811
- case 'user-password': {
1812
- const username=target.dataset.username;if(username===ui.session.user.username)throw new Error('请通过我的账户凭原密码修改自己的密码');
1813
- const input=document.getElementById(`reset-${username}`),value=input.value;if(value.length<8 || value.length>200)throw new Error('新密码需要 8–200 个字符');
1814
- await post(`/api/users/${encodeURIComponent(username)}`,{password:value});input.value='';toast('账户密码已重置');break;
1815
- }
1816
- case 'user-delete': {
1817
- const username=target.dataset.username,user=ui.users?.find(user=>user.username===username);if(!user)throw new Error('账户不存在,请刷新人员列表');
1818
- if(user.super_admin || username==='zsh')throw new Error('超级管理员不能删除');
1819
- if(!await confirmAction('删除账户?',`<p>${e(username)} 将无法再登录,并从人员列表移除;此用户名不能再次使用。</p><p>工作区文件、任务、操作审计与上架历史记录保留。</p>`,'删除账户'))break;
1820
- if(target.disabled)break;target.disabled=true;try{await post(`/api/users/${encodeURIComponent(username)}/delete`,{confirm:true});ui.users=(await request('/api/users')).users;render();toast('账户已删除,历史记录保留');}finally{target.disabled=false;}break;
1821
- }
1822
- case 'store-open': if($('#store-popover').hidden)openStorePicker();else closeStorePicker();break;
1823
- case 'store-select': {await viewAction('stores.select',{id:target.dataset.id});resetQualityDraft();closeStorePicker();toast('目标店铺已切换,需重新预检');render();$('#store-choice').focus();break;}
1824
- case 'quality-list': {const current=qualityScope(),result=await viewAction('listing.quality.list');if(current!==qualityScope())throw staleView();if(String(result.client_id)!==String(ui.data.config.seller.client_id) || String(result.store_id || '')!==String(ui.data.config.seller.selected_id || '') || !Array.isArray(result.certificates))throw new Error('质量文件店铺不匹配,请重新读取');ui.qualityDocuments=result.certificates;ui.qualityLoaded=true;if(ui.qualityId!==null && !ui.qualityDocuments.some(item=>item.certificate_id===ui.qualityId && !qualityUnavailable(item))){ui.qualityId=null;ui.qualityDirty=Boolean(ui.data.plan);}render();break;}
1825
- case 'list-page': {const key=target.dataset.list==='history'?'historyPage':target.dataset.list==='library'?'libraryPage':target.dataset.list==='models'?'modelPage':'queuePage';ui[key]=Number(target.dataset.number);render();$('#page').focus();break;}
1826
- case 'queue-detail': if(filterQueue(queueGroups(),ui.search,ui.queueFilter,ui.data.plan).some(item=>String(item.id)===target.dataset.id)){ui.queueDetail=target.dataset.id;render();$('#queue-detail-title')?.focus();}break;
1827
- case 'queue-detail-close': ui.queueDetail=null;render();$('#search')?.focus();break;
1828
- case 'queue-detail-step': {
1829
- const items=filterQueue(queueGroups(),ui.search,ui.queueFilter,ui.data.plan),index=items.findIndex(item=>String(item.id)===String(ui.queueDetail)),next=items[index+Number(target.dataset.offset)];
1830
- if(index>=0 && next){ui.queueDetail=next.id;ui.queuePage=Math.floor((index+Number(target.dataset.offset))/ui.pageSize)+1;render();$('#queue-detail-title')?.focus();}break;
1831
- }
1832
- case 'queue-density': ui.queueDensity=ui.queueDensity==='compact'?'comfortable':'compact';render();$('[data-action="queue-density"]')?.focus();break;
1833
- case 'queue-view-save': case 'queue-view-clear': {
1834
- loadQueueView();
1835
- const saved=action==='queue-view-save'?normalizeQueueView({search:ui.search,filter:ui.queueFilter,size:ui.pageSize,density:ui.queueDensity,columns:ui.queueColumns,columns_version:3}):null;
1836
- try{if(!window.localStorage)throw new Error('不可用');if(saved)window.localStorage.setItem(ui.queueSavedKey,JSON.stringify(saved));else window.localStorage.removeItem(ui.queueSavedKey);}catch{toast('当前浏览器无法保存视图;仍可使用筛选和布局设置');break;}
1837
- ui.queueSaved=saved;render();$('[data-action="queue-view-save"]')?.focus();toast(saved?'常用视图已保存到此浏览器、当前工作区':'常用视图已清除');break;
1838
- }
1839
- case 'queue-view-load': {
1840
- if(!ui.queueSaved)break;
1841
- const saved=normalizeQueueView(ui.queueSaved);Object.assign(ui,{search:saved.search,queueSearch:saved.search,queueFilter:saved.filter,pageSize:saved.size,queueDensity:saved.density,queueColumns:saved.columns,queuePage:1,queueDetail:null});render();ui.queueScroll=[0,0];const table=$('.queue-table');if(table){table.scrollLeft=0;table.scrollTop=0;}$('[data-action="queue-view-load"]')?.focus();break;
1842
- }
1843
- case 'library-filter-reset': ui.libraryFilter={category:'all',type:'all',reference:'all',sort:'manual'};ui.libraryPage=1;render();break;
1844
- case 'theme': {const config=structuredClone(ui.data.config); config.ui.theme=config.ui.theme === 'dark' ? 'light' : 'dark'; await viewAction('config.save',{config}); if(['queue','generation','library'].includes(ui.page))render();else renderChrome(); if($('#cfg-ui-theme'))$('#cfg-ui-theme').value=ui.data.config.ui.theme; break;}
1845
- case 'store-keys-pick': {const paths=await viewPick('credentials');if(paths[0])$('#store-key-sheet').value=paths[0];break;}
1846
- case 'store-keys-import': {const result=await viewAction('stores.keys.import',{path:$('#store-key-sheet').value.trim()});toast(`已导入 ${result.imported} 个店铺的本地凭据`);render();break;}
1847
- case 'updates-check': await checkUpdates();break;
1848
- case 'updates-download': {if(cloudMode||!ui.updateStatus?.available||ui.updateStatus.pending_version)throw new Error('请先检查可用的新版本');ui.updateStatus=await viewAction('updates.download',{version:ui.updateStatus.latest_version});toast('更新已下载,下次启动生效');render();break;}
1849
- case 'design-folder': {if(cloudMode)throw new Error('请使用本地工作台选择目录');const paths=await viewPick('directory');if(!paths.length)break;const config=structuredClone(ui.data.config);config.generator.scan_dir=paths[0];await viewAction('config.save',{config});await viewAction('design.scan');ui.search='';render();break;}
1850
- case 'design-scan': await viewAction('design.scan'); render(); break;
1851
- case 'recognize': {const result=await viewAction('design.recognize',{ids:[...ui.designs]});toast(itemSummary(result,'识别'));render();break;}
1852
- case 'recognize-one': {const result=await viewAction('design.recognize',{ids:[ui.design]}); const value=preview(); if(value)ui.copy=structuredClone(value);toast(itemSummary(result,'识别'));render();break;}
1853
- case 'write-designs': await writeDesigns([...ui.designs]); break;
1854
- case 'regenerate-designs': await writeDesigns([...ui.designs],true); break;
1855
- case 'copy': {ui.design=target.dataset.id; const value=preview(); ui.copy=value ? structuredClone(value) : {title:'',description:'',attrs:{}}; await navigate('copy'); break;}
1856
- case 'copy-check': {const draft=collectCopy(); await viewAction('copy.preview',{id:ui.design,title:draft.title,description:draft.description,attrs:draft.attrs}); ui.copy=structuredClone(preview() || draft); render(); toast('文案预览已检查'); break;}
1857
- case 'copy-write': {const draft=collectCopy(); await viewAction('copy.preview',{id:ui.design,title:draft.title,description:draft.description,attrs:draft.attrs}); await writeDesigns([ui.design]); break;}
1858
- case 'manual-templates': {collectManual(); const result=await viewAction('manual.templates',{file_type:Number(ui.manual.file_type)}); ui.manualPaths=result.paths || []; if(!ui.manual.source_path && ui.manualPaths.length)ui.manual.source_path=ui.manualPaths[0]; render(); break;}
1859
- case 'pick-manual': {collectManual(); const paths=await viewPick('workbooks'); if(paths[0])ui.manual.source_path=paths[0]; ui.manualPreview=null; render(); break;}
1860
- case 'manual-check': {const draft=collectManual(); ui.manualPreview=await viewAction('manual.preview',{source_path:draft.source_path,new_number:draft.new_number,title:draft.title,description:draft.description}); render(); break;}
1861
- case 'manual-write': {collectManual(); if(!ui.manualPreview)throw new Error('请先生成手动建表预览'); if(!await overwriteAllowed([],true))return; await viewAction('manual.write',{id:ui.manualPreview.id,allow_overwrite:Boolean(ui.manualPreview.target_exists)}); ui.manualPreview=null; toast('写表任务已返回,成功表格已加入队列'); render(); break;}
1862
- case 'queue-scan': await viewAction('queue.scan'); render(); break;
1863
- case 'auto-open': ui.autoDraft=null;await navigate('autoMedia');if(ui.data.groups.length){ui.autoDraft=await viewAction('media.auto.preview');render();}break;
1864
- case 'auto-read': ui.autoDraft=await viewAction('media.auto.preview');render();break;
1865
- case 'auto-rename': {if(await confirmAction('按款式给图片编号','<p>将设计图片按原扫描顺序改为“款式名_01、02…”。保存改名记录并更新媒体和识别清单引用;当前文案预览需重新读取。</p>','开始编号')){const result=await viewAction('media.rename');ui.autoDraft=null;toast(`已编号 ${result.renamed} 张图片`);render();if(ui.data.groups.length){ui.autoDraft=await viewAction('media.auto.preview');render();}}break;}
1866
- case 'auto-apply': {await viewAction('media.auto.apply',{groups:ui.autoDraft.groups,binding:ui.autoDraft.binding});toast('逐型号媒体清单已应用,请选择店铺与商品后上传预检');await navigate('queue');break;}
1867
- case 'add-books': await navigate('queueAdd');break;
1868
- case 'pick-books': {ui.addBooks=$('#book-paths').value;const paths=await viewPick('workbooks');ui.addBooks=[...pathLines(ui.addBooks),...paths].join('\n');render();break;}
1869
- case 'queue-add': await viewAction('queue.add',{paths:pathLines($('#book-paths').value)});ui.addBooks='';await navigate('queue');break;
1870
- case 'queue-remove': if(await confirmAction('移除队列关联',`<p>移除当前选中的 ${ui.data.selected_groups.length} 款,只清除队列关联。</p>`,'确认移除')){await viewAction('queue.remove',{ids:ui.data.selected_groups});render();} break;
1871
- case 'navigate-manifest': ui.importText=''; await navigate('manifest'); break;
1872
- case 'edit-media': {if(!await leaveMedia())break;ui.group=target.dataset.id;ui.media=structuredClone(group());ui.mediaBase=mediaSignature(ui.media);ui.mediaUndo=null;ui.mediaTyping=null;await navigate('media');break;}
1873
- case 'media-scroll': {const strip=target.closest('.media-rail')?.querySelector('.media-thumbs');strip?.scrollBy({left:Number(target.dataset.offset)*Math.max(144,strip.clientWidth*.75),behavior:motionAllowed()?'smooth':'instant'});break;}
1874
- case 'media-pick': {collectMedia();const draft=structuredClone(ui.media),kind=target.dataset.kind,paths=await viewPick(['videos','cover'].includes(kind)?'videos':'images');if(kind==='primary' && paths[0])draft.primary_image=paths[0];else if(kind==='cover' && paths[0])draft.video_cover=paths[0];else if(kind==='images')draft.images.push(...paths);else if(kind==='videos')draft.videos.push(...paths.map(path=>({path,name:path.split(/[\\/]/).at(-1).replace(/\.[^.]+$/,'')})));changeMedia(draft);render();break;}
1875
- case 'media-move': {collectMedia();const draft=structuredClone(ui.media);draft[target.dataset.kind]=moveItem(draft[target.dataset.kind],Number(target.dataset.index),Number(target.dataset.offset));changeMedia(draft);render();break;}
1876
- case 'media-path-refresh': collectMedia();render();break;
1877
- case 'media-remove': {collectMedia();const draft=structuredClone(ui.media);draft[target.dataset.kind].splice(Number(target.dataset.index),1);changeMedia(draft);render();toast('已移出草稿,可点击“撤销”恢复');break;}
1878
- case 'media-undo': {if(!ui.mediaUndo)break;ui.media=ui.mediaUndo;ui.mediaUndo=null;ui.mediaTyping=null;render();toast('已撤销上一次媒体调整');break;}
1879
- case 'media-add-video-paths': {collectMedia();const draft=structuredClone(ui.media);draft.videos.push(...pathLines($('#video-paths').value).map(path=>({path,name:path.split(/[\\/]/).at(-1).replace(/\.[^.]+$/,'')})));changeMedia(draft);render();break;}
1880
- case 'media-save': await inView(()=>saveMedia());await navigate('queue');break;
1881
- case 'book': {ui.group=target.dataset.id;const result=await viewAction('queue.book',{id:group().id});ui.bookRecords=result.records || [];await navigate('book');break;}
1882
- case 'business-pattern-save': {const config=structuredClone(ui.data.config);config.commerce.patterns[design().code]=collectBusiness('pattern');await viewAction('config.save',{config});toast('本图案经营参数已保存');break;}
1883
- case 'business-open': ui.bookRecords=(await viewAction('queue.book',{id:group().id,all_source:true})).records;ui.businessRow=null;ui.businessDraft=null;ui.businessChanged=false;ui.businessSelected=new Set((ui.bookRecords || []).map(row=>row.item.offer_id));await navigate('commerce');break;
1884
- case 'business-one': ui.businessDraft=null;ui.businessChanged=false;ui.businessRow=Number(target.dataset.index);ui.businessSelected=new Set([ui.bookRecords[ui.businessRow].item.offer_id]);await navigate('commerce');break;
1885
- case 'business-all': collectBusinessDraft();ui.businessSelected=new Set(ui.bookRecords.map(row=>row.item.offer_id));render();break;
1886
- case 'business-none': collectBusinessDraft();ui.businessSelected.clear();render();break;
1887
- case 'business-warehouses': {const config=['settings/commerce','settings/categoryTemplates'].includes(ui.page)?readConfigForm():null;if(ui.page==='commerce')collectBusinessDraft();if(categoryModule && ui.page==='settings/categoryTemplates'){categoryModule.collectEditor(categoryState(),document);categoryState().compatibilityDraft=config.commerce;}const result=await viewAction('commerce.warehouses');ui.warehouses=result.warehouses;ui.warehouseClient=result.client_id;if(config && ui.page==='settings/commerce')ui.data.config=config;render();break;}
1888
- case 'business-save': {const editedOffer=ui.businessRow==null?null:ui.bookRecords[ui.businessRow]?.item.offer_id,warehouse=$('#business-warehouse').value,result=await viewAction('commerce.save',{offer_ids:[...ui.businessSelected],values:collectBusiness('edit'),...(warehouse?{warehouse_id:Number(warehouse)}:{})});ui.businessDraft=null;ui.businessChanged=false;ui.bookRecords=(await viewAction('queue.book',{id:group().id,all_source:true})).records;if(editedOffer)ui.businessRow=ui.bookRecords.findIndex(row=>row.item.offer_id===editedOffer);toast(`已保存 ${result.changed} 个 SKU;原表备份已保留`);render();break;}
1889
- case 'business-preview-prices': case 'business-preview-stocks': {if(ui.businessChanged)throw new Error('请先保存当前修改,再生成提交预览');await viewAction('commerce.prepare',{offer_ids:[...ui.businessSelected],kind:action.endsWith('stocks')?'stocks':'prices'});await navigate('commercePlan');break;}
1890
- case 'business-submit': {const plan=ui.data.commerce_plan;if(!plan)throw new Error('请先生成提交预览');const kind=plan.kind==='stocks'?'库存':'价格';if(await confirmAction(`确认提交${kind}`,`<p>提交到当前店铺 ${e(plan.client_id)},共 ${plan.rows.length} 个 SKU。最终数值见当前预览;提交后请回读核验。</p>`,`提交${kind}`)){const result=await viewAction('commerce.submit',{id:plan.id,confirm:true});toast(`${kind}提交:${statusText(result.state)}`);ui.commerceOperations=[result];render();}break;}
1891
- case 'business-query': {const result=await viewAction('commerce.query');ui.commerceOperations=result.operations;await navigate('commercePlan');break;}
1892
- case 'product': ui.record=Number(target.dataset.index);await navigate('product');break;
1893
- case 'prepare': {
1894
- if(!await leaveMedia())break;
1895
- if(ui.page==='media')await navigate('queue');
1896
- const current=scopeLease(),page=ui.page,navigation=navigationEpoch,shop=qualityScope(),ids=[...ui.data.selected_groups],qualityId=ui.qualityId;
1897
- const review=await viewAction('listing.existing',{ids});
1898
- if(!review?.binding || !Array.isArray(review.existing) || String(review.client_id)!==String(ui.data.config.seller.client_id) || String(review.store_id || '')!==String(ui.data.config.seller.selected_id || ''))throw new Error('已有货号检查不完整或店铺变化,请重新检查');
1899
- let policy='skip';
1900
- if(review.existing.length){
1901
- const rows=review.existing.map(row=>`<tr><td>${e(row.offer_id)}</td><td>${e(row.model || '型号未提供')}<small class="hint">${e(row.current_name || '')}</small></td><td>${e(row.status || '店铺已有记录')}</td></tr>`).join('');
1902
- const body=`<p>当前店铺:<strong>${e(review.store_name || review.client_id)}</strong>。本批 ${e(review.total)} 个 SKU,已有 ${e(review.existing_count)} 个,待新建 ${e(review.new_count)} 个。</p><p>以下货号已在店铺建立记录;导入、审核和可售状态仍需查询确认。</p><div class="existing-products"><table><thead><tr><th>已有货号</th><th>手机型号 / 商品名</th><th>状态</th></tr></thead><tbody>${rows}</tbody></table></div><fieldset class="existing-policy"><legend>本批已有商品如何处理</legend><label><input type="radio" name="existing-policy" value="skip" checked><span><strong>跳过已有商品</strong><small>仅对本批新货号继续上传与预检。</small></span></label><label><input type="radio" name="existing-policy" value="update"><span><strong>用当前参数更新已有商品</strong><small>更新图文、属性、价格与包装参数;未提供字段保留。下一步展示差异,确认提交后才写入平台。库存将在下一步一并核对数量与仓库。</small></span></label></fieldset>`;
1903
- if(!await confirmAction('检测到店铺已有商品',body,'按所选方式继续预检'))break;
1904
- policy=$('input[name="existing-policy"]:checked')?.value;
1905
- if(!['skip','update'].includes(policy))throw new Error('请选择跳过或更新已有商品');
1906
- }
1907
- if(!current() || page!==ui.page || navigation!==navigationEpoch || shop!==qualityScope() || !sameScope(ids,ui.data.selected_groups))throw new Error('店铺、选择范围或页面发生变化,请重新检查');
1908
- const result=await viewAction('listing.prepare',{ids,quality_certificate_id:qualityId,existing_policy:policy,existing_binding:review.binding});
1909
- if(result.count===0){ui.data.plan=null;toast(`本批全部跳过,共 ${result.skipped_count} 个已有商品;未提交任何商品。`);await navigate('queue');break;}
1910
- await navigate('preflight');break;
1911
- }
1912
- case 'submit': {if(!await leaveMedia())break;if(ui.page==='media')await navigate('queue');const plan=ui.data.plan;if(!plan)throw new Error('请先上传与预检');if(ui.qualityDirty)throw new Error('质量文件已更改,请重新预检');if(!sameScope(plan.scope || [],ui.data.selected_groups))throw new Error('选择范围已变化,请重新预检');const current=pageLease();if(await confirmAction('确认向 Ozon 提交',`<p>${listingCounts(plan)},实际提交 ${e(plan.count)} 个 SKU,到客户端标识 <strong>${e(plan.client_id)}</strong>(${e(plan.store_name || '当前店铺')})。</p><p>更新图文、属性、价格和包装参数,未提供字段保留;价格在商品请求受理后单独更新并回读。库存按本批已核对的数量和仓库,在商品建立后提交并回读。</p>${listingStocks(plan)}<p class="quality-snapshot">本批质量文件:<strong>${e(qualityLabel(plan.quality_document))}</strong>${plan.quality_document?`<br>${e(qualityReview(plan.quality_document.status_code))} · 商品建立后自动关联,关联不代表审核通过或可售。`:''}</p><p>提交后需要查询导入任务与商品;未决结果显示“结果待核实”,不会自动重发。</p>`,'确认提交')){if(!current() || plan!==ui.data.plan || !sameScope(plan.scope || [],ui.data.selected_groups) || ui.qualityDirty)throw new Error('确认期间计划或范围发生变化,请重新预检');await viewAction('listing.submit',{confirm:true,operation_id:plan.operation_id,payload_sha256:plan.payload_sha256,stock_plan_id:plan.stock_plan?.id});resetQualityDraft();await navigate('history');}break;}
1913
- case 'query': {const current=scopeLease();ui.historyReadError='';ui.historyReadJobId='';try{await viewAction('listing.query');render();}catch(error){if(current() && error?.code!=='STALE_VIEW'){ui.historyReadError=error.message || String(error);ui.historyReadJobId=error.jobId || '';if(ui.page==='history')render();}throw error;}break;}
1914
- case 'record': ui.record=Number(target.dataset.index);await navigate('record');break;
1915
- case 'toggle-task-log': ui.taskLogOpen=!ui.taskLogOpen;renderTaskbar(true);if(ui.taskLogOpen)$('#task-log-content').focus();break;
1916
- case 'close-task-log': closeTaskLog();break;
1917
- case 'job-detail': await navigate('task');break;
1918
- case 'library-refresh': await viewAction('library.refresh');render();break;
1919
- case 'file': ui.file=target.dataset.name;await navigate('file');break;
1920
- case 'phone-model-import': {const paths=await viewPick('file');if(paths[0]){await viewAction('models.import',{path:paths[0]});toast('完整型号表已导入,现有热门设置已保留');render();}break;}
1921
- case 'phone-model-popular': await viewAction('models.popular',{material:target.dataset.material,sku_model:target.dataset.sku,popular:target.dataset.popular==='true'});toast('热门名单已保存');render();break;
1922
- case 'file-category': await viewAction('library.category',{name:file().name,categories:[...document.querySelectorAll('[data-file-category]:checked')].map(item=>item.dataset.fileCategory)});toast('媒体用途已保存');render();break;
1923
- case 'file-note': await viewAction('library.note',{name:file().name,note:$('#file-note').value});toast('备注已保存');render();break;
1924
- case 'pick-replacement': {const paths=await viewPick('file');if(paths[0])$('#replacement-path').value=paths[0];break;}
1925
- case 'file-replace': {const result=await viewAction('library.replace',{name:file().name,path:$('#replacement-path').value.trim()});ui.lastResult={action:'library.replace',result};toast('替换任务已返回,原文件保留;新 URL 可在媒体库查看');await navigate('library');break;}
1926
- case 'library-delete': await deleteFiles([...ui.files]);break;
1927
- case 'file-delete': await deleteFiles([file().name]);break;
1928
- case 'pick-upload-images': case 'pick-upload-videos': {ui.upload=$('#upload-paths').value;const paths=await viewPick(action.endsWith('images')?'images':'videos');ui.upload=[...new Set([...pathLines(ui.upload),...paths])].join('\n');syncUploadEntries();render();break;}
1929
- case 'upload': syncUploadEntries();await runUploads(ui.uploadItems.filter(item=>item.state==='waiting').map(item=>item.path));break;
1930
- case 'upload-retry': {const item=ui.uploadItems[Number(target.dataset.index)];if(item?.state==='failed')await runUploads([item.path]);break;}
1931
- case 'config-pick': {const config=readConfigForm(),paths=await viewPick(target.dataset.kind);if(paths[0]){ui.data.config=config;render();$(`#${target.dataset.target}`).value=paths[0];}break;}
1932
- case 'config-save': await saveConfig();break;
1933
- case 'clear-credential': await viewAction('secrets.set',{[`${target.dataset.kind}_key`]:''});toast('会话密钥已清除');render();break;
1934
- case 'model-catalog': {await inView(()=>saveConfig());const result=await viewAction('model.catalog');ui.models=result.models || [];render();break;}
1935
- case 'model-test': case 'seller-test': case 'server-test': await inView(()=>saveConfig());await viewAction(action.replace('-','.'));render();break;
1936
- case 'import-config': {const config=JSON.parse($('#import-text').value);if(await confirmAction('导入配置','<p>将检查并替换当前无密钥配置,现有上架计划需要重新预检。</p>','检查并导入')){await viewAction('config.import',{config});ui.importText='';await navigate('settings');}break;}
1937
- case 'import-media': {const value=JSON.parse($('#import-text').value),groups=Array.isArray(value)?value:value.groups;if(!Array.isArray(groups))throw new Error('媒体清单需要 groups 数组');await viewAction('media.import',{groups});ui.importText='';await navigate('queue');break;}
1938
- default: throw new Error('此操作未找到对应功能');
1939
- }
1940
- }
1941
- const cardInteractive = target => target.closest('button,input,textarea,select,a,summary,video,[contenteditable]');
1942
- document.addEventListener('pointerdown',event => {
1943
- cardPointerBlocked=Boolean(event.target.closest('[data-card-list]') && cardInteractive(event.target));
1944
- });
1945
- document.addEventListener('dragstart',event => {
1946
- const sortable=event.target.closest('[data-card-list]');
1947
- if(sortable){
1948
- const blocked=cardPointerBlocked || cardInteractive(event.target);
1949
- clearCardDrag();clearMediaDrag();
1950
- const list=sortable.dataset.cardList,id=sortable.dataset.cardId;
1951
- if(blocked || !['designs','library'].includes(list) || ui.page!==(list==='designs'?'generation':'library') || !visibleCardItems(list).some(item=>cardId(list,item)===id)){event.preventDefault();return;}
1952
- cardDrag={list,id,key:cardOrderKey()};
1953
- event.dataTransfer.effectAllowed='move';event.dataTransfer.setData(cardMime,JSON.stringify(cardDrag));
1954
- beginCardVisual(sortable,event);sortable.classList.add('card-dragging');return;
1955
- }
1956
- clearCardDrag();
1957
- const card=event.target.closest('[data-media-kind]');
1958
- if(!card || ui.page!=='media' || event.target.closest('button,input,textarea'))return;
1959
- if(!collectMediaForSort()){event.preventDefault();return;}
1960
- mediaDrag={...mediaSlot(card),group:String(ui.group)};
1961
- event.dataTransfer.effectAllowed='move';event.dataTransfer.setData('application/x-ozon-media','move');
1962
- card.classList.add('dragging');
1963
- });
1964
- document.addEventListener('dragover',event => {
1965
- const sortable=event.target.closest('[data-card-list]');
1966
- if(sortable || cardDrag){
1967
- const target=cardTargetAt(event),compatible=compatibleCard(target,event.dataTransfer);event.dataTransfer.dropEffect=compatible?'move':'none';
1968
- if(compatible)event.preventDefault();
1969
- previewCardInsertion(compatible?target:null,event);
1970
- return;
1971
- }
1972
- if(ui.page!=='media')return;
1973
- scrollMediaAtEdge(event);
1974
- const card=event.target.closest('[data-media-kind]');if(!card)return;
1975
- event.preventDefault();
1976
- const compatible=mediaDrag && mediaDrag.group===String(ui.group) && ['images','primary'].includes(mediaDrag.kind)===['images','primary'].includes(card.dataset.mediaKind);
1977
- event.dataTransfer.dropEffect=compatible?'move':'none';
1978
- document.querySelectorAll('.media-tile.drop-target').forEach(item=>item.classList.remove('drop-target'));
1979
- if(compatible)card.classList.add('drop-target');
1980
- });
1981
- document.addEventListener('dragleave',event => {
1982
- const card=event.target.closest('[data-card-list]');
1983
- if(cardVisual && !cardVisual.container.contains(event.relatedTarget))previewCardInsertion(null,event);
1984
- else if(card && !card.contains(event.relatedTarget))card.classList.remove('card-drop-target');
1985
- });
1986
- document.addEventListener('drop',event => {
1987
- const sortable=event.target.closest('[data-card-list]');
1988
- if(sortable || cardDrag){
1989
- event.preventDefault();const from=cardDrag,target=cardTargetAt(event,true),compatible=compatibleCard(target,event.dataTransfer,true),visual=compatible?releaseCardVisual():null;clearCardDrag();
1990
- if(compatible && moveDisplayCards(from.list,from.id,target.dataset.cardId))landCardVisual(visual,from.list,from.id);
1991
- else visual?.overlay.remove();
1992
- return;
1993
- }
1994
- const card=event.target.closest('[data-media-kind]');if(!card || ui.page!=='media'){clearMediaDrag();return;}
1995
- event.preventDefault();
1996
- const from=mediaDrag,to=mediaSlot(card);clearMediaDrag();
1997
- if(from?.group===String(ui.group))changeMediaPosition(from,to);
1998
- });
1999
- document.addEventListener('drag',event=>{if(cardVisual && validCardVisual() && event.clientX>0 && event.clientY>0)positionCardOverlay(event);});
2000
- document.addEventListener('dragend',()=>{clearMediaDrag();clearCardDrag();});
2001
- window.addEventListener('pagehide',clearCardDrag);
2002
- window.addEventListener('resize',clearCardDrag);
2003
- document.addEventListener('keydown',event => {
2004
- if(event.key==='Escape' && cardDrag){event.preventDefault();clearCardDrag();return;}
2005
- if($('#image-viewer').open && ['ArrowLeft','ArrowRight'].includes(event.key)){event.preventDefault();showImageAt(imageIndex+(event.key==='ArrowRight'?1:-1));return;}
2006
- const sortable=event.target.closest('[data-card-list]');
2007
- if(sortable && event.altKey && event.target.tagName!=='IMG' && !cardInteractive(event.target) && ['ArrowLeft','ArrowUp','ArrowRight','ArrowDown'].includes(event.key)){
2008
- event.preventDefault();const list=sortable.dataset.cardList,ids=visibleCardItems(list).map(item=>cardId(list,item)),index=ids.indexOf(sortable.dataset.cardId);
2009
- const target=ids[index+(['ArrowLeft','ArrowUp'].includes(event.key)?-1:1)];
2010
- if(target && moveDisplayCards(list,sortable.dataset.cardId,target)){
2011
- const card=Array.from(document.querySelectorAll('[data-card-list]')).find(item=>item.dataset.cardList===list && item.dataset.cardId===sortable.dataset.cardId);
2012
- card?.focus?.({preventScroll:true});card?.scrollIntoView?.({block:'nearest'});
2013
- }
2014
- return;
2015
- }
2016
- if(event.target.closest('.queue-inspector') && ['Escape','ArrowLeft','ArrowRight'].includes(event.key)){
2017
- event.preventDefault();if(event.key==='Escape')handleAction('queue-detail-close',{});else handleAction('queue-detail-step',{dataset:{offset:event.key==='ArrowRight'?'1':'-1'}});return;
2018
- }
2019
- if(event.target.closest('.store-selector')){
2020
- if(event.key==='Escape'){event.preventDefault();closeStorePicker();$('#store-choice').focus();return;}
2021
- if(['ArrowDown','ArrowUp'].includes(event.key)){
2022
- event.preventDefault();if($('#store-popover').hidden)openStorePicker();
2023
- const choices=Array.from(document.querySelectorAll('#store-options [role="option"]')),index=choices.indexOf(event.target);
2024
- const next=index<0?(event.key==='ArrowDown'?0:choices.length-1):Math.max(0,Math.min(choices.length-1,index+(event.key==='ArrowDown'?1:-1)));
2025
- choices[next]?.focus();return;
2026
- }
2027
- }
2028
- if(event.key==='Escape' && ui.taskLogOpen && !event.defaultPrevented && !document.querySelector('dialog[open]')){event.preventDefault();closeTaskLog();return;}
2029
- if(event.target.tagName==='IMG' && event.target.closest('#page') && !event.altKey && ['Enter',' '].includes(event.key)){event.preventDefault();openImageView(event.target);return;}
2030
- const card=event.target.closest('[data-media-kind]');
2031
- if(!event.altKey || !card || event.target.closest('input,textarea') || !['images','videos'].includes(card.dataset.mediaKind))return;
2032
- const from=mediaSlot(card);let to;
2033
- if(event.key==='Home')to={kind:from.kind==='images'?'primary':'cover',index:0};
2034
- else if(['ArrowLeft','ArrowUp','ArrowRight','ArrowDown'].includes(event.key))to={kind:from.kind,index:from.index+(['ArrowLeft','ArrowUp'].includes(event.key)?-1:1)};
2035
- else return;
2036
- event.preventDefault();changeMediaPosition(from,to);
2037
- });
2038
- document.addEventListener('click',async event => {
2039
- if(ui.closed)return;
2040
- if(Date.now()<cardClickBlockedUntil && event.target.closest('[data-card-list]')){event.preventDefault();return;}
2041
- if(!event.target.closest('.store-selector'))closeStorePicker();
2042
- if(event.target.tagName==='IMG' && event.target.closest('#page')){event.preventDefault();openImageView(event.target);return;}
2043
- const target=event.target.closest('button');if(!target || target.disabled || target.closest('#confirm-dialog'))return;
2044
- if(!ui.data && !['refresh','login-theme','dismiss-error'].includes(target.dataset.action))return;
2045
- try {if(target.dataset.page){if(target.dataset.page==='manual')ui.manualPreview=null;await navigate(target.dataset.page);}else if(target.dataset.action)await handleAction(target.dataset.action,target);}catch(error){if(error?.code==='STALE_VIEW')return;showError(error);if(!['OPERATION_CONFLICT','CONFIG_SAVE_UNCONFIRMED'].includes(error?.code))try{await reload(false);}catch{}renderChrome();}
2046
- });
2047
- document.addEventListener('input',event => {
2048
- if(['task-log-level','task-log-job'].includes(event.target.id))return;
2049
- inputEpoch++;
2050
- const target=event.target;
2051
- if(categoryModule && ui.page==='settings/categoryTemplates' && (target.id.startsWith('category-') || target.dataset.templateAttribute!==undefined || target.dataset.templateBusiness!==undefined || target.dataset.templateStyleBusiness!==undefined)){
2052
- if(target.dataset.templateBusiness!==undefined && target.value.trim()){const clear=document.querySelector(`[data-template-clear="${target.dataset.templateBusiness}"]`);if(clear)clear.checked=false;}
2053
- if(target.dataset.templateStyleBusiness!==undefined && target.value.trim()){const clear=document.querySelector(`[data-template-style-clear="${target.dataset.templateStyleBusiness}"]`);if(clear)clear.checked=false;}
2054
- categoryModule.collectEditor(categoryState(),document);updateCategoryStatus();
2055
- }
2056
- if(categoryModule && ui.page==='categoryCreate' && target.id.startsWith('category-')){categoryModule.collectNew(categoryState(),document);updateCategoryStatus();}
2057
- if(target.id==='store-search'){ui.storeSearch=target.value;$('#store-options').innerHTML=renderStoreOptions();}
2058
- if(ui.page==='media' && (['primary-path','cover-path','image-paths'].includes(target.id) || target.dataset.videoName!==undefined)){
2059
- const before=structuredClone(ui.media);collectMedia();
2060
- if(mediaSignature(before)!==mediaSignature(ui.media) && ui.mediaTyping!==target){ui.mediaUndo=before;ui.mediaTyping=target;}
2061
- updateMediaStatus();
2062
- }
2063
- if(target.id==='search'){ui.search=target.value;ui.libraryPage=1;ui.queuePage=1;ui.modelPage=1;const position=target.selectionStart;render();$('#search').focus();$('#search').setSelectionRange(position,position);}
2064
- if(target.id==='history-search'){ui.historySearch=target.value;ui.historyPage=1;const position=target.selectionStart;render();$('#history-search').focus();$('#history-search').setSelectionRange(position,position);}
2065
- if(target.id==='upload-paths'){ui.upload=target.value;syncUploadEntries();updateUploads();const upload=document.querySelector('[data-action="upload"]');setActionDisabled(upload,!ui.uploadItems.some(item=>item.state==='waiting'));}
2066
- if(target.id==='book-paths'){ui.addBooks=target.value;const add=document.querySelector('[data-action="queue-add"]');setActionDisabled(add,!pathLines(ui.addBooks).length);}
2067
- if(target.id==='import-text')ui.importText=target.value;
2068
- if(target.dataset.business && target.value.trim()){const clear=document.querySelector(`[data-business-clear="${target.dataset.business}"][data-key="${target.dataset.key}"]`);if(clear)clear.checked=false;}
2069
- if(target.dataset.business || target.dataset.businessClear || ['business-default-warehouse','business-no-stock-patterns'].includes(target.id))collectCompatibilityDraft();
2070
- if(ui.page==='commerce' && (target.dataset.business || target.id==='business-warehouse')){ui.businessChanged=true;collectBusinessDraft();}
2071
- if(target.id.startsWith('copy-') || target.dataset.attr || target.id==='case-design')collectCopy();
2072
- if(target.id.startsWith('manual-')){collectManual();ui.manualPreview=null;const write=document.querySelector('[data-action="manual-write"]');if(write)write.disabled=true;}
2073
- if(target.id==='cfg-generator-reasoning_control')updateThinkingFields();
2074
- });
2075
- document.addEventListener('change',async event => {
2076
- if(event.target.id==='task-log-job'){ui.selectedJobId=event.target.value;renderTaskbar(true);$('#task-log-job')?.focus();return;}
2077
- if(event.target.id==='task-log-level'){const level=event.target.value;if(!['INFO','WARN','ERROR'].includes(level))return;ui.taskLogLevel=level;try{window.localStorage?.setItem('ozon.taskLogLevel',level);}catch{toast('日志显示已更新,浏览器未能保存显示偏好');}renderTaskbar(true);$('#task-log-level')?.focus();return;}
2078
- inputEpoch++;
2079
- if(event.target.id==='job-choice'){ui.selectedJobId=event.target.value;$('#page').innerHTML=renderJob();$('#job-choice')?.focus();return;}
2080
- if(event.target.id==='history-filter'){ui.historyFilter=['all','failed','pending','mismatch'].includes(event.target.value)?event.target.value:'all';ui.historyPage=1;render();$('#history-filter').focus();return;}
2081
- if(event.target.id==='history-size'){if([30,60,100].includes(Number(event.target.value)))ui.historySize=Number(event.target.value);ui.historyPage=1;render();$('#history-size').focus();return;}
2082
- if(event.target.id==='phone-model-filter'){ui.modelFilter=event.target.value;ui.modelPage=1;render();$('#phone-model-filter').focus();return;}
2083
- if(event.target.id==='phone-model-material'){ui.modelMaterial=event.target.value;ui.modelPage=1;render();return;}
2084
- if(event.target.id==='upload-category'){ui.uploadCategory=event.target.value;return;}
2085
- if(event.target.id==='phone-model-scope'){
2086
- const current=pageLease();
2087
- try{const config=structuredClone(ui.data.config);config.listing.model_scope=event.target.value;await viewAction('config.save',{config});toast('型号范围已保存,预检计划需重新生成');render();}catch(error){if(current()){showError(error);render();}}return;
2088
- }
2089
- if(event.target?.dataset?.businessOffer){const offer=event.target.dataset.businessOffer;if(event.target.checked)ui.businessSelected.add(offer);else ui.businessSelected.delete(offer);$('#business-count').textContent=ui.businessSelected.size;return;}
2090
-
2091
- const target=event.target;
2092
- if(target.dataset.businessClear && target.checked){const input=document.querySelector(`[data-business="${target.dataset.businessClear}"][data-key="${target.dataset.key}"]`);if(input)input.value='';}
2093
- if(target.dataset.business || target.dataset.businessClear || ['business-default-warehouse','business-no-stock-patterns'].includes(target.id))collectCompatibilityDraft();
2094
- if(ui.page==='commerce' && (target.dataset.business || target.dataset.businessClear || target.id==='business-warehouse')){ui.businessChanged=true;collectBusinessDraft();}
2095
- try {
2096
- if(categoryModule && target.id==='category-edit-choice'){collectCompatibilityDraft();try{await selectCategoryTemplate(target.value);}finally{target.value=categoryState().editingId;}return;}
2097
- if(categoryModule && target.id==='category-style-choice'){collectCompatibilityDraft();categoryModule.collectEditor(categoryState(),document);categoryModule.editStyle(categoryState(),target.value);render();return;}
2098
- if(categoryModule && ui.page==='settings/categoryTemplates' && ['category-split-material','category-material'].includes(target.id)){collectCompatibilityDraft();categoryModule.collectEditor(categoryState(),document);render();$(`#${target.id}`)?.focus();return;}
2099
- if(categoryModule && ui.page==='settings/categoryTemplates' && (target.id==='category-type' || target.dataset.templateClear!==undefined || target.dataset.templateStyleClear!==undefined)){
2100
- if(target.dataset.templateClear && target.checked){const input=document.querySelector(`[data-template-business="${target.dataset.templateClear}"]`);if(input)input.value='';}
2101
- if(target.dataset.templateStyleClear && target.checked){const input=document.querySelector(`[data-template-style-business="${target.dataset.templateStyleClear}"]`);if(input)input.value='';}
2102
- categoryModule.collectEditor(categoryState(),document);if(target.id==='category-type')render();else updateCategoryStatus();return;
2103
- }
2104
- if(categoryModule && ['category-generation-choice','category-generation-style'].includes(target.id)){categoryModule.collectNew(categoryState(),document);render();return;}
2105
- if(cloudMode && target.id==='workspace-choice'){await switchWorkspace(target.value);return;}
2106
- if(target.id==='store-choice'){await viewAction('stores.select',{id:target.value});resetQualityDraft();toast('目标店铺已切换,需重新预检');render();}
2107
- if(target.id==='quality-certificate'){const id=target.value?Number(target.value):null;if(id!==null && (!Number.isSafeInteger(id) || id<=0 || !ui.qualityDocuments.some(item=>item.certificate_id===id && !qualityUnavailable(item))))throw new Error('请选择当前店铺可用的质量文件');if(id!==ui.qualityId){ui.qualityId=id;ui.qualityDirty=Boolean(ui.data.plan);}render();$('#quality-certificate')?.focus();}
2108
- if(target.dataset.libraryFilter){ui.libraryFilter[target.dataset.libraryFilter]=target.value;ui.libraryPage=1;render();document.querySelector(`[data-library-filter="${target.dataset.libraryFilter}"]`)?.focus();}
2109
- if(target.id==='queue-filter'){ui.queueFilter=target.value;ui.queuePage=1;ui.queueDetail=null;render();$('#queue-filter').focus();}
2110
- if(target.dataset.queueColumn){const columns=new Set(ui.queueColumns);target.checked?columns.add(target.dataset.queueColumn):columns.delete(target.dataset.queueColumn);ui.queueColumns=normalizeQueueView({columns:[...columns],columns_version:3}).columns;render();document.querySelector(`[data-queue-column="${target.dataset.queueColumn}"]`)?.focus();}
2111
- if(target.id==='list-size'){if([30,60,100,500].includes(Number(target.value)))ui.pageSize=Number(target.value);ui.libraryPage=ui.queuePage=ui.modelPage=1;render();$('#list-size').focus();}
2112
- if(target.id==='auto-code'){ui.autoCode=target.value;render();}
2113
- if(target.dataset.autoPrimary){const i=Number(target.dataset.autoPrimary),row=ui.autoDraft.rows[i],g=ui.autoDraft.groups[i],index=row.images.indexOf(target.value);g.primary_image=target.value;g.images=(index>=0?[...row.images.slice(index+1),...row.images.slice(0,index)]:[...row.images]).slice(0,49);}
2114
- if(target.dataset.groupCheck || target.id==='group-all'){
2115
- let ids=[...ui.data.selected_groups];
2116
- const candidates=queueView().items;
2117
- const changed=target.id==='group-all'?candidates.map(item=>item.id):[ui.data.groups.find(item=>String(item.id)===target.dataset.groupCheck).id];
2118
- ids=target.checked?[...new Set([...ids,...changed])]:ids.filter(id=>!changed.includes(id));await viewAction('queue.select',{ids});render();
2119
- }
2120
- if(target.dataset.designCheck){target.checked?ui.designs.add(target.dataset.designCheck):ui.designs.delete(target.dataset.designCheck);renderChrome();const recognize=document.querySelector('[data-action="recognize"]'),write=document.querySelector('[data-action="write-designs"]');setActionDisabled(recognize,!ui.designs.size);setActionDisabled(write,!ui.designs.size);}
2121
- if(target.id==='design-all'){ui.data.designs.filter(item=>`${item.code} ${item.folder}`.toLowerCase().includes(ui.search.toLowerCase())).forEach(item=>target.checked?ui.designs.add(String(item.id)):ui.designs.delete(String(item.id)));render();}
2122
- if(target.dataset.fileCheck){target.checked?ui.files.add(target.dataset.fileCheck):ui.files.delete(target.dataset.fileCheck);const remove=document.querySelector('[data-action="library-delete"]');setActionDisabled(remove,!ui.files.size);const summary=$('#selected-media-summary');if(summary)summary.textContent=`已选 ${ui.files.size} 个 · ${bytes(ui.data.library.filter(item=>ui.files.has(item.name)).reduce((sum,item)=>sum+item.size,0))}`;const candidates=libraryView().items;if($('#file-all'))$('#file-all').checked=candidates.length>0 && candidates.every(item=>ui.files.has(item.name));}
2123
- if(target.id==='file-all'){libraryView().items.forEach(item=>target.checked?ui.files.add(item.name):ui.files.delete(item.name));render();}
2124
- if(target.id==='model-choice' && target.value)$('#cfg-generator-model_id').value=target.value;
2125
- if(target.id==='import-json' && target.files[0]){const text=await target.files[0].text();JSON.parse(text);ui.importText=text;$('#import-text').value=text;target.value='';}
2126
- if(target.id==='manual-type'){collectManual();ui.manualPaths=[];ui.manualPreview=null;render();}
2127
- if(target.id==='cfg-generator-reasoning_control')updateThinkingFields();
2128
- }catch(error){showError(error);}
2129
- });
2130
- document.addEventListener('submit',async event=>{
2131
- if(!cloudMode || !['login-form','workspace-form','user-form','password-form','server-settings-form'].includes(event.target.id))return;
2132
- event.preventDefault();clearError();const form=event.target,submit=form.querySelector('[type="submit"]');if(submit.disabled)return;submit.disabled=true;let operationEpoch=sessionEpoch;
2133
- try{
2134
- if(form.id==='login-form'){
2135
- const username=$('#login-username').value.trim(),password=$('#login-password').value;
2136
- if(!username || !password)throw new Error('请填写用户名与密码');
2137
- sessionEpoch++;navigationEpoch++;operationEpoch=sessionEpoch;setLoginLoading('正在验证账户…');const session=await post('/api/login',{username,password});$('#login-password').value='';if(await initCloud(session))await reload();
2138
- }else if(form.id==='workspace-form'){
2139
- const name=$('#workspace-name').value.trim();if(!name || name.length>60)throw new Error('工作区名称需要 1–60 个字符');
2140
- const current=$('#workspace-new-tab').value==='current';if(current && busy())throw new Error('当前工作区任务运行中,请选择在新标签页打开');
2141
- if(current && !await leaveMedia())return;
2142
- const workspace=await post('/api/workspaces',{name});await refreshWorkspaces();$('#workspace-name').value='';
2143
- if(current)await switchWorkspace(workspace.id);else{openWorkspace(workspace.id);render();}
2144
- }else if(form.id==='password-form'){
2145
- const current_password=$('#current-password').value,new_password=$('#new-password').value;
2146
- if(!current_password || new_password.length<8 || new_password.length>200)throw new Error('请填写原密码和 8–200 个字符的新密码');
2147
- if(new_password!==$('#confirm-password').value)throw new Error('两次新密码不一致,请重新确认');
2148
- await post('/api/password',{current_password,new_password});$('#current-password').value='';$('#new-password').value='';$('#confirm-password').value='';toast('自己的密码已修改');
2149
- }else if(form.id==='user-form'){
2150
- if(!isSuperAdmin())throw new Error('创建账户仅限超级管理员');
2151
- const username=$('#new-username').value.trim(),password=$('#user-password').value,role=$('#user-role').value;
2152
- if(!/^[A-Za-z0-9_-]{1,32}$/.test(username))throw new Error('用户名需要 1–32 位字母、数字、下划线或横线');
2153
- if(password.length<8 || password.length>200)throw new Error('初始密码需要 8–200 个字符');
2154
- if(!['admin','member'].includes(role))throw new Error('请选择有效的账户角色');
2155
- await post('/api/users',{username,password,role});$('#user-password').value='';$('#new-username').value='';ui.users=(await request('/api/users')).users;render();toast(`${role==='admin'?'管理员':'普通人员'}已创建,可直接登录`);
2156
- }else if(form.id==='server-settings-form'){
2157
- if(!isAdmin())throw new Error('运行限制仅限管理员');const limits={};document.querySelectorAll('[data-server-limit]').forEach(input=>{const value=Number(input.value);if(!Number.isSafeInteger(value) || value<1)throw new Error('运行限制必须是正整数');limits[input.dataset.serverLimit]=value;});
2158
- await post('/api/server-settings',limits);ui.serverSettings=await request('/api/server-settings');render();toast('小组运行限制已保存');
2159
- }
2160
- }catch(error){showError(error);}finally{submit.disabled=false;if(operationEpoch===sessionEpoch && form.id==='login-form' && !ui.ready)setLoginLoading();}
2161
- });
2162
- reload().then(async () => {
2163
- if(!ui.data)return;
2164
- await resumeJobs();
2165
- if(!cloudMode&&ui.data.update_enabled)setTimeout(()=>checkUpdates(true),1500);
2166
- }).catch(error=>{if(error?.code==='STALE_VIEW')return;if(cloudMode && !ui.ready){setLoginLoading();if(!$('#login-form'))showLogin();showError(error);return;}showError(error);$('#connection').textContent=cloudMode?'云端连接失败':'本地服务连接失败';$('#connection').className='status bad';$('#page').innerHTML=empty(cloudMode?'未连接到云端工作台':'未连接到本地服务',cloudMode?'检查网络后刷新页面;已有后台任务继续运行。':'检查工作台进程后刷新页面。');});
2167
- }