ProductTemplateReplace.vue 104 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898
  1. <template>
  2. <div>
  3. <layout>
  4. <layout-header>
  5. <!-- 搜索区域 -->
  6. <el-form inline>
  7. <el-form-item>
  8. <el-input v-model="searchInfo" placeholder="搜索关键字" clearable style="width: 300px;" />
  9. <el-button type="primary" icon="search" @click="onSubmit" title="搜索查询">查询</el-button>
  10. <el-button type="primary" @click="onADD" >新增产品</el-button>
  11. </el-form-item>
  12. </el-form>
  13. </layout-header>
  14. <layout>
  15. <layout-sider v-if="treeData.length > 0" :resize-directions="['right']" :width="290" style="margin-right: 10px">
  16. <div class="JKWTree-tree" style="height: 200px;">
  17. <h3>商户名称</h3>
  18. <el-tree
  19. :data="treeData"
  20. class="treecolor"
  21. node-key="value"
  22. highlight-current
  23. :current-node-key="nodeid"
  24. @node-click="handleNodeClick"
  25. />
  26. </div>
  27. </layout-sider>
  28. <!-- 右侧区域 -->
  29. <layout-content>
  30. <el-main style="padding: 0;">
  31. <div class="gva-table-box">
  32. <!-- 表格展示 -->
  33. <el-table
  34. ref="multipleTable"
  35. style="width: 100%; height: 62vh;"
  36. :row-style="{ height: '20px' }"
  37. :header-cell-style="{ padding: '0px' }"
  38. :cell-style="{ padding: '0px' }"
  39. :header-row-style="{ height: '20px' }"
  40. border tooltip-effect="dark"
  41. :data="tableData1"
  42. row-key="ID"
  43. highlight-current-row
  44. :cell-class-name="tableDataCellClass"
  45. @row-dblclick="onRowDblClick"
  46. :show-overflow-tooltip="true">
  47. <el-table-column label="序号" prop="id" width="60" />
  48. <el-table-column label="产品名称" prop="产品名称" width="260" />
  49. <el-table-column label="产品编码" prop="产品编码" width="150" />
  50. <el-table-column label="产品图片" width="120">
  51. <template #default="{ row }">
  52. <el-image
  53. :src="formatImageUrl(row.产品图片)"
  54. :preview-src-list="[formatImageUrl(row.产品图片)]"
  55. style="width: 90px; height: 70px; cursor: pointer;"
  56. fit="contain"
  57. lazy
  58. preview-teleported
  59. :hide-on-click-modal="true"
  60. >
  61. <template #placeholder>
  62. <div class="img-placeholder-mini" />
  63. </template>
  64. </el-image>
  65. </template>
  66. </el-table-column>
  67. <el-table-column label="效果图" width="120">
  68. <template #default="{ row }">
  69. <el-image
  70. :src="formatImageUrl(row.产品效果图)"
  71. :preview-src-list="[formatImageUrl(row.产品效果图)]"
  72. style="width: 90px; height: 70px; cursor: pointer;"
  73. fit="contain"
  74. lazy
  75. preview-teleported
  76. :hide-on-click-modal="true"
  77. >
  78. <template #placeholder>
  79. <div class="img-placeholder-mini" />
  80. </template>
  81. <template #error>
  82. <div style="width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #f5f7fa; color: #909399; font-size: 12px;">
  83. <el-icon><Picture /></el-icon>
  84. <span>暂无效果图</span>
  85. </div>
  86. </template>
  87. </el-image>
  88. </template>
  89. </el-table-column>
  90. <el-table-column label="创建人" prop="创建人" width="160" />
  91. <el-table-column label="创建时间" prop="创建时间" width="160" />
  92. <el-table-column label="操作" width="140" fixed="right" align="center" :show-overflow-tooltip="false">
  93. <template #default="{ row }">
  94. <div class="table-operations">
  95. <el-button type="primary" link size="small" @click="onRowDesignClick(row)">
  96. <el-icon><EditPen /></el-icon>
  97. 产品设计
  98. </el-button>
  99. <el-button type="primary" link size="small" @click="handleDeleteProduct(row)">
  100. <el-icon><Delete /></el-icon>
  101. 删除
  102. </el-button>
  103. </div>
  104. </template>
  105. </el-table-column>
  106. </el-table>
  107. <div class="gva-pagination">
  108. <el-pagination
  109. @size-change="handleSizeChange"
  110. @current-change="handleCurrentChange"
  111. :current-page="page"
  112. :page-sizes="[10, 30, 50, 100]"
  113. :page-size="pageSize"
  114. layout="total, sizes, prev, pager, next, jumper"
  115. :total="total"/>
  116. </div>
  117. </div>
  118. </el-main>
  119. </layout-content>
  120. </layout>
  121. </layout>
  122. <el-dialog v-model="editDialogVisible" title="" fullscreen :modal="true" :show-close="false" @close="handleDialogClose">
  123. <!-- 关闭按钮 -->
  124. <div style="position: absolute; top: 10px; right: 20px; z-index: 1000;">
  125. <el-button type="danger" size="large" @click="editDialogVisible = false">
  126. <el-icon><Close /></el-icon>关闭
  127. </el-button>
  128. </div>
  129. <div class="image-edit-container">
  130. <!-- 左侧:原图新图 + 输入框 -->
  131. <div class="left-column">
  132. <!-- 标题 -->
  133. <div style="font-size: 18px; font-weight: bold; color: white; margin-bottom: 10px; background-color: #404040; padding: 10px; border-radius: 4px; text-align: center;">②出图</div>
  134. <!-- 图片对比区域 -->
  135. <div class="image-comparison-section">
  136. <!-- 原图区域 -->
  137. <div class="image-preview" style="flex: 1; min-width: 120px; display: flex; flex-direction: column; align-items: center;">
  138. <h3 style="margin-top: 0; margin-bottom: 8px; font-size: 14px; font-weight: bold; color: #303133; align-self: flex-start;">上传商品白底图</h3>
  139. <div v-if="editFormData.original_image_url" class="upload-image-box" style="cursor: pointer;" @click="handleImageZoom(formatImageUrl(editFormData.original_image_url))">
  140. <el-image
  141. :src="formatImageUrl(editFormData.original_image_url)"
  142. style="width: 100%; height: 100%;"
  143. fit="contain"
  144. >
  145. <template #error>
  146. <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: #f5f7fa;"><el-icon><Picture /></el-icon></div>
  147. </template>
  148. </el-image>
  149. </div>
  150. <div v-else class="image-placeholder image-placeholder-small">
  151. <el-icon :size="30"><Picture /></el-icon>
  152. <span style="margin-top: 5px; font-size: 10px;">暂无原图</span>
  153. </div>
  154. </div>
  155. <!-- 商品图例、案例:右侧一列,与图片等高上下对齐,不用固定 margin 避免小屏跑偏 -->
  156. <div class="case-links-column">
  157. <el-button type="text" size="small" @click="toggleProductExample" class="case-link-btn">
  158. <template #icon>
  159. <el-icon :size="16">
  160. <component :is="productExampleVisible ? 'ArrowDown' : 'ArrowRight'" />
  161. </el-icon>
  162. </template>
  163. 商品图例
  164. </el-button>
  165. <el-button type="text" size="small" @click="toggleCaseExample" class="case-link-btn">
  166. <template #icon>
  167. <el-icon :size="16">
  168. <component :is="caseExampleVisible ? 'ArrowDown' : 'ArrowRight'" />
  169. </el-icon>
  170. </template>
  171. 案例
  172. </el-button>
  173. </div>
  174. </div>
  175. <!-- 输入框区域 -->
  176. <div class="edit-section" style="border: 1px solid #e4e7ed; border-radius: 4px; display: flex; flex-direction: column; position: relative;">
  177. <el-form :model="editFormData" label-width="80px" style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
  178. <el-input
  179. type="textarea"
  180. v-model="editFormData.chinese_description"
  181. :rows="14"
  182. placeholder="点击右侧模板图片可自动填充描述内容"
  183. show-word-limit
  184. maxlength="500"
  185. style="width: 100%; resize: none;"
  186. :disabled="loadingStatus"
  187. />
  188. </el-form>
  189. <!-- 加载遮罩 -->
  190. <div v-if="loadingStatus" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.8); display: flex; align-items: center; justify-content: center; border-radius: 4px; z-index: 10;">
  191. <el-icon style="font-size: 24px; color: #409eff;"><Loading /></el-icon>
  192. <span style="margin-left: 10px; color: #409eff;">正在生成中...</span>
  193. </div>
  194. <!-- 按钮放在表单下方 -->
  195. <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e4e7ed;">
  196. <!-- 尺寸选择区域 -->
  197. <div style="margin-bottom: 15px;">
  198. <div style="display: flex; gap: 15px; flex-wrap: wrap; align-items: center;">
  199. <div v-for="ratio in [ '1:1','4:3','3:2','2:3','16:9']" :key="ratio" style="display: flex; align-items: center; gap: 5px;">
  200. <el-radio v-model="selectedSize" :label="ratio" @change="handleSizeChange" border style="border-radius: 2px;">
  201. {{ ratio }}
  202. </el-radio>
  203. </div>
  204. <!-- <div style="display: flex; align-items: center; gap: 5px;">
  205. <el-radio v-model="selectedSize" label="custom" @change="handleSizeChange" border style="border-radius: 2px;">
  206. 自由尺寸
  207. </el-radio>
  208. </div> -->
  209. </div>
  210. <!-- 自由尺寸的像素输入 -->
  211. <div v-if="selectedSize === 'custom'" style="display: flex; align-items: center; gap: 12px; margin-top: 15px; padding-top: 10px; border-top: 1px dashed #ebeef5;">
  212. <span style="font-size: 14px; color: #606266; white-space: nowrap;">自定义尺寸:</span>
  213. <el-input
  214. v-model="customWidth"
  215. size="small"
  216. style="width: 100px;"
  217. placeholder="宽度"
  218. @input="validateCustomSize">
  219. <template #append>px</template>
  220. </el-input>
  221. <span style="color: #909399;">×</span>
  222. <el-input
  223. v-model="customHeight"
  224. size="small"
  225. style="width: 100px;"
  226. placeholder="高度"
  227. @input="validateCustomSize">
  228. <template #append>px</template>
  229. </el-input>
  230. </div>
  231. <!-- 显示当前尺寸信息 -->
  232. <div v-if="selectedSize !== 'custom'" style="font-size: 13px; color: #909399; margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ebeef5;">
  233. 当前尺寸: {{ getSizeInfo(selectedSize) }}
  234. </div>
  235. </div>
  236. <!-- 操作按钮 -->
  237. <div style="display: flex; justify-content: flex-end; gap: 12px;">
  238. <!-- <el-button size="medium" @click="clearInput">清空</el-button> -->
  239. <el-button size="medium" type="success" @click="optimizeContent" :loading="loadingStatus || pollStatus === 'optimizing'">
  240. {{ loadingStatus || pollStatus === 'optimizing' ? '正在生成中' : '扩写提示词' }} <span style="margin-left: 8px;">⚡ 25 <span style="font-size: 12px;"></span></span>
  241. </el-button>
  242. <el-button size="medium" type="primary" @click="generateImage" :loading="loadingStatus || pollStatus === 'polling'">
  243. {{ loadingStatus || pollStatus === 'polling' ? '正在生成中' : '立即生成' }} <span style="margin-left: 8px;">⚡ 150 <span style="font-size: 12px;"></span></span>
  244. </el-button>
  245. </div>
  246. </div>
  247. </div>
  248. </div>
  249. <!-- 中间:产品信息和历史记录(保证两侧边框可见,不与其他列重叠) -->
  250. <div class="middle-column">
  251. <!-- 历史记录覆盖层 -->
  252. <div v-if="showHistoryPanel" style="position: absolute; top: 0; left: 0; right: 0; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
  253. <div style="width: 95%; height: 100%; overflow-y: auto;">
  254. <div style="padding: 20px;">
  255. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e4e7ed;">
  256. <h3 style="margin: 0; font-size: 16px; font-weight: bold; color: #303133;">历史记录</h3>
  257. <el-button type="danger" size="small" @click="toggleHistoryPanel">
  258. <el-icon><Close /></el-icon>关闭
  259. </el-button>
  260. </div>
  261. <!-- 模板分类树 -->
  262. <el-tree
  263. :data="templateTreeData"
  264. node-key="id"
  265. default-expand-all
  266. @node-click="handleTemplateTreeClick"
  267. style="margin-bottom: 20px;"
  268. >
  269. <template #default="{ node, data }">
  270. <span>{{ data.label }}</span>
  271. <span style="margin-left: 10px; font-size: 12px; color: #909399;">({{ data.count }}张)</span>
  272. </template>
  273. </el-tree>
  274. <!-- 选中模板的历史图片 -->
  275. <div>
  276. <h5 style="margin: 0 0 10px 0; font-size: 13px; font-weight: bold;">
  277. {{ currentTemplateName || '历史图片' }}
  278. </h5>
  279. <div class="history-images-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px;">
  280. <div v-for="(image, index) in filteredImages" :key="index" class="history-image-item" style="border: 1px solid #e4e7ed; border-radius: 4px; overflow: hidden; padding: 10px;">
  281. <div style="height: 120px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden;">
  282. <el-image
  283. :src="formatImageUrl(image.url)"
  284. :preview-src-list="[formatImageUrl(image.url)]"
  285. style="width: 100%; height: 100%;"
  286. fit="contain"
  287. lazy
  288. preview-teleported
  289. />
  290. </div>
  291. <div style="margin-top: 10px; font-size: 12px; color: #606266; line-height: 1.4; height: 50px; overflow: hidden;">
  292. {{ image.product_content || '' }}
  293. </div>
  294. <div style="margin-top: 5px; font-size: 11px; color: #909399; text-align: right;">
  295. {{ image.createTime || '' }}
  296. </div>
  297. </div>
  298. <div v-if="filteredImages.length === 0" style="text-align: center; padding: 40px; color: #909399; font-size: 14px; grid-column: 1 / -1;">
  299. 暂无历史图片
  300. </div>
  301. </div>
  302. </div>
  303. </div>
  304. </div>
  305. </div>
  306. <!-- 标题 -->
  307. <div style="font-size: 18px; font-weight: bold; color: white; margin-bottom: 15px; background-color: #404040; padding: 10px; border-radius: 4px; text-align: center; display: flex; justify-content: space-between; align-items: center;">
  308. <span>③效果图</span>
  309. <el-button type="primary" size="small" @click="toggleHistoryPanel">
  310. {{ showHistoryPanel ? '隐藏历史记录' : '显示历史记录' }}
  311. </el-button>
  312. </div>
  313. <!-- 主内容区域 -->
  314. <div style="display: flex; height: calc(100% - 90px);">
  315. <!-- 左侧:历史记录面板 (已移至覆盖层) -->
  316. <!-- 右侧:原始内容 -->
  317. <div class="original-content" style="flex: 1; overflow-y: auto; min-width: 0;">
  318. <!-- 商品图例覆盖层 -->
  319. <div v-if="productExampleVisible" style="position: absolute; top: 0; left: 0; right: 0; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
  320. <div style="width: 95%; height: 100%; overflow-y: auto;">
  321. <div style="padding: 20px;">
  322. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e4e7ed;">
  323. <h3 style="margin: 0; font-size: 16px; font-weight: bold; color: #303133;">商品图例</h3>
  324. <el-button type="text" size="small" @click="toggleProductExample">
  325. <el-icon><Close /></el-icon>
  326. </el-button>
  327. </div>
  328. <p style="margin: 0 0 20px 0; color: #606266; line-height: 1.5; font-size: 14px;">
  329. 建议上传商品单一、完整清晰、占据画面中心的白底图,否则可能会影响商品识别效果和生成的背景图质量。
  330. </p>
  331. <!-- 商品图例对比 -->
  332. <div style="margin-bottom: 20px;">
  333. <div style="display: flex; gap: 15px;">
  334. <!-- 正确示例 -->
  335. <div style="flex: 1; background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); border: 1px solid #e4e7ed;">
  336. <div style="display: flex; align-items: center; margin-bottom: 10px;">
  337. <el-icon :size="14" style="color: #67c23a; margin-right: 8px;"><SuccessFilled /></el-icon>
  338. <span style="font-weight: 500; color: #303133; font-size: 13px;">商品单一</span>
  339. </div>
  340. <div style="height: 120px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  341. <el-image
  342. :src="`/src/assets/ai案例图/001.png`"
  343. style="width: 100%; height: 100%;"
  344. fit="contain"
  345. >
  346. <template #error>
  347. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  348. <span>图片占位</span>
  349. </div>
  350. </template>
  351. </el-image>
  352. </div>
  353. </div>
  354. <!-- 错误示例 -->
  355. <div style="flex: 1; background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); border: 1px solid #e4e7ed;">
  356. <div style="display: flex; align-items: center; margin-bottom: 10px;">
  357. <el-icon :size="14" style="color: #f56c6c; margin-right: 8px;"><Close /></el-icon>
  358. <span style="font-weight: 500; color: #303133; font-size: 13px;">画面包含多件商品</span>
  359. </div>
  360. <div style="height: 120px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  361. <el-image
  362. :src="`/src/assets/ai案例图/002.png`"
  363. style="width: 100%; height: 100%;"
  364. fit="contain"
  365. >
  366. <template #error>
  367. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  368. <span>图片占位</span>
  369. </div>
  370. </template>
  371. </el-image>
  372. </div>
  373. </div>
  374. </div>
  375. </div>
  376. <div style="margin-bottom: 20px;">
  377. <div style="display: flex; gap: 15px;">
  378. <!-- 正确示例 -->
  379. <div style="flex: 1; background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); border: 1px solid #e4e7ed;">
  380. <div style="display: flex; align-items: center; margin-bottom: 10px;">
  381. <el-icon :size="14" style="color: #67c23a; margin-right: 8px;"><SuccessFilled /></el-icon>
  382. <span style="font-weight: 500; color: #303133; font-size: 13px;">画面完整清晰</span>
  383. </div>
  384. <div style="height: 120px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  385. <el-image
  386. :src="`/src/assets/ai案例图/003.png`"
  387. style="width: 100%; height: 100%;"
  388. fit="contain"
  389. >
  390. <template #error>
  391. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  392. <span>图片占位</span>
  393. </div>
  394. </template>
  395. </el-image>
  396. </div>
  397. </div>
  398. <!-- 错误示例 -->
  399. <div style="flex: 1; background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); border: 1px solid #e4e7ed;">
  400. <div style="display: flex; align-items: center; margin-bottom: 10px;">
  401. <el-icon :size="14" style="color: #f56c6c; margin-right: 8px;"><Close /></el-icon>
  402. <span style="font-weight: 500; color: #303133; font-size: 13px;">商品残缺/遮挡</span>
  403. </div>
  404. <div style="height: 120px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  405. <el-image
  406. :src="`/src/assets/ai案例图/004.png`"
  407. style="width: 100%; height: 100%;"
  408. fit="contain"
  409. >
  410. <template #error>
  411. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  412. <span>图片占位</span>
  413. </div>
  414. </template>
  415. </el-image>
  416. </div>
  417. </div>
  418. </div>
  419. </div>
  420. <div style="margin-bottom: 20px;">
  421. <div style="display: flex; gap: 15px;">
  422. <!-- 正确示例 -->
  423. <div style="flex: 1; background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); border: 1px solid #e4e7ed;">
  424. <div style="display: flex; align-items: center; margin-bottom: 10px;">
  425. <el-icon :size="14" style="color: #67c23a; margin-right: 8px;"><SuccessFilled /></el-icon>
  426. <span style="font-weight: 500; color: #303133; font-size: 13px;">商品占据画面中心</span>
  427. </div>
  428. <div style="height: 120px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  429. <el-image
  430. :src="`/src/assets/ai案例图/005.jpg`"
  431. style="width: 100%; height: 100%;"
  432. fit="contain"
  433. >
  434. <template #error>
  435. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  436. <span>图片占位</span>
  437. </div>
  438. </template>
  439. </el-image>
  440. </div>
  441. </div>
  442. <!-- 错误示例 -->
  443. <div style="flex: 1; background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); border: 1px solid #e4e7ed;">
  444. <div style="display: flex; align-items: center; margin-bottom: 10px;">
  445. <el-icon :size="14" style="color: #f56c6c; margin-right: 8px;"><Close /></el-icon>
  446. <span style="font-weight: 500; color: #303133; font-size: 13px;">商铺主题不突出</span>
  447. </div>
  448. <div style="height: 120px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  449. <el-image
  450. :src="`/src/assets/ai案例图/006.jpg`"
  451. style="width: 100%; height: 100%;"
  452. fit="contain"
  453. >
  454. <template #error>
  455. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  456. <span>图片占位</span>
  457. </div>
  458. </template>
  459. </el-image>
  460. </div>
  461. </div>
  462. </div>
  463. </div>
  464. </div>
  465. </div>
  466. </div>
  467. <!-- 案例覆盖层 -->
  468. <div v-if="caseExampleVisible" style="position: absolute; top: 0; left: 0; right: 0; width: 100%; max-width: 600px; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
  469. <div style="width: 95%; height: 100%; overflow-y: auto;">
  470. <div style="padding: 20px;">
  471. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e4e7ed;">
  472. <h3 style="margin: 0; font-size: 16px; font-weight: bold; color: #303133;">合成商品图例</h3>
  473. <el-button type="text" size="small" @click="toggleCaseExample">
  474. <el-icon><Close /></el-icon>
  475. </el-button>
  476. </div>
  477. <div style="display: flex; margin-bottom: 15px; padding: 10px 0; border-bottom: 1px solid #e4e7ed;">
  478. <div style="flex: 1; font-weight: 600; color: #303133; font-size: 14px; text-align: center;">原图</div>
  479. <div style="width: 40px;"></div>
  480. <div style="flex: 1; font-weight: 500; color: #606266; font-size: 14px; text-align: center;">标准模式</div>
  481. <div style="width: 40px;"></div>
  482. <div style="flex: 1; font-weight: 500; color: #606266; font-size: 14px; text-align: center;">专业模式</div>
  483. </div>
  484. <!-- 案例图片网格 -->
  485. <div style="display: flex; flex-direction: column; gap: 15px;">
  486. <!-- 案例1 -->
  487. <div style="display: flex; align-items: center; gap: 10px;">
  488. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  489. <el-image
  490. :src="`/src/assets/ai案例图/007.png`"
  491. style="width: 100%; height: 100%;"
  492. fit="contain"
  493. >
  494. <template #error>
  495. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  496. <span>图片占位</span>
  497. </div>
  498. </template>
  499. </el-image>
  500. </div>
  501. <div style="width: 40px; display: flex; justify-content: center;">
  502. <el-icon style="color: #409eff;"><ArrowRight /></el-icon>
  503. </div>
  504. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  505. <el-image
  506. :src="`/src/assets/ai案例图/008.png`"
  507. style="width: 100%; height: 100%;"
  508. fit="contain"
  509. >
  510. <template #error>
  511. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  512. <span>图片占位</span>
  513. </div>
  514. </template>
  515. </el-image>
  516. </div>
  517. <div style="width: 40px; display: flex; justify-content: center;">
  518. <el-icon style="color: #409eff;"><ArrowRight /></el-icon>
  519. </div>
  520. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  521. <el-image
  522. :src="`/src/assets/ai案例图/009.png`"
  523. style="width: 100%; height: 100%;"
  524. fit="contain"
  525. >
  526. <template #error>
  527. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  528. <span>图片占位</span>
  529. </div>
  530. </template>
  531. </el-image>
  532. </div>
  533. </div>
  534. <!-- 案例2 -->
  535. <div style="display: flex; align-items: center; gap: 10px;">
  536. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  537. <el-image
  538. :src="`/src/assets/ai案例图/010.jpg`"
  539. style="width: 100%; height: 100%;"
  540. fit="contain"
  541. >
  542. <template #error>
  543. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  544. <span>图片占位</span>
  545. </div>
  546. </template>
  547. </el-image>
  548. </div>
  549. <div style="width: 40px; display: flex; justify-content: center;">
  550. <el-icon style="color: #409eff;"><ArrowRight /></el-icon>
  551. </div>
  552. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  553. <el-image
  554. :src="`/src/assets/ai案例图/011.png`"
  555. style="width: 100%; height: 100%;"
  556. fit="contain"
  557. >
  558. <template #error>
  559. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  560. <span>图片占位</span>
  561. </div>
  562. </template>
  563. </el-image>
  564. </div>
  565. <div style="width: 40px; display: flex; justify-content: center;">
  566. <el-icon style="color: #409eff;"><ArrowRight /></el-icon>
  567. </div>
  568. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  569. <el-image
  570. :src="`/src/assets/ai案例图/012.png`"
  571. style="width: 100%; height: 100%;"
  572. fit="contain"
  573. >
  574. <template #error>
  575. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  576. <span>图片占位</span>
  577. </div>
  578. </template>
  579. </el-image>
  580. </div>
  581. </div>
  582. <!-- 案例3 -->
  583. <div style="display: flex; align-items: center; gap: 10px;">
  584. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  585. <el-image
  586. :src="`/src/assets/ai案例图/013.jpg`"
  587. style="width: 100%; height: 100%;"
  588. fit="contain"
  589. >
  590. <template #error>
  591. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  592. <span>图片占位</span>
  593. </div>
  594. </template>
  595. </el-image>
  596. </div>
  597. <div style="width: 40px; display: flex; justify-content: center;">
  598. <el-icon style="color: #409eff;"><ArrowRight /></el-icon>
  599. </div>
  600. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  601. <el-image
  602. :src="`/src/assets/ai案例图/014.png`"
  603. style="width: 100%; height: 100%;"
  604. fit="contain"
  605. >
  606. <template #error>
  607. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  608. <span>图片占位</span>
  609. </div>
  610. </template>
  611. </el-image>
  612. </div>
  613. <div style="width: 40px; display: flex; justify-content: center;">
  614. <el-icon style="color: #409eff;"><ArrowRight /></el-icon>
  615. </div>
  616. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  617. <el-image
  618. :src="`/src/assets/ai案例图/015.png`"
  619. style="width: 100%; height: 100%;"
  620. fit="contain"
  621. >
  622. <template #error>
  623. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  624. <span>图片占位</span>
  625. </div>
  626. </template>
  627. </el-image>
  628. </div>
  629. </div>
  630. <!-- 案例4 -->
  631. <div style="display: flex; align-items: center; gap: 10px;">
  632. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  633. <el-image
  634. :src="`/src/assets/ai案例图/016.png`"
  635. style="width: 100%; height: 100%;"
  636. fit="contain"
  637. >
  638. <template #error>
  639. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  640. <span>图片占位</span>
  641. </div>
  642. </template>
  643. </el-image>
  644. </div>
  645. <div style="width: 40px; display: flex; justify-content: center;">
  646. <el-icon style="color: #409eff;"><ArrowRight /></el-icon>
  647. </div>
  648. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  649. <el-image
  650. :src="`/src/assets/ai案例图/017.png`"
  651. style="width: 100%; height: 100%;"
  652. fit="contain"
  653. >
  654. <template #error>
  655. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  656. <span>图片占位</span>
  657. </div>
  658. </template>
  659. </el-image>
  660. </div>
  661. <div style="width: 40px; display: flex; justify-content: center;">
  662. <el-icon style="color: #409eff;"><ArrowRight /></el-icon>
  663. </div>
  664. <div style="flex: 1; height: 80px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
  665. <el-image
  666. :src="`/src/assets/ai案例图/018.png`"
  667. style="width: 100%; height: 100%;"
  668. fit="contain"
  669. >
  670. <template #error>
  671. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399; font-size: 12px;">
  672. <span>图片占位</span>
  673. </div>
  674. </template>
  675. </el-image>
  676. </div>
  677. </div>
  678. </div>
  679. </div>
  680. </div>
  681. </div>
  682. <div style="height: 100%; display: flex; flex-direction: column; width: 100%; min-width: 0; align-items: center;">
  683. <!-- 上:留出的空白区域 -->
  684. <div style="width: 100%; height: 90px; background-image: url('/src/assets/top-bg.png'); background-size: cover; background-position: center; border-top-left-radius: 8px; border-top-right-radius: 8px;"></div>
  685. <!-- 上中:图片显示区域 -->
  686. <div style="display: flex; flex-direction: column; align-items: center; width: 100%;">
  687. <!-- 产品图片显示(默认) -->
  688. <div style="width: 100%; height: 290px; max-width: 430px; display: flex; justify-content: center; align-items: center; background-color: white; border-radius: 4px;">
  689. <el-carousel v-if="newImages.length > 0" indicator-position="outside" style="width: 100%; height: 100%;">
  690. <el-carousel-item v-for="(image, index) in newImages" :key="index">
  691. <div style="width: 100%; height: 100%; cursor: pointer;" @click="handleImageZoom(formatImageUrl(image.url))">
  692. <el-image
  693. :src="formatImageUrl(image.url)"
  694. style="width: 100%; height: 100%; object-fit: contain;"
  695. fit="contain"
  696. >
  697. <template #error>
  698. <div style="width: 100%; height: 100%; background-color: white;" />
  699. </template>
  700. </el-image>
  701. </div>
  702. </el-carousel-item>
  703. </el-carousel>
  704. <div v-else-if="editFormData.new_image_url && showProductImage" style="width: 100%; height: 100%; cursor: pointer;" @click="handleImageZoom(formatImageUrl(editFormData.new_image_url))">
  705. <el-image
  706. :src="formatImageUrl(editFormData.new_image_url)"
  707. style="width: 100%; height: 100%; object-fit: contain;"
  708. fit="contain"
  709. >
  710. <template #error>
  711. <div style="width: 100%; height: 100%; background-color: white;" />
  712. </template>
  713. </el-image>
  714. </div>
  715. <div v-else-if="!showProductImage || !editFormData.new_image_url" class="image-placeholder">
  716. <el-icon :size="60"><Picture /></el-icon>
  717. <span style="margin-top: 10px; display: block;">暂无效果图</span>
  718. </div>
  719. </div>
  720. </div>
  721. <!-- 中:商品信息表单(宽度 100% 含边框,避免右侧边框被裁) -->
  722. <div class="product-info-container" style="width: 100%; max-width: 430px; height: 320px; border: 12px solid #f6E0dd; border-radius: 4px; padding: 15px; box-sizing: border-box;">
  723. <div class="product-info-item">
  724. <span class="product-info-label">商品条码:</span>
  725. <span class="product-info-value">{{ editFormData.barcode || '-' }}</span>
  726. </div>
  727. <div class="product-info-item">
  728. <span class="product-info-label">宏伟编码:</span>
  729. <span class="product-info-value">{{ editFormData.code || '-' }}</span>
  730. </div>
  731. <div class="product-info-item">
  732. <span class="product-info-label">产品名称:</span>
  733. <span class="product-info-value">{{ editFormData.product_name || '-' }}</span>
  734. </div>
  735. <div class="product-info-item">
  736. <span class="product-info-label">品牌名称:</span>
  737. <span class="product-info-value">{{ editFormData.brand_name || '-' }}</span>
  738. </div>
  739. <div class="product-info-item">
  740. <span class="product-info-label">产品规格:</span>
  741. <span class="product-info-value">{{ editFormData.specification || '-' }}</span>
  742. </div>
  743. <div class="product-info-item">
  744. <span class="product-info-label">单 位:</span>
  745. <span class="product-info-value">{{ editFormData.unit || '-' }}</span>
  746. </div>
  747. </div>
  748. <!-- 下:空出来的部分 -->
  749. <div style="width: 100%; max-width: 430px; height: 61px;"></div>
  750. </div>
  751. </div>
  752. </div>
  753. </div>
  754. <!-- 右侧:模版列表(与中间列左右留白一致) -->
  755. <div class="right-column">
  756. <div class="right-template" style="height: 100%; display: flex; flex-direction: column;">
  757. <!-- 标题 -->
  758. <div style="font-size: 18px; font-weight: bold; color: white; margin-bottom: 15px; background-color: #404040; padding: 10px; border-radius: 4px; text-align: center;margin-top:15px">①模版</div>
  759. <!-- 搜索框 -->
  760. <div class="template-search" style="margin-bottom: 15px;">
  761. <el-input v-model="searchKeyword" placeholder="请描述你想搜索的模版关键字..." clearable @input="handleSearch" @clear="handleClearSearch">
  762. <template #prefix>
  763. <el-icon><Search /></el-icon>
  764. </template>
  765. <template #append>
  766. <el-button @click="handleSearch" >搜索</el-button>
  767. </template>
  768. </el-input>
  769. </div>
  770. <!-- 关键词搜索次数最多的 -->
  771. <!-- 模板列表 -->
  772. <div style="flex: 1; min-height: 0;">
  773. <h4 style="margin-bottom: 10px;margin: 0px 0px 20px 0px;padding: 0px;">模版分类 ({{ templateList.length }})</h4>
  774. <el-scrollbar style="height: 100%;">
  775. <div class="template-list">
  776. <!-- 搜索无结果提示 -->
  777. <div v-if="!searchLoading && searchKeyword && templateList.length === 0" class="empty-search">
  778. <el-icon :size="50"><Search /></el-icon>
  779. <p>未找到相关模板</p>
  780. </div>
  781. <!-- 模板项 -->
  782. <div v-for="template in templateList"
  783. :key="template.id" class="template-item"
  784. :class="{ 'active': currentTemplateId === template.id }"
  785. @click="selectTemplate(template)" >
  786. <div class="template-thumbnail" style="position: relative;">
  787. <div class="image-container" @click.stop="selectTemplate(template)">
  788. <el-image
  789. :src="formatImageUrl(template.thumbnail_image)"
  790. style="width: 100%; height: 100%;"
  791. fit="contain"
  792. lazy
  793. >
  794. <template #placeholder>
  795. <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f5f7fa;"><el-icon class="is-loading"><Loading /></el-icon></div>
  796. </template>
  797. <template #error>
  798. <div class="thumbnail-error">
  799. <el-icon><Picture /></el-icon>
  800. </div>
  801. </template>
  802. </el-image>
  803. <div class="zoom-icon" @click.stop="handleImageZoom(formatImageUrl(template.thumbnail_image))">
  804. <el-icon :size="20"><ZoomIn /></el-icon>
  805. </div>
  806. </div>
  807. <div class="template-id">模版 {{ template.id }}</div>
  808. <el-button
  809. type="primary"
  810. circle
  811. size="small"
  812. style="position: absolute; top: 10px; right: 10px; opacity: 0; transition: opacity 0.3s; z-index: 10;"
  813. @click.stop="downloadTemplateImage(formatImageUrl(template.thumbnail_image))"
  814. class="image-download-btn" >
  815. <el-icon><Download /></el-icon>
  816. </el-button>
  817. </div>
  818. <div class="template-name" :title="template.template_name || `模板 ${template.id}`">
  819. {{ truncateText(template.template_name || `模板 ${template.id}`, 20) }}
  820. </div>
  821. <div class="template-desc" :title="template.chinese_description">
  822. {{ truncateText(template.chinese_description, 25) }}
  823. </div>
  824. </div>
  825. <!-- 初始无数据提示 -->
  826. <div v-if="!searchKeyword && !searchLoading && templateList.length === 0" class="empty-templates">
  827. <el-icon :size="40"><Picture /></el-icon>
  828. <p>暂无模板数据</p>
  829. </div>
  830. </div>
  831. </el-scrollbar>
  832. <!-- 图片预览组件 -->
  833. <el-image-viewer
  834. v-if="previewVisible"
  835. :url-list="previewImageUrl ? [previewImageUrl] : []"
  836. :hide-on-click-modal="true"
  837. @close="previewVisible = false"
  838. />
  839. </div>
  840. </div>
  841. </div>
  842. <!-- 商品图例右侧面板 (已移至中间列) -->
  843. <div v-if="false" class="product-example-panel" style="width: 420px; border-left: 1px solid #e4e7ed; padding: 20px; background: #ffffff; overflow-y: auto; box-shadow: -2px 0 10px rgba(0,0,0,0.05);">
  844. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
  845. <h3 style="margin: 0; font-size: 16px; font-weight: bold; color: #303133;">商品图例</h3>
  846. <el-button type="text" size="small" @click="toggleProductExample">
  847. <el-icon><Close /></el-icon>
  848. </el-button>
  849. </div>
  850. <p style="margin: 0 0 15px 0; color: #606266; line-height: 1.5;">
  851. 建议上传商品单一、完整清晰、占据画面中心的白底图,否则可能会影响商品识别效果和生成的背景图质量。
  852. </p>
  853. <!-- 商品单一 -->
  854. <div style="margin-bottom: 20px;">
  855. <div style="font-weight: 600; color: #303133; margin-bottom: 10px;">商品单一</div>
  856. <div style="background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 10px;">
  857. <div style="display: flex; align-items: center; margin-bottom: 8px;">
  858. <el-icon :size="16" style="color: #67c23a; margin-right: 8px;"><SuccessFilled /></el-icon>
  859. <span style="font-weight: 500; color: #303133;">商品完整清晰</span>
  860. </div>
  861. <div style="height: 180px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden;">
  862. <el-image
  863. :src="'@/assets/ai案例图/001.png'"
  864. style="width: 100%; height: 100%;"
  865. fit="contain"
  866. >
  867. <template #error>
  868. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399;">
  869. <span>图片占位</span>
  870. </div>
  871. </template>
  872. </el-image>
  873. </div>
  874. </div>
  875. <div style="background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
  876. <div style="display: flex; align-items: center; margin-bottom: 8px;">
  877. <el-icon :size="16" style="color: #67c23a; margin-right: 8px;"><SuccessFilled /></el-icon>
  878. <span style="font-weight: 500; color: #303133;">商品占据画面中心</span>
  879. </div>
  880. <div style="height: 180px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden;">
  881. <el-image
  882. :src="'@/assets/ai案例图/002.png'"
  883. style="width: 100%; height: 100%;"
  884. fit="contain"
  885. >
  886. <template #error>
  887. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399;">
  888. <span>图片占位</span>
  889. </div>
  890. </template>
  891. </el-image>
  892. </div>
  893. </div>
  894. </div>
  895. <!-- 画面包含多件商品 -->
  896. <div style="margin-bottom: 20px;">
  897. <div style="font-weight: 600; color: #303133; margin-bottom: 10px;">画面包含多件商品</div>
  898. <div style="background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
  899. <div style="display: flex; align-items: center; margin-bottom: 8px;">
  900. <el-icon :size="16" style="color: #f56c6c; margin-right: 8px;"><Close /></el-icon>
  901. <span style="font-weight: 500; color: #303133;">不推荐</span>
  902. </div>
  903. <div style="height: 180px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden;">
  904. <el-image
  905. :src="'@/assets/ai案例图/003.png'"
  906. style="width: 100%; height: 100%;"
  907. fit="contain"
  908. >
  909. <template #error>
  910. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399;">
  911. <span>图片占位</span>
  912. </div>
  913. </template>
  914. </el-image>
  915. </div>
  916. </div>
  917. </div>
  918. <!-- 商品残缺/遮挡 -->
  919. <div style="margin-bottom: 20px;">
  920. <div style="font-weight: 600; color: #303133; margin-bottom: 10px;">商品残缺/遮挡</div>
  921. <div style="background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
  922. <div style="display: flex; align-items: center; margin-bottom: 8px;">
  923. <el-icon :size="16" style="color: #e6a23c; margin-right: 8px;"><WarningFilled /></el-icon>
  924. <span style="font-weight: 500; color: #303133;">商品主体不突出</span>
  925. </div>
  926. <div style="height: 180px; display: flex; justify-content: center; align-items: center; background: #f9f9f9; border-radius: 4px; overflow: hidden;">
  927. <el-image
  928. :src="'@/assets/ai案例图/004.png'"
  929. style="width: 100%; height: 100%;"
  930. fit="contain"
  931. >
  932. <template #error>
  933. <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #909399;">
  934. <span>图片占位</span>
  935. </div>
  936. </template>
  937. </el-image>
  938. </div>
  939. </div>
  940. </div>
  941. </div>
  942. </div>
  943. </el-dialog>
  944. <!-- 图片预览 -->
  945. <el-dialog v-model="dialogVisible" title="图片预览" width="70%" top="5vh" destroy-on-close>
  946. <div style="text-align: center;">
  947. <el-image :src="currentImageUrl" style="max-width: 100%; max-height: 70vh;" fit="contain" />
  948. </div>
  949. </el-dialog>
  950. <el-dialog
  951. v-model="AdddialogVisible"
  952. title="新增产品"
  953. width="600px"
  954. :close-on-click-modal="false"
  955. @close="handleClose"
  956. >
  957. <el-form
  958. ref="productFormRef"
  959. :model="productForm"
  960. label-width="100px"
  961. label-position="right"
  962. size="medium"
  963. >
  964. <!--商户名称 -->
  965. <el-form-item label="商户名称" prop="merchant_id">
  966. <el-input
  967. :model-value="addDialogMerchantName"
  968. placeholder="请先在左侧选择商户"
  969. style="width: 100%"
  970. readonly
  971. disabled
  972. />
  973. </el-form-item>
  974. <!-- 产品名称 -->
  975. <el-form-item label="产品名称" prop="product_name">
  976. <el-input
  977. v-model="productForm.product_name"
  978. placeholder="请输入产品名称"
  979. clearable
  980. show-word-limit
  981. />
  982. </el-form-item>
  983. <!-- 产品编码 -->
  984. <el-form-item label="产品编码" prop="product_code">
  985. <el-input
  986. v-model="productForm.product_code"
  987. placeholder="请输入产品编码"
  988. clearable
  989. show-word-limit
  990. />
  991. </el-form-item>
  992. <!-- 产品图片 -->
  993. <el-form-item label="产品图片" prop="product_img">
  994. <div class="add-dialog-upload-wrap">
  995. <!-- 已上传:预览 + 替换/删除 -->
  996. <div v-if="productForm.product_img" class="add-dialog-preview-box">
  997. <div class="add-dialog-preview-img">
  998. <img
  999. v-if="addDialogPreviewSrc"
  1000. :src="addDialogPreviewSrc"
  1001. class="add-dialog-preview-img-inner"
  1002. alt="产品图片预览"
  1003. />
  1004. </div>
  1005. <div v-if="addDialogImageSizeText" class="add-dialog-image-size">
  1006. 当前大小:{{ addDialogImageSizeText }}(限制 {{ MAX_IMAGE_SIZE_MB }}MB 以内)
  1007. </div>
  1008. <div class="add-dialog-preview-actions">
  1009. <el-upload
  1010. :show-file-list="false"
  1011. :before-upload="handleAddDialogSelectImage"
  1012. accept="image/jpeg,image/png,image/jpg,image/webp"
  1013. >
  1014. <el-button type="primary" size="small">
  1015. <el-icon><Upload /></el-icon>
  1016. 替换
  1017. </el-button>
  1018. </el-upload>
  1019. <el-button type="danger" size="small" plain @click="handleRemoveImage">
  1020. 删除
  1021. </el-button>
  1022. </div>
  1023. </div>
  1024. <!-- 未上传:点击上传区域 -->
  1025. <el-upload
  1026. v-else
  1027. class="add-dialog-upload-area"
  1028. :show-file-list="false"
  1029. :before-upload="handleAddDialogSelectImage"
  1030. accept="image/jpeg,image/png,image/jpg,image/webp"
  1031. >
  1032. <div class="add-dialog-upload-inner">
  1033. <el-icon class="add-dialog-upload-icon"><Plus /></el-icon>
  1034. <span class="add-dialog-upload-text">点击上传图片</span>
  1035. <span class="add-dialog-upload-tip">支持 jpg/png/webp,大小不超过 1MB</span>
  1036. </div>
  1037. </el-upload>
  1038. </div>
  1039. </el-form-item>
  1040. </el-form>
  1041. <template #footer>
  1042. <span class="dialog-footer">
  1043. <el-button @click="handleClose">取消</el-button>
  1044. <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
  1045. 确定
  1046. </el-button>
  1047. </span>
  1048. </template>
  1049. </el-dialog>
  1050. </div>
  1051. </template>
  1052. <script setup>
  1053. import { ref, reactive, computed, toRaw, onMounted, watch } from 'vue'
  1054. import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
  1055. import { getTable, imageToText, Template_ids,txttoimg_moxing,txttoimg_update, getSide,merchantGetab,productList,productDetail,
  1056. product_template,GetTxtToTxt,GetProductFind,productAdd,productDelete,getMerchantId,GetImageStatus } from '@/api/mes/job'
  1057. import { useUserStore } from '@/pinia/modules/user'
  1058. import { ZoomIn, Camera, SuccessFilled, WarningFilled, More, ArrowRight, ArrowDown, Loading, Plus, Upload, Picture, EditPen, Delete } from '@element-plus/icons-vue'
  1059. import { Layout, LayoutHeader, LayoutSider, LayoutContent } from '@arco-design/web-vue'
  1060. // 商品图例显示状态
  1061. const productExampleVisible = ref(false)
  1062. const caseExampleVisible = ref(false)
  1063. // 图片历史记录相关状态
  1064. const templateTreeData = ref([])
  1065. const currentTemplateId = ref(null)
  1066. const currentTemplateName = ref('')
  1067. const filteredImages = ref([])
  1068. const showHistoryPanel = ref(false)
  1069. // 切换历史记录面板显示
  1070. const toggleHistoryPanel = () => {
  1071. showHistoryPanel.value = !showHistoryPanel.value
  1072. }
  1073. // 切换商品图例显示
  1074. const toggleProductExample = () => {
  1075. productExampleVisible.value = !productExampleVisible.value
  1076. if (productExampleVisible.value) {
  1077. caseExampleVisible.value = false
  1078. }
  1079. }
  1080. // 切换案例显示
  1081. const toggleCaseExample = () => {
  1082. caseExampleVisible.value = !caseExampleVisible.value
  1083. if (caseExampleVisible.value) {
  1084. productExampleVisible.value = false
  1085. }
  1086. }
  1087. const props = defineProps({
  1088. editFormData: {
  1089. type: Object,
  1090. default: () => ({})
  1091. }
  1092. })
  1093. //获取登录用户信息
  1094. const userStore = useUserStore()
  1095. const _username = ref('')
  1096. _username.value = userStore.userInfo.userName + '/' + userStore.userInfo.nickName
  1097. console.log('获取用户信息',_username.value)
  1098. console.log('获取用户名称',userStore.userInfo.nickName)
  1099. const searchInfo = ref('')
  1100. const tableData1 = ref([])
  1101. const total = ref(0)
  1102. const page = ref(1)
  1103. const pageSize = ref(10)
  1104. const dialogVisible = ref(false)
  1105. const currentImageUrl = ref('')
  1106. const width = ref('')
  1107. const height = ref('')
  1108. const isLoading = ref(false)
  1109. const txttotxt_selectedOption = ref('gemini-2.0-flash')
  1110. const selectedOption = ref('dall-e-3')
  1111. const num = ref(1)
  1112. const _parh = ref([])
  1113. const folderList = ref([])
  1114. const selectedFolder = ref('')
  1115. // 左侧树形数据
  1116. const treeData = ref([])
  1117. const getTreeData = async () => {
  1118. try {
  1119. const data = await merchantGetab();
  1120. console.log(data)
  1121. treeData.value = data.data.map(item => {
  1122. return {
  1123. label: item.tab || item.merchant_name,
  1124. value: item.merchant_code,
  1125. // 如果需要原始数据可以保留
  1126. originalData: item
  1127. }
  1128. })
  1129. } catch (error) {
  1130. console.error(error)
  1131. treeData.value = [] // 出错时清空数据
  1132. }
  1133. }
  1134. getTreeData();
  1135. const defaultProps = {
  1136. children: 'children',
  1137. label: 'label'
  1138. }
  1139. const editDialogVisible = ref(false)
  1140. const editFormData = reactive({
  1141. id: '',
  1142. chinese_description: '',
  1143. english_description: '',
  1144. new_image_url: '',
  1145. new_video_url: '',
  1146. imgtoimg_url: '',
  1147. img_name: '',
  1148. video_name: ''
  1149. })
  1150. // 定义文生图表单对象
  1151. const form = reactive({
  1152. chinese_description: ''
  1153. })
  1154. // 控制产品信息部分图片是否显示
  1155. const showProductImage = ref(false)
  1156. // 存储生成的多个新图
  1157. const newImages = ref([])
  1158. // 加载状态控制
  1159. const loadingStatus = ref(false)
  1160. // 图片 URL:优先用相对路径走代理(无跨域);路径补全 /uploads/
  1161. const formatImageUrl = (path) => {
  1162. if (!path || typeof path !== 'string') return ''
  1163. const p = path.trim()
  1164. if (!p) return ''
  1165. if (p.startsWith('data:')) return p
  1166. if (p.startsWith('http://') || p.startsWith('https://')) {
  1167. try {
  1168. const u = new URL(p)
  1169. const port = import.meta.env.VITE_UPLOADS_PORT || '9093'
  1170. if (u.port === '9090') return `${u.protocol}//${u.hostname}:${port}${u.pathname}${u.search}`
  1171. } catch { /* ignore */ }
  1172. return p
  1173. }
  1174. let cleanPath = p.replace(/^public\//, '').replace(/^\//, '')
  1175. if (!cleanPath.startsWith('uploads/') && !cleanPath.startsWith('/uploads/')) {
  1176. cleanPath = 'uploads/' + cleanPath
  1177. }
  1178. const pathNorm = cleanPath.startsWith('/') ? cleanPath : '/' + cleanPath
  1179. const port = (import.meta.env.VITE_UPLOADS_PORT || '9093').toString().trim()
  1180. // 开发环境:相对路径走 Vite 代理 /uploads -> VITE_BASE_PATH:VITE_UPLOADS_PORT,避免跨域
  1181. if (import.meta.env.DEV) {
  1182. return pathNorm
  1183. }
  1184. // 生产环境:使用当前页 host + 端口(需确保该端口提供图片服务或 nginx 代理 /uploads)
  1185. if (typeof window !== 'undefined') {
  1186. const host = `${window.location.protocol}//${window.location.hostname}:${port}`
  1187. return `${host.replace(/\/$/, '')}${pathNorm}`
  1188. }
  1189. return pathNorm
  1190. }
  1191. // 点击图片时更新产品信息部分的图片和对应的product_content
  1192. const updateProductImage = (imageUrl) => {
  1193. editFormData.new_image_url = imageUrl
  1194. showProductImage.value = true
  1195. // 查找对应的图片对象,更新product_content到输入框
  1196. const selectedImage = newImages.value.find(img => img.url === imageUrl)
  1197. if (selectedImage && selectedImage.product_content) {
  1198. editFormData.chinese_description = selectedImage.product_content
  1199. }
  1200. // ElMessage.success('图片已更新到产品信息区域')
  1201. }
  1202. const txttoimg_modelList = ref([]); // 存储所有模型数据
  1203. const txtimgselectedModel = ref(''); // 当前选中的模型名称
  1204. const usedId = ref(null); // 当前使用的模型ID(兼容旧代码)
  1205. const selectedId = ref(null); // 用户选择的模型ID(兼容旧代码)
  1206. const isGenerating = ref(false); // 文生图生成中状态
  1207. const usedIds = ref({}); // 当前使用的模型ID集合
  1208. const selectedIds = ref({}); // 用户选择的模型ID集合
  1209. // 获取文生图模型列表
  1210. const fetchTxtImgModels = async () => {
  1211. try {
  1212. const response = await txttoimg_moxing();
  1213. txttoimg_modelList.value = response.data.models.wenshengtu;
  1214. // 获取当前使用的模型ID
  1215. usedIds.value = response.data.used_ids;
  1216. const currentUsedId = usedIds.value.wenshengtu;
  1217. // 设置默认选中的模型
  1218. const defaultModel = txttoimg_modelList.value.find(item => item.id === currentUsedId);
  1219. if (defaultModel) {
  1220. txtimgselectedModel.value = defaultModel.txttoimg;
  1221. usedId.value = defaultModel.id;
  1222. selectedId.value = defaultModel.id;
  1223. selectedIds.value.wenshengtu = defaultModel.id;
  1224. selectedOption.value = defaultModel.txttoimg;
  1225. }
  1226. console.log('文生图模型列表:', txttoimg_modelList.value);
  1227. console.log('当前使用的模型ID:', currentUsedId);
  1228. console.log('默认选中的模型:', defaultModel);
  1229. } catch (error) {
  1230. console.error('获取文生图模型列表失败:', error);
  1231. ElMessage.error('获取模型列表失败');
  1232. }
  1233. }
  1234. fetchTxtImgModels();
  1235. // const getTableData = async () => {
  1236. // const res = await getTable({
  1237. // search: searchInfo.value,
  1238. // limit: pageSize.value,
  1239. // page: page.value,
  1240. // folder: selectedFolder.value
  1241. // })
  1242. // tableData1.value = res.data.list
  1243. // total.value = res.data.total
  1244. // // 更新文件夹列表
  1245. // if (res.data.folder) {
  1246. // folderList.value = res.data.folder
  1247. // }
  1248. // }
  1249. const nodeid = ref('')
  1250. // 刷新产品列表(新增/编辑/删除后调用)
  1251. const refreshProductList = async () => {
  1252. if (!nodeid.value) return
  1253. try {
  1254. const res = await productList({
  1255. search: searchInfo.value,
  1256. limit: pageSize.value,
  1257. page: page.value,
  1258. code: nodeid.value
  1259. })
  1260. if (res.code === 0) {
  1261. const sortedData = (res.data.list || []).sort((b, a) => a.id - b.id)
  1262. tableData1.value = sortedData
  1263. total.value = res.data.total
  1264. }
  1265. } catch (e) {
  1266. console.error('刷新产品列表失败:', e)
  1267. }
  1268. }
  1269. // 处理树节点点击
  1270. const handleNodeClick = async(node) => {
  1271. console.log('点击的节点:', node.value);
  1272. nodeid.value = node.value;
  1273. const res = await productList({
  1274. search: searchInfo.value,
  1275. limit: pageSize.value,
  1276. page: page.value,
  1277. code: node.value
  1278. });
  1279. if (res.code === 0) {
  1280. // 按照ID进行排序,保持数据顺序
  1281. const sortedData = res.data.list.sort((b, a) => a.id - b.id);
  1282. tableData1.value = sortedData;
  1283. total.value = res.data.total;
  1284. }
  1285. };
  1286. // 当选择模型变化时
  1287. const handleModelChange = (modelName) => {
  1288. const model = txttoimg_modelList.value.find(item => item.txttoimg === modelName);
  1289. if (model) {
  1290. selectedId.value = model.id;
  1291. selectedIds.value.wenshengtu = model.id;
  1292. selectedOption.value = model.txttoimg;
  1293. console.log('选择的模型ID:', model.id);
  1294. }
  1295. }
  1296. // 设置默认模型
  1297. const setActive = async (type) => {
  1298. const id = selectedIds.value[type]
  1299. if (id) {
  1300. try {
  1301. // 调用API设置默认模型
  1302. const res = await txttoimg_update({ id: id,type:"wenshengtu"})
  1303. if (res.code === 0) {
  1304. usedIds.value[type] = id
  1305. ElMessage.success('默认模型设置成功')
  1306. } else {
  1307. ElMessage.error(res.msg || '设置默认模型失败')
  1308. }
  1309. } catch (error) {
  1310. console.error('设置默认模型失败', error)
  1311. ElMessage.error('设置默认模型失败')
  1312. }
  1313. }
  1314. }
  1315. // 处理模板分类树数据
  1316. const processTemplateTreeData = (images) => {
  1317. // 按template_id分组
  1318. const templateMap = {}
  1319. images.forEach(item => {
  1320. if (item.template_id) {
  1321. if (!templateMap[item.template_id]) {
  1322. templateMap[item.template_id] = []
  1323. }
  1324. templateMap[item.template_id].push(item)
  1325. }
  1326. })
  1327. // 构建树结构数据
  1328. const treeData = []
  1329. Object.keys(templateMap).forEach(templateId => {
  1330. treeData.push({
  1331. id: templateId,
  1332. label: `模板 ${templateId}`,
  1333. count: templateMap[templateId].length
  1334. })
  1335. })
  1336. templateTreeData.value = treeData
  1337. // 默认选中第一个模板
  1338. if (treeData.length > 0) {
  1339. currentTemplateId.value = treeData[0].id
  1340. currentTemplateName.value = treeData[0].label
  1341. filterImagesByTemplate(treeData[0].id)
  1342. }
  1343. }
  1344. // 根据模板ID过滤图片
  1345. const filterImagesByTemplate = (templateId) => {
  1346. filteredImages.value = newImages.value.filter(image => image.template_id == templateId)
  1347. }
  1348. // 处理模板树点击事件
  1349. const handleTemplateTreeClick = (data) => {
  1350. currentTemplateId.value = data.id
  1351. currentTemplateName.value = data.label
  1352. filterImagesByTemplate(data.id)
  1353. }
  1354. // 产品设计按钮点击(与双击行一致)
  1355. const onRowDesignClick = (row) => {
  1356. onRowDblClick(row)
  1357. }
  1358. // 删除产品(带确认)
  1359. const handleDeleteProduct = async (row) => {
  1360. try {
  1361. await ElMessageBox.confirm(
  1362. `确定要删除产品「${row.产品名称 || row.id}」吗?`,
  1363. '确认删除',
  1364. {
  1365. confirmButtonText: '确定',
  1366. cancelButtonText: '取消',
  1367. type: 'warning'
  1368. }
  1369. )
  1370. const res = await productDelete({ id: row.id })
  1371. if (res?.code === 0) {
  1372. ElMessage.success('删除成功')
  1373. await refreshProductList()
  1374. } else {
  1375. ElMessage.error(res?.msg || '删除失败')
  1376. }
  1377. } catch (e) {
  1378. if (e !== 'cancel') {
  1379. console.error('删除产品失败:', e)
  1380. ElMessage.error('删除失败')
  1381. }
  1382. }
  1383. }
  1384. const onRowDblClick = async (row) => {
  1385. // 打开新产品前先清空上一产品的历史与展示,避免残留
  1386. showHistoryPanel.value = false
  1387. newImages.value = []
  1388. templateTreeData.value = []
  1389. filteredImages.value = []
  1390. currentTemplateId.value = null
  1391. currentTemplateName.value = ''
  1392. // 设置编辑表单ID
  1393. editFormData.id = row.id
  1394. editDialogVisible.value = true
  1395. // 1. 获取产品详情并设置原图
  1396. const detailResponse = await productDetail({ id: row.id })
  1397. if (detailResponse.code === 0 && detailResponse.data) {
  1398. editFormData.original_image_url = detailResponse.data['产品图片']
  1399. editFormData.new_image_url = detailResponse.data['产品效果图']
  1400. editFormData.original_name = detailResponse.data['产品名称']
  1401. editFormData.product_name = detailResponse.data['产品名称']
  1402. editFormData.product_code = detailResponse.data['产品编码']
  1403. // 如果有新图片,自动显示到产品信息区域
  1404. if (detailResponse.data['产品效果图']) {
  1405. showProductImage.value = true
  1406. } else {
  1407. showProductImage.value = false
  1408. }
  1409. // 获取image数组数据
  1410. if (detailResponse.image && Array.isArray(detailResponse.image)) {
  1411. detailResponse.image.forEach(item => {
  1412. if (item.product_new_img) {
  1413. newImages.value.push({
  1414. url: item.product_new_img,
  1415. product_content: item.product_content || '',
  1416. template_id: item.template_id,
  1417. createTime: item.createTime
  1418. })
  1419. }
  1420. })
  1421. // 如果有image数据,默认显示第一个的product_content
  1422. if (newImages.value.length > 0 && newImages.value[0].product_content) {
  1423. editFormData.chinese_description = newImages.value[0].product_content
  1424. }
  1425. // 处理模板分类树数据
  1426. processTemplateTreeData(detailResponse.image)
  1427. } else {
  1428. editFormData.chinese_description = ''
  1429. }
  1430. }
  1431. // 2. 获取所有模板数据
  1432. await fetchTemplates()
  1433. }
  1434. // 存储所有模板数据
  1435. const templateList = ref([])
  1436. // 获取所有模板
  1437. const fetchTemplates = async () => {
  1438. const response = await product_template()
  1439. if (response.code === 0 && response.data) {
  1440. templateList.value = response.data.map(item => ({
  1441. id: item.id,
  1442. template_name: item.template_name || '',
  1443. thumbnail_image: item.thumbnail_image,
  1444. chinese_description: item.chinese_description || '',
  1445. english_description: item.english_description || ''
  1446. }))
  1447. // 默认选中第一个模板
  1448. // if (templateList.value.length > 0) {
  1449. // selectTemplate(templateList.value[0])
  1450. // }
  1451. if (newImages.value.length > 0) {
  1452. updateProductImage(newImages.value[0].url)
  1453. }
  1454. }
  1455. }
  1456. // 选择模板
  1457. const selectTemplate = (template) => {
  1458. currentTemplateId.value = template.id
  1459. editFormData.thumbnail_image = template.thumbnail_image
  1460. editFormData.template_id = template.id
  1461. editFormData.chinese_description = template.chinese_description
  1462. }
  1463. // 处理图片加载错误
  1464. const handleTemplateImageError = (event) => {
  1465. event.target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2YwZjBmMCIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjEyIiBmaWxsPSIjY2NjIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+SW1hZ2UgRXJyb3I8L3RleHQ+PC9zdmc+'
  1466. }
  1467. // 修改后的 selectTemplate 函数(如果需要保留按钮)
  1468. const handleTemplateButton = () => {
  1469. // 可以留空或给提示
  1470. ElMessage.info('请直接点击上方的模板图片')
  1471. }
  1472. // 图片预览相关
  1473. const previewVisible = ref(false)
  1474. const previewImageUrl = ref('')
  1475. // 处理图片放大
  1476. const handleImageZoom = (url) => {
  1477. previewImageUrl.value = url
  1478. previewVisible.value = true
  1479. }
  1480. // 下载模板图片
  1481. const downloadTemplateImage = async (imageUrl) => {
  1482. try {
  1483. // 创建一个临时的a标签用于下载
  1484. const link = document.createElement('a')
  1485. link.href = imageUrl
  1486. // 设置文件名,从URL中提取或使用默认名
  1487. const fileName = imageUrl.split('/').pop() || `template_${Date.now()}.jpg`
  1488. link.download = fileName
  1489. // 触发下载
  1490. document.body.appendChild(link)
  1491. link.click()
  1492. // 清理
  1493. document.body.removeChild(link)
  1494. ElMessage.success('图片下载成功')
  1495. } catch (error) {
  1496. console.error('图片下载失败:', error)
  1497. ElMessage.error('图片下载失败,请稍后重试')
  1498. }
  1499. }
  1500. // 截断文本函数
  1501. const truncateText = (text, length) => {
  1502. if (!text) return ''
  1503. if (text.length <= length) return text
  1504. return text.substring(0, length) + '...'
  1505. }
  1506. // 清空输入框内容
  1507. const clearInput = () => {
  1508. editFormData.chinese_description = ''
  1509. ElMessage.success('已清空输入框内容')
  1510. }
  1511. // 内容优化(使用轮询机制)
  1512. const optimizeContent = async () => {
  1513. try {
  1514. if (!editFormData.chinese_description) {
  1515. ElMessage.warning('请输入要优化的内容')
  1516. return
  1517. }
  1518. // 设置加载状态
  1519. loadingStatus.value = true
  1520. // 重置状态
  1521. pollStatus.value = 'optimizing'
  1522. pollCount.value = 0
  1523. // 第一步:调用生成接口触发生成任务
  1524. const generateResponse = await GetTxtToTxt({
  1525. status_val: '文生文',
  1526. prompt: editFormData.chinese_description,
  1527. model: 'gemini-3-pro-preview',
  1528. id: editFormData.id,
  1529. path: editFormData.original_image_url,
  1530. })
  1531. if (generateResponse.code === 0) {
  1532. ElMessage.success('内容优化任务已开始,请稍候...')
  1533. // 清除之前的定时器
  1534. if (pollInterval.value) {
  1535. clearInterval(pollInterval.value)
  1536. }
  1537. // 开始轮询查询结果
  1538. pollInterval.value = setInterval(async () => {
  1539. pollCount.value++
  1540. try {
  1541. // 调用查询接口
  1542. const checkResponse = await GetProductFind({ id: editFormData.id })
  1543. if (checkResponse.code === 0 && checkResponse.data) {
  1544. // 检查是否有content字段
  1545. if (checkResponse.data.content) {
  1546. // 停止轮询
  1547. clearInterval(pollInterval.value)
  1548. pollInterval.value = null
  1549. pollStatus.value = 'success'
  1550. loadingStatus.value = false
  1551. // 更新内容到输入框
  1552. editFormData.chinese_description = checkResponse.data.content
  1553. ElMessage.success(`内容优化成功!耗时约${pollCount.value * 10}秒`)
  1554. return
  1555. }
  1556. }
  1557. // 如果轮询超过30次(5分钟),停止轮询
  1558. if (pollCount.value >= 30) {
  1559. clearInterval(pollInterval.value)
  1560. pollInterval.value = null
  1561. pollStatus.value = 'error'
  1562. loadingStatus.value = false
  1563. ElMessage.warning('优化超时,请稍后手动检查结果')
  1564. }
  1565. } catch (pollError) {
  1566. console.error('优化轮询查询失败:', pollError)
  1567. // 轮询失败继续尝试
  1568. }
  1569. }, 5000) // 每5秒轮询一次
  1570. } else {
  1571. pollStatus.value = 'error'
  1572. loadingStatus.value = false
  1573. ElMessage.error('优化任务启动失败: ' + (generateResponse.msg || '未知错误'))
  1574. }
  1575. } catch (generateError) {
  1576. pollStatus.value = 'error'
  1577. loadingStatus.value = false
  1578. console.error('内容优化失败:', generateError)
  1579. ElMessage.error('内容优化失败: ' + (generateError.message || '未知错误'))
  1580. }
  1581. }
  1582. // 在script中定义
  1583. const pollStatus = ref(null) // 轮询状态:'polling', 'success', 'error'
  1584. const pollCount = ref(0) // 轮询次数
  1585. const pollInterval = ref(null) // 轮询定时器
  1586. const task_id = ref('')
  1587. // 生成图片(简化版,只做轮询)
  1588. const generateImage = async () => {
  1589. try {
  1590. if (!editFormData.template_id) {
  1591. ElMessage.warning('请先选择模版')
  1592. return
  1593. }
  1594. if (!editFormData.chinese_description) {
  1595. ElMessage.warning('请输入描述内容')
  1596. return
  1597. }
  1598. // 设置加载状态
  1599. loadingStatus.value = true
  1600. // 重置状态
  1601. pollStatus.value = 'polling'
  1602. pollCount.value = 0
  1603. console.log(editFormData.template_id)
  1604. // 第一步:调用生成接口触发生成任务
  1605. const generateResponse = await GetTxtToTxt({
  1606. status_val: '文生图',
  1607. prompt: editFormData.chinese_description,
  1608. model: 'gemini-3-pro-image-preview',
  1609. id: editFormData.id,
  1610. size: selectedSize.value,
  1611. template_id:editFormData.template_id,
  1612. })
  1613. if (generateResponse.code === 0) {
  1614. ElMessage.success('图片生成任务已开始,请稍候...')
  1615. task_id.value = generateResponse.data.task_id
  1616. // 稍后在轮询中使用GetImageStatus接口
  1617. // 清除之前的定时器
  1618. if (pollInterval.value) {
  1619. clearInterval(pollInterval.value)
  1620. }
  1621. // 开始轮询查询结果
  1622. pollInterval.value = setInterval(async () => {
  1623. pollCount.value++
  1624. try {
  1625. // 调用GetImageStatus接口查询生成状态
  1626. const statusResponse = await GetImageStatus({ task_id: task_id.value })
  1627. if (statusResponse.code === 0 && statusResponse.data) {
  1628. // 如果获取到数据且有图片URL
  1629. if (statusResponse.data.image_url) {
  1630. // 停止轮询
  1631. clearInterval(pollInterval.value)
  1632. pollInterval.value = null
  1633. pollStatus.value = 'success'
  1634. loadingStatus.value = false
  1635. // 更新新图URL
  1636. const imageUrl = statusResponse.data.image_url
  1637. editFormData.new_image_url = imageUrl
  1638. editFormData.img_name = `生成图片_${new Date().getTime()}.jpg`
  1639. // 更新历史记录
  1640. const newImageItem = {
  1641. url: imageUrl,
  1642. product_content: editFormData.chinese_description || '',
  1643. template_id: editFormData.template_id,
  1644. createTime: statusResponse.data.completed_at || new Date().toISOString()
  1645. }
  1646. // 添加到newImages数组(走马灯轮播)
  1647. newImages.value.unshift(newImageItem)
  1648. // 处理模板分类树数据
  1649. processTemplateTreeData([newImageItem])
  1650. ElMessage.success(`图片生成成功!耗时约${pollCount.value * 5}秒`)
  1651. } else if (statusResponse.data.error) {
  1652. // 如果返回错误
  1653. clearInterval(pollInterval.value)
  1654. pollInterval.value = null
  1655. pollStatus.value = 'error'
  1656. loadingStatus.value = false
  1657. ElMessage.error('图片生成失败: ' + statusResponse.data.error)
  1658. }
  1659. } else {
  1660. // 如果接口返回错误
  1661. clearInterval(pollInterval.value)
  1662. pollInterval.value = null
  1663. pollStatus.value = 'error'
  1664. loadingStatus.value = false
  1665. ElMessage.error('查询生成状态失败: ' + (statusResponse.msg || '未知错误'))
  1666. }
  1667. // 如果轮询超过30次(2.5分钟),停止轮询
  1668. if (pollCount.value >= 30) {
  1669. clearInterval(pollInterval.value)
  1670. pollInterval.value = null
  1671. pollStatus.value = 'error'
  1672. loadingStatus.value = false
  1673. ElMessage.warning('生成超时,请稍后手动检查结果')
  1674. }
  1675. } catch (pollError) {
  1676. console.error('轮询查询失败:', pollError)
  1677. // 轮询失败继续尝试
  1678. }
  1679. }, 5000) // 每5秒轮询一次
  1680. } else {
  1681. pollStatus.value = 'error'
  1682. loadingStatus.value = false
  1683. ElMessage.error('生成任务启动失败: ' + (generateResponse.msg || '未知错误'))
  1684. }
  1685. } catch (generateError) {
  1686. pollStatus.value = 'error'
  1687. loadingStatus.value = false
  1688. console.error('生成图片失败:', generateError)
  1689. ElMessage.error('生成图片失败: ' + (generateError.message || '未知错误'))
  1690. }
  1691. }
  1692. // 尺寸相关的数据
  1693. const selectedSize = ref('1:1') // 默认选择1:1
  1694. const customWidth = ref('')
  1695. const customHeight = ref('')
  1696. const sizePresets = {
  1697. '1:1': { width: 1024, height: 1024 },
  1698. '4:3': { width: 1024, height: 768 },
  1699. '3:2': { width: 1024, height: 683 },
  1700. '2:3': { width: 683, height: 1024 },
  1701. '9:16': { width: 675, height: 1200 },
  1702. '16:9': { width: 1200, height: 675 },
  1703. '21:9': { width: 1200, height: 514 },
  1704. '3:4': { width: 768, height: 1024 }
  1705. }
  1706. // 获取尺寸信息
  1707. const getSizeInfo = (size) => {
  1708. if (size === 'custom') return '自定义尺寸'
  1709. const preset = sizePresets[size]
  1710. return `${preset.width}×${preset.height}`
  1711. }
  1712. // 尺寸变化处理
  1713. const handleSizeChange = (value) => {
  1714. const selectedValue = value || selectedSize.value
  1715. if (selectedValue !== 'custom') {
  1716. // 如果是预设尺寸,可以在这里执行相关操作
  1717. console.log('选择了尺寸:', selectedValue, sizePresets[selectedValue])
  1718. // 可以更新生成图片的参数
  1719. }
  1720. }
  1721. // 验证自定义尺寸
  1722. const validateCustomSize = () => {
  1723. // 确保输入的是数字
  1724. if (customWidth.value && !/^\d+$/.test(customWidth.value)) {
  1725. customWidth.value = customWidth.value.replace(/\D/g, '')
  1726. }
  1727. if (customHeight.value && !/^\d+$/.test(customHeight.value)) {
  1728. customHeight.value = customHeight.value.replace(/\D/g, '')
  1729. }
  1730. // 限制最大最小值
  1731. const maxSize = 4096
  1732. const minSize = 64
  1733. if (customWidth.value) {
  1734. let width = parseInt(customWidth.value)
  1735. if (width > maxSize) customWidth.value = maxSize.toString()
  1736. if (width < minSize) customWidth.value = minSize.toString()
  1737. }
  1738. if (customHeight.value) {
  1739. let height = parseInt(customHeight.value)
  1740. if (height > maxSize) customHeight.value = maxSize.toString()
  1741. if (height < minSize) customHeight.value = minSize.toString()
  1742. }
  1743. }
  1744. // 获取当前选择的尺寸
  1745. const getCurrentSize = () => {
  1746. if (selectedSize.value === 'custom') {
  1747. return {
  1748. type: 'custom',
  1749. width: customWidth.value ? parseInt(customWidth.value) : 1024,
  1750. height: customHeight.value ? parseInt(customHeight.value) : 1024
  1751. }
  1752. } else {
  1753. return {
  1754. type: selectedSize.value,
  1755. ...sizePresets[selectedSize.value]
  1756. }
  1757. }
  1758. }
  1759. // 可选:添加常用尺寸按钮
  1760. const quickSizes = [
  1761. { label: '512×512', width: 512, height: 512 },
  1762. { label: '768×768', width: 768, height: 768 },
  1763. { label: '1024×1024', width: 1024, height: 1024 },
  1764. { label: '1920×1080', width: 1920, height: 1080 }
  1765. ]
  1766. const selectQuickSize = (size) => {
  1767. selectedSize.value = 'custom'
  1768. customWidth.value = size.width.toString()
  1769. customHeight.value = size.height.toString()
  1770. }
  1771. const downloadImage = async (type) => {
  1772. console.log('开始下载...')
  1773. let imagePath = ''
  1774. let originalFileName = ''
  1775. if (type === 'original') {
  1776. imagePath = editFormData.original_image_url
  1777. originalFileName = editFormData.original_name || ''
  1778. if (!imagePath) {
  1779. ElMessage.warning('原图不存在')
  1780. return
  1781. }
  1782. } else if (type === 'new') {
  1783. imagePath = editFormData.new_image_url
  1784. originalFileName = editFormData.img_name || ''
  1785. if (!imagePath) {
  1786. ElMessage.warning('新图不存在')
  1787. return
  1788. }
  1789. } else {
  1790. console.error('未知的下载类型:', type)
  1791. ElMessage.error('未知的下载类型')
  1792. return
  1793. }
  1794. console.log(`下载${type}图信息:`, { imagePath, originalFileName })
  1795. // 获取完整URL
  1796. const fullUrl = formatImageUrl(imagePath)
  1797. console.log('完整下载URL:', fullUrl)
  1798. // 生成安全的文件名
  1799. const safeFileName = generateSafeFilename(originalFileName, imagePath, type)
  1800. console.log('安全文件名:', safeFileName)
  1801. try {
  1802. // 调用下载
  1803. await downloadWithBlob(fullUrl, safeFileName)
  1804. console.log('下载函数调用成功')
  1805. } catch (error) {
  1806. console.error('下载函数调用失败:', error)
  1807. ElMessage.error('下载过程中发生错误')
  1808. }
  1809. }
  1810. // 生成安全的文件名(处理中文和特殊字符)
  1811. const generateSafeFilename = (originalName, imagePath, type) => {
  1812. // 如果有原始文件名,使用它(但需要处理中文)
  1813. if (originalName && originalName.trim()) {
  1814. // 如果文件名包含中文,转换为拼音或使用安全字符
  1815. if (/[\u4e00-\u9fa5]/.test(originalName)) {
  1816. // 方法1:直接使用中文(现代浏览器支持)
  1817. // 方法2:转换为拼音(需要拼音库)或使用时间戳
  1818. return encodeURIComponent(originalName) // 对中文进行编码
  1819. }
  1820. return originalName
  1821. }
  1822. // 从路径提取文件名
  1823. const pathParts = imagePath.split('/')
  1824. const lastPart = pathParts[pathParts.length - 1]
  1825. if (lastPart && lastPart.includes('.')) {
  1826. return lastPart
  1827. }
  1828. // 默认使用时间戳
  1829. const timestamp = Date.now()
  1830. const extension = getFileExtension(imagePath)
  1831. return `${type}_${timestamp}.${extension}`
  1832. }
  1833. // 获取文件扩展名
  1834. const getFileExtension = (filepath) => {
  1835. if (!filepath) return 'jpg'
  1836. const match = filepath.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/)
  1837. return match ? match[1].toLowerCase() : 'jpg'
  1838. }
  1839. // 直接下载函数(备用方案)
  1840. const directDownload = (url, filename) => {
  1841. try {
  1842. const link = document.createElement('a')
  1843. link.href = url
  1844. link.download = filename
  1845. link.style.display = 'none'
  1846. document.body.appendChild(link)
  1847. link.click()
  1848. document.body.removeChild(link)
  1849. ElMessage.success('下载成功!')
  1850. } catch (error) {
  1851. console.error('直接下载失败:', error)
  1852. throw error
  1853. }
  1854. }
  1855. // 添加时间戳避免缓存
  1856. const addTimestamp = (url) => {
  1857. if (!url) return url
  1858. const separator = url.includes('?') ? '&' : '?'
  1859. return `${url}${separator}t=${Date.now()}`
  1860. }
  1861. // 使用Blob下载(推荐,支持中文文件名)
  1862. const downloadWithBlob = async (url, filename) => {
  1863. try {
  1864. console.log('开始Blob下载:', { url, filename })
  1865. // 添加时间戳避免缓存
  1866. const urlWithTimestamp = addTimestamp(url)
  1867. const response = await fetch(urlWithTimestamp, {
  1868. method: 'GET',
  1869. headers: {
  1870. 'Cache-Control': 'no-cache'
  1871. }
  1872. })
  1873. if (!response.ok) {
  1874. throw new Error(`HTTP ${response.status}: ${response.statusText}`)
  1875. }
  1876. const blob = await response.blob()
  1877. // 检查blob类型
  1878. if (blob.size === 0) {
  1879. throw new Error('文件内容为空')
  1880. }
  1881. // 创建blob URL
  1882. const blobUrl = window.URL.createObjectURL(blob)
  1883. // 创建下载链接
  1884. const link = document.createElement('a')
  1885. link.href = blobUrl
  1886. // 处理文件名:现代浏览器支持中文文件名
  1887. // 如果需要更兼容,可以编码文件名
  1888. link.download = filename
  1889. link.style.display = 'none'
  1890. document.body.appendChild(link)
  1891. link.click()
  1892. // 清理
  1893. setTimeout(() => {
  1894. window.URL.revokeObjectURL(blobUrl)
  1895. document.body.removeChild(link)
  1896. }, 100)
  1897. ElMessage.success('下载成功!')
  1898. } catch (error) {
  1899. console.error('Blob下载失败:', error)
  1900. // 备用方案1:直接下载
  1901. try {
  1902. console.log('尝试直接下载...')
  1903. directDownload(url, filename)
  1904. } catch (directError) {
  1905. console.error('直接下载失败:', directError)
  1906. // 备用方案2:使用更简单的下载方法
  1907. try {
  1908. console.log('尝试使用简单下载方法...')
  1909. const link = document.createElement('a')
  1910. link.href = url
  1911. link.download = filename
  1912. link.style.display = 'none'
  1913. document.body.appendChild(link)
  1914. link.click()
  1915. document.body.removeChild(link)
  1916. ElMessage.success('下载成功!')
  1917. } catch (simpleError) {
  1918. console.error('所有方法都失败:', simpleError)
  1919. ElMessage.error('下载失败,请检查网络或联系管理员')
  1920. }
  1921. }
  1922. }
  1923. }
  1924. const searchKeyword = ref('')
  1925. const handleSearch = async () => {
  1926. try {
  1927. const response = await product_template({ search: searchKeyword.value })
  1928. if (response.code === 0 && response.data) {
  1929. templateList.value = response.data.map(item => ({
  1930. id: item.id,
  1931. template_name: item.template_name || '',
  1932. thumbnail_image: item.thumbnail_image,
  1933. chinese_description: item.chinese_description || '',
  1934. english_description: item.english_description || ''
  1935. }))
  1936. // 默认选中第一个模板
  1937. if (templateList.value.length > 0) {
  1938. selectTemplate(templateList.value[0])
  1939. }
  1940. }
  1941. } catch (error) {
  1942. console.error('获取模板失败:', error)
  1943. templateList.value = []
  1944. }
  1945. }
  1946. // 弹窗显示控制
  1947. const AdddialogVisible = ref(false)
  1948. const productFormRef = ref()
  1949. const submitLoading = ref(false)
  1950. // 新增产品:选中的图片文件(点确定时随 productAdd 一起提交,不单独调 ImgUpload)
  1951. const productImageFile = ref(null)
  1952. // 表单数据
  1953. const productForm = reactive({
  1954. product_name: '',
  1955. product_code: '',
  1956. create_name: '',
  1957. merchant_id: '',
  1958. product_img: '' // 预览用:blob URL 或空
  1959. })
  1960. const onADD = async() => {
  1961. if (!nodeid.value) {
  1962. ElMessage.error('请先选择商户')
  1963. return
  1964. }
  1965. if (productForm.product_img && productForm.product_img.startsWith('blob:')) {
  1966. URL.revokeObjectURL(productForm.product_img)
  1967. }
  1968. productForm.product_img = ''
  1969. productImageFile.value = null
  1970. AdddialogVisible.value = true
  1971. const res = await getMerchantId({
  1972. merchant_code: nodeid.value,
  1973. })
  1974. if (res.code === 0) {
  1975. productForm.merchant_id = res.data
  1976. }
  1977. }
  1978. // 将 File 转为 data:image/xxx;base64,... 字符串
  1979. const fileToDataUrl = (file) => {
  1980. return new Promise((resolve, reject) => {
  1981. const reader = new FileReader()
  1982. reader.onload = () => resolve(reader.result)
  1983. reader.onerror = reject
  1984. reader.readAsDataURL(file)
  1985. })
  1986. }
  1987. const handleSubmit = async()=>{
  1988. if (!productImageFile.value) {
  1989. ElMessage.warning('请先选择产品图片')
  1990. return
  1991. }
  1992. productForm.create_name = userStore.userInfo.nickName
  1993. try {
  1994. submitLoading.value = true
  1995. const productImgBase64 = await fileToDataUrl(productImageFile.value)
  1996. const payload = {
  1997. product_name: productForm.product_name,
  1998. product_code: productForm.product_code,
  1999. merchant_id: productForm.merchant_id,
  2000. create_name: productForm.create_name,
  2001. product_img: productImgBase64
  2002. }
  2003. const response = await productAdd(payload)
  2004. if (response.code === 0) {
  2005. ElMessage.success('新增成功')
  2006. await refreshProductList()
  2007. handleClose()
  2008. } else {
  2009. ElMessage.error(response.message || response.msg || '新增失败')
  2010. }
  2011. } catch (error) {
  2012. console.error('新增失败:', error)
  2013. ElMessage.error('新增失败,请联系管理员')
  2014. } finally {
  2015. submitLoading.value = false
  2016. }
  2017. }
  2018. // 新增产品弹窗:商户显示 merchant_name 字段
  2019. const addDialogMerchantName = computed(() => {
  2020. const item = treeData.value.find(t => t.value === nodeid.value)
  2021. return item?.originalData?.merchant_name ?? item?.label ?? ''
  2022. })
  2023. // 新增产品弹窗:预览图地址(blob 或后端返回的 URL)
  2024. const addDialogPreviewSrc = computed(() => {
  2025. const p = productForm.product_img
  2026. if (!p) return ''
  2027. if (p.startsWith('blob:')) return p
  2028. return formatImageUrl(p)
  2029. })
  2030. // 新增产品弹窗:已选图片大小显示(如 "856 KB"、"0.8 MB")
  2031. const addDialogImageSizeText = computed(() => {
  2032. const f = productImageFile.value
  2033. if (!f || !f.size) return ''
  2034. const bytes = f.size
  2035. if (bytes < 1024) return `${bytes} B`
  2036. if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  2037. return `${(bytes / 1024 / 1024).toFixed(2)} MB`
  2038. })
  2039. // 新增产品:图片大小限制 1MB
  2040. const MAX_IMAGE_SIZE_MB = 1
  2041. // 新增产品:选择图片仅做本地预览,不调上传接口;点确定时随 productAdd 一起提交
  2042. const handleAddDialogSelectImage = (file) => {
  2043. const isImage = file.type.startsWith('image/')
  2044. const sizeMB = file.size / 1024 / 1024
  2045. const isLt1M = sizeMB <= MAX_IMAGE_SIZE_MB
  2046. if (!isImage) {
  2047. ElMessage.error('只能上传图片文件')
  2048. return false
  2049. }
  2050. if (!isLt1M) {
  2051. ElMessage.error(`图片大小不能超过 ${MAX_IMAGE_SIZE_MB}MB,当前为 ${sizeMB.toFixed(2)}MB`)
  2052. return false
  2053. }
  2054. if (productForm.product_img && productForm.product_img.startsWith('blob:')) {
  2055. URL.revokeObjectURL(productForm.product_img)
  2056. }
  2057. productImageFile.value = file
  2058. productForm.product_img = URL.createObjectURL(file)
  2059. return false // 阻止 el-upload 默认上传
  2060. }
  2061. // 打开弹窗方法(外部调用)
  2062. const openDialog = () => {
  2063. // 清空表单
  2064. Object.keys(productForm).forEach(key => {
  2065. productForm[key] = ''
  2066. })
  2067. dialogVisible.value = true
  2068. // 如果有默认值可以在这里设置
  2069. // productForm.create_name = '默认创建人'
  2070. }
  2071. // 关闭新增产品弹窗并重置表单
  2072. const handleClose = () => {
  2073. if (productForm.product_img && productForm.product_img.startsWith('blob:')) {
  2074. URL.revokeObjectURL(productForm.product_img)
  2075. }
  2076. productForm.product_img = ''
  2077. productForm.product_name = ''
  2078. productForm.product_code = ''
  2079. productImageFile.value = null
  2080. submitLoading.value = false
  2081. productFormRef.value?.resetFields()
  2082. AdddialogVisible.value = false
  2083. }
  2084. // 删除图片(仅清空预览与文件,不调接口)
  2085. const handleRemoveImage = () => {
  2086. if (productForm.product_img && productForm.product_img.startsWith('blob:')) {
  2087. URL.revokeObjectURL(productForm.product_img)
  2088. }
  2089. productForm.product_img = ''
  2090. productImageFile.value = null
  2091. }
  2092. // 暴露方法给父组件
  2093. defineExpose({
  2094. openDialog
  2095. })
  2096. // onMounted(() => {
  2097. // getTableData()
  2098. // })
  2099. </script>
  2100. <style scoped>
  2101. /* 保持原有通用样式 */
  2102. :deep(.el-table td .cell) {
  2103. line-height: 22px !important;
  2104. }
  2105. /* :deep(.el-dialog__body) {
  2106. padding: 10px 20px;
  2107. } */
  2108. .gva-pagination {
  2109. margin-top: 10px;
  2110. text-align: right;
  2111. }
  2112. .img-placeholder-mini {
  2113. width: 100%;
  2114. height: 100%;
  2115. min-height: 70px;
  2116. background: #f5f7fa;
  2117. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23c0c4cc'%3E%3Cpath d='M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'/%3E%3C/svg%3E");
  2118. background-repeat: no-repeat;
  2119. background-position: center;
  2120. background-size: 24px;
  2121. }
  2122. /* 新增产品弹窗 - 上传与预览 */
  2123. .add-dialog-upload-wrap {
  2124. width: 100%;
  2125. }
  2126. .add-dialog-preview-box {
  2127. border: 1px solid #e4e7ed;
  2128. border-radius: 8px;
  2129. overflow: hidden;
  2130. background: #fafafa;
  2131. }
  2132. .add-dialog-image-size {
  2133. padding: 6px 12px;
  2134. font-size: 12px;
  2135. color: #606266;
  2136. background: #f5f7fa;
  2137. border-top: 1px solid #e4e7ed;
  2138. }
  2139. .add-dialog-preview-img {
  2140. width: 100%;
  2141. height: 200px;
  2142. display: flex;
  2143. align-items: center;
  2144. justify-content: center;
  2145. background: #f5f7fa;
  2146. }
  2147. .add-dialog-preview-img-inner {
  2148. max-width: 100%;
  2149. max-height: 200px;
  2150. width: auto;
  2151. height: auto;
  2152. object-fit: contain;
  2153. display: block;
  2154. }
  2155. .add-dialog-preview-actions {
  2156. display: flex;
  2157. align-items: center;
  2158. justify-content: center;
  2159. gap: 12px;
  2160. padding: 12px;
  2161. border-top: 1px solid #e4e7ed;
  2162. background: #fff;
  2163. }
  2164. .add-dialog-upload-area {
  2165. width: 100%;
  2166. }
  2167. .add-dialog-upload-area :deep(.el-upload) {
  2168. width: 100%;
  2169. display: block;
  2170. }
  2171. .add-dialog-upload-inner {
  2172. width: 100%;
  2173. height: 160px;
  2174. border: 2px dashed #dcdfe6;
  2175. border-radius: 8px;
  2176. display: flex;
  2177. flex-direction: column;
  2178. align-items: center;
  2179. justify-content: center;
  2180. gap: 8px;
  2181. background: #fafafa;
  2182. color: #606266;
  2183. transition: border-color 0.2s, background 0.2s;
  2184. }
  2185. .add-dialog-upload-inner:hover {
  2186. border-color: #409eff;
  2187. background: #ecf5ff;
  2188. color: #409eff;
  2189. }
  2190. .add-dialog-upload-icon {
  2191. font-size: 36px;
  2192. color: #c0c4cc;
  2193. }
  2194. .add-dialog-upload-inner:hover .add-dialog-upload-icon {
  2195. color: #409eff;
  2196. }
  2197. .add-dialog-upload-text {
  2198. font-size: 14px;
  2199. font-weight: 500;
  2200. }
  2201. .add-dialog-upload-tip {
  2202. font-size: 12px;
  2203. color: #909399;
  2204. }
  2205. :deep(.el-table__body tr.current-row) > td {
  2206. background: #ff80ff !important;
  2207. }
  2208. /* 表格操作按钮样式 */
  2209. .table-operations {
  2210. display: flex;
  2211. align-items: center;
  2212. justify-content: center;
  2213. gap: 4px;
  2214. }
  2215. .table-operations .op-btn {
  2216. display: inline-flex;
  2217. align-items: center;
  2218. gap: 4px;
  2219. padding: 4px 8px;
  2220. font-size: 12px;
  2221. border-radius: 4px;
  2222. transition: all 0.2s;
  2223. }
  2224. .table-operations .op-btn-design:hover {
  2225. color: #409eff;
  2226. background: #ecf5ff;
  2227. }
  2228. .table-operations .op-btn-delete:hover {
  2229. color: #f56c6c;
  2230. background: #fef0f0;
  2231. }
  2232. .table-operations .op-divider {
  2233. color: #dcdfe6;
  2234. font-size: 12px;
  2235. padding: 0 2px;
  2236. user-select: none;
  2237. }
  2238. .JKWTree-tree {
  2239. background-color: #fff;
  2240. padding: 10px;
  2241. border-right: 1px solid #e4e7ed;
  2242. }
  2243. .JKWTree-tree h3 {
  2244. font-size: 15px;
  2245. font-weight: 700;
  2246. margin: 10px 0;
  2247. padding-bottom: 10px;
  2248. border-bottom: 1px solid #e4e7ed;
  2249. }
  2250. /* 商户树:选中节点文字变红 */
  2251. .JKWTree-tree :deep(.el-tree-node.is-current > .el-tree-node__content) {
  2252. color: #f56c6c !important;
  2253. font-weight: 600;
  2254. }
  2255. /* 编辑弹窗三列布局:自适应,小屏横向滚动防跑偏 */
  2256. .image-edit-container {
  2257. display: flex;
  2258. height: 94vh;
  2259. padding-top: 20px;
  2260. gap: 0;
  2261. width: 100%;
  2262. overflow-x: auto;
  2263. overflow-y: hidden;
  2264. box-sizing: border-box;
  2265. }
  2266. .left-column {
  2267. flex: 0.7;
  2268. min-width: 280px;
  2269. max-width: 560px;
  2270. display: flex;
  2271. flex-direction: column;
  2272. gap: 15px;
  2273. padding: 15px;
  2274. overflow-y: auto;
  2275. overflow-x: hidden;
  2276. flex-shrink: 1;
  2277. }
  2278. .middle-column {
  2279. width: 430px;
  2280. min-width: 380px;
  2281. flex-shrink: 0;
  2282. border-left: 1px solid #e4e7ed;
  2283. border-right: 1px solid #e4e7ed;
  2284. padding: 15px 12px;
  2285. position: relative;
  2286. box-sizing: border-box;
  2287. background: #fff;
  2288. overflow-y: auto;
  2289. overflow-x: hidden;
  2290. }
  2291. .right-column {
  2292. flex: 1;
  2293. min-width: 280px;
  2294. min-height: 0;
  2295. flex-shrink: 1;
  2296. padding: 0 12px 0 15px;
  2297. display: flex;
  2298. flex-direction: column;
  2299. overflow: hidden;
  2300. }
  2301. .image-comparison-section {
  2302. flex: none;
  2303. display: flex;
  2304. flex-direction: row;
  2305. align-items: flex-start;
  2306. gap: 16px;
  2307. min-height: 200px;
  2308. }
  2309. .upload-image-box {
  2310. width: 200px;
  2311. height: 200px;
  2312. max-width: 100%;
  2313. display: flex;
  2314. justify-content: center;
  2315. align-items: center;
  2316. background-color: #f9f9f9;
  2317. border-radius: 4px;
  2318. overflow: hidden;
  2319. border: 1px solid #e4e7ed;
  2320. }
  2321. .image-placeholder-small {
  2322. width: 100px;
  2323. height: 100px;
  2324. display: flex;
  2325. flex-direction: column;
  2326. justify-content: center;
  2327. align-items: center;
  2328. background-color: #f9f9f9;
  2329. border-radius: 4px;
  2330. border: 1px solid #e4e7ed;
  2331. }
  2332. .case-links-column {
  2333. display: flex;
  2334. flex-direction: column;
  2335. align-items: flex-start;
  2336. justify-content: space-between;
  2337. flex-shrink: 0;
  2338. height: 200px;
  2339. }
  2340. .case-link-btn {
  2341. padding: 0 !important;
  2342. font-size: 14px !important;
  2343. font-weight: normal !important;
  2344. color: #409eff !important;
  2345. }
  2346. .image-item {
  2347. flex: 1;
  2348. display: flex;
  2349. flex-direction: column;
  2350. min-height: 0; /* 防止溢出 */
  2351. }
  2352. .image-title {
  2353. font-weight: bold;
  2354. margin-bottom: 10px;
  2355. color: #409eff;
  2356. }
  2357. .image-preview {
  2358. flex: 1;
  2359. overflow: hidden;
  2360. display: flex;
  2361. flex-direction: column;
  2362. min-height: 0; /* 防止溢出 */
  2363. }
  2364. .image-preview img {
  2365. width: 100%;
  2366. height: 160px; /* 固定高度,确保不占满屏幕 */
  2367. object-fit: contain;
  2368. background: #f5f7fa;
  2369. }
  2370. .image-placeholder {
  2371. height: 160px;
  2372. display: flex;
  2373. flex-direction: column;
  2374. align-items: center;
  2375. justify-content: center;
  2376. background: #f5f7fa;
  2377. color: #909399;
  2378. }
  2379. .image-info {
  2380. padding: 8px;
  2381. background: #f5f7fa;
  2382. font-size: 12px;
  2383. color: #606266;
  2384. border-top: 1px solid #dcdfe6;
  2385. }
  2386. .image-actions {
  2387. display: flex;
  2388. justify-content: center;
  2389. padding: 10px 0;
  2390. }
  2391. .edit-section {
  2392. overflow: hidden; /* 防止溢出 */
  2393. }
  2394. .edit-form {
  2395. height: 100%;
  2396. }
  2397. .edit-form :deep(.el-form) {
  2398. height: 100%;
  2399. }
  2400. .edit-form :deep(.el-form-item) {
  2401. height: 100%;
  2402. margin-bottom: 0;
  2403. }
  2404. .edit-form :deep(.el-textarea__inner) {
  2405. height: 100% !important;
  2406. resize: none;
  2407. }
  2408. :deep(.el-textarea__inner) {
  2409. resize: none !important;
  2410. }
  2411. .right-column {
  2412. display: flex;
  2413. flex-direction: column;
  2414. min-height: 0; /* 防止溢出 */
  2415. }
  2416. .right-template {
  2417. flex: 1;
  2418. display: flex;
  2419. flex-direction: column;
  2420. min-height: 0; /* 防止溢出 */
  2421. }
  2422. .template-header {
  2423. margin-bottom: 15px;
  2424. padding-bottom: 10px;
  2425. border-bottom: 1px solid #e4e7ed;
  2426. }
  2427. .template-header h4 {
  2428. margin: 0;
  2429. font-size: 16px;
  2430. }
  2431. .template-list-container {
  2432. flex: 1;
  2433. min-height: 0;
  2434. display: flex;
  2435. flex-direction: column;
  2436. }
  2437. .template-list {
  2438. flex: 1;
  2439. display: flex;
  2440. flex-direction: row;
  2441. flex-wrap: wrap;
  2442. gap: 10px;
  2443. overflow-y: auto;
  2444. min-height: 0; /* 防止溢出 */
  2445. padding: 5px;
  2446. }
  2447. .template-item {
  2448. display: flex;
  2449. flex-direction: column;
  2450. width: calc(33.33% - 7px);
  2451. min-width: 100px; /* 笔记本小屏时不被压得过窄 */
  2452. padding: 10px;
  2453. border: 2px solid #e4e7ed;
  2454. border-radius: 6px;
  2455. cursor: pointer;
  2456. transition: all 0.3s;
  2457. background: white;
  2458. flex-shrink: 0;
  2459. box-sizing: border-box;
  2460. }
  2461. .template-item:hover {
  2462. border-color: #409eff;
  2463. background: #f5faff;
  2464. }
  2465. .template-item.active {
  2466. border-color: #409eff;
  2467. background: #ecf5ff;
  2468. border-width: 2px;
  2469. }
  2470. .template-thumbnail {
  2471. position: relative;
  2472. width: 100%;
  2473. height: 120px;
  2474. margin-bottom: 8px;
  2475. border-radius: 4px;
  2476. overflow: hidden;
  2477. background: #f5f7fa;
  2478. }
  2479. .template-thumbnail img {
  2480. width: 100%;
  2481. height: 100%;
  2482. object-fit: contain;
  2483. }
  2484. .image-container {
  2485. position: relative;
  2486. width: 100%;
  2487. height: 100%;
  2488. cursor: pointer;
  2489. }
  2490. .zoom-icon {
  2491. position: absolute;
  2492. bottom: 8px;
  2493. right: 8px;
  2494. width: 28px;
  2495. height: 28px;
  2496. background: rgba(0, 0, 0, 0.6);
  2497. color: white;
  2498. border-radius: 50%;
  2499. display: flex;
  2500. align-items: center;
  2501. justify-content: center;
  2502. cursor: zoom-in;
  2503. opacity: 0;
  2504. transition: opacity 0.3s ease;
  2505. }
  2506. .image-container:hover .zoom-icon {
  2507. opacity: 1;
  2508. }
  2509. /* 商品信息项样式 */
  2510. .product-info-item {
  2511. margin-bottom: 10px;
  2512. display: flex;
  2513. align-items: center;
  2514. }
  2515. .product-info-label {
  2516. font-size: 14px;
  2517. color: #000000;
  2518. font-weight: bold;
  2519. width: 90px;
  2520. text-align: left;
  2521. }
  2522. .product-info-value {
  2523. font-size: 14px;
  2524. color: #000000;
  2525. flex: 1;
  2526. text-align: left;
  2527. word-break: break-all;
  2528. }
  2529. .template-id {
  2530. position: absolute;
  2531. top: 4px;
  2532. left: 4px;
  2533. background: rgba(0, 0, 0, 0.7);
  2534. color: white;
  2535. padding: 2px 6px;
  2536. border-radius: 3px;
  2537. font-size: 11px;
  2538. font-weight: bold;
  2539. }
  2540. .image-preview .image-download-btn {
  2541. opacity: 0;
  2542. }
  2543. .image-preview:hover .image-download-btn,
  2544. .image-preview > div:hover .image-download-btn,
  2545. .image-preview .image-download-btn:hover {
  2546. opacity: 1 !important;
  2547. }
  2548. .image-download-btn:hover {
  2549. background: rgba(0, 0, 0, 0.8) !important;
  2550. }
  2551. /* 修改单选框为方形 */
  2552. .el-radio__input.is-border {
  2553. border-radius: 2px !important;
  2554. }
  2555. .el-radio__input.is-border .el-radio__inner {
  2556. border-radius: 2px !important;
  2557. }
  2558. .template-name {
  2559. font-size: 13px;
  2560. font-weight: 600;
  2561. color: #303133;
  2562. margin-bottom: 4px;
  2563. overflow: hidden;
  2564. text-overflow: ellipsis;
  2565. white-space: nowrap;
  2566. }
  2567. .template-desc {
  2568. flex: 1;
  2569. font-size: 12px;
  2570. color: #606266;
  2571. line-height: 1.4;
  2572. overflow: hidden;
  2573. text-overflow: ellipsis;
  2574. display: -webkit-box;
  2575. -webkit-line-clamp: 2;
  2576. -webkit-box-orient: vertical;
  2577. }
  2578. .empty-templates {
  2579. flex: 1;
  2580. display: flex;
  2581. flex-direction: column;
  2582. align-items: center;
  2583. justify-content: center;
  2584. color: #909399;
  2585. }
  2586. .empty-templates .el-icon {
  2587. margin-bottom: 10px;
  2588. }
  2589. .empty-search {
  2590. width: 100%;
  2591. height: 100%;
  2592. display: flex;
  2593. flex-direction: column;
  2594. align-items: center;
  2595. justify-content: center;
  2596. padding: 50px 20px;
  2597. color: #909399;
  2598. }
  2599. .empty-search .el-icon {
  2600. margin-bottom: 15px;
  2601. color: #c0c4cc;
  2602. }
  2603. .empty-search p {
  2604. margin: 0;
  2605. font-size: 14px;
  2606. }
  2607. </style>