excessive.vue 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448
  1. <template>
  2. <div class="page-container">
  3. <el-descriptions title="" :column="1" border>
  4. <el-descriptions-item label="图片上传">
  5. <div class="upload-container">
  6. <el-upload
  7. ref="uploadRef"
  8. class="upload-list"
  9. :action="uploadUrl"
  10. :http-request="customUpload"
  11. :headers="uploadHeaders"
  12. list-type="picture-card"
  13. :on-preview="handlePreview"
  14. :on-remove="handleRemove"
  15. :on-change="handleChange"
  16. :before-upload="beforeUpload"
  17. :on-success="handleSuccess"
  18. :on-error="handleError"
  19. :on-exceed="handleExceed"
  20. :limit="30"
  21. :multiple="true"
  22. accept="image/jpeg,image/png">
  23. <el-icon><Plus /></el-icon>
  24. <template #tip>
  25. <div class="el-upload__tip">
  26. 只能上传 <b>jpg/png</b> 文件,且 <b>不超过30张</b>
  27. </div>
  28. </template>
  29. <template #file="{ file }">
  30. </template>
  31. </el-upload>
  32. </div>
  33. </el-descriptions-item>
  34. </el-descriptions>
  35. <el-descriptions
  36. title=""
  37. column="7"
  38. border
  39. class="queue-status-box">
  40. <el-descriptions-item label="总任务数">
  41. <el-tag type="info">{{ queuestats['总任务数'] }}</el-tag>
  42. </el-descriptions-item>
  43. <el-descriptions-item label="待处理">
  44. <el-tag type="warning">{{ queuestats['待处理'] }}</el-tag>
  45. </el-descriptions-item>
  46. <el-descriptions-item label="处理中">
  47. <el-tag type="primary">{{ queuestats['处理中'] }}</el-tag>
  48. </el-descriptions-item>
  49. <el-descriptions-item label="成功">
  50. <el-tag type="success">{{ queuestats['成功'] }}</el-tag>
  51. </el-descriptions-item>
  52. <el-descriptions-item label="失败">
  53. <el-tag type="danger">{{ queuestats['失败'] }}</el-tag>
  54. </el-descriptions-item>
  55. <el-descriptions-item label="操作">
  56. <el-button type="danger" size="small" @click="stopQueueclick" :loading="isLoading" :disabled="isLoading">全部停止处理</el-button>
  57. <el-button type="primary" size="small" @click="handleViewQueue" :loading="isLoading">任务列表</el-button>
  58. </el-descriptions-item>
  59. </el-descriptions>
  60. <el-dialog
  61. v-model="queueDialogVisible"
  62. :before-close="queueDialog"
  63. title="队列任务详情"
  64. width="700px"
  65. destroy-on-close
  66. >
  67. <div class="dialog-scroll-wrapper">
  68. <template v-if="queueData.tasks_preview && queueData.tasks_preview.length">
  69. <el-card
  70. v-for="(task, index) in queueData.tasks_preview"
  71. :key="task.id"
  72. class="task-card"
  73. >
  74. <div class="task-grid">
  75. <div><strong>任务 ID:</strong>{{ task.id }}</div>
  76. <div><strong>尝试次数:</strong>{{ task.attempts }}</div>
  77. <div>
  78. <strong>状态:</strong>
  79. <el-tag :type="index === 0 ? 'success' : 'warning'">
  80. {{ index === 0 ? '处理中' : '待处理' }}
  81. </el-tag>
  82. </div>
  83. <div class="task-image">
  84. <el-image
  85. style="width: 120px; height: 120px; border: 1px solid #ccc"
  86. :src="formatImageUrl(task.data.sourceDir + '/' + task.data.file_name)"
  87. fit="cover"
  88. :preview-src-list="[formatImageUrl(task.data.sourceDir + '/' + task.data.file_name)]"
  89. />
  90. </div>
  91. </div>
  92. </el-card>
  93. </template>
  94. <template v-else>
  95. <div class="empty-queue">当前无任务在队列中。</div>
  96. </template>
  97. </div>
  98. </el-dialog>
  99. <br>
  100. <el-table :data="tableData" border height="450" v-loading="loading">
  101. <el-table-column prop="id" label="ID" width="50" />
  102. <el-table-column prop="old_image_url" label="原图目录" width="360" />
  103. <el-table-column prop="new_image_url" label="出图目录" width="360" />
  104. <el-table-column prop="old_img_count" label="图片数量" width="90" />
  105. <el-table-column prop="new_image_count" label="总数量" width="90" />
  106. <el-table-column prop="queueLog_model_name" label="任务执行状态" width="110" />
  107. <el-table-column align="center" label="操作" width="230">
  108. <template #default="{ row, $index }" >
  109. <el-button @click="record_deleteRow(row,$index)" type="primary" size="small"
  110. style="font-size: 16px;padding: 0px;width: 80px;">
  111. 查看详情
  112. </el-button>
  113. <el-button @click="packRow(row,$index)" type="primary" size="small"
  114. style="font-size: 16px;padding: 0px;width: 100px;">
  115. 打包压缩zip
  116. </el-button>
  117. </template>
  118. </el-table-column>
  119. </el-table>
  120. <el-dialog v-model="zipview" title="请选择打包尺寸" width="500px" top="20%">
  121. <el-radio-group v-model="zipselectedOption" size="medium">
  122. <el-radio label="1024x1024">1024 x 1024</el-radio>
  123. <el-radio label="1024x1303">1024 x 1303</el-radio>
  124. <el-radio label="图生图">图生图</el-radio>
  125. <el-radio label="高清放大">高清放大</el-radio>
  126. </el-radio-group>
  127. <template #footer>
  128. <div style="text-align: right">
  129. <el-button @click="zipview = false">取消</el-button>
  130. <el-button type="primary" @click="confirmPack">确认打包</el-button>
  131. </div>
  132. </template>
  133. </el-dialog>
  134. <el-dialog v-model="showDescDialog" title="" width="890px" top="0%">
  135. <div style="display: flex; gap: 10px; max-height: 70vh; overflow: hidden;">
  136. <!-- 左侧图片区域,自适应图片尺寸,可滚动查看完整图片 -->
  137. <div style="flex: 1; display: flex; align-items: start; justify-content: center; border: 1px solid #eee; overflow: auto; max-height: 80vh;">
  138. <el-image
  139. :src="formatImageUrl(path_image_url)"
  140. fit="contain"
  141. style="max-width: 100%; height: auto;"
  142. :preview-src-list="[formatImageUrl(path_image_url)]"
  143. :initial-index="0"
  144. />
  145. </div>
  146. <!-- 图像描述弹窗内容区域 -->
  147. <div style="width: 350px; white-space: pre-wrap; overflow-y: auto; max-height: 63vh; border: 1px solid #f0f0f0; padding: 10px;">
  148. <div v-html="activeDescription"></div>
  149. </div>
  150. </div>
  151. <el-descriptions title="" :column="1" border>
  152. <el-descriptions-item v-if="img_id" label="ID">
  153. <span>{{ img_id }}</span>
  154. </el-descriptions-item>
  155. <el-descriptions-item v-if="img_id" label="模型">
  156. <span>{{ model }}</span>
  157. </el-descriptions-item>
  158. <el-descriptions-item label="图片路径" width="50">
  159. <span>{{ path_image_url }}</span>
  160. </el-descriptions-item>
  161. </el-descriptions>
  162. </el-dialog>
  163. <el-dialog
  164. v-model="TaskqueueVisible"
  165. :before-close="TaskqueueDialog"
  166. title=""
  167. width="95%"
  168. top="4vh"
  169. destroy-on-close
  170. >
  171. <el-table
  172. :data="table_Taskqueue"
  173. row-key="id"
  174. border
  175. style="width: 100%; height: 60vh;"
  176. v-loading="loading"
  177. highlight-current-row
  178. tooltip-effect="dark"
  179. :row-style="{ height: '40px' }"
  180. :cell-style="{ padding: '4px' }"
  181. :header-cell-style="{ padding: '4px' }"
  182. :header-row-style="{ height: '40px' }"
  183. @selection-change="SelectionChange"
  184. :show-overflow-tooltip="true"
  185. >
  186. <el-table-column type="selection" width="60" align="center" />
  187. <el-table-column prop="id" label="任务ID" width="100" align="center" />
  188. <el-table-column prop="task_id" label="子任务ID" width="100" align="center" />
  189. <el-table-column prop="log" label="状态" width="160" align="center" />
  190. </el-table>
  191. </el-dialog>
  192. <el-dialog
  193. v-model="mlinventoryVisible"
  194. :before-close="mlcloseDialog"
  195. title=""
  196. width="100%"
  197. style="height: 97%;margin: 0px;"
  198. top="2vh"
  199. destroy-on-close
  200. >
  201. <div class="dialog-body">
  202. <!-- 左侧表格 -->
  203. <div class="dialog-left">
  204. <el-form ref="elSearchFormRef" class="demo-form-inline">
  205. <el-form-item>
  206. <el-button type="primary" title="" @click="Refresh_Status" :loading="isLoading">刷新执行状态</el-button>
  207. &nbsp;&nbsp;&nbsp;
  208. <span>选择状态:</span>
  209. <el-select
  210. v-model="filterStatus"
  211. placeholder="全部"
  212. @change="handleFilterChange"
  213. style="width: 120px;"
  214. >
  215. <el-option label="全部" value="全部" />
  216. <el-option label="已出图" value="已出图" />
  217. <el-option label="未出图" value="未出图" />
  218. <el-option label="图生文" value="图生文" />
  219. <el-option label="文生文" value="文生文" />
  220. <el-option label="文生图" value="文生图" />
  221. <el-option label="图生图" value="图生图" />
  222. <el-option label="高清放大" value="高清放大" />
  223. </el-select>
  224. </el-form-item>
  225. </el-form>
  226. <el-table
  227. :data="tableData_lsit"
  228. row-key="id"
  229. border
  230. style="width: 100%; height: 80vh;"
  231. v-loading="loading"
  232. highlight-current-row
  233. tooltip-effect="dark"
  234. :row-style="{ height: '0px' }"
  235. :cell-style="{ padding: '0px' }"
  236. :header-cell-style="{ padding: '0px' }"
  237. :header-row-style="{ height: '0px' }"
  238. @selection-change="SelectionChange"
  239. :show-overflow-tooltip="true"
  240. @row-click="handleRowClick"
  241. >
  242. <el-table-column type="selection" width="50" align="center" />
  243. <el-table-column prop="id" label="序号" width="60" align="center" />
  244. <el-table-column label="状态" width="86" align="center">
  245. <template #default="{ row }">
  246. <el-tag v-if="row.status === 1" type="success">已出图</el-tag>
  247. <el-tag v-else type="danger">未出图</el-tag>
  248. </template>
  249. </el-table-column>
  250. <el-table-column prop="status_name" label="操作" width="70" align="center" />
  251. <el-table-column prop="queue_status" label="状态说明" width="140" align="center" />
  252. <el-table-column prop="same_count" label="出图数量" width="90" align="center" />
  253. <el-table-column label="原图预览" width="120">
  254. <template #default="{ row }">
  255. <img
  256. :src="formatImageUrl(row.path)"
  257. style="width: 90px; height: 70px; object-fit: contain; cursor: pointer;"
  258. @click="showDescription(row, 'old')"
  259. />
  260. </template>
  261. </el-table-column>
  262. <el-table-column label="原图尺寸" width="120" align="center">
  263. <template #default="{ row }">
  264. {{ row.width }} x {{ row.height }}
  265. </template>
  266. </el-table-column>
  267. </el-table>
  268. <div class="gva-pagination">
  269. <el-pagination
  270. @size-change="handleSizeChange"
  271. @current-change="handleCurrentChange"
  272. :current-page="page"
  273. :page-sizes="[10, 30, 50, 100, 500,1000]"
  274. :page-size="pageSize"
  275. layout="total, sizes, prev, pager, next"
  276. :total="total"
  277. />
  278. </div>
  279. </div>
  280. <!-- 右侧详情 -->
  281. <div class="dialog-right">
  282. <div class="dialog-right-scroll">
  283. <el-card shadow="hover" body-style="padding: 0px;">
  284. <el-form :model="mlformdata" label-position="top">
  285. <el-descriptions column="1" border>
  286. <el-descriptions-item label="操作">
  287. <el-button type="primary" title="选择图片,图生文" @click="imgtotext" :loading="isLoading">图生文</el-button>
  288. <el-button type="primary" title="选择图片,文生文" @click="texttotxt" :loading="isLoading">文生文</el-button>
  289. <el-button type="primary" title="选择图片,文生图" @click="texttoimg" :loading="isLoading">文生图</el-button>
  290. <el-button type="primary" title="选择图片,图生图" @click="imgtoimg" :loading="isLoading">图生图</el-button>
  291. <el-button type="primary" title="选择图片,图像高清放大处理" @click="singleimg" :loading="isLoading">图像高清放大处理</el-button>
  292. <el-button type="primary" title="选中图片,执行顺序:图生文->文生文->文生图->图生图" @click="handleSelected" :loading="isLoading" :disabled="isLoading">执选择任务</el-button>
  293. <el-button type="primary" title="执行当前所有图片,执行顺序:图生文->文生文->文生图->图生图" @click="handleAll" :loading="isLoading">执行全部任务</el-button>
  294. </el-descriptions-item>
  295. <el-descriptions-item label="文生文模型">
  296. <el-radio-group v-model="txttotxt_selectedOption">
  297. <el-radio label="gemini-2.0-flash">gemini-2.0-flash</el-radio>
  298. <el-radio label="gtp-4">gtp-4</el-radio>
  299. </el-radio-group>
  300. </el-descriptions-item>
  301. <el-descriptions-item label="文生图模型">
  302. <el-radio-group v-model="selectedOption">
  303. <el-radio label="black-forest-labs/FLUX.1-kontext-pro">flux-pro</el-radio>
  304. <el-radio label="dall-e-3">dall-e-3</el-radio>
  305. <el-radio label="gpt-image-1">gpt-image-1</el-radio>
  306. </el-radio-group>
  307. </el-descriptions-item>
  308. <!-- <el-descriptions-item label="图生图模型">
  309. <el-select
  310. v-model="selectedModel"
  311. placeholder="请选择模型"
  312. filterable
  313. style="width: 100%"
  314. >
  315. <el-option
  316. v-for="(model, index) in modelList"
  317. :key="index"
  318. :label="model.title"
  319. :value="model.model_name"
  320. />
  321. </el-select>
  322. </el-descriptions-item> -->
  323. <el-descriptions-item label="出图预览">
  324. <div style="display: flex; gap: 20px; align-items: flex-start;">
  325. <!-- 左侧:图片列表区域 -->
  326. <div style="flex: 1;">
  327. <div class="image-preview-wrap" style="height: 140px;">
  328. <div
  329. v-for="(img, index) in imageList"
  330. :key="'fixed-' + index"
  331. class="image-item"
  332. @click="showDescription(img, 'new')"
  333. >
  334. <img :src="formatImageUrl(img.new_image_url)" />
  335. 1024x1024
  336. </div>
  337. </div>
  338. <div class="image-preview-wrap" style="margin-top: 16px; height: 167px;">
  339. <div
  340. v-for="(img, index) in imageList"
  341. :key="'imgtoimg-' + index"
  342. class="image-itemsimg"
  343. @click="showDescription(img, 'san')"
  344. >
  345. <img :src="formatImageUrl(img.imgtoimg_url)" />
  346. 图生图
  347. </div>
  348. </div>
  349. <div class="image-preview-wrap" style="margin-top: 16px; height: 140px;">
  350. <div
  351. v-for="(img, index) in imageList"
  352. :key="'custom-' + index"
  353. class="image-items"
  354. @click="showDescription(img, 'custom')"
  355. >
  356. <img :src="formatImageUrl(img.custom_image_url)" />
  357. 高清放大
  358. </div>
  359. </div>
  360. </div>
  361. <!-- 右侧:文字描述区域 -->
  362. <div
  363. style="
  364. width: 500px;
  365. white-space: pre-wrap;
  366. overflow-y: auto;
  367. max-height: 50vh;
  368. border: 1px solid #f0f0f0;
  369. padding: 10px;
  370. "
  371. >
  372. <div v-html="activeDescription"></div>
  373. </div>
  374. </div>
  375. </el-descriptions-item>
  376. <el-descriptions-item label="出图尺寸">
  377. <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 12px;">
  378. <el-input v-model="width" placeholder="宽度" size="small" style="width: 80px;" type="number" min="1" />
  379. <span style="font-weight: bold;">×</span>
  380. <el-input v-model="height" placeholder="高度" size="small" style="width: 80px;" type="number" min="1" />
  381. </div>
  382. </el-descriptions-item>
  383. <!-- <el-descriptions-item label="执行次数">
  384. <el-input-number v-model="num" :step="1" />
  385. </el-descriptions-item> -->
  386. <el-descriptions-item label="任务队列">
  387. <el-table
  388. :data="table_queue" border
  389. :show-overflow-tooltip="true"
  390. v-loading="loading"
  391. tooltip-effect="dark"
  392. :row-style="{ height: '30px' }"
  393. :cell-style="{ padding: '2px' }"
  394. :header-cell-style="{ padding: '0px' }"
  395. :header-row-style="{ height: '30px' }"
  396. style="width: 100%; height: 21vh;"
  397. >
  398. <el-table-column prop="id" label="ID" width="50" />
  399. <el-table-column prop="status" label="状态" width="100" />
  400. <el-table-column prop="model" label="队列模型" width="170" />
  401. <el-table-column prop="model_name" label="队列模型" width="90" />
  402. <el-table-column prop="image_count" label="执行数量" width="90" />
  403. <el-table-column prop="排队中的数量" label="队列中" width="70" />
  404. <el-table-column prop="处理中数量" label="处理中" width="70" />
  405. <el-table-column prop="已完成数量" label="成功" width="70" />
  406. <el-table-column prop="失败数量" label="失败" width="70" />
  407. </el-table>
  408. </el-descriptions-item>
  409. </el-descriptions>
  410. </el-form>
  411. </el-card>
  412. </div>
  413. </div>
  414. </div>
  415. </el-dialog>
  416. </div>
  417. </template>
  418. <script setup>
  419. import {ref,reactive,toRaw} from 'vue'
  420. import {ElMessage, ElMessageBox,ElLoading } from 'element-plus'
  421. import axios from 'axios'
  422. import {Plus,Delete,CircleCheck,CircleClose} from '@element-plus/icons-vue'
  423. import {stopQueueProcesses, queueStats, viewQueueStatus,ImgUpload,getPreviewSubDirs,getPreviewimg,getlsit,
  424. imageToText,
  425. textToImage,Template_ids,sd_models,
  426. packImagess,getUploadPath,get_queue_logs,getTaskProgress
  427. } from '@/api/mes/job'
  428. //获取服务器地址
  429. const formatImageUrl = (path) => {
  430. if (!path) return ''
  431. const base = 'http://20.0.16.128:9093'
  432. return `${base}/${path.replace(/^public\//, '')}`
  433. }
  434. // --------------------- 图片上传 ---------------------
  435. const tableData = ref([])
  436. const getPreviewSubDirslist = async () => {
  437. const res = await getPreviewSubDirs()
  438. const processedData = res.data.map(item => {
  439. return {
  440. ...item,
  441. new_image_url: `${item.new_image_url}${item.name}/`
  442. };
  443. });
  444. tableData.value = processedData;
  445. }
  446. getPreviewSubDirslist()
  447. // 环境配置
  448. const basePath = import.meta.env.VITE_BASE_PATH || 'http://20.0.16.128'
  449. const uploadsPort = import.meta.env.VITE_UPLOADS_PORT || '9093'
  450. const uploadUrl = ref(`${basePath}:${uploadsPort}/api/Facility/ImgUpload`)
  451. const uploadHeaders = {
  452. 'Content-Type': 'multipart/form-data',
  453. 'Authorization': 'Bearer ' + localStorage.getItem('token')
  454. }
  455. // 文件列表
  456. const fileList = ref([])
  457. const uploadRef = ref(null)
  458. // 获取本地文件URL
  459. const getObjectUrl = (file) => {
  460. return URL.createObjectURL(file)
  461. }
  462. // 上传前校验
  463. const beforeUpload = (file) => {
  464. const isImage = file.type === 'image/jpeg' || file.type === 'image/png'
  465. const isLt5M = file.size / 1024 / 1024 < 5 // 限制5MB
  466. if (!isImage) {
  467. ElMessage.error('只能上传 JPG/PNG 格式的图片!')
  468. return false
  469. }
  470. if (!isLt5M) {
  471. ElMessage.error('图片大小不能超过 5MB!')
  472. return false
  473. }
  474. // 显示加载中
  475. const loading = ElLoading.service({
  476. lock: true,
  477. text: '正在上传图片...',
  478. background: 'rgba(0, 0, 0, 0.7)'
  479. })
  480. // 将loading实例附加到file对象上,以便后续关闭
  481. file.loadingInstance = loading
  482. return true
  483. }
  484. // 自定义上传逻辑
  485. const customUpload = async (options) => {
  486. const { file, onProgress, onSuccess, onError } = options
  487. try {
  488. const formData = new FormData()
  489. formData.append('image', file)
  490. const response = await axios.post(uploadUrl.value, formData, {
  491. headers: uploadHeaders,
  492. onUploadProgress: (progressEvent) => {
  493. const percent = Math.round(
  494. (progressEvent.loaded * 100) / progressEvent.total
  495. )
  496. onProgress({ percent }, file)
  497. },
  498. timeout: 60000 // 60秒超时
  499. })
  500. } catch (error) {
  501. } finally {
  502. // 关闭加载中
  503. if (file.loadingInstance) {
  504. file.loadingInstance.close()
  505. }
  506. }
  507. }
  508. // 预览图片
  509. const handlePreview = (file) => {
  510. if (file.url) {
  511. window.open(file.url, '_blank')
  512. } else if (file.raw) {
  513. const url = getObjectUrl(file.raw)
  514. window.open(url, '_blank')
  515. URL.revokeObjectURL(url) // 释放内存
  516. }
  517. }
  518. // 上传成功
  519. const handleSuccess = (response, file, fileList) => {
  520. getPreviewSubDirslist()
  521. file.loadingInstance?.close()
  522. }
  523. // 上传失败
  524. const handleError = (error, file, fileList) => {
  525. console.error('上传错误:', error)
  526. ElMessage.error(`${file.name} 上传失败`)
  527. file.loadingInstance?.close()
  528. }
  529. // 超出限制
  530. const handleExceed = (files, fileList) => {
  531. ElMessage.warning(`最多只能上传 30 张图片,您已选择 ${files.length} 张,共 ${files.length + fileList.length} 张`)
  532. }
  533. // --------------------- 查看队列任务 ---------------------
  534. const queuestats = ref({
  535. 总任务数: 0,
  536. 待处理: 0,
  537. 处理中: 0,
  538. 成功: 0,
  539. 失败: 0
  540. })
  541. //队列状态
  542. const queueStats_list = async () => {
  543. const res = await queueStats()
  544. console.log(res)
  545. if (res.code === 0) {
  546. queuestats.value = res.data
  547. }
  548. }
  549. queueStats_list()
  550. //停止处理
  551. const stopQueueLoading = ref(false)
  552. const stopQueueclick = async () => {
  553. if (stopQueueLoading.value) return
  554. try {
  555. await ElMessageBox.confirm(
  556. '确定要停止所有队列任务吗?',
  557. '确认操作',
  558. {
  559. confirmButtonText: '清空队列',
  560. cancelButtonText: '取消',
  561. type: 'warning',
  562. }
  563. )
  564. stopQueueLoading.value = true
  565. const res = await stopQueueProcesses()
  566. if (res.code === 0) {
  567. ElMessage.success(res.msg || '已成功停止队列任务')
  568. }
  569. } catch (err) {
  570. if (err !== 'cancel') {
  571. ElMessage.error('操作失败,请稍后重试')
  572. }
  573. } finally {
  574. stopQueueLoading.value = false
  575. }
  576. }
  577. //任务列表
  578. const queueDialogVisible = ref(false)
  579. // 队列数据
  580. const queueData = ref({
  581. tasks_preview: [],
  582. count: 0
  583. })
  584. const QueueStatusisLoading = ref(false)
  585. // 任务列表按钮
  586. const handleViewQueue = async () => {
  587. queueStats_list()
  588. getPreviewSubDirslist()
  589. QueueStatusisLoading.value = true
  590. try {
  591. const res = await viewQueueStatus()
  592. if (res.code === 0) {
  593. queueData.value = res
  594. queueDialogVisible.value = true
  595. } else {
  596. ElMessage.error(res.msg || '查询失败')
  597. }
  598. } catch (err) {
  599. ElMessage.error('请求异常')
  600. } finally {
  601. QueueStatusisLoading.value = false
  602. }
  603. }
  604. const queueDialog = async () => {
  605. queueDialogVisible.value = false
  606. }
  607. // --------------------- 查看详情 ---------------------
  608. const fetchPreviewImages = async () => {
  609. loading.value = true;
  610. const params = {
  611. path: _resrow.value.old_image_url,
  612. page: page.value,
  613. limit: pageSize.value
  614. };
  615. if (filterStatus.value === '已出图') {
  616. params.status = 1
  617. } else if (filterStatus.value === '未出图') {
  618. params.status = 0
  619. }else if(filterStatus.value === '图生文') {
  620. params.status_name = "图生文"
  621. }else if(filterStatus.value === '文生文') {
  622. params.status_name = "文生文"
  623. }else if(filterStatus.value === '文生图') {
  624. params.status_name = "文生图"
  625. }else if(filterStatus.value === '图生图') {
  626. params.status_name = "图生图"
  627. }else if(filterStatus.value === '高清放大') {
  628. params.status_name = "高清放大"
  629. }
  630. // 获取分页数据
  631. const res = await getPreviewimg(params);
  632. if (res.code === 0) {
  633. tableData_lsit.value = res.data;
  634. total.value = res.total;
  635. }
  636. loading.value = false;
  637. };
  638. // 分页
  639. const page = ref(1)
  640. const total = ref(0)
  641. const pageSize = ref(50)
  642. // 页码变化
  643. const handleCurrentChange = (val) => {
  644. page.value = val;
  645. fetchPreviewImages();
  646. };
  647. // 每页数量变化
  648. const handleSizeChange = (val) => {
  649. pageSize.value = val;
  650. page.value = 1;
  651. fetchPreviewImages();
  652. };
  653. //---------------选择框----------------
  654. const filterStatus = ref('全部')
  655. const handleFilterChange = () => {
  656. // console.log('当前选择状态:', filterStatus.value);
  657. page.value = 1;
  658. fetchPreviewImages();
  659. };
  660. const mlinventoryVisible = ref(false)
  661. const loading = ref(false)
  662. const width = ref('')
  663. const height = ref('')
  664. const mlformdata = ref({})
  665. const tableData_lsit = ref([])
  666. const _resrow = ref('')
  667. const TaskqueueVisible = ref(false)
  668. const table_queue = ref([])
  669. const table_Taskqueue = ref([])
  670. //SD模型
  671. const selectedModel = ref('Realistic_Vision_V5.0-inpainting.safetensors [cf5d9eb09b]')
  672. const modelList = ref([])
  673. //查看详情按钮
  674. const record_deleteRow = async (row) => {
  675. //打开弹窗
  676. mlinventoryVisible.value = true;
  677. //获取当前文件夹行信息
  678. _resrow.value = row;
  679. //清空信息保持数据最新
  680. imageList.value = []
  681. img_id.value = '';
  682. model.value = '';
  683. new_image_url.value = '';
  684. imgtoimg_url.value = '';
  685. path_image_url.value = '';
  686. activeDescription.value = '';
  687. // 获取 SD 模型列表
  688. const sd_models_list = await sd_models();
  689. modelList.value = sd_models_list.data
  690. // 任务队列
  691. const queue_logs = await get_queue_logs({ old_image_file: row['old_image_url'] });
  692. table_queue.value = queue_logs.data;
  693. // 出图尺寸
  694. const res = await Template_ids();
  695. console.log(res)
  696. height.value = res.data.height
  697. width.value = res.data.width
  698. await fetchPreviewImages();
  699. }
  700. const record_queueRow = async (row) => {
  701. TaskqueueVisible.value = true;
  702. console.log(row['id'])
  703. const getTaskProgress_data = await getTaskProgress({ id: row['id'] });
  704. table_Taskqueue.value = getTaskProgress_data.data.data;
  705. }
  706. const TaskqueueDialog = async (row) => {
  707. TaskqueueVisible.value = false;
  708. }
  709. const imageList = ref([]);
  710. const _oldrow = ref('');
  711. //点击当前行
  712. const handleRowClick = async (row) => {
  713. imgtoimg_url.value = ''
  714. activeDescription.value = ''
  715. // fetchPreviewImages();
  716. console.log("当前行信息",row)
  717. // 文本内容高亮处理
  718. const zh = highlightKeywords(row['chinese_description'] || '');
  719. const en = highlightKeywords(row['english_description'] || '');
  720. const name = highlightKeywords(row['img_name'] || '');
  721. activeDescription.value = `第一段:\n${zh}\n\n第二段:\n${en}\n\n第三段:\n${name}`;
  722. _oldrow.value = row
  723. const res = await getlsit({ path: row['path'] });
  724. if (res.data === null) {
  725. imageList.value = [];
  726. } else {
  727. imageList.value = res.data.map(item => ({
  728. id: item.id,
  729. model: item.model,
  730. chinese_description: item.chinese_description,
  731. english_description: item.english_description,
  732. img_name: item.img_name,
  733. new_image_url: item.new_image_url,
  734. imgtoimg_url:item.imgtoimg_url,
  735. custom_image_url: item.custom_image_url,
  736. size: item.size
  737. }));
  738. const queue_logs = await get_queue_logs({ old_image_file: _resrow.value['old_image_url'] });
  739. table_queue.value = queue_logs.data;
  740. }
  741. };
  742. //关闭按钮
  743. const mlcloseDialog = () => {
  744. mlinventoryVisible.value = false
  745. }
  746. /**
  747. * 点击图片查看显示
  748. */
  749. const activeDescription = ref('');
  750. const path_image_url = ref('');
  751. const img_id = ref('');
  752. const model = ref('');
  753. const new_image_url = ref('');
  754. const imgtoimg_url = ref('');
  755. const showDescDialog = ref(false);
  756. // 高亮关键词为红色
  757. const highlightKeywords = (text) => {
  758. const keywords = ['几何', 'geometry', 'geometric'];
  759. keywords.forEach(word => {
  760. const reg = new RegExp(word, 'gi');
  761. text = text.replace(reg, match => `<span style="color:red">${match}</span>`);
  762. });
  763. return text;
  764. };
  765. // 显示图像描述
  766. const showDescription = async (img, type) => {
  767. showDescDialog.value = true;
  768. // 任务队列
  769. Refresh_Status()
  770. // 公共字段初始化
  771. img_id.value = img.id || '';
  772. model.value = img.model || '';
  773. path_image_url.value = '';
  774. // 文本内容高亮处理
  775. const zh = highlightKeywords(img.chinese_description || '');
  776. const en = highlightKeywords(img.english_description || '');
  777. const name = highlightKeywords(img.img_name || '');
  778. if (type === 'new') {
  779. path_image_url.value = img.new_image_url || '图片未生成';
  780. activeDescription.value = `第一段:\n${zh}\n\n第二段:\n${en}\n\n第三段:\n${name}`;
  781. imgtoimg_url.value = img.imgtoimg_url || '图片未生成';
  782. } else if (type === 'custom') {
  783. path_image_url.value = img.custom_image_url || '图片未生成';
  784. activeDescription.value = `第一段:\n${zh}\n\n第二段:\n${en}\n\n第三段:\n${name}`;
  785. imgtoimg_url.value = img.imgtoimg_url || '图片未生成';
  786. }else if (type === 'san') {
  787. path_image_url.value = img.imgtoimg_url || '图片未生成';
  788. imgtoimg_url.value = img.imgtoimg_url || '图片未生成';
  789. activeDescription.value = '';
  790. model.value = '';
  791. }else if (type === 'old') {
  792. path_image_url.value = img.path || '暂无说明';
  793. imgtoimg_url.value = '';
  794. activeDescription.value = '';
  795. } else {
  796. // console.log('无效的类型');
  797. }
  798. };
  799. //复选框
  800. const _parh = ref('')
  801. const SelectionChange = (rows) => {
  802. _parh.value = rows.map(item => item.path);
  803. };
  804. //查看详情右侧区域
  805. const isLoading = ref(false)
  806. //执行次数默认值
  807. const num = ref(1)
  808. //文生文模型下拉(默认值)
  809. const txttotxt_selectedOption = ref('gemini-2.0-flash')
  810. //文生图模型下拉(默认值)
  811. const selectedOption = ref('black-forest-labs/FLUX.1-kontext-pro')
  812. //刷新表格任务状态
  813. const Refresh_Status = async () => {
  814. // 开始 loading
  815. loading.value = true;
  816. fetchPreviewImages();
  817. // 任务队列
  818. const queue_logs = await get_queue_logs({ old_image_file: _resrow.value['old_image_url'] });
  819. table_queue.value = queue_logs.data;
  820. // 延迟2秒再关闭loading
  821. await new Promise(resolve => setTimeout(resolve, 3000));
  822. isLoading.value = false;
  823. }
  824. //执行图生文、选择执行选择图片,没有选择执行当前页图片处理
  825. const imgtotext = () => {
  826. processImages(toRaw(_parh.value),'图生文');
  827. };
  828. //执行文生文、选择执行选择图片,没有选择执行当前页图片处理
  829. const texttotxt = () => {
  830. processImages(toRaw(_parh.value),'文生文');
  831. };
  832. //执行文生图、选择执行选择图片
  833. const texttoimg = () => {
  834. processImages(toRaw(_parh.value),'文生图');
  835. };
  836. //执行图生图、选择执行选择图片
  837. const imgtoimg = () => {
  838. processImages(toRaw(_parh.value),'图生图');
  839. };
  840. const singleimg = () => {
  841. processImages(toRaw(_parh.value),'高清放大');
  842. };
  843. //执行选择任务
  844. const handleSelected = () => {
  845. processImages(toRaw(_parh.value),'');
  846. };
  847. //执行全部任务
  848. const allTableData = ref([]); // 全部数据缓存
  849. const handleAll = async () => {
  850. try {
  851. await ElMessageBox.confirm(
  852. '确定要执行全部任务吗?',
  853. '确认操作',
  854. {
  855. confirmButtonText: '确定',
  856. cancelButtonText: '取消',
  857. type: 'warning'
  858. }
  859. );
  860. // 开始 loading
  861. loading.value = true;
  862. // 发起全量请求
  863. const allParams = {
  864. path: _resrow.value.old_image_url,
  865. page: 1,
  866. limit: 10000
  867. };
  868. const allRes = await getPreviewimg(allParams);
  869. console.log(allRes);
  870. if (allRes.code === 0) {
  871. // 全部原始数据
  872. allTableData.value = allRes.data;
  873. // 只保留 status 为 0 且 status_name 为空的项
  874. const filteredPaths = toRaw(allTableData.value)
  875. .filter(item => item.status === 0 && !item.status_name)
  876. .map(item => item.path);
  877. processImages(filteredPaths, '');
  878. } else {
  879. ElMessage.error(allRes.msg || '获取全部图片失败');
  880. }
  881. } catch (err) {
  882. // 用户点击取消时,不做任何操作
  883. if (err !== 'cancel') {
  884. ElMessage.error('操作取消');
  885. }
  886. } finally {
  887. // 无论成功/失败/异常,最后关闭 loading
  888. loading.value = false;
  889. }
  890. };
  891. //操作 统一执行操作按钮处理方法
  892. const processImages = async (files,type) => {
  893. //判断是否选择图片列表
  894. if (!Array.isArray(files) || files.length === 0) {
  895. ElMessage.warning('请选择要处理的图片')
  896. return
  897. }
  898. //生成图片是1024,所有需要限制尺寸不可以超过原始尺寸
  899. // if (width.value >= 1024 || height.value >= 1024) {
  900. // ElMessage.warning('尺寸不可超过1024')
  901. // return
  902. // }
  903. //加载开启
  904. isLoading.value = true
  905. const failList = []
  906. const payload = {
  907. old_image_file:_resrow.value['old_image_url'],
  908. type:type,
  909. selectedOption:selectedOption.value,
  910. txttotxt_selectedOption:txttotxt_selectedOption.value,
  911. batch:files,
  912. num:num.value,
  913. width: width.value,
  914. height: height.value
  915. }
  916. //打印调试区域
  917. // console.log(payload);
  918. // await new Promise(resolve => setTimeout(resolve, 2000));
  919. // isLoading.value = false;
  920. // return;
  921. try {
  922. const res = await imageToText(payload)
  923. } catch (e) {
  924. failList.push(`第 ${batchStart + i + 1} 张 第 ${j + 1} 次`)
  925. }
  926. ElMessage.success(`共处理 ${files.length} 张 * ${num.value} 次,共 ${files.length * num.value} 次`)
  927. if (failList.length > 0) {
  928. console.warn('失败列表:', failList)
  929. }
  930. // 延迟2秒再关闭loading
  931. await new Promise(resolve => setTimeout(resolve, 3000));
  932. isLoading.value = false;
  933. const queue_logs = await get_queue_logs({ old_image_file: _resrow.value['old_image_url'] });
  934. table_queue.value = queue_logs.data;
  935. }
  936. // --------------------- 打包zip ---------------------
  937. const zipview = ref(false)
  938. const ziprow = ref('')
  939. const zipselectedOption = ref('1024x1303') // 默认选中
  940. //点击打包按钮
  941. const packRow = async (row) => {
  942. zipview.value = true;
  943. ziprow.value = row
  944. }
  945. //确定按钮
  946. const confirmPack = async (row) => {
  947. isLoading.value = true;
  948. try {
  949. const res = await getPreviewimg({
  950. path: ziprow.value.old_image_url,
  951. page: 1,
  952. limit: 10000
  953. });
  954. if (res.code !== 0 || !res.data || res.data.length === 0) {
  955. ElMessage.warning('未找到出图记录');
  956. return;
  957. }
  958. const imagePaths = res.data.flatMap(item => {
  959. const paths = [];
  960. if (item.custom_image_url?.trim()) {
  961. paths.push({ type: 'custom_image_url', path: item.custom_image_url });
  962. }
  963. if (item.imgtoimg_url?.trim()) {
  964. paths.push({ type: 'imgtoimg_url', path: item.imgtoimg_url });
  965. }
  966. return paths;
  967. });
  968. if (imagePaths.length === 0) {
  969. ElMessage.warning('没有可打包的图片');
  970. return;
  971. }
  972. const packRes = await packImagess({ paths: imagePaths });
  973. if (packRes.code === 0 && packRes.download_url) {
  974. ElMessage.success('打包成功,正在下载...');
  975. // ✅ 替换为服务的真实 IP 和端口
  976. const basePath = import.meta.env.VITE_BASE_PATH || 'http://20.0.16.128';
  977. const uploadsPort = import.meta.env.VITE_UPLOADS_PORT || '9093';
  978. let url = packRes.download_url;
  979. try {
  980. const parsedUrl = new URL(url);
  981. if (parsedUrl.hostname === '127.0.0.1' || parsedUrl.hostname === 'localhost') {
  982. parsedUrl.hostname = new URL(basePath).hostname;
  983. parsedUrl.port = uploadsPort;
  984. url = parsedUrl.toString();
  985. }
  986. } catch (e) {
  987. console.error('URL 解析失败:', e);
  988. }
  989. const link = document.createElement('a');
  990. link.href = url;
  991. link.setAttribute('download', '');
  992. document.body.appendChild(link);
  993. link.click();
  994. document.body.removeChild(link);
  995. } else {
  996. ElMessage.error(`打包失败:${packRes.msg || '未知错误'}`);
  997. }
  998. } catch (err) {
  999. ElMessage.error('打包请求异常');
  1000. console.error(err);
  1001. } finally {
  1002. isLoading.value = false;
  1003. }
  1004. };
  1005. </script>
  1006. <style scoped>
  1007. .page-container {
  1008. padding: 20px;
  1009. background: #fff;
  1010. }
  1011. .input-container {
  1012. margin-bottom: 20px;
  1013. }
  1014. .gva-table-box {
  1015. padding-bottom: 20px;
  1016. }
  1017. .table-container {
  1018. margin-top: 20px;
  1019. }
  1020. /* 表格行高细节调整 */
  1021. :deep(.el-table td .cell) {
  1022. line-height: 20px !important;
  1023. }
  1024. :deep(.el-tabs__header) {
  1025. margin-bottom: 0;
  1026. }
  1027. /* 选中高亮 */
  1028. :deep(.el-table__body tr.current-row) > td {
  1029. background: #ff80ff !important;
  1030. }
  1031. /* 新增上传区域样式 */
  1032. .upload-container {
  1033. margin: 20px 0;
  1034. padding: 20px;
  1035. border: 1px dashed #dcdfe6;
  1036. border-radius: 4px;
  1037. background-color: #f5f7fa;
  1038. }
  1039. .upload-list {
  1040. display: flex;
  1041. flex-wrap: wrap;
  1042. gap: 10px;
  1043. }
  1044. .upload-item {
  1045. position: relative;
  1046. width: 100px;
  1047. height: 100px;
  1048. border-radius: 4px;
  1049. overflow: hidden;
  1050. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  1051. }
  1052. .upload-item-thumbnail {
  1053. width: 100%;
  1054. height: 100%;
  1055. object-fit: cover;
  1056. }
  1057. /* 修改上传卡片容器 */
  1058. :deep(.el-upload--picture-card) {
  1059. --el-upload-picture-card-size: 100px;
  1060. }
  1061. :deep(.el-upload-list--picture-card) {
  1062. --el-upload-list-picture-card-size: 100px;
  1063. }
  1064. .upload-item-actions {
  1065. position: absolute;
  1066. bottom: 0;
  1067. left: 0;
  1068. right: 0;
  1069. padding: 8px;
  1070. background: rgba(0, 0, 0, 0.6);
  1071. color: white;
  1072. display: flex;
  1073. align-items: center;
  1074. justify-content: space-between;
  1075. }
  1076. .upload-item-name {
  1077. flex: 1;
  1078. font-size: 12px;
  1079. white-space: nowrap;
  1080. overflow: hidden;
  1081. text-overflow: ellipsis;
  1082. margin-right: 8px;
  1083. }
  1084. .upload-progress {
  1085. font-size: 12px;
  1086. color: #409eff;
  1087. }
  1088. .upload-item-status {
  1089. margin: 0 8px;
  1090. }
  1091. .success-icon {
  1092. color: #67c23a;
  1093. }
  1094. .error-icon {
  1095. color: #f56c6c;
  1096. }
  1097. .upload-item-delete {
  1098. cursor: pointer;
  1099. color: #f56c6c;
  1100. }
  1101. .upload-item-delete:hover {
  1102. color: #ff0000;
  1103. }
  1104. :deep(.desc-overlay) {
  1105. position: absolute;
  1106. top: 0;
  1107. left: 0;
  1108. width: 120px;
  1109. height: 120px;
  1110. background: rgba(0, 0, 0, 0.75);
  1111. color: #fff;
  1112. font-size: 12px;
  1113. display: flex;
  1114. align-items: center;
  1115. justify-content: center;
  1116. padding: 8px;
  1117. box-sizing: border-box;
  1118. border-radius: 8px;
  1119. text-align: center;
  1120. word-break: break-word;
  1121. line-height: 1.4;
  1122. z-index: 10;
  1123. }
  1124. :deep(.fade-enter-active,
  1125. .fade-leave-active) {
  1126. transition: opacity 0.3s;
  1127. }
  1128. :deep(.fade-enter-from,
  1129. .fade-leave-to) {
  1130. opacity: 0;
  1131. }
  1132. .dialog-scroll-wrapper {
  1133. max-height: 500px;
  1134. overflow-y: auto;
  1135. padding-right: 10px;
  1136. }
  1137. .task-card {
  1138. margin-bottom: 12px;
  1139. }
  1140. .task-grid {
  1141. display: grid;
  1142. grid-template-columns: 1fr;
  1143. row-gap: 6px;
  1144. font-size: 14px;
  1145. line-height: 1.6;
  1146. }
  1147. .task-prompt {
  1148. word-break: break-word;
  1149. white-space: pre-wrap;
  1150. }
  1151. .task-image {
  1152. margin-top: 8px;
  1153. }
  1154. .image-preview-overlay {
  1155. position: fixed;
  1156. top: 0;
  1157. left: 0;
  1158. right: 0;
  1159. bottom: 0;
  1160. background: rgba(0, 0, 0, 0.7);
  1161. display: flex;
  1162. justify-content: center;
  1163. align-items: center;
  1164. z-index: 9999;
  1165. }
  1166. .image-preview {
  1167. max-width: 90%;
  1168. max-height: 90%;
  1169. object-fit: contain;
  1170. cursor: pointer;
  1171. }
  1172. .dialog-body {
  1173. height: 93vh;
  1174. display: flex;
  1175. gap: 20px;
  1176. overflow: hidden;
  1177. }
  1178. /* 左侧表格区域 */
  1179. .dialog-left {
  1180. flex: 1.4;
  1181. height: 100%;
  1182. overflow-y: auto;
  1183. }
  1184. /* 右侧详情区域 */
  1185. .dialog-right {
  1186. flex: 2;
  1187. height: 100%;
  1188. overflow: hidden;
  1189. display: flex;
  1190. flex-direction: column;
  1191. }
  1192. .dialog-right-scroll {
  1193. flex: 1;
  1194. overflow-y: auto;
  1195. padding-right: 5px;
  1196. }
  1197. /* 按钮区域自动换行不拥挤 */
  1198. .button-group {
  1199. display: flex;
  1200. flex-wrap: wrap;
  1201. gap: 10px;
  1202. margin-top: 5px;
  1203. }
  1204. /* 图片容器 */
  1205. .image-preview-wrap {
  1206. display: flex;
  1207. flex-wrap: wrap;
  1208. gap: 16px;
  1209. }
  1210. .image-item {
  1211. position: relative;
  1212. text-align: center;
  1213. width: 110px;
  1214. }
  1215. .image-item img {
  1216. width: 120px;
  1217. height: 120px;
  1218. border: 1px solid #ccc;
  1219. object-fit: contain;
  1220. }
  1221. .image-item div {
  1222. margin-top: 4px;
  1223. font-size: 12px;
  1224. color: #666;
  1225. }
  1226. .image-items {
  1227. position: relative;
  1228. text-align: center;
  1229. width: 107px;
  1230. }
  1231. .image-items img {
  1232. width: 95px;
  1233. height: 120px;
  1234. border: 1px solid #ccc;
  1235. object-fit: contain;
  1236. }
  1237. .image-items div {
  1238. margin-top: 4px;
  1239. font-size: 12px;
  1240. color: #666;
  1241. }
  1242. .image-itemsimg {
  1243. position: relative;
  1244. text-align: center;
  1245. width: 110px;
  1246. }
  1247. .image-itemsimg img {
  1248. width: 118px;
  1249. height: 150px;
  1250. border: 1px solid #ccc;
  1251. object-fit: contain;
  1252. }
  1253. .image-itemsimg div {
  1254. margin-top: 4px;
  1255. font-size: 12px;
  1256. color: #666;
  1257. }
  1258. </style>