detail.vue 75 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614
  1. <template>
  2. <BlueHeaderBar />
  3. <div class="bg-F5F6F7 detail-page">
  4. <div class="page—wrap max-w-full">
  5. <!-- 服务标签页 -->
  6. <div class="service-tabs">
  7. <div
  8. class="service-tab"
  9. :class="{
  10. 'active': form.services.includes('is_product_scene'),
  11. 'disabled': false
  12. }"
  13. @click="toggleService('is_product_scene')"
  14. v-log="{ describe: { action: '点击服务标签', service: '场景图生成' } }"
  15. >
  16. <el-checkbox
  17. :model-value="form.services.includes('is_product_scene')"
  18. @change="toggleService('is_product_scene')"
  19. @click.stop
  20. class="tab-checkbox"
  21. />
  22. <div class="tab-content">
  23. <div class="tab-img flex">
  24. <img v-if="form.services.includes('is_product_scene')" src="@/assets/images/detail/cjt_h.svg" alt="场景图生成" class="tab-icon" />
  25. <img v-else src="@/assets/images/detail/cjt.svg" alt="场景图生成" class="tab-icon" />
  26. </div>
  27. <span class="tab-name">场景图生成</span>
  28. </div>
  29. <div class="tab-edit-btn" @click.stop="openScenePromptDialog" v-log="{ describe: { action: '点击编辑场景图', service: '场景图生成-弹窗' } }">
  30. <el-icon><EditPen /></el-icon>
  31. </div>
  32. </div>
  33. <div
  34. class="service-tab"
  35. :class="{
  36. 'active': form.services.includes('is_upper_footer'),
  37. 'disabled': false
  38. }"
  39. @click="toggleService('is_upper_footer')"
  40. v-log="{ describe: { action: '点击服务标签', service: '模特图生成' } }"
  41. >
  42. <el-checkbox
  43. :model-value="form.services.includes('is_upper_footer')"
  44. @change="toggleService('is_upper_footer')"
  45. @click.stop
  46. class="tab-checkbox"
  47. />
  48. <div class="tab-content">
  49. <div class="tab-img flex">
  50. <img v-if="form.services.includes('is_upper_footer')" src="@/assets/images/detail/mtt_h.svg" alt="模特图生成" class="tab-icon" />
  51. <img v-else src="@/assets/images/detail/mtt.svg" alt="模特图生成" class="tab-icon" />
  52. </div>
  53. <span class="tab-name">模特图生成</span>
  54. </div>
  55. <div class="tab-edit-btn" @click.stop="openModelDialog" v-log="{ describe: { action: '点击编辑模特图', service: '模特图生成-弹窗' } }">
  56. <el-icon><EditPen /></el-icon>
  57. </div>
  58. </div>
  59. <div
  60. class="service-tab"
  61. :class="{
  62. 'active': form.services.includes('is_detail'),
  63. 'disabled': false
  64. }"
  65. @click="toggleService('is_detail')"
  66. v-log="{ describe: { action: '点击服务标签', service: '详情页生成' } }"
  67. >
  68. <el-checkbox
  69. :model-value="form.services.includes('is_detail')"
  70. @change="toggleService('is_detail')"
  71. @click.stop
  72. class="tab-checkbox"
  73. />
  74. <div class="tab-content">
  75. <div class="tab-img flex">
  76. <img v-if="form.services.includes('is_detail')" src="@/assets/images/detail/xqy_h.svg" alt="详情页生成" class="tab-icon" />
  77. <img v-else src="@/assets/images/detail/xqy.svg" alt="详情页生成" class="tab-icon" />
  78. </div>
  79. <span class="tab-name">详情页生成</span>
  80. </div>
  81. </div>
  82. <!-- 未开发的功能 -->
  83. <div class="service-tab disabled" title="功能开发中">
  84. <div class="tab-content">
  85. <div class="tab-img flex">
  86. <img src="@/assets/images/detail/xqmb.svg" alt="详情页模板自定义" class="tab-icon" />
  87. </div>
  88. <span class="tab-name">详情页模板自定义</span>
  89. </div>
  90. </div>
  91. <div class="service-tab disabled" title="功能开发中">
  92. <div class="tab-content">
  93. <div class="tab-img flex">
  94. <img src="@/assets/images/detail/bdt.svg" alt="白底图批量导出" class="tab-icon" />
  95. </div>
  96. <span class="tab-name">白底图批量导出</span>
  97. </div>
  98. </div>
  99. <div class="service-tab disabled" title="功能开发中">
  100. <div class="tab-content">
  101. <div class="tab-img flex">
  102. <img src="@/assets/images/detail/cptc.svg" alt="产品图册生成" class="tab-icon" />
  103. </div>
  104. <span class="tab-name">产品图册生成</span>
  105. </div>
  106. </div>
  107. </div>
  108. <div class="detail-container">
  109. <div class="detail-content">
  110. <!-- 主图LOGO部分 -->
  111. <!--
  112. <div class="logo-section flex left top" >
  113. <div class="section-title" style="margin-bottom: 0px;">
  114. <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
  115. 主图LOGO 与 选择详情模板:
  116. </div>
  117. </div>
  118. <div class="logo-section flex left top multi-line">
  119. <upload v-for="item,index in logoList" :value="item" :key="item"
  120. v-show="item"
  121. @input="onRemove(index)"
  122. class="mar-right-10 upload-item"
  123. :class="{
  124. active: item === form.logo_path
  125. }"
  126. @click.native="form.logo_path = item"
  127. ></upload>
  128. <upload @input="onInput"></upload>
  129. </div>
  130. -->
  131. <!-- &lt;!&ndash; 图片抠图与货号图生成 &ndash;&gt;
  132. <div class="section">
  133. <div class="section-title">
  134. <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
  135. 图片抠图与货号图生成
  136. </div>
  137. <div class="section-content">
  138. <div v-if="showTips" class="instruction-out flex top left">
  139. <img style="fill: #000" src="@/assets/images/xinxi.svg" />
  140. <ol class="instruction-list">
  141. <li>请在下方确认图片拍摄过程中的顺序,确保所有拍摄的图片的顺序一致。</li>
  142. <li>使用中英文语号分隔。</li>
  143. <li>图片的名称不能随意修改,否则无法正常生成详情页。</li>
  144. <li>现有图片名称有:俯视、侧视、后视、鞋底、内里</li>
  145. </ol>
  146. <el-icon @click="showTips = false" class="close-icon">
  147. <Close />
  148. </el-icon>
  149. </div>
  150. &lt;!&ndash; 货号文件夹 &ndash;&gt;
  151. &lt;!&ndash; <div class="form-item">
  152. <div class="label">货号文件夹:</div>
  153. <div class="folder-warp">
  154. <div class="folder-input">
  155. <el-input style="width: 60%;" v-model="folderPath" type="textarea" :rows="2" readonly
  156. placeholder="请选择货号文件夹" />
  157. <el-button class="check-button" type="primary" @click="selectFolder">
  158. <img src="@/assets/images/Photography/wenjian.png" style="width: 14px; " />
  159. 选择目标文件夹</el-button>
  160. </div>
  161. <div class="hint">
  162. <el-icon>
  163. <Warning />
  164. </el-icon> <text>选择货号的上级文件夹</text>
  165. </div>
  166. </div>
  167. </div>
  168. &ndash;&gt;
  169. </div>
  170. </div>
  171. <el-divider />-->
  172. <!-- 左右布局 -->
  173. <div class="main-layout">
  174. <!-- 左侧:选择详情模板 -->
  175. <div class="left-panel">
  176. <div :class="['template-section', { 'template-section--disabled': !isDetailServiceSelected }]">
  177. <div class="section-header">
  178. <div class="section-title">
  179. 选择详情模版
  180. </div>
  181. <div class="template-pagination">
  182. <el-pagination
  183. background
  184. layout="prev, pager, next"
  185. v-model:current-page="queryParams.current"
  186. v-model:page-size.sync="queryParams.size"
  187. :total="totalPage"
  188. @current-change="onCurrentChange"
  189. @size-change="onSizeChange"
  190. />
  191. </div>
  192. </div>
  193. <div class="template-list">
  194. <div
  195. v-for="(template, index) in visibleTemplates"
  196. :key="index"
  197. class="template-item"
  198. :class="form.selectTemplate?.id == template.id ? 'active' : ''"
  199. @click="handleTemplateItemClick(template)"
  200. v-log="{ describe: { action: '点击选择详情模板', template_name: template.template_name } }"
  201. >
  202. <el-image :src="template.template_preview_image" fit="contain" class="cur-p" style="width: 100%; display: block;" />
  203. <div class="select-warp" :class="form.selectTemplate?.id == template.id ? 'active' : ''">
  204. <el-icon color="#FFFFFF">
  205. <Select />
  206. </el-icon>
  207. </div>
  208. <div class="template-info">
  209. <span class="mar-left-10 chaochu_1">{{ template.template_name }}</span>
  210. <div class="template-view" @click.stop="viewTemplate(template)" v-log="{ describe: { action: '点击查看模板详情', template_name: template.template_name } }">查看</div>
  211. </div>
  212. </div>
  213. </div>
  214. <div class="template-tips c-333 fs-14 line-20 te-l mar-top-20 flex left">
  215. <el-icon><Warning /></el-icon>
  216. <span class="mar-left-10">该模版需提供{{form.selectTemplate?.template_image_order?.split(',').length || 5}}张标准视角的商品图:{{form.selectTemplate?.template_image_order || '俯视、侧视、后跟、鞋底、内里'}}。请确保图片清晰度高,背景干净。</span>
  217. </div>
  218. </div>
  219. </div>
  220. <!-- 右侧:LOGO、详情资料准备、一键上架 -->
  221. <div class="right-panel">
  222. <!-- 主图LOGO -->
  223. <div class="right-section">
  224. <div class="section-title">
  225. <div class="section-title-line"></div>
  226. 主图LOGO
  227. </div>
  228. <div class="logo-upload-area">
  229. <div v-if="!form.logo_path" class="logo-upload-placeholder" @click="openLogoUpload">
  230. <div class="logo-upload-icon">
  231. <img src="@/assets/images/detail/sctp.png" />
  232. </div>
  233. <div class="logo-upload-text">点击或拖拽上传</div>
  234. <div class="logo-upload-hint">支持PNG、JPG格式</div>
  235. </div>
  236. <div v-else class="logo-upload-preview">
  237. <img :src="'file:///' + form.logo_path" alt="LOGO预览" class="logo-preview-image" />
  238. <div class="logo-upload-actions">
  239. <span class="logo-action-btn" @click.stop="previewLogo">
  240. <el-icon><ZoomIn /></el-icon>
  241. </span>
  242. <span class="logo-action-btn" @click.stop="removeLogo">
  243. <el-icon><Delete /></el-icon>
  244. </span>
  245. </div>
  246. </div>
  247. </div>
  248. </div>
  249. <!-- 详情资料准备 -->
  250. <div class="right-section data-prep-section">
  251. <div class="section-title">
  252. <div class="section-title-line"></div>
  253. 详情资料准备
  254. </div>
  255. <div class="data-prep-content">
  256. <el-radio-group v-model="form.dataType" class="data-type-radio">
  257. <el-radio-button label="1" size="large">
  258. <img v-if="form.dataType == 1" src="@/assets/images/detail/excel_h.png" />
  259. <img v-else src="@/assets/images/detail/excel.png" />
  260. Excel上传</el-radio-button>
  261. <el-radio-button label="2" size="large">
  262. <img v-if="form.dataType == 2" src="@/assets/images/detail/xtdj_h.png" />
  263. <img v-else src="@/assets/images/detail/xtdj.png" />
  264. 系统对接</el-radio-button>
  265. </el-radio-group>
  266. <div v-if="form.dataType == '1'" class="excel-upload-section">
  267. <div class="excel-upload-area" @click="selectExcel" v-log="{ describe: { action: '点击选择Excel文件' } }">
  268. <div class="excel-icon">
  269. <img src="@/assets/images/detail/file-excel.png" class="tab-icon" />
  270. </div>
  271. <div class="excel-upload-text">点击选择文件</div>
  272. </div>
  273. <el-button
  274. type="text"
  275. class="download-link"
  276. @click="downloadExcel"
  277. v-log="{ describe: { action: '点击下载Excel模板' } }"
  278. >
  279. 下载商品基础资料模版
  280. </el-button>
  281. </div>
  282. </div>
  283. </div>
  284. <!-- 一键上架平台 -->
  285. <div
  286. class="right-section publish-section"
  287. :class="{ 'publish-section--disabled': !canUsePublishSection }"
  288. v-if="onlineStoreTempList.length || onlineStoreTempListForeign.length"
  289. >
  290. <div class="section-title">
  291. <div class="section-title-line"></div>
  292. 一键上架平台
  293. </div>
  294. <div class="publish-content">
  295. <div class="publish-form-item" v-if="onlineStoreTempList.length">
  296. <div class="publish-label">国内电商平台:</div>
  297. <el-select
  298. v-model="domesticPlatforms"
  299. multiple
  300. placeholder="请选择"
  301. class="publish-select"
  302. :disabled="!canUsePublishSection"
  303. >
  304. <el-option
  305. v-for="store in onlineStoreTempList"
  306. :key="store.show_name"
  307. :label="store.show_name"
  308. :value="store.online_store_name"
  309. :disabled="!store.channel_status"
  310. />
  311. </el-select>
  312. </div>
  313. <div class="publish-form-item" v-if="onlineStoreTempListForeign.length">
  314. <div class="publish-label">跨境电商平台:</div>
  315. <el-select
  316. v-model="foreignPlatforms"
  317. multiple
  318. placeholder="请选择"
  319. class="publish-select"
  320. :disabled="!canUsePublishSection"
  321. >
  322. <el-option
  323. v-for="store in onlineStoreTempListForeign"
  324. :key="store.show_name"
  325. :label="store.show_name"
  326. :value="store.online_store_name"
  327. :disabled="!store.channel_status"
  328. />
  329. </el-select>
  330. </div>
  331. </div>
  332. </div>
  333. <!-- 底部按钮 -->
  334. <div class="footer">
  335. <el-button
  336. v-loading="requesting"
  337. class="button--primary1 footer-button"
  338. type="primary"
  339. @click="generate"
  340. v-log="{ describe: { action: '点击开始生成详情页' } }"
  341. >
  342. <img src="@/assets/images/processImage.vue/sc.png" />
  343. 开始生成
  344. <img src="@/assets/images/processImage.vue/go.png" class="go"/>
  345. </el-button>
  346. </div>
  347. </div>
  348. </div>
  349. <!-- 详情高级配置 -->
  350. <!-- <div class="section">
  351. <div class="section-title">
  352. <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
  353. 详情高级配置
  354. </div>
  355. <div class="section-content">
  356. &lt;!&ndash; 图片顺序 &ndash;&gt;
  357. <div class="form-item">
  358. <div class="label">图片顺序:</div>
  359. <el-input v-model="imageOrder" placeholder="请输入图片顺序" class="specific-page-input">
  360. <template #append>
  361. <el-button class="explain-btn" link type="primary">说明</el-button>
  362. </template>
  363. </el-input>
  364. </div>
  365. &lt;!&ndash; 同款检验 &ndash;&gt;
  366. &lt;!&ndash; <div class="form-item">
  367. <div class="label">同款检验:</div>
  368. <el-checkbox v-model="checkSimilar">同款下货号必须齐全</el-checkbox>
  369. </div>
  370. &ndash;&gt;
  371. &lt;!&ndash; 可指定页面独修改 &ndash;&gt;
  372. &lt;!&ndash; <div class="form-item">
  373. <div class="label">可指定页面独修改:</div>
  374. <el-input v-model="specificPage" placeholder="请输入入需要单独修改的页面,示例:4:1 (需修改模版的编号:第一张)"
  375. class="specific-page-input">
  376. <template #append>
  377. <el-button class="explain-btn" link type="primary">说明</el-button>
  378. </template>
  379. </el-input>
  380. </div>
  381. &ndash;&gt;
  382. </div>
  383. </div>
  384. <el-divider />-->
  385. </div>
  386. </div>
  387. </div>
  388. </div>
  389. <loading-dialog v-if="loadingDialogVisible" v-model="loadingDialogVisible" :requesting="requesting" :progress="progress" :message="message"
  390. :disabled-button="disabledButton" :use-new-progress="useNewProgress" :progress-steps="progressSteps" @button-click="handleComplete" :on-open-folder="openOutputDir">
  391. <template v-if="partErrList && partErrList.length > 0" #errList>
  392. <div v-for="(item, idx) in partErrList" :key="idx">
  393. <span v-if="item.goods_art_no">{{ item.goods_art_no }}:</span><span>{{ item.info }}</span>
  394. </div>
  395. </template>
  396. <template #progressMessages>
  397. <div class="progress-messages" v-if="progressMessages.length">
  398. <div class="message-header">
  399. <span>处理进度</span>
  400. <div class="flex right" style="gap:8px; align-items:center;">
  401. <el-button type="text" @click="openOutputDir" v-log="{ describe: { action: '点击打开输出目录' } }">打开目录</el-button>
  402. <el-button type="text" @click="showMessageHistory = !showMessageHistory" v-log="{ describe: { action: '点击查看进度详情' } }">
  403. {{ showMessageHistory ? '收起' : '查看详情' }}
  404. </el-button>
  405. </div>
  406. </div>
  407. <div class="message-list" v-if="showMessageHistory" ref="messageListRef">
  408. <div v-for="(msg, index) in progressMessages" :key="index" class="message-item flex left">
  409. <div class="message-time">{{ formatTime(msg.timestamp) }}</div>
  410. <div class="message-content mar-left-10" v-if="msg">
  411. <span class="goods-no" v-if="msg.goods_art_nos && msg.goods_art_nos.length > 0">货号{{ msg.goods_art_nos.join(', ') }}:</span>
  412. <span class="message-text">{{ msg.msg }}</span>
  413. </div>
  414. </div>
  415. </div>
  416. </div>
  417. </template>
  418. </loading-dialog>
  419. <el-dialog v-model="dialogVisible">
  420. <img style="width: 100%;" :src="dialogImageUrl" alt="Preview Image" />
  421. </el-dialog>
  422. <!-- LOGO预览弹窗 -->
  423. <el-dialog v-model="logoPreviewVisible" title="LOGO预览">
  424. <img style="width: 100%;" :src="logoPreviewUrl" alt="LOGO Preview" />
  425. </el-dialog>
  426. <!-- 模特生成弹窗 -->
  427. <ModelGenerationDialog
  428. v-model="modelDialogVisible"
  429. :initial-models="selectedModels"
  430. @confirm="handleModelSelection"
  431. @cancel="modelDialogVisible = false"
  432. />
  433. <!-- 场景提示词弹窗 -->
  434. <ScenePromptDialog
  435. v-model="scenePromptDialogVisible"
  436. :initial-prompt="scenePrompt"
  437. @confirm="handleScenePromptConfirm"
  438. @cancel="scenePromptDialogVisible = false"
  439. />
  440. </template>
  441. <script lang="ts" setup>
  442. import { getCompanyTemplatesApi } from '@/apis/other'
  443. import tokenInfo from '@/stores/modules/token';
  444. import useUserInfo from "@/stores/modules/user";
  445. import { useRoute, useRouter } from 'vue-router'
  446. import { clickLog, setLogInfo } from '@/utils/log'
  447. import { ElMessage, ElMessageBox } from 'element-plus'
  448. import BlueHeaderBar from '@/components/header-bar/blue-header.vue'
  449. import { ref, computed, reactive, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
  450. import { Select, EditPen, ZoomIn, Delete, Picture, Document } from '@element-plus/icons-vue'
  451. // import upload from '@/components/upload' // 不再需要,改为单LOGO上传
  452. import client from "@/stores/modules/client";
  453. import icpList from '@/utils/ipc'
  454. const clientStore = client();
  455. import { getRouterUrl } from '@/utils/appfun'
  456. import { useUuidStore } from '@/stores/modules/uuid'
  457. import socket from "@/stores/modules/socket";
  458. const socketStore = socket();
  459. import ModelGenerationDialog from '@/components/ModelGeneration/index.vue'
  460. import ScenePromptDialog from '@/components/ScenePromptDialog/index.vue'
  461. import { Close, Warning } from '@element-plus/icons-vue'
  462. import LoadingDialog from '@/views/Photography/components/LoadingDialog.vue'
  463. import configInfo from "@/stores/modules/config";
  464. const useConfigInfoStore = configInfo();
  465. import { useCheckInfo } from '@/composables/userCheck';
  466. useCheckInfo();
  467. const showTips = ref(true)
  468. const folderPath = ref('') //货号文件夹
  469. // const reportMode = ref('normal') // 抠图模式
  470. const imageOrder = ref('俯视、侧视、后跟、鞋底、内里、组合、组合2、组合3') // 图片顺序
  471. const checkSimilar = ref(false) // 同款检验
  472. const specificPage = ref('') // 可指定页面独修改
  473. // 路由和状态管理初始化
  474. const route = useRoute();
  475. const router = useRouter();
  476. const uuidStore = useUuidStore();
  477. // 完成目录
  478. const completeDirectory = ref('')
  479. const loadingDialogVisible = ref(false)
  480. const progress = ref(0)
  481. const message = ref('正在为您处理,请稍后')
  482. const disabledButton = ref(true)
  483. // 新的进度条相关数据
  484. const useNewProgress = ref(false)
  485. const progressSteps = ref<Array<{
  486. msg_type: string
  487. goods_art_no: string
  488. name: string
  489. status: '等待处理' | '正在处理' | '处理完成' | '处理失败'
  490. current: number
  491. total: number
  492. error: number
  493. }>>([])
  494. // 更新进度步骤
  495. const updateProgressStep = (msgType: string, stepData: any) => {
  496. console.log('updateProgressStep called:', msgType, stepData)
  497. const stepIndex = progressSteps.value.findIndex(step => step.msg_type === msgType)
  498. if (stepIndex !== -1) {
  499. // 更新现有步骤 - 使用新数组来确保响应式更新
  500. const newSteps = [...progressSteps.value]
  501. newSteps[stepIndex] = {
  502. ...newSteps[stepIndex],
  503. name: stepData.name, // 保持原有名称或使用新名称
  504. goods_art_no: stepData.goods_art_no,
  505. status: stepData.status,
  506. current: stepData.current || 0,
  507. total: stepData.total || 0,
  508. error: stepData.error || 0,
  509. folder: newSteps[stepIndex].folder || stepData.folder // 保持已有的folder或使用新的
  510. }
  511. progressSteps.value = newSteps
  512. console.log('Updated step:', newSteps[stepIndex])
  513. } else {
  514. // 添加新步骤
  515. progressSteps.value = [...progressSteps.value, {
  516. msg_type: msgType,
  517. name: stepData.name,
  518. goods_art_no: stepData.goods_art_no,
  519. status: stepData.status,
  520. current: stepData.current || 0,
  521. total: stepData.total || 0,
  522. error: stepData.error || 0,
  523. folder: stepData.folder // 添加folder字段
  524. }]
  525. console.log('Added new step:', progressSteps.value[progressSteps.value.length - 1])
  526. }
  527. }
  528. // 获取步骤名称
  529. // 初始化进度步骤 - 从后端数据动态获取
  530. const initProgressSteps = (backendSteps?: any[]) => {
  531. if (backendSteps && backendSteps.length > 0) {
  532. // 使用后端返回的步骤数据
  533. progressSteps.value = backendSteps.map(step => ({
  534. msg_type: step.msg_type,
  535. goods_art_no: step.goods_art_no,
  536. name: step.name,
  537. status: '等待处理',
  538. current: 0,
  539. total: step.total || 0,
  540. error: 0
  541. }))
  542. } else {
  543. // 默认步骤(当后端没有返回步骤数据时使用)
  544. progressSteps.value = []
  545. }
  546. }
  547. // 进度消息队列
  548. const progressMessages = ref<Array<{
  549. goods_no: string
  550. temp_name: string
  551. status: string
  552. goods_art_nos: string[]
  553. msg: string
  554. timestamp: number
  555. }>>([])
  556. const showMessageHistory = ref(true)
  557. const messageListRef = ref<HTMLElement | null>(null)
  558. // 新消息时自动滚动到底部
  559. const scrollMessageListToBottom = () => {
  560. nextTick(() => {
  561. const el = messageListRef.value
  562. if (el) {
  563. el.scrollTop = el.scrollHeight
  564. }
  565. })
  566. }
  567. let templates = ref([])
  568. let goods_art_nos = ref([])
  569. let partErrList = ref([])
  570. const excel_template_url = ref('')
  571. // 是否正在请求接口
  572. const requesting = ref(false)
  573. // 定义一个定时器变量
  574. const INTERVAL = ref<number | NodeJS.Timeout | null>(null);
  575. // 状态变量
  576. const totalPage = ref(0);
  577. const itemsPerPage = 3; // 每页显示的模板数量
  578. const dialogVisible = ref(false);
  579. const dialogImageUrl = ref('');
  580. const queryParams = reactive({ // 分页查询参数
  581. size: 1,
  582. current: 1,
  583. })
  584. const form = reactive({
  585. selectTemplate: {}, //选中的模板
  586. dataType: '1', // 1: 选择excel文件 2: 系统对接
  587. logo_path: '', // 主图LOGO
  588. excel_path: '', // 商品基础资料EXCEL文件选择
  589. services: ['is_detail'], // 勾选服务内容(多选)默认包含详情页生成
  590. })
  591. const templatesLoaded = ref(false)
  592. const lastSelectedTemplateId = ref<string | number | null>(null)
  593. const isDetailServiceSelected = computed(() => form.services.includes('is_detail'))
  594. const hasTemplates = computed(() => templates.value.length > 0)
  595. const canUsePublishSection = computed(() => isDetailServiceSelected.value && hasTemplates.value)
  596. onMounted(() => {
  597. // 页面访问埋点
  598. const goods_art_data = route.query.goods_art_nos
  599. goods_art_nos.value = Array.isArray(goods_art_data) ? goods_art_data : [goods_art_data]
  600. getCompanyTemplates()
  601. getLogolist()
  602. loadDetailCache()
  603. // 初始化目录打开状态标记
  604. window.segmentFolderOpened = false;
  605. window.modelOrSceneFolderOpened = false;
  606. // 监听子组件发出的打开目录事件
  607. window.addEventListener('openFolder', handleOpenFolder);
  608. })
  609. // 页面卸载时清理监听器
  610. onBeforeUnmount(() => {
  611. clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
  612. clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
  613. clientStore.ipc.removeAllListeners(icpList.socket.message + '_upper_footer_progress');
  614. clientStore.ipc.removeAllListeners(icpList.socket.message + '_scene_progress');
  615. clientStore.ipc.removeAllListeners(icpList.socket.message + '_upload_goods_progress');
  616. clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
  617. clearInterval(INTERVAL.value);
  618. // 移除事件监听器
  619. window.removeEventListener('openFolder', handleOpenFolder);
  620. })
  621. // 计算属性,获取当前页可见的模板
  622. const visibleTemplates = computed(() => {
  623. const startIndex = (queryParams.current - 1) * itemsPerPage;
  624. const data = templates.value.slice(startIndex, startIndex + itemsPerPage);
  625. return data
  626. });
  627. const selectTemplate = (template: any, options: { saveCache?: boolean; ensureVisible?: boolean } = {}) => {
  628. if (!isDetailServiceSelected.value) return
  629. if (!template || !template.id) return
  630. form.selectTemplate = template
  631. lastSelectedTemplateId.value = template.id
  632. if (options.ensureVisible) {
  633. jumpToTemplatePageById(template.id)
  634. }
  635. const shouldSaveCache = options.saveCache !== undefined ? options.saveCache : true
  636. if (shouldSaveCache) {
  637. try {
  638. localStorage.setItem(DETAIL_TEMPLATE_CACHE_KEY, JSON.stringify(template))
  639. console.log('selectTemplate - saved to cache:', template);
  640. } catch {}
  641. }
  642. };
  643. const handleTemplateItemClick = (template: any) => {
  644. if (!isDetailServiceSelected.value) return
  645. selectTemplate(template)
  646. };
  647. const clearTemplateSelection = () => {
  648. form.selectTemplate = {}
  649. };
  650. const findTemplateById = (templateId: string | number | null) => {
  651. if (templateId === null || templateId === undefined) return null
  652. return templates.value.find(template => String(template.id) === String(templateId)) || null
  653. };
  654. const jumpToTemplatePageById = (templateId: string | number | null) => {
  655. if (templateId === null || templateId === undefined) return
  656. const index = templates.value.findIndex(template => String(template.id) === String(templateId))
  657. if (index === -1) return
  658. const targetPage = Math.floor(index / itemsPerPage) + 1
  659. if (queryParams.current !== targetPage) {
  660. queryParams.current = targetPage
  661. }
  662. };
  663. const ensureDefaultTemplateSelection = (options: { ensureVisible?: boolean } = {}) => {
  664. if (!templatesLoaded.value || !isDetailServiceSelected.value || !templates.value.length) {
  665. return
  666. }
  667. if (form.selectTemplate && (form.selectTemplate as any).id) {
  668. if (options.ensureVisible) {
  669. jumpToTemplatePageById((form.selectTemplate as any).id)
  670. }
  671. return
  672. }
  673. const cachedTemplate = findTemplateById(lastSelectedTemplateId.value)
  674. const templateToSelect = cachedTemplate || templates.value[0]
  675. if (templateToSelect) {
  676. selectTemplate(templateToSelect, {
  677. ensureVisible: options.ensureVisible,
  678. saveCache: !cachedTemplate
  679. })
  680. }
  681. };
  682. watch(isDetailServiceSelected, (selected) => {
  683. if (selected) {
  684. ensureDefaultTemplateSelection({ ensureVisible: true })
  685. } else {
  686. clearTemplateSelection()
  687. }
  688. });
  689. // 查看模板详情
  690. const viewTemplate = (template) => {
  691. // 展示大图
  692. dialogVisible.value = true
  693. dialogImageUrl.value = template.template_preview_image
  694. };
  695. // 获取模版列表
  696. const getCompanyTemplates = async () => {
  697. const { data } = await getCompanyTemplatesApi()
  698. console.log(data);
  699. templates.value = data.list || []
  700. // 获取电商平台列表 - 支持新的数据结构
  701. if (data.online_store_temp_list) {
  702. onlineStoreTempList.value = data.online_store_temp_list
  703. }
  704. if (data.online_store_temp_list_foreign) {
  705. onlineStoreTempListForeign.value = data.online_store_temp_list_foreign
  706. }
  707. templatesLoaded.value = true
  708. // 获取模板列表后,尝试从缓存恢复模板选择
  709. loadTemplateFromCache()
  710. excel_template_url.value = data.excel_template_url
  711. // 计算总页数
  712. totalPage.value = Math.ceil(templates.value.length / itemsPerPage);
  713. }
  714. const downloadExcel = () => {
  715. const a = document.createElement('a')
  716. a.href = excel_template_url.value,
  717. a.download = '商品基础资料模版'
  718. document.body.appendChild(a)
  719. a.click()
  720. setTimeout(() => {
  721. document.body.removeChild(a);
  722. }, 1000);
  723. }
  724. // 服务内容切换
  725. const toggleService = (key: string) => {
  726. const idx = form.services.indexOf(key)
  727. if (idx > -1) form.services.splice(idx, 1)
  728. else form.services.push(key)
  729. // 保存服务选择状态到缓存
  730. saveServicesToCache(form.services)
  731. }
  732. // 电商平台多选与一键上架
  733. const domesticPlatforms = ref<string[]>([])
  734. const foreignPlatforms = ref<string[]>([])
  735. const onlineStoreTempList = ref<any[]>([]) // 国内电商平台列表
  736. const onlineStoreTempListForeign = ref<any[]>([]) // 国外电商平台列表
  737. // 模特与场景弹窗
  738. const modelDialogVisible = ref(false)
  739. const scenePromptDialogVisible = ref(false)
  740. const selectedModels = ref<{ female: any; male: any } | null>(null)
  741. const scenePrompt = ref('')
  742. // 本地缓存键(与弹窗组件保持一致)
  743. const DETAIL_MODEL_CACHE_KEY = 'model_selection_cache'
  744. const DETAIL_SCENE_PROMPT_CACHE_KEY = 'scene_prompt_cache'
  745. const DETAIL_LOGO_CACHE_KEY = 'detail_logo_cache'
  746. const DETAIL_DATA_TYPE_CACHE_KEY = 'detail_data_type_cache'
  747. const DETAIL_SERVICES_CACHE_KEY = 'detail_services_cache'
  748. const DETAIL_TEMPLATE_CACHE_KEY = 'detail_template_cache'
  749. // 读取本地缓存
  750. const loadDetailCache = () => {
  751. console.log('loadDetailCache');
  752. try {
  753. const m = localStorage.getItem(DETAIL_MODEL_CACHE_KEY)
  754. if (m) {
  755. const parsed = JSON.parse(m)
  756. if (parsed && (parsed.female || parsed.male)) {
  757. selectedModels.value = parsed
  758. console.log('loadDetailCache');
  759. console.log(selectedModels.value);
  760. }
  761. }
  762. } catch {}
  763. try {
  764. const p = localStorage.getItem(DETAIL_SCENE_PROMPT_CACHE_KEY)
  765. if (p) {
  766. console.log('scenePrompt');
  767. scenePrompt.value = p
  768. console.log(scenePrompt.value);
  769. }
  770. } catch {}
  771. // 加载LOGO缓存
  772. try {
  773. const logo = localStorage.getItem(DETAIL_LOGO_CACHE_KEY)
  774. if (logo) {
  775. form.logo_path = logo
  776. console.log('loadDetailCache - logo:', logo);
  777. }
  778. } catch {}
  779. // 加载数据类型缓存
  780. try {
  781. const dataType = localStorage.getItem(DETAIL_DATA_TYPE_CACHE_KEY)
  782. if (dataType) {
  783. form.dataType = dataType
  784. console.log('loadDetailCache - dataType:', dataType);
  785. }
  786. } catch {}
  787. // 模板缓存加载将在获取模板列表后执行
  788. // 加载服务选择状态缓存
  789. try {
  790. const services = localStorage.getItem(DETAIL_SERVICES_CACHE_KEY)
  791. if (services) {
  792. const parsed = JSON.parse(services)
  793. if (Array.isArray(parsed)) {
  794. form.services = parsed
  795. console.log('loadDetailCache - services:', parsed);
  796. }
  797. }
  798. } catch {}
  799. }
  800. // 保存到本地缓存(仅保存必要字段)
  801. const saveModelsToCache = (models: { female: any; male: any }) => {
  802. try {
  803. const payload = {
  804. female: models?.female ? {
  805. id: models.female.id,
  806. name: models.female.name,
  807. image_url: models.female.image_url,
  808. gender: models.female.gender,
  809. keywords: models.female.keywords,
  810. status: models.female.status
  811. } : null,
  812. male: models?.male ? {
  813. id: models.male.id,
  814. name: models.male.name,
  815. image_url: models.male.image_url,
  816. gender: models.male.gender,
  817. keywords: models.male.keywords,
  818. status: models.male.status
  819. } : null
  820. }
  821. localStorage.setItem(DETAIL_MODEL_CACHE_KEY, JSON.stringify(payload))
  822. } catch {}
  823. }
  824. const saveScenePromptToCache = (prompt: string) => {
  825. try {
  826. const v = (prompt || '').trim()
  827. if (v) localStorage.setItem(DETAIL_SCENE_PROMPT_CACHE_KEY, v)
  828. } catch {}
  829. }
  830. // 保存LOGO到缓存
  831. const saveLogoToCache = (logoPath: string) => {
  832. try {
  833. if (logoPath) {
  834. localStorage.setItem(DETAIL_LOGO_CACHE_KEY, logoPath)
  835. console.log('saveLogoToCache:', logoPath);
  836. }
  837. } catch {}
  838. }
  839. // 保存数据类型到缓存
  840. const saveDataTypeToCache = (dataType: string) => {
  841. try {
  842. if (dataType) {
  843. localStorage.setItem(DETAIL_DATA_TYPE_CACHE_KEY, dataType)
  844. console.log('saveDataTypeToCache:', dataType);
  845. }
  846. } catch {}
  847. }
  848. const saveServicesToCache = (services: string[]) => {
  849. try {
  850. localStorage.setItem(DETAIL_SERVICES_CACHE_KEY, JSON.stringify(services))
  851. console.log('saveServicesToCache:', services);
  852. } catch {}
  853. }
  854. // 从缓存加载模板选择
  855. const loadTemplateFromCache = () => {
  856. lastSelectedTemplateId.value = null
  857. try {
  858. const template = localStorage.getItem(DETAIL_TEMPLATE_CACHE_KEY)
  859. if (template) {
  860. const parsed = JSON.parse(template)
  861. if (parsed && parsed.id) {
  862. lastSelectedTemplateId.value = parsed.id
  863. console.log('loadTemplateFromCache - template id:', parsed.id);
  864. }
  865. }
  866. } catch (error) {
  867. console.error('加载模板缓存失败:', error);
  868. }
  869. ensureDefaultTemplateSelection({ ensureVisible: true })
  870. }
  871. const openModelDialog = () => {
  872. modelDialogVisible.value = true
  873. }
  874. const openScenePromptDialog = () => {
  875. scenePromptDialogVisible.value = true
  876. }
  877. // 选择LOGO
  878. // selectLogo 函数已移除,现在只支持单个LOGO上传
  879. const handleModelSelection = (models: { female: any; male: any }) => {
  880. selectedModels.value = models
  881. saveModelsToCache(models)
  882. modelDialogVisible.value = false
  883. ElMessage.success('模特选择完成!')
  884. }
  885. const handleScenePromptConfirm = (prompt: string) => {
  886. scenePrompt.value = prompt
  887. saveScenePromptToCache(prompt)
  888. }
  889. const onCurrentChange = (page) => {
  890. queryParams.current = page;
  891. };
  892. const onSizeChange = (data) => {
  893. };
  894. // 格式化时间
  895. const formatTime = (timestamp: number) => {
  896. const date = new Date(timestamp)
  897. return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`
  898. }
  899. // 处理进度消息
  900. const handleProgressMessage = (data: any) => {
  901. console.log("detail_progress", data);
  902. if (data.code === 0 && data.msg_type === 'detail_progress') {
  903. const messageData = {
  904. goods_no: data.data.goods_no,
  905. temp_name: data.data.temp_name,
  906. status: data.data.status,
  907. goods_art_nos: data.data.goods_art_nos,
  908. msg: data.msg,
  909. timestamp: Date.now()
  910. }
  911. progressMessages.value.push(messageData)
  912. // 更新当前显示的消息
  913. message.value = data.data.goods_art_nos ? `货号${data.data.goods_art_nos.join(', ')}:${data.msg}` : `${data.msg}`
  914. scrollMessageListToBottom()
  915. // 更新新的进度条
  916. if (data.progress) {
  917. updateProgressStep('detail_progress', data.progress)
  918. }
  919. }
  920. }
  921. // 处理抠图进度消息
  922. const handleSegmentProgressMessage = (data: any) => {
  923. console.log("segment_progress", data);
  924. if (data.code === 0 && data.msg_type === 'segment_progress') {
  925. const messageData = {
  926. goods_no: '',
  927. temp_name: '',
  928. status: data.data?.status || '',
  929. goods_art_nos: data.data?.goods_art_nos || [],
  930. msg: data.msg,
  931. timestamp: Date.now()
  932. }
  933. progressMessages.value.push(messageData)
  934. // 更新当前显示的消息
  935. message.value = data.data.goods_art_nos ? `货号${data.data.goods_art_nos.join(', ')}:${data.msg}` : `${data.msg}`
  936. scrollMessageListToBottom()
  937. // 更新新的进度条
  938. if (data.progress) {
  939. if(['处理完成','处理失败'].includes( progressSteps.value[0]?.status)){
  940. return;
  941. }
  942. updateProgressStep('segment_progress', data.progress)
  943. if(data.progress?.folder && !window.segmentFolderOpened){
  944. window.segmentFolderOpened = true;
  945. openOutputDir(data.progress?.folder)
  946. }
  947. }
  948. }
  949. }
  950. // 处理上脚图进度
  951. const handleUpperFooterProgressMessage = (data: any) => {
  952. console.log("upper_footer_progress", data);
  953. if (data.code === 0 && data.msg_type === 'upper_footer_progress') {
  954. const messageData = {
  955. goods_no: '',
  956. temp_name: '',
  957. status: data.data?.status || '',
  958. goods_art_nos: data.data?.goods_art_nos || [],
  959. msg: data.msg,
  960. timestamp: Date.now()
  961. }
  962. progressMessages.value.push(messageData)
  963. message.value = data.data.goods_art_nos ? `货号${data.data.goods_art_nos.join(', ')}:${data.msg}` : `${data.msg}`
  964. scrollMessageListToBottom()
  965. // 更新新的进度条
  966. if (data.progress) {
  967. updateProgressStep('upper_footer_progress', data.progress)
  968. // 如果是处理完成状态且目录还未打开,则打开目录
  969. if(data.progress?.folder && !window.modelOrSceneFolderOpened){
  970. window.modelOrSceneFolderOpened = true;
  971. openOutputDir(data.progress?.folder)
  972. }
  973. }
  974. }
  975. }
  976. // 处理场景图进度
  977. const handleSceneProgressMessage = (data: any) => {
  978. console.log("scene_progress", data);
  979. if (data.code === 0 && data.msg_type === 'scene_progress') {
  980. const messageData = {
  981. goods_no: '',
  982. temp_name: '',
  983. status: data.data?.status || '',
  984. goods_art_nos: data.data?.goods_art_nos || [],
  985. msg: data.msg,
  986. timestamp: Date.now()
  987. }
  988. progressMessages.value.push(messageData)
  989. message.value = data.data.goods_art_nos ? `货号${data.data.goods_art_nos.join(', ')}:${data.msg}` : `${data.msg}`
  990. scrollMessageListToBottom()
  991. // 更新新的进度条
  992. if (data.progress) {
  993. updateProgressStep('scene_progress', data.progress)
  994. // 如果是处理完成状态且目录还未打开,则打开目录
  995. if(data.progress?.folder && !window.modelOrSceneFolderOpened){
  996. window.modelOrSceneFolderOpened = true;
  997. openOutputDir(data.progress?.folder)
  998. }
  999. }
  1000. }
  1001. }
  1002. // 处理商品上传进度
  1003. const handleUploadGoodsProgressMessage = (data: any) => {
  1004. console.log("upload_goods_progress", data);
  1005. if (data.code === 0 && data.msg_type === 'upload_goods_progress') {
  1006. const messageData = {
  1007. goods_no: '',
  1008. temp_name: '',
  1009. status: data.data?.status || '',
  1010. goods_art_nos: data.data?.goods_art_nos || [],
  1011. msg: data.msg,
  1012. timestamp: Date.now()
  1013. }
  1014. progressMessages.value.push(messageData)
  1015. message.value = data.data.goods_art_nos ? `货号${data.data.goods_art_nos.join(', ')}:${data.msg}` : `${data.msg}`
  1016. scrollMessageListToBottom()
  1017. // 更新新的进度条
  1018. if (data.progress) {
  1019. updateProgressStep('upload_goods_progress', data.progress)
  1020. }
  1021. }
  1022. }
  1023. // 打开输出目录:appConfig.appPath + '/build/extraResources/py/output'
  1024. const openOutputDir = (path) => {
  1025. try {
  1026. let fullPath = ''
  1027. if(path){
  1028. fullPath = path
  1029. }else{
  1030. const appPath = useConfigInfoStore?.appConfig?.appPath || ''
  1031. if (!appPath) {
  1032. ElMessage.error('未获取到应用目录 appPath')
  1033. return
  1034. }
  1035. fullPath = `${pyPath}\\output`
  1036. }
  1037. clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
  1038. clientStore.ipc.send(icpList.utils.shellFun, {
  1039. action: 'openPath',
  1040. params: fullPath.replace(/\//g, '\\')
  1041. });
  1042. } catch (e) {
  1043. console.error(e)
  1044. ElMessage.error('打开目录失败')
  1045. }
  1046. }
  1047. // 检测参数是否有效
  1048. const checkParams = async function () {
  1049. const useConfigInfoStore = configInfo();
  1050. const tokenInfoStore = tokenInfo();
  1051. const token = tokenInfoStore.getToken;
  1052. let temp_list = []
  1053. templates.value.map(item => {
  1054. temp_list.push({
  1055. template_id: item.template_id,
  1056. template_local_classes: item.template_local_classes,
  1057. })
  1058. })
  1059. // 根据选择的服务内容设置参数
  1060. const isDetail = form.services.includes('is_detail') ? 1 : 0
  1061. const isProductScene = form.services.includes('is_product_scene') ? 1 : 0
  1062. const isUpperFooter = form.services.includes('is_upper_footer') ? 1 : 0
  1063. const params = {
  1064. goods_art_no: JSON.parse(JSON.stringify(goods_art_nos.value)),
  1065. logo_path: form.logo_path || '',
  1066. temp_name: form.selectTemplate?.template_id || '',
  1067. excel_path: form.dataType == '1' ? form.excel_path : '',
  1068. template_image_order: form.selectTemplate?.template_image_order,
  1069. temp_list,
  1070. token,
  1071. uuid: uuidStore.getUuid || '',
  1072. // 新增服务参数 - 合并国内和国外平台
  1073. online_stores: [...(domesticPlatforms.value || []), ...(foreignPlatforms.value || [])],
  1074. is_detail: isDetail,
  1075. is_product_scene: isProductScene,
  1076. is_upper_footer: isUpperFooter,
  1077. upper_footer_params: selectedModels.value ? {
  1078. man_id: selectedModels.value.male?.id || "",
  1079. women_id: selectedModels.value.female?.id || ""
  1080. } : {},
  1081. product_scene_prompt: scenePrompt.value || '',
  1082. is_check: 1 // 仅检测,不生成
  1083. }
  1084. try {
  1085. // 直接调用API进行检测,因为检测模式会直接返回结果
  1086. const response = await clientStore.ipc.invoke(icpList.generate.generatePhotoDetail, params);
  1087. console.log('=======checkParamscheckParamscheckParamscheckParams===========');
  1088. console.log(params);
  1089. console.log(response);
  1090. if (response.code === 0) {
  1091. return response;
  1092. } else {
  1093. throw new Error(response.msg || '检测失败');
  1094. }
  1095. } catch (error) {
  1096. throw new Error(error.message || '检测失败');
  1097. }
  1098. }
  1099. // 开始生成操作
  1100. const generate = async function () {
  1101. if (requesting.value) {
  1102. return
  1103. }
  1104. // 重置目录打开状态
  1105. window.segmentFolderOpened = false;
  1106. window.modelOrSceneFolderOpened = false;
  1107. if(form.services.length == 0){
  1108. ElMessage.error('请选择服务内容')
  1109. return
  1110. }
  1111. // 必填验证
  1112. if (form.services.includes('is_upper_footer') && !( selectedModels.value && selectedModels.value.male?.id && selectedModels.value.female?.id)) {
  1113. openModelDialog();
  1114. setTimeout(()=>{
  1115. ElMessage.error('请选择模特')
  1116. },200)
  1117. return
  1118. }
  1119. if (form.services.includes('is_product_scene') && !scenePrompt.value) {
  1120. openScenePromptDialog();
  1121. setTimeout(()=>{
  1122. ElMessage.error('请设置场景提示词')
  1123. },200)
  1124. return
  1125. }
  1126. if(form.services.includes('is_detail')){
  1127. if ( form.dataType == '1' && !form.excel_path) {
  1128. ElMessage.error('请上传商品基础资料')
  1129. return
  1130. }
  1131. }
  1132. // 埋点:开始生成详情页
  1133. clickLog({
  1134. describe: {
  1135. action: '点击开始生成详情页',
  1136. services: form.services,
  1137. dataType: form.dataType,
  1138. template_name: form.selectTemplate?.template_name,
  1139. goods_count: goods_art_nos.value.length,
  1140. goods_art_nos: goods_art_nos.value
  1141. }
  1142. }, route);
  1143. // 先进行检测
  1144. let checkResult;
  1145. try {
  1146. requesting.value = true
  1147. // ElMessage.info('正在检测参数,请稍候...');
  1148. checkResult = await checkParams();
  1149. // ElMessage.success('检测通过,开始生成...');
  1150. } catch (error) {
  1151. ElMessage.error(error.message || '检测失败,请检查参数');
  1152. requesting.value = false
  1153. return;
  1154. }
  1155. const useConfigInfoStore = configInfo();
  1156. console.log(useConfigInfoStore.appConfig);
  1157. const tokenInfoStore = tokenInfo();
  1158. const token = tokenInfoStore.getToken; // 使用 getToken() 获取 token
  1159. let temp_list = []
  1160. templates.value.map(item => {
  1161. temp_list.push({
  1162. template_id: item.template_id,
  1163. template_local_classes: item.template_local_classes,
  1164. })
  1165. })
  1166. // 根据选择的服务内容设置参数
  1167. const isDetail = form.services.includes('is_detail') ? 1 : 0
  1168. const isProductScene = form.services.includes('is_product_scene') ? 1 : 0
  1169. const isUpperFooter = form.services.includes('is_upper_footer') ? 1 : 0
  1170. const params = {
  1171. goods_art_no: JSON.parse(JSON.stringify(goods_art_nos.value)),
  1172. logo_path: form.logo_path || '',
  1173. temp_name: form.selectTemplate?.template_id || '',
  1174. excel_path: form.dataType == '1' ? form.excel_path : '',
  1175. template_image_order: form.selectTemplate?.template_image_order,
  1176. temp_list,
  1177. token,
  1178. uuid: uuidStore.getUuid || '',
  1179. // 新增服务参数 - 合并国内和国外平台
  1180. online_stores: [...(domesticPlatforms.value || []), ...(foreignPlatforms.value || [])],
  1181. is_detail: isDetail,
  1182. is_product_scene: isProductScene,
  1183. is_upper_footer: isUpperFooter,
  1184. upper_footer_params: selectedModels.value ? {
  1185. man_id: selectedModels.value.male?.id || "",
  1186. women_id: selectedModels.value.female?.id || ""
  1187. } : {},
  1188. product_scene_prompt: scenePrompt.value || '',
  1189. is_check: 0 // 正式生成
  1190. }
  1191. console.log(params)
  1192. // 开启进度弹窗
  1193. requesting.value = true
  1194. partErrList.value = []
  1195. message.value = '正在为您处理,请稍后'
  1196. progress.value = 0
  1197. // 清空之前的进度消息
  1198. progressMessages.value = []
  1199. showMessageHistory.value = true
  1200. // 启用新的进度条并初始化步骤
  1201. useNewProgress.value = true
  1202. // 清空之前的进度步骤
  1203. progressSteps.value = []
  1204. // 从检测结果中获取步骤信息
  1205. const backendSteps = checkResult?.data?.progress || []
  1206. initProgressSteps(backendSteps)
  1207. openLoadingDialog(goods_art_nos.value.length * 10)
  1208. clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
  1209. clientStore.ipc.send(icpList.generate.generatePhotoDetail, params);
  1210. /*
  1211. * detail_result_progress
  1212. *
  1213. * */
  1214. // 监听进度消息
  1215. clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
  1216. clientStore.ipc.on(icpList.socket.message + '_detail_progress', (event, data) => {
  1217. console.log('_detail_progress',data);
  1218. handleProgressMessage(data)
  1219. });
  1220. // 监听抠图进度消息
  1221. clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
  1222. clientStore.ipc.on(icpList.socket.message + '_segment_progress', (event, data) => {
  1223. console.log('_segment_progress',data);
  1224. handleSegmentProgressMessage(data)
  1225. });
  1226. // 监听上脚图进度消息
  1227. clientStore.ipc.removeAllListeners(icpList.socket.message + '_upper_footer_progress');
  1228. clientStore.ipc.on(icpList.socket.message + '_upper_footer_progress', (event, data) => {
  1229. console.log('_upper_footer_progress',data);
  1230. handleUpperFooterProgressMessage(data)
  1231. });
  1232. // 监听场景图进度消息
  1233. clientStore.ipc.removeAllListeners(icpList.socket.message + '_scene_progress');
  1234. clientStore.ipc.on(icpList.socket.message + '_scene_progress', (event, data) => {
  1235. console.log('_scene_progress',data);
  1236. handleSceneProgressMessage(data)
  1237. });
  1238. // 监听商品上传进度消息
  1239. clientStore.ipc.removeAllListeners(icpList.socket.message + '_upload_goods_progress');
  1240. clientStore.ipc.on(icpList.socket.message + '_upload_goods_progress', (event, data) => {
  1241. console.log('_upload_goods_progress',data);
  1242. handleUploadGoodsProgressMessage(data)
  1243. });
  1244. clientStore.ipc.on(icpList.socket.message + '_detail_result_progress', (event, result) => {
  1245. if(result.code !== 0 ){
  1246. if(result.msg){
  1247. handleFail(result.msg)
  1248. message.value = 'result.msg'
  1249. }
  1250. progress.value = 0
  1251. loadingDialogVisible.value = false
  1252. requesting.value = false
  1253. return;
  1254. }
  1255. console.log('result', result)
  1256. requesting.value = true
  1257. setTimeout(() => {
  1258. clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
  1259. clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
  1260. clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
  1261. clientStore.ipc.removeAllListeners(icpList.socket.message + '_upper_footer_progress');
  1262. clientStore.ipc.removeAllListeners(icpList.socket.message + '_scene_progress');
  1263. clientStore.ipc.removeAllListeners(icpList.socket.message + '_upload_goods_progress');
  1264. }, 100)
  1265. clearInterval(INTERVAL.value)
  1266. if (result.code === 0) {
  1267. const { output_folder, list } = result.data
  1268. const allSuccess = list.every(item => item.success);
  1269. const allFailure = list.every(item => !item.success);
  1270. if (allSuccess) {
  1271. console.log("全部成功")
  1272. handleSuccess(output_folder, '全部生成成功')
  1273. } else if (allFailure) {
  1274. console.log("全部失败");
  1275. handleFailure(list)
  1276. } else {
  1277. console.log("部分成功,部分失败");
  1278. handlePartSuccess(output_folder, list)
  1279. }
  1280. } else {
  1281. console.log('code全部生成失败')
  1282. handleFail(result.msg)
  1283. }
  1284. //生成失败 (接口请求失败)
  1285. function handleFail(errorMsg: string) {
  1286. // loadingDialogVisible.value = false
  1287. // disabledButton.value = false
  1288. if (errorMsg) {
  1289. ElMessage.error(errorMsg)
  1290. }
  1291. requesting.value = false // 重置请求状态,允许再次生成
  1292. // 处理完成后停止监听进度消息
  1293. // clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
  1294. // clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
  1295. }
  1296. // 全部生成成功
  1297. function handleSuccess(href, loadingMsg) {
  1298. // 埋点:生成完成
  1299. setLogInfo(route, { action: '生成完成', output_folder: href, message: loadingMsg });
  1300. completeDirectory.value = href
  1301. progress.value = 100
  1302. disabledButton.value = false
  1303. message.value = loadingMsg
  1304. requesting.value = false // 重置请求状态,允许再次生成
  1305. // handleComplete()
  1306. }
  1307. // 部分成功
  1308. function handlePartSuccess(output_folder: string, partSuccessList) {
  1309. let errorList = []
  1310. partSuccessList.map(item => {
  1311. if (!item.success) {
  1312. errorList.push(item)
  1313. }
  1314. })
  1315. partErrList.value = errorList
  1316. handleSuccess(output_folder, '部分货号生成失败')
  1317. }
  1318. // 全部生成失败
  1319. function handleFailure(partSuccessList) {
  1320. // 埋点:生成失败(携带货号)
  1321. const failedGoods = (partSuccessList || []).map(item => item.goods_art_no).filter(Boolean)
  1322. setLogInfo(route, { action: '生成失败', error_count: partSuccessList.length, goods_art_nos: goods_art_nos.value, failed_goods_art_nos: failedGoods });
  1323. let errorList = []
  1324. partSuccessList.map(item => {
  1325. if (!item.success) {
  1326. errorList.push(item)
  1327. }
  1328. })
  1329. partErrList.value = errorList
  1330. completeDirectory.value = ''
  1331. progress.value = 100
  1332. disabledButton.value = true
  1333. message.value = '全部货号生成失败'
  1334. requesting.value = false // 重置请求状态,允许再次生成
  1335. }
  1336. requesting.value = false
  1337. });
  1338. }
  1339. const openLoadingDialog = (timer: number) => {
  1340. loadingDialogVisible.value = true
  1341. disabledButton.value = true
  1342. // 根据传入的秒数计算每次增加的进度值
  1343. const step = 100 / timer
  1344. INTERVAL.value = setInterval(() => {
  1345. if (progress.value < 50) {
  1346. progress.value = Math.min(progress.value + step, 100)
  1347. } else if (progress.value < 70) {
  1348. progress.value = Math.min(progress.value + step / 2, 100)
  1349. } if (progress.value < 85) {
  1350. progress.value = Math.min(progress.value + step / 5, 100)
  1351. } else if (progress.value < 90) {
  1352. progress.value = Math.min(progress.value + step / 10, 100)
  1353. } else if (progress.value < 95) {
  1354. progress.value = Math.min(progress.value + step / 50, 100)// 新增中间阶段
  1355. } else {
  1356. progress.value = Math.min(progress.value + step / 100, 100)
  1357. }
  1358. }, 1000)
  1359. }
  1360. //logo
  1361. const logoList = ref([])
  1362. const logoPreviewVisible = ref(false)
  1363. const logoPreviewUrl = ref('')
  1364. // 打开LOGO上传
  1365. const openLogoUpload = () => {
  1366. clientStore.ipc.removeAllListeners(icpList.utils.openImage);
  1367. clientStore.ipc.send(icpList.utils.openImage);
  1368. clientStore.ipc.on(icpList.utils.openImage, async (event, result) => {
  1369. if (result && result.filePath) {
  1370. await addLogo(result.filePath)
  1371. }
  1372. clientStore.ipc.removeAllListeners(icpList.utils.openImage);
  1373. })
  1374. }
  1375. // 预览LOGO
  1376. const previewLogo = () => {
  1377. if (form.logo_path) {
  1378. logoPreviewUrl.value = 'file:///' + form.logo_path
  1379. logoPreviewVisible.value = true
  1380. }
  1381. }
  1382. // 删除LOGO
  1383. const removeLogo = () => {
  1384. if (form.logo_path) {
  1385. const currentLogoPath = form.logo_path
  1386. clientStore.ipc.send(icpList.generate.deleteLogo, {
  1387. path: currentLogoPath
  1388. });
  1389. clientStore.ipc.on(icpList.generate.deleteLogo, async (event, result) => {
  1390. console.log('deleteLogo', result);
  1391. form.logo_path = ''
  1392. saveLogoToCache('')
  1393. // 从列表中移除
  1394. const index = logoList.value.indexOf(currentLogoPath)
  1395. if (index > -1) {
  1396. logoList.value.splice(index, 1)
  1397. }
  1398. clientStore.ipc.removeAllListeners(icpList.generate.deleteLogo);
  1399. })
  1400. }
  1401. }
  1402. const getLogolist = async () => {
  1403. clientStore.ipc.send(icpList.generate.getLogoList);
  1404. clientStore.ipc.on(icpList.generate.getLogoList, async (event, result) => {
  1405. // 保持数组格式,兼容老的格式
  1406. logoList.value = result.data || []
  1407. console.log('getLogoList', result.data)
  1408. // 只使用第一个LOGO(如果存在且当前没有选择)
  1409. if(logoList.value.length && !form.logo_path){
  1410. form.logo_path = logoList.value[0]
  1411. // 保存默认LOGO到缓存
  1412. saveLogoToCache(form.logo_path)
  1413. }
  1414. clientStore.ipc.removeAllListeners(icpList.generate.getLogoList);
  1415. })
  1416. }
  1417. const addLogo = async (path) => {
  1418. console.log('addLogo', path);
  1419. clientStore.ipc.send(icpList.generate.addLogo, {
  1420. logo_path: path
  1421. });
  1422. clientStore.ipc.on(icpList.generate.addLogo, async (event, result) => {
  1423. console.log('addLogo result', result);
  1424. if (result.code === 0) {
  1425. console.log("添加成功")
  1426. if(result.data.logo){
  1427. const newLogo = result.data.logo
  1428. form.logo_path = newLogo
  1429. // 保存新添加的LOGO到缓存
  1430. saveLogoToCache(form.logo_path)
  1431. // 保持数组格式:如果列表中没有,添加到数组;如果已有,更新数组(保持兼容性)
  1432. const index = logoList.value.indexOf(newLogo)
  1433. if(index < 0){
  1434. // 新LOGO,添加到数组(保持数组格式兼容性)
  1435. logoList.value.push(newLogo)
  1436. } else {
  1437. // 已存在的LOGO,移动到第一个位置(UI只显示第一个)
  1438. logoList.value.splice(index, 1)
  1439. logoList.value.unshift(newLogo)
  1440. }
  1441. }
  1442. }
  1443. clientStore.ipc.removeAllListeners(icpList.generate.addLogo);
  1444. })
  1445. }
  1446. function selectExcel() {
  1447. clientStore.ipc.removeAllListeners(icpList.utils.openFile);
  1448. clientStore.ipc.send(icpList.utils.openFile, {
  1449. filters: [
  1450. { name: '支持xls,xlsx', extensions: ['xlsx', 'xls'] }
  1451. ],
  1452. title: "选择基础文件资料"
  1453. });
  1454. clientStore.ipc.on(icpList.utils.openFile, async (event, result) => {
  1455. form.excel_path = result
  1456. clientStore.ipc.removeAllListeners(icpList.utils.openFile);
  1457. })
  1458. }
  1459. const Router = useRouter()
  1460. //打开主图详情
  1461. function openPhotographySeniorDetail() {
  1462. const { href } = Router.resolve({
  1463. name: 'PhotographySeniorDetail'
  1464. })
  1465. clientStore.ipc.removeAllListeners(icpList.utils.openMain);
  1466. let params = {
  1467. title: '详情高级配置',
  1468. width: 1000,
  1469. height: 630,
  1470. frame: true,
  1471. id: "PhotographySeniorDetail",
  1472. url: getRouterUrl(href)
  1473. }
  1474. clientStore.ipc.send(icpList.utils.openMain, params);
  1475. }
  1476. // 处理打开目录事件
  1477. const handleOpenFolder = (event) => {
  1478. const { folder } = event.detail;
  1479. if (folder) {
  1480. openOutputDir(folder);
  1481. }
  1482. }
  1483. const handleComplete = () => {
  1484. // loadingDialogVisible.value = false
  1485. // 这里可以添加打开目录的逻辑
  1486. if(!completeDirectory.value){
  1487. openOutputDir()
  1488. return
  1489. }
  1490. clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
  1491. let params = {
  1492. action: 'openPath',
  1493. params: completeDirectory.value?.replace(/\//g, '\\')
  1494. }
  1495. clientStore.ipc.send(icpList.utils.shellFun, params);
  1496. }
  1497. const selectFolder = () => {
  1498. clientStore.ipc.removeAllListeners(icpList.utils.openDirectory);
  1499. clientStore.ipc.send(icpList.utils.openDirectory);
  1500. clientStore.ipc.on(icpList.utils.openDirectory, async (event, result) => {
  1501. folderPath.value = result
  1502. clientStore.ipc.removeAllListeners(icpList.utils.openDirectory);
  1503. })
  1504. }
  1505. </script>
  1506. <style lang="scss" scoped>
  1507. // 服务标签页样式
  1508. .detail-page {
  1509. height: calc(100vh - 50px);
  1510. }
  1511. .service-tabs {
  1512. display: flex;
  1513. gap: 10px;
  1514. padding: 20px;
  1515. .service-tab {
  1516. display: flex;
  1517. flex: 1;
  1518. align-items: center;
  1519. justify-content: center;
  1520. gap: 10px;
  1521. padding: 10px 20px;
  1522. border: 1px solid #e8e8e8;
  1523. border-radius: 8px;
  1524. cursor: pointer;
  1525. transition: all 0.3s;
  1526. position: relative;
  1527. background: #fff;
  1528. height: 140px;
  1529. .tab-checkbox {
  1530. margin-right: 0;
  1531. position: absolute;
  1532. left: 10px;
  1533. top:10px;
  1534. ::v-deep {
  1535. .is-checked .el-checkbox__inner {
  1536. background: #2957FF;
  1537. border-color: #2957FF;
  1538. }
  1539. .el-checkbox__inner {
  1540. border-radius: 18px;
  1541. transform: scale(1.2);
  1542. }
  1543. }
  1544. }
  1545. .tab-img {
  1546. width: 48px;
  1547. height: 48px;
  1548. background: #EFF6FF;
  1549. border-radius: 10px;
  1550. margin: 0 auto 5px;
  1551. }
  1552. .tab-icon {
  1553. width: 24px;
  1554. height: 24px;
  1555. }
  1556. .tab-name {
  1557. font-size: 14px;
  1558. color: #333;
  1559. font-weight: 500;
  1560. }
  1561. .tab-edit-btn {
  1562. position: absolute;
  1563. right: 10px;
  1564. top:10px;
  1565. width: 24px;
  1566. height: 24px;
  1567. display: flex;
  1568. align-items: center;
  1569. justify-content: center;
  1570. background: rgba(41, 87, 255, 0.1);
  1571. border-radius: 4px;
  1572. opacity: 0;
  1573. transition: opacity 0.3s;
  1574. cursor: pointer;
  1575. .el-icon {
  1576. color: #2957FF;
  1577. font-size: 16px;
  1578. }
  1579. }
  1580. &:hover {
  1581. border-color: #2957FF;
  1582. .tab-edit-btn {
  1583. opacity: 1;
  1584. }
  1585. }
  1586. &.active {
  1587. border-color: #2957FF;
  1588. border-bottom: 4px solid #2957FF;
  1589. .tab-img {
  1590. background: #2957FF;
  1591. }
  1592. }
  1593. &.disabled {
  1594. opacity: 0.8;
  1595. cursor: not-allowed;
  1596. background: #f5f5f5;
  1597. }
  1598. }
  1599. }
  1600. .detail-container {
  1601. background: #F5F6F7;
  1602. padding: 0 20px 20px 20px; // 底部留出空间给固定按钮
  1603. .detail-content {
  1604. max-width: 100%;
  1605. }
  1606. width: 100%;
  1607. min-width: 600px;
  1608. overflow: hidden;
  1609. }
  1610. .service-section {
  1611. margin-bottom: 20px;
  1612. .service-cards {
  1613. display: flex;
  1614. gap: 60px;
  1615. margin-top: 16px;
  1616. }
  1617. .service-card {
  1618. width: 240px;
  1619. height: 140px;
  1620. border-radius: 8px;
  1621. background: #fff;
  1622. border: 1px solid #e0e0e0;
  1623. display: flex;
  1624. align-items: flex-start;
  1625. padding: 12px;
  1626. cursor: pointer;
  1627. transition: all 0.3s ease;
  1628. position: relative;
  1629. &:hover {
  1630. border-color: #2957FF;
  1631. box-shadow: 0 2px 8px rgba(41, 87, 255, 0.1);
  1632. }
  1633. &.active {
  1634. border: 3px solid #2957FF;
  1635. background: #f8f9ff;
  1636. box-shadow: 0 4px 12px rgba(41, 87, 255, 0.25);
  1637. }
  1638. .service-checkbox {
  1639. position: absolute;
  1640. left: -40px;
  1641. top: 10px;
  1642. width: 30px;
  1643. transform: scale(1.5);
  1644. z-index: 10;
  1645. ::v-deep{
  1646. .el-checkbox {
  1647. display: block;
  1648. margin-right: 0;
  1649. }
  1650. .el-checkbox__input {
  1651. cursor: pointer;
  1652. }
  1653. }
  1654. }
  1655. .service-content {
  1656. flex: 1;
  1657. display: flex;
  1658. flex-direction: column;
  1659. align-items: center;
  1660. position: absolute;
  1661. left: 0;
  1662. top: 0;
  1663. right: 0;
  1664. bottom: 0;;
  1665. }
  1666. .service-image {
  1667. position: absolute;
  1668. width: 100%;
  1669. height: 100%;
  1670. img {
  1671. width: 100%;
  1672. height: 100%;
  1673. object-fit: cover;
  1674. border-radius: 4px;
  1675. }
  1676. .service-icon {
  1677. position: absolute;
  1678. top: 10px;
  1679. right: 10px;
  1680. width: 30px;
  1681. height: 30px;
  1682. background: #2957FF;
  1683. border-radius: 50%;
  1684. display: flex;
  1685. align-items: center;
  1686. justify-content: center;
  1687. color: white;
  1688. font-size: 18px;
  1689. }
  1690. }
  1691. .service-name {
  1692. font-size: 14px;
  1693. font-weight: 500;
  1694. text-align: center;
  1695. position: absolute;
  1696. background: rgba(0,0,0,.5);
  1697. height: 30px;
  1698. line-height: 30px;
  1699. color: #fff;
  1700. left: 0;
  1701. right: 0;
  1702. bottom:0px
  1703. }
  1704. }
  1705. }
  1706. .logo-section,
  1707. .template-section,
  1708. .data-prep-section {
  1709. }
  1710. .template-section {
  1711. background: #fff;
  1712. padding: 10px 20px 20px;
  1713. border-radius: 8px;
  1714. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  1715. .section-header {
  1716. display: flex;
  1717. justify-content: space-between;
  1718. align-items: center;
  1719. }
  1720. .template-tips {
  1721. height: 40px;
  1722. line-height: 40px;
  1723. padding: 0 10px;
  1724. background: #FFFBEA;
  1725. border-radius: 10px;
  1726. border: 1px solid #FEEEB0;
  1727. color: #9B4D26;
  1728. }
  1729. }
  1730. .template-section--disabled {
  1731. opacity: 0.8;
  1732. .template-list,
  1733. .template-pagination {
  1734. pointer-events: none;
  1735. }
  1736. }
  1737. .publish-section--disabled {
  1738. opacity: 0.6;
  1739. }
  1740. // 主布局:左右分栏
  1741. .main-layout {
  1742. display: flex;
  1743. gap: 20px;
  1744. align-items: flex-start;
  1745. .left-panel {
  1746. flex: 1;
  1747. min-width: 0;
  1748. }
  1749. .right-panel {
  1750. width: 400px;
  1751. flex-shrink: 0;
  1752. display: flex;
  1753. flex-direction: column;
  1754. gap: 20px;
  1755. }
  1756. }
  1757. .right-section {
  1758. background: #fff;
  1759. padding: 20px;
  1760. border-radius: 8px;
  1761. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  1762. &.data-prep-section {
  1763. height: 225px;
  1764. display: flex;
  1765. flex-direction: column;
  1766. box-sizing: border-box;
  1767. }
  1768. .section-title {
  1769. display: flex;
  1770. align-items: center;
  1771. gap: 10px;
  1772. font-weight: 600;
  1773. font-size: 16px;
  1774. color: #333333;
  1775. margin-bottom: 16px;
  1776. .section-title-line {
  1777. width: 3px;
  1778. height: 16px;
  1779. background: linear-gradient(135deg, #7C3AED 0%, #2957FF 100%);
  1780. border-radius: 2px;
  1781. }
  1782. .section-title-hint {
  1783. font-weight: normal;
  1784. font-size: 14px;
  1785. color: #999;
  1786. }
  1787. }
  1788. .data-prep-content {
  1789. display: flex;
  1790. flex-direction: column;
  1791. gap: 12px;
  1792. flex: 1;
  1793. .data-type-radio {
  1794. padding:5px;
  1795. background:#F3F5F6;
  1796. border-radius:10px;
  1797. ::v-deep {
  1798. .el-radio {
  1799. margin-right: 20px;
  1800. }
  1801. .el-radio-button {
  1802. flex:1;
  1803. box-shadow:none !important;
  1804. .el-radio-button__inner {
  1805. width: 100%;
  1806. background: none !important;
  1807. border: none !important;
  1808. display: flex;
  1809. align-items: center;
  1810. border-radius:10px !important;
  1811. justify-content: center;
  1812. img {
  1813. height: 14px;
  1814. margin: 0 5px;
  1815. position: relative;
  1816. top:-1px;
  1817. }
  1818. }
  1819. &.is-active {
  1820. background: #F8F9FF;
  1821. .el-radio-button__inner {
  1822. background: #fff !important;
  1823. color: #2957FF !important;
  1824. border-radius: 10px !important;
  1825. box-shadow: none !important;
  1826. }
  1827. }
  1828. }
  1829. }
  1830. }
  1831. .excel-upload-section {
  1832. display: flex;
  1833. flex-direction: column;
  1834. gap: 8px;
  1835. flex: 1;
  1836. .excel-upload-area {
  1837. width: 100%;
  1838. height: 64px;
  1839. border: 1px dashed #D9D9D9;
  1840. border-radius: 8px;
  1841. display: flex;
  1842. align-items: center;
  1843. padding: 0 16px;
  1844. cursor: pointer;
  1845. transition: all 0.3s;
  1846. background: #fff;
  1847. &:hover {
  1848. border-color: #2957FF;
  1849. background: #F8F9FF;
  1850. }
  1851. .excel-icon {
  1852. width: 32px;
  1853. height:32px;
  1854. margin-right: 10px;
  1855. img {
  1856. width: 32px;
  1857. height:32px;
  1858. display: block;
  1859. }
  1860. }
  1861. .excel-upload-text {
  1862. font-size: 14px;
  1863. color: #333;
  1864. font-weight: 500;
  1865. }
  1866. }
  1867. .download-link {
  1868. color: #2957FF;
  1869. text-decoration: underline;
  1870. padding: 0;
  1871. font-size: 14px;
  1872. margin-top: 0;
  1873. justify-content: flex-start;
  1874. }
  1875. }
  1876. }
  1877. .publish-content {
  1878. display: flex;
  1879. flex-direction: column;
  1880. gap: 15px;
  1881. .publish-form-item {
  1882. display: flex;
  1883. flex-direction: column;
  1884. gap: 8px;
  1885. .publish-label {
  1886. font-size: 14px;
  1887. color: #333;
  1888. margin-bottom: 4px;
  1889. text-align: left;
  1890. }
  1891. .publish-select {
  1892. width: 100%;
  1893. ::v-deep {
  1894. .el-input.is-disabled .el-input__inner {
  1895. background-color: #F5F6F7;
  1896. border-color: #E8E8E8;
  1897. color: #999;
  1898. }
  1899. }
  1900. }
  1901. }
  1902. }
  1903. }
  1904. .logo-template-row {
  1905. display: flex;
  1906. gap: 24px;
  1907. align-items: flex-start;
  1908. .logo-col { flex: 2; min-width: 300px; }
  1909. .template-col { flex: 3; }
  1910. }
  1911. .logo-section {
  1912. .upload-item {
  1913. border: 2px solid rgba(0,0,0,0);
  1914. }
  1915. .active {
  1916. border: 2px solid #2957FF;
  1917. border-radius: 6px;
  1918. overflow: hidden;;
  1919. }
  1920. &.multi-line {
  1921. flex-direction: row; // 默认横向排列
  1922. flex-wrap: wrap; // 允许换行
  1923. align-items: flex-start; // 对齐方式调整为顶部对齐
  1924. .upload-item {
  1925. margin-bottom: 10px; // 每行之间增加间距
  1926. width: 90px; // 每行显示 4 个元素,减去外边距
  1927. box-sizing: border-box; // 确保宽度计算包含 padding 和 border
  1928. }
  1929. }
  1930. }
  1931. // LOGO上传区域样式
  1932. .logo-upload-area {
  1933. width: 100%;
  1934. }
  1935. .logo-upload-placeholder {
  1936. width: 100%;
  1937. height: 160px;
  1938. border: 1px dashed #D9D9D9;
  1939. border-radius: 10px;
  1940. display: flex;
  1941. flex-direction: column;
  1942. align-items: center;
  1943. justify-content: center;
  1944. cursor: pointer;
  1945. transition: all 0.3s;
  1946. background: #fff;
  1947. &:hover {
  1948. border-color: #2957FF;
  1949. background: #F8F9FF;
  1950. }
  1951. .logo-upload-icon {
  1952. width: 42px;
  1953. height: 42px;
  1954. margin-bottom: 12px;
  1955. display: flex;
  1956. align-items: center;
  1957. justify-content: center;
  1958. background: linear-gradient(135deg, #EFF6FF 0%, #F3E8FF 100%);
  1959. border-radius: 8px;
  1960. color: #2957FF;
  1961. img {
  1962. width: 42px;
  1963. height: 42px;
  1964. }
  1965. }
  1966. .logo-upload-text {
  1967. font-size: 14px;
  1968. color: #333;
  1969. margin-bottom: 8px;
  1970. font-weight: 500;
  1971. }
  1972. .logo-upload-hint {
  1973. font-size: 12px;
  1974. color: #999;
  1975. }
  1976. }
  1977. .logo-upload-preview {
  1978. width: 100%;
  1979. height: 160px;
  1980. border: 1px solid #E8E8E8;
  1981. border-radius: 8px;
  1982. position: relative;
  1983. overflow: hidden;
  1984. background: #F5F5F5;
  1985. cursor: pointer;
  1986. .logo-preview-image {
  1987. width: 100%;
  1988. height: 100%;
  1989. object-fit: contain;
  1990. }
  1991. .logo-upload-actions {
  1992. position: absolute;
  1993. bottom: 0;
  1994. left: 0;
  1995. right: 0;
  1996. height: 40px;
  1997. background: rgba(0, 0, 0, 0.5);
  1998. display: flex;
  1999. align-items: center;
  2000. justify-content: center;
  2001. gap: 20px;
  2002. opacity: 0;
  2003. transition: opacity 0.3s;
  2004. .logo-action-btn {
  2005. width: 32px;
  2006. height: 32px;
  2007. display: flex;
  2008. align-items: center;
  2009. justify-content: center;
  2010. background: rgba(255, 255, 255, 0.2);
  2011. border-radius: 4px;
  2012. cursor: pointer;
  2013. color: #fff;
  2014. transition: background 0.3s;
  2015. &:hover {
  2016. background: rgba(255, 255, 255, 0.3);
  2017. }
  2018. .el-icon {
  2019. font-size: 18px;
  2020. }
  2021. }
  2022. }
  2023. &:hover .logo-upload-actions {
  2024. opacity: 1;
  2025. }
  2026. }
  2027. .logo-upload {
  2028. border: 1px dashed #ccc;
  2029. border-radius: 5px;
  2030. padding: 50px 0;
  2031. text-align: center;
  2032. cursor: pointer;
  2033. }
  2034. .template-pagination {
  2035. background: #EFF3F6;
  2036. padding:2px 0;
  2037. border-radius:5px;
  2038. ::v-deep {
  2039. .is-active {
  2040. background: #fff !important;
  2041. color: #2957FF !important;
  2042. font-size: 14px !important;
  2043. font-weight: normal !important;
  2044. border-radius: 2px !important;
  2045. }
  2046. .number {
  2047. margin: 0px !important;
  2048. }
  2049. .btn-prev {
  2050. display: none;
  2051. }
  2052. .btn-next {
  2053. display: none;
  2054. }
  2055. }
  2056. }
  2057. .template-pagination button {
  2058. margin-right: 5px;
  2059. }
  2060. .template-pagination span {
  2061. display: inline-block;
  2062. width: 20px;
  2063. height: 20px;
  2064. line-height: 20px;
  2065. text-align: center;
  2066. border: 1px solid #ccc;
  2067. border-radius: 5px;
  2068. margin-right: 5px;
  2069. cursor: pointer;
  2070. }
  2071. .template-list {
  2072. display: flex;
  2073. flex-wrap: wrap;
  2074. gap: 20px;
  2075. margin-top: 10px;
  2076. .template-item {
  2077. flex:1;
  2078. border: 1px solid #ccc;
  2079. border-radius: 10px;
  2080. cursor: pointer;
  2081. background: #f0f0f0;
  2082. position: relative;
  2083. height: 660px;
  2084. overflow: hidden;
  2085. &.active {
  2086. border-color: #1677FF;
  2087. }
  2088. .template-info {
  2089. position: absolute;
  2090. bottom: 0;
  2091. left: 0;
  2092. background: #fff;
  2093. width: 100%;
  2094. height: 40px;
  2095. line-height: 40px;
  2096. color: #333;
  2097. display: flex;
  2098. align-items: center;
  2099. justify-content: space-between;
  2100. .template-view {
  2101. color: #3366FF;
  2102. height: 30px;
  2103. line-height: 30px;
  2104. padding: 0 10px;
  2105. border-radius: 4px;
  2106. margin-right: 10px;
  2107. font-size: 14px;
  2108. }
  2109. }
  2110. }
  2111. }
  2112. .publish-section {
  2113. .label {
  2114. min-width: 120px;
  2115. margin-right: 12px;
  2116. }
  2117. }
  2118. .excel-upload {
  2119. width: 100%;
  2120. background: #F7F7F7;
  2121. padding: 20px 0;
  2122. }
  2123. .generate-button {
  2124. padding: 10px 20px;
  2125. color: white;
  2126. border: none;
  2127. border-radius: 5px;
  2128. cursor: pointer;
  2129. }
  2130. .select-button {
  2131. background: #DFE2E3 !important;
  2132. color: #3366FF !important;
  2133. height: 30px;
  2134. line-height: 30px;
  2135. padding: 0 10px;
  2136. border-radius: 4px;
  2137. margin-right: 10px;
  2138. font-size: 14px;
  2139. font-weight: 550;
  2140. }
  2141. .select-warp {
  2142. width: 18px;
  2143. height: 18px;
  2144. border-radius: 18px;
  2145. background-color: #fff;
  2146. position: absolute;
  2147. font-size: 12px;
  2148. line-height: 18px;
  2149. display: flex;
  2150. justify-content: center;
  2151. align-items: center;
  2152. top: 10px;
  2153. left: 10px;
  2154. &.active {
  2155. background-color: #1677FF;
  2156. }
  2157. }
  2158. .section-title {
  2159. display: flex;
  2160. align-items: center;
  2161. gap: 8px;
  2162. font-weight: bold;
  2163. color: #474747;
  2164. }
  2165. .section {
  2166. margin-bottom: 24px;
  2167. .section-title {
  2168. display: flex;
  2169. align-items: center;
  2170. gap: 8px;
  2171. font-weight: bold;
  2172. margin-bottom: 16px;
  2173. color: #474747;
  2174. }
  2175. .section-content {
  2176. padding-left: 16px;
  2177. }
  2178. }
  2179. .instruction-out {
  2180. background: #EAF3FF;
  2181. border-radius: 4px;
  2182. border: 1px solid #CBE1FF;
  2183. padding: 10px 20px;
  2184. width: 80%;
  2185. position: relative;
  2186. .instruction-list {
  2187. margin: 0px 0 0 10px;
  2188. padding-left: 20px;
  2189. padding-right: 40px;
  2190. width: 100%;
  2191. li {
  2192. margin-bottom: 4px;
  2193. text-align: left;
  2194. font-size: 14px;
  2195. }
  2196. }
  2197. .close-icon {
  2198. position: absolute;
  2199. top: 12px;
  2200. right: 19px;
  2201. }
  2202. }
  2203. .form-item {
  2204. margin: 16px 0;
  2205. display: flex;
  2206. .label {
  2207. min-width: 120px;
  2208. margin-right: 12px;
  2209. }
  2210. .folder-warp {
  2211. width: 100%;
  2212. display: flex;
  2213. flex-direction: column;
  2214. .folder-input {
  2215. flex: 1;
  2216. display: flex;
  2217. align-items: center;
  2218. .check-button {
  2219. background: #DFE2E3;
  2220. border-radius: 6px;
  2221. padding: 6px 12px;
  2222. color: #2957FF;
  2223. margin-left: 20px;
  2224. }
  2225. }
  2226. }
  2227. .hint {
  2228. text-align: left;
  2229. color: #999;
  2230. margin-top: 6px;
  2231. font-size: 14px;
  2232. color: #FF4C00;
  2233. font-style: normal;
  2234. }
  2235. .specific-page-input {
  2236. flex: 1;
  2237. }
  2238. }
  2239. .image-order {
  2240. flex: 1;
  2241. display: flex;
  2242. justify-content: space-between;
  2243. align-items: center;
  2244. }
  2245. .footer {
  2246. display: flex;
  2247. z-index: 100;
  2248. .footer-button {
  2249. padding: 12px 40px;
  2250. font-size: 16px;
  2251. border-radius: 8px;
  2252. display: block;
  2253. width: 100%;
  2254. height: 50px;
  2255. img {
  2256. height: 16px;;
  2257. margin: 0 10px;
  2258. }
  2259. .go {
  2260. opacity: .5;
  2261. }
  2262. }
  2263. }
  2264. .explain-btn {
  2265. padding-left: 20px;
  2266. padding-right: 20px;
  2267. color: #2957FF !important;
  2268. }
  2269. .progress-messages {
  2270. margin-top: 20px;
  2271. border-top: 1px solid #e4e7ed;
  2272. padding-top: 20px;
  2273. width: 100%;
  2274. display: none;
  2275. .message-header {
  2276. display: flex;
  2277. justify-content: space-between;
  2278. align-items: center;
  2279. margin-bottom: 15px;
  2280. font-weight: 500;
  2281. color: #606266;
  2282. }
  2283. .message-list {
  2284. max-height: 200px;
  2285. overflow-y: auto;
  2286. border: 1px solid #e4e7ed;
  2287. border-radius: 4px;
  2288. padding: 10px;
  2289. background: #fafafa;
  2290. .message-item {
  2291. margin-bottom: 12px;
  2292. padding-bottom: 12px;
  2293. border-bottom: 1px solid #f0f0f0;
  2294. &:last-child {
  2295. margin-bottom: 0;
  2296. padding-bottom: 0;
  2297. border-bottom: none;
  2298. }
  2299. .message-time {
  2300. font-size: 12px;
  2301. color: #909399;
  2302. margin-bottom: 4px;
  2303. }
  2304. .message-content {
  2305. font-size: 14px;
  2306. line-height: 1.4;
  2307. .goods-no {
  2308. color: #2957FF;
  2309. font-weight: 500;
  2310. }
  2311. .message-text {
  2312. color: #606266;
  2313. }
  2314. }
  2315. }
  2316. }
  2317. }
  2318. </style>