GoodsSelectDialog.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. <template>
  2. <el-dialog
  3. :model-value="visible"
  4. title="请选择一个货号作为自定义商品的模板"
  5. width="60%"
  6. :close-on-click-modal="false"
  7. :close-on-press-escape="false"
  8. custom-class="goods-select-dialog"
  9. @update:model-value="handleDialogChange"
  10. >
  11. <template #title>
  12. <span>请选择一个货号作为自定义商品的模板</span>
  13. <div class="fs-12 c-666">
  14. *请注意,选择货号后,该模板会自动使用此货号的相关配置!<span v-if="selectedGoodsArtNo">选中货号 {{ selectedGoodsArtNo }} 包含 {{ getSelectedGoodsImageCount }} 张图片({{ getSelectedGoodsImageName.join(',') }})</span>
  15. </div>
  16. </template>
  17. <div class="goods-select-content" v-if="visible" ref="containerRef">
  18. <el-radio-group :model-value="selectedGoodsArtNo" class="goods-radio-group">
  19. <div
  20. v-for="item in goodsList"
  21. :key="item.goods_art_no"
  22. class="goods-item"
  23. :class="{ 'selected': selectedGoodsArtNo === item.goods_art_no }"
  24. @click="item.syncConfig && handleGoodsSelection(item.goods_art_no)"
  25. >
  26. <div class="goods-item-header">
  27. <div class="goods-item-left flex left">
  28. <el-radio
  29. :label="item.goods_art_no"
  30. class="goods-radio"
  31. :disabled="!item.syncConfig"
  32. ></el-radio>
  33. <span class="goods-art-no">{{ item.goods_art_no }}</span>
  34. <div class="goods-item-meta">
  35. <span class="action-time flex left">
  36. <img src="@/assets/images/processImage.vue/riq.png" />
  37. {{ getTime(item.action_time) }}
  38. </span>
  39. <span class="image-count mar-left-10 flex left">
  40. <img src="@/assets/images/processImage.vue/tup.png" />
  41. {{ item.items?.length || 0 }}张图片
  42. </span>
  43. <span v-if="!item.syncConfig" class="mar-left-10 c-FF4C00">无法读取到该商品拍摄配置,无法选择</span>
  44. </div>
  45. </div>
  46. </div>
  47. <div class="goods-item-images">
  48. <div
  49. v-for="(image, index) in item.items"
  50. :key="image.action_id || image.action_name"
  51. class="goods-item_image"
  52. >
  53. <span class="tag" v-if="!image.PhotoRecord?.image_path">{{
  54. image.action_name
  55. }}</span>
  56. <el-image
  57. v-if="image.PhotoRecord?.image_path"
  58. :src="thumbnailMap[image.PhotoRecord.image_path] ? thumbnailMap[image.PhotoRecord.image_path] : ''"
  59. :preview-src-list="getPreviewImageList(item)"
  60. :initial-index="getPreviewIndex(item, index)"
  61. class="preview-image"
  62. fit="contain"
  63. :preview-teleported="true"
  64. >
  65. <template #placeholder>
  66. <span class="tag">{{ image.action_name }}</span>
  67. </template>
  68. <template #error>
  69. <div class="image-slot">
  70. <span class="tag">{{ image.action_name }}</span>
  71. </div>
  72. </template>
  73. </el-image>
  74. <div class="image-placeholder">
  75. <span class="tag">{{ image.action_name }}</span>
  76. </div>
  77. </div>
  78. </div>
  79. </div>
  80. </el-radio-group>
  81. </div>
  82. <template #footer>
  83. <div class="dialog-footer">
  84. <div class="flex between" style="width: 100%">
  85. <div class="fs-12 c-666">
  86. </div>
  87. <div style="display:flex; align-items:center; ">
  88. <div class="pagination-container" style="padding: 0 10px;">
  89. <el-pagination
  90. background
  91. layout="prev, pager, next"
  92. :page-size="pageSize"
  93. :current-page="currentPage"
  94. :page-count="totalPages"
  95. @current-change="onCurrentChange"
  96. />
  97. </div>
  98. <el-button @click="handleCancel">取消</el-button>
  99. <el-button
  100. type="primary"
  101. :loading="goodsGenerateLoading"
  102. :disabled="goodsGenerateLoading"
  103. @click="handleConfirm"
  104. >
  105. {{ goodsGenerateLoading ? '正在生成自定义商品模版' : '确定' }}
  106. </el-button>
  107. </div>
  108. </div>
  109. </div>
  110. </template>
  111. </el-dialog>
  112. </template>
  113. <script setup lang="ts">
  114. import { computed, defineProps, defineEmits, ref, watch, nextTick, onBeforeUnmount } from 'vue'
  115. import tokenInfo from '@/stores/modules/token'
  116. import client from '@/stores/modules/client'
  117. import icpList from '@/utils/ipc'
  118. import usePhotography from '../mixin/usePhotography'
  119. import { ElMessage } from 'element-plus'
  120. interface Props {
  121. visible: boolean
  122. }
  123. interface Emits {
  124. (e: 'update:visible', value: boolean): void
  125. (e: 'confirm', selectedGoodsArtNo: string): void
  126. (e: 'cancel'): void
  127. }
  128. const props = defineProps<Props>()
  129. const emit = defineEmits<Emits>()
  130. const clientStore = client()
  131. // 使用 usePhotography 中的状态与方法,复用已有逻辑
  132. const {
  133. goodsList,
  134. getPhotoRecords,
  135. getTime,
  136. getFilePath,
  137. loading,
  138. currentPage,
  139. pageSize,
  140. totalPages,
  141. } = usePhotography()
  142. const selectedGoodsArtNo = ref<string>('')
  143. const goodsGenerateLoading = ref(false)
  144. // 使用复用的 thumbnails 组合函数
  145. import { useThumbnails } from '../composables/useThumbnails'
  146. const { thumbnailMap, observe, stop } = useThumbnails(getFilePath)
  147. const containerRef = ref<HTMLElement | null>(null)
  148. onBeforeUnmount(() => {
  149. stop()
  150. })
  151. // 当 goodsList 更新或 loading 结束时,设置 IntersectionObserver
  152. watch(goodsList, () => {
  153. if (props.visible) nextTick(() => observe(containerRef, goodsList))
  154. })
  155. watch(() => loading.value, (val) => {
  156. if (val === false && props.visible) nextTick(() => observe(containerRef, goodsList))
  157. })
  158. // getPhotoRecords 使用 usePhotography 中的实现(通过解构得到)
  159. // 根据选中的货号调用后端生成商品模板
  160. const generateGoodsTemplate = (goodsArtNo: string) => {
  161. return new Promise<any>((resolve, reject) => {
  162. try {
  163. console.log('调用 get_goods_image_json, goods_art_no:', goodsArtNo)
  164. // 按后端要求带上 token(参考其他生成类接口)
  165. const tokenStore = tokenInfo()
  166. const token = (tokenStore as any)?.getToken || ''
  167. // 先清理监听,避免重复回调
  168. clientStore.ipc.removeAllListeners(icpList.generate.getGoodsImageJson)
  169. clientStore.ipc.send(icpList.generate.getGoodsImageJson, {
  170. goods_art_no: goodsArtNo,
  171. token,
  172. })
  173. clientStore.ipc.on(icpList.generate.getGoodsImageJson, (_event, result) => {
  174. clientStore.ipc.removeAllListeners(icpList.generate.getGoodsImageJson)
  175. console.log('get_goods_image_json result:', result)
  176. if (result && result.code === 0) {
  177. resolve(result.data)
  178. } else {
  179. const msg = result?.msg || '商品模板生成失败'
  180. ElMessage.error(msg)
  181. reject(new Error(msg))
  182. }
  183. })
  184. } catch (error: any) {
  185. ElMessage.error(error?.message || '商品模板生成异常')
  186. reject(error)
  187. }
  188. })
  189. }
  190. // 监听弹框显示状态,当显示时获取数据
  191. watch(() => props.visible, (newVisible) => {
  192. if (newVisible) {
  193. // 重置状态
  194. selectedGoodsArtNo.value = ''
  195. goodsGenerateLoading.value = false
  196. getPhotoRecords()
  197. }
  198. })
  199. // 计算属性:获取选中货号的图片数量
  200. const getSelectedGoodsImageCount = computed(() => {
  201. if (!selectedGoodsArtNo.value) return 0
  202. const selectedItem = goodsList.value.find(
  203. (item: any) => item.goods_art_no === selectedGoodsArtNo.value
  204. )
  205. return selectedItem?.items?.length || 0
  206. })
  207. // 计算属性:获取选中货号的步骤
  208. const getSelectedGoodsImageName = computed(() => {
  209. if (!selectedGoodsArtNo.value) return []
  210. const selectedItem = goodsList.value.find(
  211. (item: any) => item.goods_art_no === selectedGoodsArtNo.value
  212. )
  213. return selectedItem?.items?.map((item: any) => item.action_name) || []
  214. })
  215. // 获取预览图片列表(只包含有图片路径的,保持原始顺序)
  216. const getPreviewImageList = (item: any) => {
  217. if (!item || !item.items) return []
  218. return item.items
  219. .filter((img: any) => img.PhotoRecord?.image_path)
  220. .map((img: any) => getFilePath(img.PhotoRecord.image_path))
  221. }
  222. // 获取当前图片在预览列表中的索引
  223. const getPreviewIndex = (item: any, currentIndex: number) => {
  224. if (!item || !item.items) return 0
  225. // 计算当前图片在过滤后的预览列表中的索引
  226. let previewIndex = 0
  227. for (let i = 0; i <= currentIndex; i++) {
  228. if (item.items[i]?.PhotoRecord?.image_path) {
  229. if (i === currentIndex) break
  230. previewIndex++
  231. }
  232. }
  233. return previewIndex
  234. }
  235. const handleDialogChange = (value: boolean) => {
  236. emit('update:visible', value)
  237. }
  238. // 当页码改变时,按照 processImage.vue 的风格发起分页请求
  239. const onCurrentChange = (page: number) => {
  240. // 切换分页时清除当前已选货号,避免跨页误选
  241. selectedGoodsArtNo.value = ''
  242. getPhotoRecords({ page })
  243. }
  244. const handleGoodsSelection = (goodsArtNo: string) => {
  245. const item = goodsList.value.find((g: any) => g.goods_art_no === goodsArtNo)
  246. if (!item) return
  247. if (!item.syncConfig) {
  248. ElMessage.warning('该货号配置不完整,无法选择')
  249. return
  250. }
  251. selectedGoodsArtNo.value = goodsArtNo
  252. }
  253. const handleConfirm = async () => {
  254. if (!selectedGoodsArtNo.value) {
  255. ElMessage.warning('请选择一个货号')
  256. return
  257. }
  258. console.log('选中的货号:', selectedGoodsArtNo.value)
  259. goodsGenerateLoading.value = true
  260. try {
  261. const data = await generateGoodsTemplate(selectedGoodsArtNo.value)
  262. console.log('商品模版数据', data)
  263. // 组装 goods_images 数组:[{ key: 模板顺序项, value: 对应图片地址 }, ...]
  264. const goodsImages: Array<{ key: string; value: string }> = []
  265. const orderArr = (data?.template_image_order || '')
  266. .split(',')
  267. .map((s: string) => s.trim())
  268. .filter((s: string) => s)
  269. const imagesArr = Array.isArray(data?.customer_template_images)
  270. ? data.customer_template_images
  271. : []
  272. const len = Math.min(orderArr.length, imagesArr.length)
  273. for (let i = 0; i < len; i++) {
  274. goodsImages.push({
  275. key: orderArr[i],
  276. value: imagesArr[i],
  277. })
  278. }
  279. // 商品文字字段直接由后端返回并保存在 sessionStorage 中(无需本地映射)
  280. // 使用sessionStorage传递数据,避免URL长度限制
  281. const templateData = {
  282. customer_template_images: goodsImages,
  283. template_image_order: data?.template_image_order || [],
  284. template_excel_headers: data?.template_excel_headers || [],
  285. timestamp: Date.now() // 添加时间戳用于标识数据
  286. }
  287. sessionStorage.setItem('addTpl_template_data', JSON.stringify(templateData))
  288. // 关闭弹框并通知父组件
  289. emit('update:visible', false)
  290. emit('confirm', selectedGoodsArtNo.value)
  291. } finally {
  292. goodsGenerateLoading.value = false
  293. }
  294. }
  295. const handleCancel = () => {
  296. emit('update:visible', false)
  297. emit('cancel')
  298. }
  299. </script>
  300. <style lang="scss">
  301. // 货号选择弹窗样式(非 scoped,因为弹窗挂载在 body 上)
  302. .goods-select-dialog {
  303. max-width: 60vw;
  304. .el-dialog__body {
  305. padding: 20px;
  306. max-height: 70vh;
  307. overflow-y: auto;
  308. height: 60vh;
  309. }
  310. .goods-select-content {
  311. width: 100%;
  312. .goods-radio-group {
  313. width: 100%;
  314. display: flex;
  315. flex-direction: column;
  316. gap: 20px;
  317. }
  318. .goods-item {
  319. background: #FFFFFF;
  320. box-shadow: 0px 2px 4px 0px rgba(23, 33, 71, 0.1);
  321. border-radius: 10px;
  322. border: 1px solid #D9DEE6;
  323. margin-bottom: 20px;
  324. cursor: pointer;
  325. transition: all 0.3s;
  326. width: 100%;
  327. .goods-radio {
  328. width: 30px;
  329. margin: 0;
  330. padding: 0;
  331. position: relative;
  332. margin-top: -10px;
  333. .el-radio__input {
  334. position: absolute;
  335. top: 14px;
  336. left: 18px;
  337. z-index: 10;
  338. transform: scale(1);
  339. }
  340. .el-radio__inner {
  341. border-width: 2px;
  342. }
  343. &.is-checked {
  344. .el-radio__inner {
  345. background-color: #409eff;
  346. border-color: #409eff;
  347. }
  348. }
  349. .el-radio__label {
  350. width: 100%;
  351. padding-left: 40px;
  352. display: none;
  353. }
  354. }
  355. .goods-item-header {
  356. display: flex;
  357. justify-content: flex-start;
  358. align-items: center;
  359. height: 40px;
  360. background: linear-gradient(90deg, #F4ECFF 0%, #DFEDFF 100%);
  361. border-radius: 10px 10px 0px 0px;
  362. .goods-item-left {
  363. display: flex;
  364. align-items: center;
  365. gap: 16px;
  366. .goods-art-no {
  367. font-size: 16px;
  368. font-weight: 500;
  369. color: #333;
  370. }
  371. .goods-item-meta {
  372. display: flex;
  373. justify-content: space-between;
  374. align-items: center;
  375. font-size: 12px;
  376. color: #666;
  377. img {
  378. height: 14px;
  379. margin-right: 2px;
  380. }
  381. .action-time {
  382. color: #666;
  383. }
  384. .image-count {
  385. color: #666;
  386. }
  387. }
  388. }
  389. }
  390. .goods-item-images {
  391. display: grid;
  392. grid-template-columns: repeat(5, 1fr);
  393. gap: 10px;
  394. padding: 15px;
  395. border-top: 1px solid #f0f0f0;
  396. overflow-x: auto;
  397. width: 100%;
  398. @media (min-width: 1200px) {
  399. grid-template-columns: repeat(5, 1fr);
  400. }
  401. @media (max-width: 768px) {
  402. grid-template-columns: repeat(3, 1fr);
  403. }
  404. }
  405. .goods-item_image {
  406. position: relative;
  407. width: 100%;
  408. aspect-ratio: 1;
  409. background: #F7F7F7;
  410. border-radius: 10px;
  411. overflow: hidden;
  412. cursor: pointer;
  413. border: 1px solid #D9DEE6;
  414. transition: all 0.3s;
  415. .tag {
  416. color: #bbb;
  417. position: absolute;
  418. left: 0;
  419. right: 0;
  420. top: 50%;
  421. margin-top: -10px;
  422. line-height: 20px;
  423. text-align: center;
  424. font-size: 12px;
  425. z-index: 1;
  426. pointer-events: none;
  427. }
  428. .preview-image {
  429. width: 100%;
  430. height: 100%;
  431. .el-image__inner {
  432. width: 100%;
  433. height: 100%;
  434. object-fit: cover;
  435. }
  436. }
  437. .image-placeholder {
  438. width: 100%;
  439. display: flex;
  440. align-items: center;
  441. justify-content: center;
  442. background: #F7F7F7;
  443. position: absolute;
  444. bottom: 0;
  445. height: 30px;
  446. line-height: 30px;
  447. }
  448. .image-slot {
  449. width: 100%;
  450. height: 100%;
  451. display: flex;
  452. align-items: center;
  453. justify-content: center;
  454. background: #F7F7F7;
  455. }
  456. &:hover {
  457. border-color: #409eff;
  458. transform: scale(1.02);
  459. box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
  460. }
  461. }
  462. }
  463. }
  464. .dialog-footer {
  465. display: flex;
  466. justify-content: flex-end;
  467. gap: 10px;
  468. }
  469. }
  470. /* empty block removed */
  471. </style>