detail.vue 86 KB

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