index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. <template>
  2. <div class="app-container">
  3. <div class="filter-container">
  4. <div class="filter-container-lt">
  5. <el-form :inline="true" label-position="left" :model="listQuery">
  6. <el-form-item>
  7. <el-input clearable v-model="listQuery.sku" placeholder="搜索SKU"
  8. ></el-input>
  9. </el-form-item>
  10. <el-form-item>
  11. <el-select clearable v-model="listQuery.status" placeholder="请选择状态">
  12. <el-option
  13. v-for="item in statusSets"
  14. :key="item.value"
  15. :label="item.label"
  16. :value="item.value">
  17. </el-option>
  18. </el-select>
  19. </el-form-item>
  20. <el-button type="primary" @click="getList()">搜索</el-button >
  21. <el-button type="primary" @click="showCropSizeModal()">裁图尺寸</el-button >
  22. <el-button type="primary" @click="aiCropPreview()" :loading="uploadLoading">AI裁图</el-button>
  23. </el-form>
  24. </div>
  25. </div>
  26. <div class="table-container">
  27. <el-table style="width: 100%" v-loading="listLoading" :key="tableKey" :data="list" row-key="id"
  28. stripe border fit highlight-current-row @selection-change="handleSelectionChange">
  29. <!-- <el-table-column type="selection" width="55" :selectable="selectable"></el-table-column> -->
  30. <el-table-column label="SKU名称" align="center">
  31. <template slot-scope="scope">
  32. <el-popover
  33. placement="bottom"
  34. width="260"
  35. trigger="manual"
  36. v-model="scope.row._popoverVisible"
  37. >
  38. <div class="input-block">
  39. <span style="display: block;">SKU:</span>
  40. <el-input
  41. v-model="editValue"
  42. type="textarea"
  43. :rows="2"
  44. />
  45. </div>
  46. <div class="input-block" style="margin-top: 10px;" v-if="scope.row.status >= 5">
  47. <span style="display: block;">颜色代码:</span>
  48. <el-input
  49. v-model="editValueColor"
  50. type="textarea"
  51. :rows="2"
  52. />
  53. </div>
  54. <div class="popover-footer">
  55. <el-button size="mini" @click="cancelEdit(scope.row)">取消</el-button>
  56. <el-button size="mini" :loading="skuLoading" type="primary" @click="confirmEdit(scope.row)">确认</el-button>
  57. </div>
  58. <span slot="reference" class="click-text" @click="openEdit(scope.row)">
  59. {{ scope.row.sku }}
  60. <template v-if="scope.row.colorCode">
  61. <br>
  62. {{ scope.row.colorCode }}
  63. </template>
  64. </span>
  65. </el-popover>
  66. </template>
  67. </el-table-column>
  68. <el-table-column label="原图" min-width="100" align="center">
  69. <template slot-scope="scope">
  70. <p v-if="scope.row.type == 1 && scope.row.referenceImagesList && scope.row.referenceImagesList.length" @click="openGallery(scope.row, 'default')">
  71. <img style="max-width: 100px;" :src="scope.row.referenceImagesList[0]" alt=""></img>
  72. </p>
  73. <p style="margin: 0;cursor: pointer;" v-if="scope.row.type == 2 && scope.row.aiGeneratedImageVOList && scope.row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1).length" @click="openGallery(scope.row, 'default')">
  74. <img style="max-width: 100px;" :src="scope.row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1)[0].imageUrl" alt="">
  75. </p>
  76. </template>
  77. </el-table-column>
  78. <el-table-column label="AI生成图" min-width="100" align="center">
  79. <template slot-scope="scope">
  80. <p style="margin: 0;cursor: pointer;" v-if="scope.row.aiGeneratedImageVOList && scope.row.aiGeneratedImageVOList.length" @click="openGallery(scope.row)">
  81. <img style="max-width: 100px;" :src="scope.row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1)[0].imageUrl" alt="">
  82. </p>
  83. </template>
  84. </el-table-column>
  85. <el-table-column label="预计完成时间" min-width="100" align="center" prop="expectedCompletionTime" />
  86. <el-table-column label="状态" min-width="100" align="center">
  87. <template slot-scope="scope">
  88. <el-tag v-if="scope.row.status == 0">提交中</el-tag>
  89. <el-tag v-if="scope.row.status == 1">AI生成中</el-tag>
  90. <el-tag type="warning" v-if="scope.row.status == 2">待审核</el-tag>
  91. <el-tag type="warning" v-if="scope.row.status == 3">待复核</el-tag>
  92. <el-tag type="success" v-if="scope.row.status == 4">待裁图</el-tag>
  93. <el-tag type="warning" v-if="scope.row.status == 5">裁图待复核</el-tag>
  94. <el-tag type="success" v-if="scope.row.status == 6">已完成</el-tag>
  95. <el-tag type="danger" v-if="scope.row.status == 7">生图失败</el-tag>
  96. <el-tag v-if="scope.row.status == 8">待推送</el-tag>
  97. <el-tag v-if="scope.row.status == 9">裁图中</el-tag>
  98. </template>
  99. </el-table-column>
  100. <el-table-column label="处理人" min-width="100" align="center" prop="creator" />
  101. <el-table-column label="处理时间" min-width="100" align="center" prop="updateTime" />
  102. <el-table-column label="操作" align="center" width="250" fixed="right">
  103. <template slot-scope="scope">
  104. <!-- scope.row.status == 2 || scope.row.status == 3 -->
  105. <el-button size="mini" v-if="scope.row.status == 2 || scope.row.status == 3" @click="showImagesGroupModal(scope.row, 'review')">审核</el-button>
  106. <el-button type="success" size="mini" v-if="scope.row.status * 1 >= 4 && scope.row.status != 7 && scope.row.status != 9 && scope.row.status != 6" @click="showImagesGroupModal(scope.row, 'crop')">裁图</el-button>
  107. <el-button type="primary" v-if="scope.row.status * 1 > 1 && scope.row.status != 7" size="mini" @click="handleDownload(scope.row)">下载</el-button>
  108. <!-- 自动生成 1 手动生成 2 -->
  109. <el-button type="info" size="mini" v-if="scope.row.type != 2" @click="handleRegenerate(scope.row)">重生成</el-button>
  110. <el-dropdown v-if="scope.row.status == 8" @command="uploadGalleryList($event, scope.row)">
  111. <el-button type="warning" size="mini">
  112. 上传图库<i class="el-icon-arrow-down el-icon--right"></i>
  113. </el-button>
  114. <el-dropdown-menu slot="dropdown">
  115. <el-dropdown-item command="1">覆盖模式</el-dropdown-item>
  116. <el-dropdown-item command="2">追加模式</el-dropdown-item>
  117. </el-dropdown-menu>
  118. </el-dropdown>
  119. <el-popover
  120. placement="left"
  121. width="320"
  122. trigger="click"
  123. >
  124. <!-- 日志内容 -->
  125. <div v-if="scope.row.operationLogs && scope.row.operationLogs.length" style="max-height: 500px;overflow-y: auto;">
  126. <div
  127. v-for="(log, index) in scope.row.operationLogs"
  128. :key="index"
  129. class="log-item"
  130. >
  131. <div class="log-user">{{ log.userName }}</div>
  132. <div class="log-action">{{ log.msg }}</div>
  133. <div class="log-time">{{ log.updateTime }}</div>
  134. </div>
  135. </div>
  136. <div v-else class="empty-log">
  137. 暂无操作日志
  138. </div>
  139. <!-- 触发按钮 -->
  140. <el-button
  141. slot="reference"
  142. size="mini"
  143. type="danger"
  144. >
  145. 操作日志
  146. </el-button>
  147. </el-popover>
  148. </template>
  149. </el-table-column>
  150. </el-table>
  151. </div>
  152. <!-- 分页 -->
  153. <swPage v-if="total > 0" key="2" :listQuery="listQuery" :total="total" pos="btmRight" @retPage="retPage" />
  154. <crop-size-modal ref="CropSizeModal" :list="sizeList" @success="getSizeTemplate()"></crop-size-modal>
  155. <images-group-modal ref="ImagesGroupModal" :sizeList="sizeList" @update-status="updateStatus" @update-images="updateImages" @update-success="handleFilter()"></images-group-modal>
  156. </div>
  157. </template>
  158. <script>
  159. import waves from "@/directive/waves";
  160. import swPage from "@/views/common/swPage";
  161. import CropSizeModal from "./components/CropSizeModal";
  162. import ImagesGroupModal from "./components/ImagesGroupModal";
  163. import { auditStatus } from "@/constants/index";
  164. import { getValueByKey } from "@/utils/index";
  165. import permission from "@/directive/permission";
  166. import { genCidHex16 } from '@/utils/index'
  167. import { Fancybox } from "@fancyapps/ui";
  168. import request from '@/utils/request'
  169. import "@fancyapps/ui/dist/fancybox.css";
  170. import JSZip from 'jszip'
  171. import { saveAs } from 'file-saver'
  172. export default {
  173. name: "cropList",
  174. directives: {
  175. waves,
  176. permission
  177. },
  178. components: {
  179. swPage,
  180. CropSizeModal,
  181. ImagesGroupModal
  182. },
  183. computed: {
  184. },
  185. data() {
  186. return {
  187. auditStatus,
  188. tableKey: 0,
  189. list: [],
  190. total: 0,
  191. skuLoading: false,
  192. editValue: '',
  193. editValueColor: '',
  194. listLoading: false,
  195. uploadLoading: false,
  196. listQuery: {
  197. currentPage: 1,
  198. pageSize: 10,
  199. sku: null,
  200. status: ''
  201. },
  202. sizeList: [],
  203. multipleSelection: []
  204. };
  205. },
  206. //页面创建的时候执行
  207. created() {
  208. this.getSizeTemplate();
  209. this.getList();
  210. },
  211. computed: {
  212. statusSets() {
  213. return [
  214. {label: '提交中', value: '0'},
  215. {label: 'AI生成中', value: '1'},
  216. {label: '待审核', value: '2'},
  217. {label: '待复核', value: '3'},
  218. {label: '待裁图', value: '4'},
  219. {label: '裁图待复核', value: '5'},
  220. {label: '已完成', value: '6'},
  221. {label: '生图失败', value: '7'},
  222. {label: '待推送', value: '8'},
  223. {label: '裁图中', value: '9'}
  224. ]
  225. }
  226. },
  227. mounted() {
  228. document.addEventListener('click', this.handleClickOutside)
  229. },
  230. beforeDestroy() {
  231. document.removeEventListener('click', this.handleClickOutside)
  232. },
  233. methods: {
  234. aiCropPreview() {
  235. this.$refs.ImagesGroupModal.init();
  236. },
  237. selectable(row, index) {
  238. // 返回 false → 该行不可选
  239. // 返回 true → 该行可选
  240. return row.status == 6 || row.status == 8;
  241. },
  242. handleRegenerate(row) {
  243. this.listLoading = true;
  244. request({
  245. url: '/imageTask/regenerate',
  246. method: 'post',
  247. data: {
  248. sessionId: `${Date.now() + 10}_${genCidHex16()}`,
  249. applicationId: 5,
  250. id: row.id,
  251. }
  252. }).then(res => {
  253. if (res.code == 200) {
  254. this.getList();
  255. this.$message.success("操作成功");
  256. }
  257. }).finally(() => {
  258. this.listLoading = false;
  259. })
  260. },
  261. getSizeTemplate() {
  262. request({
  263. url: '/sizeTemplate/list',
  264. method: 'get'
  265. }).then(res => {
  266. if (res.code == 200) {
  267. this.sizeList = res.data;
  268. }
  269. }).finally(() => {
  270. })
  271. },
  272. updateImages(id, imageId){
  273. this.list.forEach(item => {
  274. if (item.id == id) {
  275. item.aiGeneratedImageVOList.map((acc, index) => {
  276. if (acc.id == imageId) {
  277. item.aiGeneratedImageVOList.splice(index, 1);
  278. }
  279. })
  280. }
  281. })
  282. },
  283. updateStatus(id, status) {
  284. this.list.forEach(item => {
  285. if (item.id == id) {
  286. item.status = status;
  287. }
  288. })
  289. },
  290. handleClickOutside(e) {
  291. // 如果点击发生在 popover 或 reference 内,不处理
  292. const popovers = document.querySelectorAll('.el-popover')
  293. const references = document.querySelectorAll('.click-text')
  294. for (let el of [...popovers, ...references]) {
  295. if (el.contains(e.target)) return
  296. }
  297. // 否则关闭所有 popover
  298. this.list.forEach(item => {
  299. item._popoverVisible = false
  300. })
  301. },
  302. openEdit(row) {
  303. // 先关闭所有 popover
  304. this.list.forEach(item => {
  305. item._popoverVisible = false;
  306. })
  307. row._popoverVisible = true;
  308. this.editValue = row.sku;
  309. this.editValueColor = row.colorCode;
  310. },
  311. cancelEdit(row) {
  312. row._popoverVisible = false
  313. },
  314. confirmEdit(row) {
  315. // 👉 这里可直接调用保存接口
  316. const params = {
  317. sessionId: `${Date.now() + 10}_${genCidHex16()}`,
  318. applicationId: 5,
  319. id: row.id,
  320. sku: this.editValue
  321. }
  322. if (row.status >= 5) {
  323. params['colorCode'] = this.editValueColor;
  324. }
  325. this.skuLoading = true;
  326. request({
  327. url: '/imageTask/update',
  328. method: 'post',
  329. data: params
  330. }).then(res => {
  331. if (res.code == 200) {
  332. row._popoverVisible = false;
  333. row.sku = this.editValue;
  334. if (row.status >= 5) {
  335. row.colorCode = this.editValueColor;
  336. }
  337. this.$message.success("操作成功");
  338. }
  339. }).finally(() => {
  340. this.skuLoading = false;
  341. })
  342. },
  343. getValueByKey,
  344. getList() {
  345. this.listLoading = true;
  346. const params = {}
  347. for (const key in this.listQuery) {
  348. const value = this.listQuery[key]
  349. if (value !== null && value !== '' && value !== undefined) {
  350. params[key] = value
  351. }
  352. }
  353. request({
  354. url: '/imageTask/page',
  355. method: 'get',
  356. params: params
  357. }).then(res => {
  358. if (res.code == 200) {
  359. this.total = res.data.total;
  360. this.list = res.data.rows;
  361. }
  362. }).finally(() => {
  363. this.listLoading = false
  364. })
  365. },
  366. retPage() {
  367. //分页
  368. this.getList();
  369. },
  370. handleFilter() {
  371. this.listQuery.page = 1;
  372. this.getList();
  373. },
  374. // 上传图库
  375. uploadGalleryList(command, row) {
  376. this.listLoading = true;
  377. const params = {
  378. taskId: row.id,
  379. uploadType: command || '2'
  380. }
  381. request({
  382. url: '/imageTask/uploadGallery',
  383. method: 'post',
  384. data: params
  385. }).then(res => {
  386. if (res.code == 200) {
  387. this.$message.success(res.msg || '操作成功!');
  388. }
  389. }).finally(() => {
  390. this.listLoading = false
  391. })
  392. },
  393. openGallery(row, type) {
  394. let images = [];
  395. if (type == 'default') {
  396. if (row.type == 1) {
  397. images = [...row.referenceImagesList, ...row.originalImagesList].slice(0, 10);
  398. } else {
  399. images = row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1).slice(0, 10).map(acc => {
  400. return acc.imageUrl
  401. });
  402. }
  403. } else {
  404. if (row.type == 1) {
  405. row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1).slice(0, 10).forEach(acc => {
  406. images.push(acc.imageUrl)
  407. })
  408. } else {
  409. row.aiGeneratedImageVOList.filter(acc => acc.imageType == 2).slice(0, 10).forEach(acc => {
  410. images.push(acc.imageUrl)
  411. })
  412. }
  413. }
  414. Fancybox.show(
  415. images.map((src) => ({
  416. src,
  417. type: "image",
  418. })),
  419. {
  420. startIndex: 0,
  421. }
  422. );
  423. },
  424. handleSelectionChange(val) {
  425. this.multipleSelection = val;
  426. },
  427. async runWithConcurrencyLimit(tasks, limit = 5) {
  428. const executing = []
  429. const results = []
  430. for (const task of tasks) {
  431. const p = Promise.resolve().then(task)
  432. results.push(p)
  433. executing.push(p)
  434. p.finally(() => {
  435. const index = executing.indexOf(p)
  436. if (index > -1) executing.splice(index, 1)
  437. })
  438. if (executing.length >= limit) {
  439. await Promise.race(executing)
  440. }
  441. }
  442. return Promise.allSettled(results)
  443. },
  444. async handleDownload(row) {
  445. const imagesList = row.aiGeneratedImageVOList
  446. if (!imagesList || !imagesList.length) return
  447. this.listLoading = true
  448. const zip = new JSZip()
  449. const folder = zip.folder('images')
  450. const tasks = imagesList.map((item, index) => async () => {
  451. try {
  452. const res = await fetch(item.imageUrl)
  453. const blob = await res.blob()
  454. // 更安全的文件名解析
  455. const urlObj = new URL(item.imageUrl)
  456. const fileName = urlObj.pathname.split('/').pop()
  457. folder.file(fileName || `image_${index + 1}.jpg`, blob)
  458. } catch (err) {
  459. console.error('下载失败:', item.imageUrl, err)
  460. }
  461. })
  462. // 并发限制(5 个)
  463. await this.runWithConcurrencyLimit(tasks, 5)
  464. const zipBlob = await zip.generateAsync({ type: 'blob' })
  465. saveAs(zipBlob, `${row.sku || 'images'}.zip`)
  466. this.listLoading = false
  467. },
  468. showCropSizeModal() {
  469. this.$refs.CropSizeModal.show();
  470. },
  471. showImagesGroupModal(row, type) {
  472. this.$refs.ImagesGroupModal.show(row, type);
  473. }
  474. }
  475. };
  476. </script>
  477. <style rel="stylesheet/scss" lang="scss" scoped>
  478. @import "@/styles/layout.scss";
  479. .table-container {
  480. .el-button+.el-button {
  481. margin-left: 7px;
  482. margin-bottom: 10px;
  483. }
  484. .el-button+span,
  485. .el-button+.el-dropdown {
  486. margin-left: 7px;
  487. }
  488. }
  489. .button {
  490. .el-button {
  491. margin-bottom: 10px;
  492. &:last-child {
  493. margin-bottom: 0;
  494. }
  495. }
  496. }
  497. .log-item {
  498. padding: 6px 0;
  499. border-bottom: 1px dashed #eee;
  500. }
  501. .log-item:last-child {
  502. border-bottom: none;
  503. }
  504. .log-user {
  505. font-weight: 500;
  506. color: #ae8877;
  507. }
  508. .log-action {
  509. font-size: 13px;
  510. margin: 2px 0;
  511. }
  512. .log-time {
  513. font-size: 12px;
  514. color: #999;
  515. }
  516. .empty-log {
  517. text-align: center;
  518. color: #aaa;
  519. padding: 20px 0;
  520. }
  521. .click-text {
  522. cursor: pointer;
  523. color: #409eff;
  524. }
  525. .click-text:hover {
  526. text-decoration: underline;
  527. }
  528. .popover-footer {
  529. margin-top: 10px;
  530. text-align: right;
  531. }
  532. </style>