index.vue 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501
  1. <template>
  2. <div class="p-2">
  3. <!-- 返回按钮 -->
  4. <div class="mb-4 flex items-center">
  5. <el-button link icon="ArrowLeft" @click="goBack">返回</el-button>
  6. </div>
  7. <!-- 申请入池表单头部 -->
  8. <el-card shadow="hover" class="mb-[10px]">
  9. <el-form :model="auditForm" label-width="100px">
  10. <!-- 第一行:产品池类型 + 产品册选择 + 申请类型 -->
  11. <el-row :gutter="20">
  12. <el-col :span="8">
  13. <el-form-item label="产品池类型" prop="type">
  14. <el-select v-model="auditForm.type" placeholder="请选择产品池类型" style="width: 100%" :disabled="typeDisabled">
  15. <el-option label="自营产品池" value="0" />
  16. <el-option label="标准产品池" value="1" />
  17. <el-option label="协议产品池" value="2" />
  18. <el-option label="项目产品池" value="3" />
  19. <el-option label="营销产品池" value="4" />
  20. </el-select>
  21. </el-form-item>
  22. </el-col>
  23. <el-col :span="8">
  24. <!-- type 4:营销产品池 -->
  25. <el-form-item v-if="auditForm.type === '4'" label="产品池" prop="poolId">
  26. <el-select v-model="auditForm.poolId" placeholder="请选择产品池" clearable filterable style="width: 100%">
  27. <el-option v-for="item in poolOptions" :key="item.id" :label="item.name" :value="item.id" />
  28. </el-select>
  29. </el-form-item>
  30. <!-- type 2:协议产品池 -->
  31. <el-form-item v-else-if="auditForm.type === '2'" label="协议" prop="protocolId">
  32. <el-select v-model="auditForm.protocolId" placeholder="请选择协议" clearable filterable style="width: 100%">
  33. <el-option v-for="item in protocolOptions" :key="item.id" :label="`${item.customerName}`" :value="item.id" />
  34. </el-select>
  35. </el-form-item>
  36. <!-- type 3:项目产品池 -->
  37. <el-form-item v-else-if="auditForm.type === '3'" label="项目" prop="itemId">
  38. <el-select v-model="auditForm.itemId" placeholder="请选择项目" clearable filterable style="width: 100%">
  39. <el-option v-for="item in itemOptions" :key="item.id" :label="item.itemName" :value="item.id" />
  40. </el-select>
  41. </el-form-item>
  42. </el-col>
  43. </el-row>
  44. <!-- 第二行:备注 -->
  45. <el-row>
  46. <el-col :span="16">
  47. <el-form-item label="备注" prop="remark">
  48. <el-input v-model="auditForm.remark" placeholder="请输入备注" style="width: 100%" />
  49. </el-form-item>
  50. </el-col>
  51. </el-row>
  52. <!-- 第三行:附件 -->
  53. <el-row>
  54. <el-col :span="8">
  55. <el-form-item v-if="auditForm.type !== '0'" label="申请类型" prop="applyType">
  56. <el-radio-group v-model="auditForm.applyType">
  57. <el-radio :value="0">入池</el-radio>
  58. <el-radio :value="1">出池</el-radio>
  59. </el-radio-group>
  60. </el-form-item>
  61. </el-col>
  62. <el-col :span="24">
  63. <el-form-item label="附件" prop="attachment">
  64. <file-upload v-model="auditForm.attachment" :file-size="20" :limit="5" />
  65. </el-form-item>
  66. </el-col>
  67. </el-row>
  68. </el-form>
  69. </el-card>
  70. <el-card shadow="never">
  71. <template #header>
  72. <div class="flex justify-between items-center">
  73. <span class="font-bold">入池清单</span>
  74. <div class="flex gap-2">
  75. <el-button icon="Upload" @click="handleImport">导入商品</el-button>
  76. <el-button type="primary" icon="Plus" @click="handleAddProduct">添加商品</el-button>
  77. <el-button type="danger" icon="Delete" @click="handleClearPool">清空产品池</el-button>
  78. </div>
  79. </div>
  80. </template>
  81. <el-table v-loading="loading" border :data="productList">
  82. <el-table-column label="商品编号" align="center" prop="productNo" width="120" fixed="left">
  83. <template #default="scope">
  84. <el-link type="primary" @click="handleView(scope.row)">{{ scope.row.productNo }}</el-link>
  85. </template>
  86. </el-table-column>
  87. <el-table-column label="商品图片" align="center" prop="productImage" width="100">
  88. <template #default="scope">
  89. <image-preview :src="scope.row.productImage" :width="60" :height="60" />
  90. </template>
  91. </el-table-column>
  92. <el-table-column label="商品信息" align="center" min-width="250">
  93. <template #default="scope">
  94. <div class="text-left">
  95. <div style="white-space: normal; word-break: break-all; line-height: 1.4">{{ scope.row.itemName }}</div>
  96. <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
  97. <div class="text-gray-500" style="font-size: 12px">
  98. 分类: {{ scope.row.topCategoryName + '-' + scope.row.mediumCategoryName + '-' + scope.row.bottomCategoryName }}
  99. </div>
  100. </div>
  101. </template>
  102. </el-table-column>
  103. <el-table-column label="单位" align="center" width="100">
  104. <template #default="scope">
  105. <div class="text-left">
  106. <div class="text-gray-500" style="font-size: 12px">单位: {{ scope.row.unitName || '-' }}</div>
  107. <div class="text-gray-500" style="font-size: 12px">起订量: {{ scope.row.minOrderQuantity || '-' }}</div>
  108. </div>
  109. </template>
  110. </el-table-column>
  111. <el-table-column label="价格信息" align="center" width="120">
  112. <template #default="scope">
  113. <div class="text-left" style="font-size: 12px">
  114. <div>
  115. <span class="text-gray-500">市场价:</span>
  116. <span class="text-red-500">¥{{ scope.row.marketPrice || '0.00' }}</span>
  117. </div>
  118. <div>
  119. <span class="text-gray-500">官网价:</span>
  120. <span class="text-red-500">¥{{ scope.row.memberPrice || '0.00' }}</span>
  121. </div>
  122. <div>
  123. <span class="text-gray-500">最低价:</span>
  124. <span class="text-red-500">¥{{ scope.row.minSellingPrice || '0.00' }}</span>
  125. </div>
  126. </div>
  127. </template>
  128. </el-table-column>
  129. <el-table-column v-if="auditForm.type === '2'" label="协议价" align="center" width="140">
  130. <template #default="scope">
  131. <el-input-number
  132. v-model="tempAgreementPrices[scope.row.id]"
  133. :min="0"
  134. :precision="2"
  135. :controls="false"
  136. size="small"
  137. style="width: 120px"
  138. placeholder="请输入协议价"
  139. @blur="handleAgreementPriceBlur(scope.row)"
  140. />
  141. </template>
  142. </el-table-column>
  143. <el-table-column label="采购信息" align="center" width="150">
  144. <template #default="scope">
  145. <div class="text-left" style="font-size: 12px">
  146. <div>
  147. <span class="text-gray-500">采购价:</span>
  148. <span>¥{{ scope.row.purchasingPrice || '0.00' }}</span>
  149. </div>
  150. <div>
  151. <span class="text-gray-500">暂估毛利率:</span>
  152. <span
  153. >{{
  154. scope.row.memberPrice
  155. ? (((scope.row.memberPrice - (scope.row.purchasingPrice || 0)) / scope.row.memberPrice) * 100).toFixed(2)
  156. : '0.00'
  157. }}%</span
  158. >
  159. </div>
  160. </div>
  161. </template>
  162. </el-table-column>
  163. <el-table-column v-if="auditForm.type === '3'" label="第三方售价" align="center" width="100">
  164. <template #default="scope">
  165. <span class="text-red-500">¥{{ tempProjectProductInfo[scope.row.id]?.negotiatedPrice?.toFixed(2) || '0.00' }}</span>
  166. </template>
  167. </el-table-column>
  168. <el-table-column v-if="auditForm.type === '3'" label="计价规则" align="center" width="90">
  169. <template #default="scope">
  170. {{
  171. tempProjectProductInfo[scope.row.id]?.pricingRule === '0'
  172. ? '一品一率'
  173. : tempProjectProductInfo[scope.row.id]?.pricingRule === '1'
  174. ? '折扣率'
  175. : '-'
  176. }}
  177. </template>
  178. </el-table-column>
  179. <el-table-column v-if="auditForm.type === '3'" label="第三方分类" align="center" width="160">
  180. <template #default="scope">
  181. {{ tempProjectProductInfo[scope.row.id]?.categoryName || '-' }}
  182. </template>
  183. </el-table-column>
  184. <el-table-column label="商品类型" align="center" prop="productReviewStatus" width="90">
  185. <template #default="scope">
  186. <span v-if="scope.row.productReviewStatus === 0">待采购审核</span>
  187. <span v-else-if="scope.row.productReviewStatus === 1">审核通过</span>
  188. <span v-else-if="scope.row.productReviewStatus === 2">驳回</span>
  189. <span v-else-if="scope.row.productReviewStatus === 3">待营销审核</span>
  190. <span v-else>-</span>
  191. </template>
  192. </el-table-column>
  193. <el-table-column label="上下架状态" align="center" prop="productStatus" width="100">
  194. <template #default="scope">
  195. <el-tag v-if="scope.row.productStatus === 1" type="success">已上架</el-tag>
  196. <el-tag v-else-if="scope.row.productStatus === 0" type="warning">下架</el-tag>
  197. <el-tag v-else-if="scope.row.productStatus === 2" type="info">上架中</el-tag>
  198. <el-tag v-else type="info">未知</el-tag>
  199. </template>
  200. </el-table-column>
  201. <el-table-column label="操作" align="center" width="120" fixed="right">
  202. <template #default="scope">
  203. <div class="flex flex-col gap-1">
  204. <el-link type="danger" :underline="false" @click="handleRemoveProduct(scope.row)">移除</el-link>
  205. </div>
  206. </template>
  207. </el-table-column>
  208. </el-table>
  209. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  210. </el-card>
  211. <!-- 底部操作按钮 -->
  212. <div class="mt-4 flex justify-end gap-2">
  213. <el-button @click="goBack">取消</el-button>
  214. <el-button type="primary" @click="handleSubmitAudit">确定</el-button>
  215. </div>
  216. <!-- 导入商品对话框 -->
  217. <el-dialog v-model="importDialog.open" title="导入商品" width="400px" append-to-body @close="handleImportDialogClose">
  218. <el-upload
  219. ref="uploadRef"
  220. :limit="1"
  221. accept=".xlsx, .xls"
  222. :auto-upload="false"
  223. :before-upload="() => false"
  224. :on-change="handleFileChange"
  225. :on-remove="() => (importDialog.selectedFile = null)"
  226. drag
  227. >
  228. <el-icon class="el-icon--upload"><i-ep-upload-filled /></el-icon>
  229. <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
  230. <template #tip>
  231. <div class="text-center el-upload__tip">
  232. <span>仅允许导入 xls、xlsx 格式文件。</span>
  233. <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="downloadTemplate">下载模板</el-link>
  234. </div>
  235. </template>
  236. </el-upload>
  237. <template #footer>
  238. <div class="dialog-footer">
  239. <el-button type="primary" @click="submitFileForm" :loading="importDialog.isUploading">确 定</el-button>
  240. <el-button @click="importDialog.open = false">取 消</el-button>
  241. </div>
  242. </template>
  243. </el-dialog>
  244. <!-- 添加商品对话框 -->
  245. <el-dialog title="添加商品" v-model="addProductDialog.visible" width="1400px" append-to-body top="5vh">
  246. <div class="add-product-dialog">
  247. <!-- 搜索区域 -->
  248. <el-form :model="addProductQuery" :inline="true" class="mb-4">
  249. <el-form-item label="商品名称:" label-width="80px">
  250. <el-input v-model="addProductQuery.itemName" placeholder="商品名称" clearable style="width: 200px" />
  251. </el-form-item>
  252. <el-form-item label="商品编号:" label-width="80px">
  253. <el-input v-model="addProductQuery.productNo" placeholder="商品编号" clearable style="width: 200px" />
  254. </el-form-item>
  255. <el-form-item>
  256. <el-button type="primary" icon="Search" @click="handleSearchProducts">搜索</el-button>
  257. </el-form-item>
  258. <el-form-item v-if="auditForm.type !== '2' && auditForm.type !== '3'">
  259. <el-button type="primary" icon="Plus" @click="handleBatchAdd">加入清单</el-button>
  260. </el-form-item>
  261. </el-form>
  262. <!-- 商品列表 -->
  263. <el-table
  264. ref="addProductTableRef"
  265. v-loading="addProductDialog.loading"
  266. :data="addProductDialog.productList"
  267. border
  268. @selection-change="handleSelectionChange"
  269. max-height="500"
  270. >
  271. <el-table-column type="selection" width="55" align="center" />
  272. <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
  273. <el-table-column label="商品图片" align="center" prop="productImage" width="100">
  274. <template #default="scope">
  275. <image-preview :src="scope.row.productImage" :width="60" :height="60" />
  276. </template>
  277. </el-table-column>
  278. <el-table-column label="商品信息" align="center" min-width="250">
  279. <template #default="scope">
  280. <div class="text-left">
  281. <div style="white-space: normal; word-break: break-all; line-height: 1.4">{{ scope.row.itemName }}</div>
  282. <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
  283. <div class="text-gray-500" style="font-size: 12px">
  284. 分类: {{ scope.row.topCategoryName + '-' + scope.row.mediumCategoryName + '-' + scope.row.bottomCategoryName }}
  285. </div>
  286. </div>
  287. </template>
  288. </el-table-column>
  289. <el-table-column label="单位" align="center" width="100">
  290. <template #default="scope">
  291. <div class="text-left">
  292. <div class="text-gray-500" style="font-size: 12px">单位: {{ scope.row.unitName || '-' }}</div>
  293. <div class="text-gray-500" style="font-size: 12px">起订量: {{ scope.row.minOrderQuantity || '-' }}</div>
  294. </div>
  295. </template>
  296. </el-table-column>
  297. <el-table-column label="价格信息" align="center" width="120">
  298. <template #default="scope">
  299. <div class="text-left" style="font-size: 12px">
  300. <div>
  301. <span class="text-gray-500">市场价:</span>
  302. <span class="text-red-500">¥{{ scope.row.marketPrice || '0.00' }}</span>
  303. </div>
  304. <div>
  305. <span class="text-gray-500">官网价:</span>
  306. <span class="text-red-500">¥{{ scope.row.memberPrice || '0.00' }}</span>
  307. </div>
  308. <div>
  309. <span class="text-gray-500">最低价:</span>
  310. <span class="text-red-500">¥{{ scope.row.minSellingPrice || '0.00' }}</span>
  311. </div>
  312. </div>
  313. </template>
  314. </el-table-column>
  315. <el-table-column label="采购信息" align="center" width="150">
  316. <template #default="scope">
  317. <div class="text-left" style="font-size: 12px">
  318. <div>
  319. <span class="text-gray-500">采购价:</span>
  320. <span>¥{{ scope.row.purchasingPrice || '0.00' }}</span>
  321. </div>
  322. <div>
  323. <span class="text-gray-500">暂估毛利率:</span>
  324. <span
  325. >{{
  326. scope.row.memberPrice
  327. ? (((scope.row.memberPrice - (scope.row.purchasingPrice || 0)) / scope.row.memberPrice) * 100).toFixed(2)
  328. : '0.00'
  329. }}%</span
  330. >
  331. </div>
  332. </div>
  333. </template>
  334. </el-table-column>
  335. <el-table-column label="库存情况" align="center" width="140">
  336. <template #default="scope">
  337. <div class="text-left" style="font-size: 12px">
  338. <div>
  339. <span class="text-gray-500">总库存:</span>
  340. <span>{{ scope.row.totalInventory ?? '-' }}</span>
  341. </div>
  342. <div>
  343. <span class="text-gray-500">现有库存:</span>
  344. <span>{{ scope.row.nowInventory ?? '-' }}</span>
  345. </div>
  346. <div>
  347. <span class="text-gray-500">虚拟库存:</span>
  348. <span>{{ scope.row.virtualInventory ?? '-' }}</span>
  349. </div>
  350. </div>
  351. </template>
  352. </el-table-column>
  353. <el-table-column label="是否自营" align="center" width="80">
  354. <template #default="scope">
  355. <span v-if="scope.row.isSelf === 1">是</span>
  356. <span v-else-if="scope.row.isSelf === 0">否</span>
  357. <span v-else>-</span>
  358. </template>
  359. </el-table-column>
  360. <el-table-column label="上下架状态" align="center" prop="productStatus" width="100">
  361. <template #default="scope">
  362. <el-tag v-if="scope.row.productStatus === 1" type="success">已上架</el-tag>
  363. <el-tag v-else-if="scope.row.productStatus === 0" type="warning">下架</el-tag>
  364. <el-tag v-else-if="scope.row.productStatus === 2" type="info">上架中</el-tag>
  365. <el-tag v-else type="info">未知</el-tag>
  366. </template>
  367. </el-table-column>
  368. <el-table-column label="供应情况" align="center" width="150">
  369. <template #default="scope">
  370. <div class="text-left" style="font-size: 12px">
  371. <div>供应商数量:{{ scope.row.supplierCount || 0 }}</div>
  372. </div>
  373. </template>
  374. </el-table-column>
  375. <el-table-column label="操作" align="center" width="120" fixed="right">
  376. <template #default="scope">
  377. <el-tag v-if="isProductSelected(scope.row.id)" type="success" size="small">已选({{ getPoolTypeLabel() }})</el-tag>
  378. <el-link v-else type="primary" :underline="false" @click="handleAddSingleProduct(scope.row)">加入清单</el-link>
  379. </template>
  380. </el-table-column>
  381. </el-table>
  382. <!-- 游标分页控制 -->
  383. <pagination
  384. v-show="addProductDialog.productList.length > 0"
  385. v-model:page="addProductQuery.pageNum"
  386. v-model:limit="addProductQuery.pageSize"
  387. v-model:way="addProductQuery.way"
  388. :cursor-mode="true"
  389. :has-more="addProductHasMore"
  390. @pagination="getProductList"
  391. />
  392. </div>
  393. </el-dialog>
  394. <!-- 选择第三方产品分类弹框(项目产品池) -->
  395. <el-dialog title="选择第三方产品分类" v-model="thirdPartyDialog.visible" width="520px" append-to-body :destroy-on-close="true">
  396. <el-form label-width="130px">
  397. <el-form-item label="价格模式:">
  398. <el-radio-group v-model="thirdPartyDialog.pricingRule">
  399. <el-radio label="0">一品一率</el-radio>
  400. <el-radio label="1">折扣率</el-radio>
  401. </el-radio-group>
  402. </el-form-item>
  403. <el-form-item label="* 第三方产品分类:">
  404. <el-cascader
  405. ref="categoryRef"
  406. v-model="thirdPartyDialog.categoryValue"
  407. :props="categoryLazyProps"
  408. style="width: 100%"
  409. placeholder="请选择"
  410. clearable
  411. @change="handleCategoryChange"
  412. />
  413. </el-form-item>
  414. <el-form-item v-if="thirdPartyDialog.pricingRule === '1'" label="折扣率:">
  415. <span>{{ thirdPartyDialog.discountRate !== undefined ? thirdPartyDialog.discountRate : '-' }}</span>
  416. </el-form-item>
  417. <el-form-item label="起订量:">
  418. <el-input-number v-model="thirdPartyDialog.minOrderQuantity" :min="1" controls-position="right" style="width: 100%" />
  419. </el-form-item>
  420. <el-form-item label="* 第三方平台售价:">
  421. <el-input-number
  422. v-model="thirdPartyDialog.negotiatedPrice"
  423. :disabled="thirdPartyDialog.pricingRule === '1'"
  424. :precision="2"
  425. :min="0"
  426. controls-position="right"
  427. style="width: 100%"
  428. @blur="handleThirdPartyPriceBlur"
  429. />
  430. </el-form-item>
  431. <el-form-item label="市场价:">
  432. <span>{{ thirdPartyDialog.row?.marketPrice || 0 }}</span>
  433. </el-form-item>
  434. <el-form-item label="官网价:">
  435. <span>{{ thirdPartyDialog.row?.memberPrice || 0 }}</span>
  436. </el-form-item>
  437. <el-form-item label="最低售价:">
  438. <span class="text-red-500">¥{{ thirdPartyDialog.row?.minSellingPrice || 0 }}</span>
  439. </el-form-item>
  440. </el-form>
  441. <div class="mx-1 mb-3 px-3 py-2 rounded flex items-center gap-1 text-sm" style="background: #fff9f0; border: 1px solid #ffd591; color: #d48806">
  442. <el-icon><i-ep-warning /></el-icon>
  443. <span>(第三方平台售价不能低于最低售价,不高于官网价)</span>
  444. </div>
  445. <template #footer>
  446. <el-button @click="thirdPartyDialog.visible = false">返回</el-button>
  447. <el-button type="primary" @click="handleConfirmThirdParty">确认</el-button>
  448. </template>
  449. </el-dialog>
  450. </div>
  451. </template>
  452. <script setup name="PoolLinkAudit" lang="ts">
  453. import { useRouter, useRoute } from 'vue-router';
  454. import { categoryTree, listBase } from '@/api/product/base';
  455. import { getPoolAuditProducts } from '@/api/product/baseAudit';
  456. import { BaseVO, BaseQuery } from '@/api/product/base/types';
  457. import { getPoolAudit, updatePoolAudit, addPoolAudit } from '@/api/product/poolAudit';
  458. import { listInfo } from '@/api/customer/supplierInfo';
  459. import { InfoVO } from '@/api/customer/supplierInfo/types';
  460. import { listBrand } from '@/api/product/brand';
  461. import { BrandVO } from '@/api/product/brand/types';
  462. import { PoolAuditVO, PoolAuditForm } from '@/api/product/poolAudit/types';
  463. import { getItem, listItem } from '@/api/external/item';
  464. import { ItemVO } from '@/api/external/item/types';
  465. import { getInfo as getProtocolInfo, listInfo as listProtocolInfo } from '@/api/product/protocolInfo';
  466. import { InfoVO as ProtocolInfoVO } from '@/api/product/protocolInfo/types';
  467. import { getPool, listPool } from '@/api/product/pool';
  468. import { PoolVO } from '@/api/product/pool/types';
  469. import * as XLSX from 'xlsx';
  470. import { listProductCategory } from '@/api/external/productCategory';
  471. import { ProductCategoryVO } from '@/api/external/productCategory/types';
  472. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  473. const router = useRouter();
  474. const route = useRoute();
  475. const productList = ref<BaseVO[]>([]);
  476. const loading = ref(false);
  477. const total = ref(0);
  478. const auditInfoLoading = ref(false);
  479. const auditInfo = ref<(PoolAuditVO & { createTime?: string; auditTime?: string }) | undefined>();
  480. const itemInfo = ref<ItemVO | undefined>();
  481. const protocolInfo = ref<ProtocolInfoVO | undefined>();
  482. const poolInfo = ref<PoolVO | undefined>();
  483. // 申请入池表单数据
  484. const auditForm = reactive<PoolAuditForm>({
  485. id: undefined,
  486. type: undefined,
  487. name: undefined,
  488. poolId: undefined,
  489. protocolId: undefined,
  490. itemId: undefined,
  491. remark: undefined,
  492. attachment: undefined,
  493. productIds: [],
  494. applyType: 0
  495. });
  496. /** 下拉选项数据 */
  497. const poolOptions = ref<PoolVO[]>([]);
  498. const protocolOptions = ref<ProtocolInfoVO[]>([]);
  499. const itemOptions = ref<ItemVO[]>([]);
  500. /** 根据产品池类型加载对应下拉选项 */
  501. const loadSelectOptions = async (type?: string | number) => {
  502. const t = String(type ?? auditForm.type ?? '');
  503. if (t === '4') {
  504. const res = await listPool({ type: Number(t) } as any);
  505. poolOptions.value = res.rows ?? [];
  506. } else if (t === '2') {
  507. const res = await listProtocolInfo();
  508. protocolOptions.value = res.rows ?? [];
  509. } else if (t === '3') {
  510. const res = await listItem();
  511. itemOptions.value = res.rows ?? [];
  512. }
  513. };
  514. // 回显时屏蔽 watch 副作用
  515. const isInitializing = ref(false);
  516. /** 切换产品池类型时,清空关联选择并重新加载选项,同时清空商品列表 */
  517. watch(
  518. () => auditForm.type,
  519. (newType) => {
  520. if (isInitializing.value) return;
  521. auditForm.poolId = undefined;
  522. auditForm.protocolId = undefined;
  523. auditForm.itemId = undefined;
  524. auditForm.name = undefined;
  525. // 切换类型时清空商品列表
  526. tempProductIds.value = '';
  527. Object.keys(tempAgreementPrices).forEach((key) => delete tempAgreementPrices[key]);
  528. productList.value = [];
  529. total.value = 0;
  530. if (newType !== undefined && newType !== null) {
  531. loadSelectOptions(newType);
  532. }
  533. }
  534. );
  535. /** 切换具体产品池/协议/项目时,清空商品列表 */
  536. watch(
  537. () => [auditForm.poolId, auditForm.protocolId, auditForm.itemId] as const,
  538. (newVal, oldVal) => {
  539. if (isInitializing.value) return;
  540. const changed = newVal.some((v, i) => v !== oldVal[i]);
  541. if (changed) {
  542. tempProductIds.value = '';
  543. Object.keys(tempAgreementPrices).forEach((key) => delete tempAgreementPrices[key]);
  544. productList.value = [];
  545. total.value = 0;
  546. }
  547. }
  548. );
  549. const queryParams = ref({
  550. pageNum: 1,
  551. pageSize: 10,
  552. poolAuditId: (route.params.id || route.query.id) as string | number,
  553. productNo: undefined,
  554. itemName: undefined,
  555. brandId: undefined,
  556. productStatus: undefined,
  557. bottomCategoryId: undefined,
  558. supplier: undefined,
  559. dateRange: undefined
  560. });
  561. /** 路由传入的产品池类型(存在时禁用类型下拉) */
  562. const routeType = computed(() => route.query.type as string | undefined);
  563. const typeDisabled = computed(() => !!routeType.value && routeType.value !== 'undefined' && routeType.value !== '-1');
  564. const categoryOptions = ref<any[]>([]);
  565. const categoryMap = ref<Map<string | number, any>>(new Map());
  566. const supplierOptions = ref<InfoVO[]>([]);
  567. const brandOptions = ref<BrandVO[]>([]);
  568. const brandLoading = ref(false);
  569. let brandSearchTimer: ReturnType<typeof setTimeout> | null = null;
  570. // 导入商品对话框
  571. const importDialog = reactive({
  572. open: false,
  573. isUploading: false,
  574. selectedFile: null as File | null
  575. });
  576. const uploadRef = ref<any>();
  577. // 添加商品对话框
  578. const addProductDialog = reactive({
  579. visible: false,
  580. loading: false,
  581. productList: [] as BaseVO[],
  582. total: 0
  583. });
  584. // 游标分页相关变量
  585. const addProductHasMore = ref(true);
  586. const addProductPageHistory = ref<Array<{ firstId: string | number; lastId: string | number }>>([]);
  587. // 添加商品查询参数
  588. const addProductQuery = ref<BaseQuery>({
  589. pageNum: 1,
  590. pageSize: 10,
  591. way: undefined,
  592. productNo: undefined,
  593. itemName: undefined,
  594. lastSeenId: undefined
  595. });
  596. // 选中的商品
  597. const selectedProducts = ref<BaseVO[]>([]);
  598. const addProductTableRef = ref<any>();
  599. // 协议价输入映射(type=2时使用,项为对话框内商品id)
  600. const priceInputMap = reactive<Record<string | number, number | undefined>>({});
  601. // 临时展示用:本地暂存的入池商品 ID 列表(逗号分隔的字符串)
  602. const tempProductIds = ref<string>('');
  603. // 临时展示用:本地暂存的协议价(type=2)
  604. const tempAgreementPrices = reactive<Record<string | number, number | undefined>>({});
  605. // 第三方产品分类弹框状态(type=3 项目产品池时使用)
  606. const thirdPartyDialog = reactive({
  607. visible: false,
  608. row: null as BaseVO | null,
  609. pricingRule: '0' as '0' | '1',
  610. categoryValue: [] as (string | number)[],
  611. discountRate: undefined as number | undefined,
  612. negotiatedPrice: 0 as number,
  613. minOrderQuantity: 1 as number
  614. });
  615. // 分类折扣率缓存(categoryId -> discountRate)
  616. const categoryDiscountMap = ref<Record<string | number, number | undefined>>({});
  617. // 项目产品池商品额外信息(type=3 时存储第三方售价、计价规则、分类)
  618. const tempProjectProductInfo = reactive<
  619. Record<
  620. string | number,
  621. {
  622. negotiatedPrice: number;
  623. pricingRule: '0';
  624. categoryId: string | number | null;
  625. categoryName: string;
  626. minOrderQuantity: number;
  627. }
  628. >
  629. >({});
  630. // 第三方分类级联选择器 ref
  631. const categoryRef = ref<any>();
  632. // 分类懒加载配置(最多3级,itemId 来自 auditForm.itemId)
  633. const categoryLazyProps = {
  634. lazy: true,
  635. lazyLoad: (node: any, resolve: (data: any[]) => void) => {
  636. const { level, value } = node;
  637. const parentId = level === 0 ? 0 : value;
  638. listProductCategory({ itemId: auditForm.itemId as any, parentId } as any)
  639. .then((res) => {
  640. const data = (res.rows || []) as ProductCategoryVO[];
  641. data.forEach((item) => {
  642. categoryDiscountMap.value[item.id] = item.discountRate;
  643. });
  644. resolve(
  645. data.map((item) => ({
  646. value: item.id,
  647. label: item.categoryName,
  648. leaf: (item.classLevel !== undefined && item.classLevel >= 3) || !item.hasChildren
  649. }))
  650. );
  651. })
  652. .catch(() => resolve([]));
  653. }
  654. };
  655. /** 获取审核状态标签文字 */
  656. const getStatusLabel = (status?: string): string => {
  657. const map: Record<string, string> = { '0': '待提交', '1': '待审核', '2': '审核通过', '3': '审核驳回' };
  658. return status !== undefined ? map[status] || status : '-';
  659. };
  660. /** 获取审核状态标签类型 */
  661. const getStatusTagType = (status?: string): 'success' | 'warning' | 'info' | 'danger' | 'primary' => {
  662. const map: Record<string, 'success' | 'warning' | 'info' | 'danger' | 'primary'> = {
  663. '0': 'info',
  664. '1': 'warning',
  665. '2': 'success',
  666. '3': 'danger'
  667. };
  668. return status !== undefined ? map[status] || 'primary' : 'primary';
  669. };
  670. /** 加载审核池信息(编辑回显) */
  671. const loadAuditInfo = async () => {
  672. const id = route.params.id || route.query.id || route.query.poolAuditId;
  673. if (!id) return;
  674. auditInfoLoading.value = true;
  675. try {
  676. // 调用 getPoolAudit 获取审核信息
  677. const res = await getPoolAudit(id as string | number);
  678. const data = res.data as PoolAuditVO;
  679. auditInfo.value = data as any;
  680. // 回显表单,先设标志位防止 watch 清空字段
  681. isInitializing.value = true;
  682. auditForm.id = data.id;
  683. auditForm.type = data.type !== undefined && data.type !== null ? String(data.type) : undefined;
  684. auditForm.name = data.name;
  685. auditForm.poolId = data.poolId;
  686. auditForm.protocolId = data.protocolId;
  687. auditForm.itemId = data.itemId;
  688. auditForm.remark = data.remark;
  689. auditForm.attachment = data.attachment;
  690. auditForm.applyType = data.applyType;
  691. // 加载对应下拉选项
  692. if (auditForm.type) {
  693. await loadSelectOptions(auditForm.type);
  694. }
  695. // 修复类型不匹配导致 el-select 无法回显的问题(接口返回的 id 类型可能与选项列表不一致)
  696. if (auditForm.type === '2' && auditForm.protocolId) {
  697. const match = protocolOptions.value.find((item) => String(item.id) === String(auditForm.protocolId));
  698. if (match) auditForm.protocolId = match.id;
  699. } else if (auditForm.type === '3' && auditForm.itemId) {
  700. const match = itemOptions.value.find((item) => String(item.id) === String(auditForm.itemId));
  701. if (match) auditForm.itemId = match.id;
  702. } else if (auditForm.type === '4' && auditForm.poolId) {
  703. const match = poolOptions.value.find((item) => String(item.id) === String(auditForm.poolId));
  704. if (match) auditForm.poolId = match.id;
  705. }
  706. // 所有回显赋值完成后再关闭标志位,避免 watch 异步回调清空字段
  707. isInitializing.value = false;
  708. // 从 products 初始化商品 ID 列表和协议价
  709. const products: Array<{ productId: string | number; negotiatedPrice?: number; agreementPrice?: number }> = (data as any).products || [];
  710. tempProductIds.value = products.map((p: any) => p.productId).join(',');
  711. products.forEach((p: any) => {
  712. const price = p.negotiatedPrice ?? p.agreementPrice;
  713. if (price !== undefined && price !== null) {
  714. tempAgreementPrices[p.productId] = price;
  715. }
  716. });
  717. // 刷新商品列表
  718. await getList();
  719. // 加载关联详情信息
  720. const type = auditForm.type;
  721. if (type === '3' && data.itemId) {
  722. const itemRes = await getItem(data.itemId);
  723. itemInfo.value = itemRes.data;
  724. } else if (type === '2' && data.protocolId) {
  725. const protocolRes = await getProtocolInfo(data.protocolId);
  726. protocolInfo.value = protocolRes.data;
  727. } else if ((type === '0' || type === '4') && data.poolId) {
  728. const poolRes = await getPool(data.poolId);
  729. poolInfo.value = poolRes.data;
  730. }
  731. } catch (error) {
  732. console.error('加载信息失败:', error);
  733. } finally {
  734. auditInfoLoading.value = false;
  735. }
  736. };
  737. /** 获取分类树 */
  738. const getCategoryTree = async () => {
  739. try {
  740. const res = await categoryTree();
  741. categoryOptions.value = res.data || [];
  742. buildCategoryMap(categoryOptions.value);
  743. } catch (error) {
  744. console.error('获取分类树失败:', error);
  745. }
  746. };
  747. /** 获取供应商列表 */
  748. const getSupplierList = async () => {
  749. try {
  750. const res = await listInfo();
  751. supplierOptions.value = res.data || res.rows || [];
  752. } catch (error) {
  753. console.error('获取供应商列表失败:', error);
  754. }
  755. };
  756. /** 获取供应商名称 */
  757. const getSupplierName = (supplierId: string | number | undefined): string => {
  758. if (!supplierId) return '-';
  759. const supplier = supplierOptions.value.find((item) => item.id === supplierId);
  760. return supplier?.enterpriseName || supplier?.shortName || '-';
  761. };
  762. /** 加载品牌选项(默认100条) */
  763. const loadBrandOptions = async (keyword?: string) => {
  764. brandLoading.value = true;
  765. try {
  766. const res = await listBrand({ pageNum: 1, pageSize: 100, brandName: keyword });
  767. brandOptions.value = res.rows || [];
  768. } catch (error) {
  769. console.error('加载品牌列表失败:', error);
  770. } finally {
  771. brandLoading.value = false;
  772. }
  773. };
  774. /** 品牌远程搜索(防抖) */
  775. const handleBrandSearch = (query: string) => {
  776. if (brandSearchTimer) clearTimeout(brandSearchTimer);
  777. brandSearchTimer = setTimeout(() => {
  778. loadBrandOptions(query || undefined);
  779. }, 300);
  780. };
  781. /** 构建分类映射 */
  782. const buildCategoryMap = (categories: any[], parentPath = '') => {
  783. categories.forEach((category) => {
  784. const fullPath = parentPath ? `${parentPath} > ${category.label}` : category.label;
  785. categoryMap.value.set(category.id, { ...category, fullPath });
  786. if (category.children && category.children.length > 0) {
  787. buildCategoryMap(category.children, fullPath);
  788. }
  789. });
  790. };
  791. /** 获取分类完整路径 */
  792. const getCategoryFullPath = (categoryId: string | number): string => {
  793. const category = categoryMap.value.get(categoryId);
  794. return category?.fullPath || '';
  795. };
  796. /** 查询商品列表(将本地暂存的商品 ID 传给后端查询) */
  797. const getList = async () => {
  798. // 没有临时商品时直接显示空列表
  799. if (!tempProductIds.value) {
  800. productList.value = [];
  801. total.value = 0;
  802. return;
  803. }
  804. loading.value = true;
  805. try {
  806. const params: any = {
  807. pageNum: queryParams.value.pageNum,
  808. pageSize: queryParams.value.pageSize,
  809. ids: tempProductIds.value
  810. };
  811. const res = await listBase(params);
  812. productList.value = (res.rows || res.data || []) as any;
  813. total.value = res.total || tempProductIds.value.split(',').filter((id) => id).length;
  814. } catch (error) {
  815. console.error('获取商品列表失败:', error);
  816. productList.value = [];
  817. total.value = 0;
  818. } finally {
  819. loading.value = false;
  820. }
  821. };
  822. /** 返回 */
  823. const goBack = () => {
  824. router.back();
  825. };
  826. /** 搜索 */
  827. const handleQuery = () => {
  828. queryParams.value.pageNum = 1;
  829. getList();
  830. };
  831. /** 申请入池(待提交 -> 待审核) */
  832. const handleApplyPool = async () => {
  833. await proxy?.$modal.confirm('确认要申请入池吗?提交后状态将变为待审核。');
  834. const id = auditInfo.value?.id;
  835. if (!id) return;
  836. await updatePoolAudit({ id, productReviewStatus: '0' });
  837. proxy?.$modal.msgSuccess('申请成功,状态已变为待审核');
  838. await loadAuditInfo();
  839. };
  840. /** 确定按钮 - 提交产品池审核单据 */
  841. const handleSubmitAudit = async () => {
  842. if (!auditForm.type) {
  843. proxy?.$modal.msgWarning('请选择产品池类型');
  844. return;
  845. }
  846. if (auditForm.type === '4' && !auditForm.poolId) {
  847. proxy?.$modal.msgWarning('请选择产品池');
  848. return;
  849. }
  850. if (auditForm.type === '2' && !auditForm.protocolId) {
  851. proxy?.$modal.msgWarning('请选择协议');
  852. return;
  853. }
  854. if (auditForm.type === '3' && !auditForm.itemId) {
  855. proxy?.$modal.msgWarning('请选择项目');
  856. return;
  857. }
  858. if (!tempProductIds.value) {
  859. proxy?.$modal.msgWarning('请先添加商品到入池清单');
  860. return;
  861. }
  862. // type=2 协议产品池:提交前校验所有商品的协议价
  863. if (auditForm.type === '2') {
  864. for (const product of productList.value) {
  865. const price = tempAgreementPrices[product.id];
  866. if (!price || price <= 0) {
  867. proxy?.$modal.msgWarning(`商品「${product.itemName || product.productNo}」的协议价不能为0,请检查后重试`);
  868. return;
  869. }
  870. const minPrice = Number(product.minSellingPrice) || 0;
  871. const maxPrice = Number(product.memberPrice) || 0;
  872. if (minPrice > 0 && price < minPrice) {
  873. proxy?.$modal.msgWarning(`商品「${product.itemName || product.productNo}」的协议价不能低于最低售价(¥${minPrice})`);
  874. return;
  875. }
  876. if (maxPrice > 0 && price > maxPrice) {
  877. proxy?.$modal.msgWarning(`商品「${product.itemName || product.productNo}」的协议价不能高于官网价(¥${maxPrice})`);
  878. return;
  879. }
  880. }
  881. }
  882. await proxy?.$modal.confirm('确认要提交产品池审核单据吗?');
  883. try {
  884. // 提交整个审核单据(包含本地暂存的商品 ID 及协议价)
  885. const submitData: PoolAuditForm = {
  886. ...auditForm,
  887. products: tempProductIds.value
  888. .split(',')
  889. .filter((id) => id)
  890. .map((id) => ({
  891. productId: id,
  892. negotiatedPrice: tempAgreementPrices[id],
  893. ...(auditForm.type === '3' && tempProjectProductInfo[id]
  894. ? {
  895. negotiatedPrice: tempProjectProductInfo[id].negotiatedPrice,
  896. pricingRule: tempProjectProductInfo[id].pricingRule,
  897. categoryId: tempProjectProductInfo[id].categoryId,
  898. categoryName: tempProjectProductInfo[id].categoryName
  899. }
  900. : {})
  901. })),
  902. productReviewStatus: '0' // 提交后状态为待申请
  903. };
  904. if (!auditForm.id) {
  905. await addPoolAudit(submitData);
  906. } else {
  907. await updatePoolAudit(submitData);
  908. }
  909. proxy?.$modal.msgSuccess('提交成功');
  910. goBack();
  911. } catch (error) {
  912. console.error('提交失败:', error);
  913. }
  914. };
  915. /** 清空入池清单 */
  916. const handleClearPool = async () => {
  917. await proxy?.$modal.confirm('确认要清空入池清单中的所有商品吗?');
  918. tempProductIds.value = '';
  919. Object.keys(tempAgreementPrices).forEach((key) => delete tempAgreementPrices[key]);
  920. productList.value = [];
  921. total.value = 0;
  922. proxy?.$modal.msgSuccess('清空成功');
  923. };
  924. /** 导入商品按钮 */
  925. const handleImport = () => {
  926. if (!checkPoolSelected()) return;
  927. importDialog.open = true;
  928. importDialog.isUploading = false;
  929. importDialog.selectedFile = null;
  930. nextTick(() => {
  931. uploadRef.value?.clearFiles();
  932. });
  933. };
  934. /** 关闭导入对话框时清理 */
  935. const handleImportDialogClose = () => {
  936. importDialog.selectedFile = null;
  937. importDialog.isUploading = false;
  938. uploadRef.value?.clearFiles();
  939. };
  940. /** 下载导入模板 */
  941. const downloadTemplate = () => {
  942. const url = new URL('./商品导入模版.xlsx', import.meta.url).href;
  943. const a = document.createElement('a');
  944. a.href = url;
  945. a.download = '商品导入模版.xlsx';
  946. document.body.appendChild(a);
  947. a.click();
  948. document.body.removeChild(a);
  949. };
  950. /** 选择文件时暂存 */
  951. const handleFileChange = (file: any) => {
  952. importDialog.selectedFile = file.raw as File;
  953. };
  954. /** 解析 Excel 文件,读取商品编号和协议价格两列 */
  955. const parseExcelData = (file: File): Promise<Array<{ productNo: string; negotiatedPrice?: number }>> => {
  956. return new Promise((resolve, reject) => {
  957. const reader = new FileReader();
  958. reader.onload = (e) => {
  959. try {
  960. const data = new Uint8Array(e.target!.result as ArrayBuffer);
  961. const workbook = XLSX.read(data, { type: 'array' });
  962. const sheet = workbook.Sheets[workbook.SheetNames[0]];
  963. const rows = XLSX.utils.sheet_to_json<any[]>(sheet, { header: 1 });
  964. const headerRow: any[] = rows[0] || [];
  965. // 查找「商品编号」列索引
  966. let noColIndex = headerRow.findIndex((h: any) => String(h).trim() === '商品编号');
  967. if (noColIndex === -1) noColIndex = 0;
  968. // 查找「协议价格」列索引
  969. const priceColIndex = headerRow.findIndex((h: any) => String(h).trim() === '协议价格');
  970. const result: Array<{ productNo: string; negotiatedPrice?: number }> = [];
  971. rows.slice(1).forEach((row: any[]) => {
  972. const val = row[noColIndex];
  973. if (val !== undefined && val !== null && String(val).trim() !== '') {
  974. const negotiatedPrice =
  975. priceColIndex !== -1 && row[priceColIndex] !== undefined && row[priceColIndex] !== null ? Number(row[priceColIndex]) : undefined;
  976. result.push({
  977. productNo: String(val).trim(),
  978. negotiatedPrice: !isNaN(negotiatedPrice as number) ? negotiatedPrice : undefined
  979. });
  980. }
  981. });
  982. resolve(result);
  983. } catch (err) {
  984. reject(err);
  985. }
  986. };
  987. reader.onerror = reject;
  988. reader.readAsArrayBuffer(file);
  989. });
  990. };
  991. /** 提交文件:前端解析 Excel,再查询匹配商品 ID */
  992. const submitFileForm = async () => {
  993. if (!importDialog.selectedFile) {
  994. proxy?.$modal.msgWarning('请先选择要导入的文件');
  995. return;
  996. }
  997. importDialog.isUploading = true;
  998. try {
  999. const parsedRows = await parseExcelData(importDialog.selectedFile);
  1000. if (parsedRows.length === 0) {
  1001. proxy?.$modal.msgWarning('未在文件中找到有效的商品编号,请检查模板格式');
  1002. return;
  1003. }
  1004. const productNos = parsedRows.map((r) => r.productNo);
  1005. // 按商品编号批量查询商品,获取商品对象
  1006. const res = await listBase({ productNos, pageNum: 1, pageSize: productNos.length } as any);
  1007. const products: BaseVO[] = res.rows || res.data || [];
  1008. if (products.length === 0) {
  1009. proxy?.$modal.msgWarning(`共读取 ${productNos.length} 个编号,但未匹配到任何商品`);
  1010. return;
  1011. }
  1012. // 建立 productNo -> negotiatedPrice 映射
  1013. const priceMap = new Map<string, number | undefined>();
  1014. parsedRows.forEach((r) => priceMap.set(r.productNo, r.negotiatedPrice));
  1015. const isProtocol = auditForm.type === '2';
  1016. let addedCount = 0;
  1017. products.forEach((p) => {
  1018. const idStr = String(p.id);
  1019. if (!tempProductIds.value.split(',').includes(idStr)) {
  1020. tempProductIds.value = tempProductIds.value ? `${tempProductIds.value},${idStr}` : idStr;
  1021. addedCount++;
  1022. }
  1023. // 无论是否已存在,有协议价时都更新
  1024. if (isProtocol && p.productNo) {
  1025. const price = priceMap.get(p.productNo);
  1026. if (price !== undefined) {
  1027. tempAgreementPrices[p.id] = price;
  1028. }
  1029. }
  1030. });
  1031. const skipped = productNos.length - products.length;
  1032. importDialog.open = false;
  1033. const msg = skipped > 0 ? `成功导入 ${addedCount} 个商品,${skipped} 个编号未匹配` : `成功导入 ${addedCount} 个商品到入池清单`;
  1034. proxy?.$modal.msgSuccess(msg);
  1035. await getList();
  1036. } catch (err) {
  1037. console.error('导入失败:', err);
  1038. proxy?.$modal.msgError('解析文件失败,请检查文件格式');
  1039. } finally {
  1040. importDialog.isUploading = false;
  1041. }
  1042. };
  1043. /** 检查营销/协议/项目产品池是否已选择对应池 */
  1044. const checkPoolSelected = (): boolean => {
  1045. if (auditForm.type === '4' && !auditForm.poolId) {
  1046. proxy?.$modal.msgWarning('请先选择产品池');
  1047. return false;
  1048. }
  1049. if (auditForm.type === '2' && !auditForm.protocolId) {
  1050. proxy?.$modal.msgWarning('请先选择协议');
  1051. return false;
  1052. }
  1053. if (auditForm.type === '3' && !auditForm.itemId) {
  1054. proxy?.$modal.msgWarning('请先选择项目');
  1055. return false;
  1056. }
  1057. return true;
  1058. };
  1059. /** 添加商品 */
  1060. const handleAddProduct = () => {
  1061. if (!checkPoolSelected()) return;
  1062. addProductDialog.visible = true;
  1063. addProductQuery.value = {
  1064. pageNum: 1,
  1065. pageSize: 10,
  1066. way: undefined,
  1067. productNo: undefined,
  1068. itemName: undefined,
  1069. lastSeenId: undefined
  1070. };
  1071. addProductPageHistory.value = [];
  1072. addProductHasMore.value = true;
  1073. selectedProducts.value = [];
  1074. // 清空协议价映射
  1075. Object.keys(priceInputMap).forEach((key) => delete priceInputMap[key]);
  1076. getProductList();
  1077. };
  1078. /** 获取商品列表 */
  1079. const getProductList = async () => {
  1080. addProductDialog.loading = true;
  1081. try {
  1082. // 出池模式:使用 getPoolAuditProducts 接口
  1083. if (auditForm.applyType === 1) {
  1084. const outParams: any = {
  1085. pageNum: addProductQuery.value.pageNum,
  1086. pageSize: addProductQuery.value.pageSize,
  1087. poolId: auditForm.poolId,
  1088. itemId: auditForm.itemId,
  1089. type: auditForm.type,
  1090. protocolId: auditForm.protocolId,
  1091. productNo: addProductQuery.value.productNo,
  1092. productName: addProductQuery.value.itemName
  1093. };
  1094. const res = await getPoolAuditProducts(outParams);
  1095. if (res.rows) {
  1096. addProductDialog.productList = res.rows;
  1097. addProductDialog.total = res.total || 0;
  1098. } else if (res.data) {
  1099. addProductDialog.productList = Array.isArray(res.data) ? res.data : [];
  1100. addProductDialog.total = addProductDialog.productList.length;
  1101. } else {
  1102. addProductDialog.productList = [];
  1103. addProductDialog.total = 0;
  1104. }
  1105. addProductHasMore.value = addProductDialog.productList.length === addProductQuery.value.pageSize;
  1106. } else {
  1107. // 入池模式:使用 listBase 接口
  1108. const params = { ...addProductQuery.value };
  1109. // 只查询已上架的商品
  1110. params.productStatus = 1;
  1111. // 自营池(type=0)查询非自营商品;标准产品池(type=1)或协议产品池(type=2)查询自营商品
  1112. if (auditForm.type === '0') {
  1113. params.isSelf = 0;
  1114. } else if (auditForm.type === '1' || auditForm.type === '2') {
  1115. params.isSelf = 1;
  1116. }
  1117. const currentPageNum = addProductQuery.value.pageNum;
  1118. if (currentPageNum === 1) {
  1119. delete params.lastSeenId;
  1120. delete params.firstSeenId;
  1121. delete params.way;
  1122. } else {
  1123. if (addProductQuery.value.way === 0) {
  1124. const nextPageHistory = addProductPageHistory.value[currentPageNum];
  1125. if (nextPageHistory) {
  1126. params.firstSeenId = nextPageHistory.firstId;
  1127. params.way = 0;
  1128. }
  1129. } else {
  1130. const prevPageHistory = addProductPageHistory.value[currentPageNum - 1];
  1131. if (prevPageHistory) {
  1132. params.lastSeenId = prevPageHistory.lastId;
  1133. params.way = 1;
  1134. }
  1135. }
  1136. }
  1137. const res = await listBase(params);
  1138. if (res.rows) {
  1139. addProductDialog.productList = res.rows;
  1140. addProductDialog.total = res.total || 0;
  1141. } else if (res.data) {
  1142. addProductDialog.productList = Array.isArray(res.data) ? res.data : [];
  1143. addProductDialog.total = addProductDialog.productList.length;
  1144. } else {
  1145. addProductDialog.productList = [];
  1146. addProductDialog.total = 0;
  1147. }
  1148. addProductHasMore.value = addProductDialog.productList.length === addProductQuery.value.pageSize;
  1149. if (addProductDialog.productList.length > 0) {
  1150. const firstItem = addProductDialog.productList[0];
  1151. const lastItem = addProductDialog.productList[addProductDialog.productList.length - 1];
  1152. if (addProductPageHistory.value.length <= currentPageNum) {
  1153. addProductPageHistory.value[currentPageNum] = {
  1154. firstId: firstItem.id,
  1155. lastId: lastItem.id
  1156. };
  1157. }
  1158. }
  1159. }
  1160. // 初始化协议价(只对type=2有意义,默认用官网价)
  1161. if (auditInfo.value?.type === '2') {
  1162. addProductDialog.productList.forEach((product) => {
  1163. if (priceInputMap[product.id] === undefined) {
  1164. priceInputMap[product.id] = product.standardPrice ?? product.midRangePrice ?? 0;
  1165. }
  1166. });
  1167. }
  1168. } catch (error) {
  1169. console.error('获取商品列表失败:', error);
  1170. addProductDialog.productList = [];
  1171. addProductDialog.total = 0;
  1172. } finally {
  1173. addProductDialog.loading = false;
  1174. }
  1175. };
  1176. /** 搜索商品 */
  1177. const handleSearchProducts = () => {
  1178. addProductQuery.value.pageNum = 1;
  1179. addProductQuery.value.lastSeenId = undefined;
  1180. addProductPageHistory.value = [];
  1181. addProductHasMore.value = true;
  1182. getProductList();
  1183. };
  1184. /** 选择变化 */
  1185. const handleSelectionChange = (selection: BaseVO[]) => {
  1186. selectedProducts.value = selection;
  1187. };
  1188. /** 批量加入清单(本地暂存,不调接口) */
  1189. const handleBatchAdd = async () => {
  1190. if (selectedProducts.value.length === 0) {
  1191. proxy?.$modal.msgWarning('请先选择要添加的商品');
  1192. return;
  1193. }
  1194. const isProtocol = auditForm.type === '2';
  1195. let addedCount = 0;
  1196. selectedProducts.value.forEach((product) => {
  1197. const idStr = String(product.id);
  1198. if (!tempProductIds.value.split(',').includes(idStr)) {
  1199. tempProductIds.value = tempProductIds.value ? `${tempProductIds.value},${idStr}` : idStr;
  1200. // 存储协议价(type=2 时取用户输入的值,否则取官网价)
  1201. tempAgreementPrices[product.id] = isProtocol ? (priceInputMap[product.id] ?? product.standardPrice ?? product.midRangePrice) : undefined;
  1202. addedCount++;
  1203. }
  1204. });
  1205. if (addedCount === 0) {
  1206. proxy?.$modal.msgWarning('选中的商品已全部在入池清单中');
  1207. return;
  1208. }
  1209. proxy?.$modal.msgSuccess(`成功添加 ${addedCount} 个商品到入池清单`);
  1210. addProductDialog.visible = false;
  1211. selectedProducts.value = [];
  1212. if (addProductTableRef.value) {
  1213. addProductTableRef.value.clearSelection();
  1214. }
  1215. await getList();
  1216. };
  1217. /** 添加单个商品(本地暂存,不调接口) */
  1218. const handleAddSingleProduct = async (row: BaseVO) => {
  1219. const idStr = String(row.id);
  1220. if (tempProductIds.value.split(',').includes(idStr)) {
  1221. proxy?.$modal.msgWarning('该商品已在入池清单中');
  1222. return;
  1223. }
  1224. // 项目产品池:弹出第三方产品分类弹框
  1225. if (auditForm.type === '3') {
  1226. thirdPartyDialog.row = row;
  1227. thirdPartyDialog.pricingRule = '0';
  1228. thirdPartyDialog.categoryValue = [];
  1229. thirdPartyDialog.discountRate = undefined;
  1230. thirdPartyDialog.negotiatedPrice = 0;
  1231. thirdPartyDialog.minOrderQuantity = Number(row.minOrderQuantity) || 1;
  1232. thirdPartyDialog.visible = true;
  1233. return;
  1234. }
  1235. const isProtocol = auditForm.type === '2';
  1236. tempProductIds.value = tempProductIds.value ? `${tempProductIds.value},${idStr}` : idStr;
  1237. tempAgreementPrices[row.id] = isProtocol ? (priceInputMap[row.id] ?? row.standardPrice ?? row.midRangePrice) : undefined;
  1238. proxy?.$modal.msgSuccess('添加成功');
  1239. await getList();
  1240. };
  1241. /** 处理第三方产品分类选择变化 */
  1242. const handleCategoryChange = (value: (string | number)[]) => {
  1243. if (!value || value.length === 0) {
  1244. thirdPartyDialog.discountRate = undefined;
  1245. return;
  1246. }
  1247. const lastId = value[value.length - 1];
  1248. thirdPartyDialog.discountRate = categoryDiscountMap.value[lastId];
  1249. if (thirdPartyDialog.pricingRule === '1') {
  1250. const memberPrice = Number(thirdPartyDialog.row?.memberPrice) || 0;
  1251. const rate = thirdPartyDialog.discountRate || 0;
  1252. thirdPartyDialog.negotiatedPrice = parseFloat((memberPrice * rate).toFixed(2));
  1253. }
  1254. };
  1255. /** 切换价格模式时清空价格并重新计算 */
  1256. watch(
  1257. () => thirdPartyDialog.pricingRule,
  1258. (mode) => {
  1259. // 切换模式时先清空价格
  1260. thirdPartyDialog.negotiatedPrice = 0;
  1261. // 折扣率模式:自动计算 第三方平台售价 = 官网价 × 折扣率
  1262. if (mode === '1') {
  1263. const memberPrice = Number(thirdPartyDialog.row?.memberPrice) || 0;
  1264. const rate = thirdPartyDialog.discountRate || 0;
  1265. thirdPartyDialog.negotiatedPrice = parseFloat((memberPrice * rate).toFixed(2));
  1266. }
  1267. }
  1268. );
  1269. /** 第三方售价失焦校验:不低于最低售价,不高于官网价 */
  1270. const handleThirdPartyPriceBlur = () => {
  1271. const price = thirdPartyDialog.negotiatedPrice;
  1272. const row = thirdPartyDialog.row;
  1273. if (!row || !price || price <= 0) return;
  1274. const minPrice = Number(row.minSellingPrice) || 0;
  1275. const maxPrice = Number(row.memberPrice) || 0;
  1276. if (minPrice > 0 && price < minPrice) {
  1277. proxy?.$modal.msgWarning(`第三方售价不能低于最低售价(¥${minPrice})`);
  1278. thirdPartyDialog.negotiatedPrice = minPrice;
  1279. return;
  1280. }
  1281. if (maxPrice > 0 && price > maxPrice) {
  1282. proxy?.$modal.msgWarning(`第三方售价不能高于官网价(¥${maxPrice})`);
  1283. thirdPartyDialog.negotiatedPrice = maxPrice;
  1284. }
  1285. };
  1286. /** 确认选择第三方产品分类,加入入池清单 */
  1287. const handleConfirmThirdParty = async () => {
  1288. if (!thirdPartyDialog.categoryValue || thirdPartyDialog.categoryValue.length === 0) {
  1289. proxy?.$modal.msgWarning('请选择第三方产品分类');
  1290. return;
  1291. }
  1292. if (thirdPartyDialog.negotiatedPrice <= 0) {
  1293. proxy?.$modal.msgWarning('请输入有效的第三方平台售价');
  1294. return;
  1295. }
  1296. const row = thirdPartyDialog.row!;
  1297. const idStr = String(row.id);
  1298. const categoryId = thirdPartyDialog.categoryValue[thirdPartyDialog.categoryValue.length - 1] ?? null;
  1299. // 从级联选择器获取路径标签
  1300. let categoryName = '';
  1301. const checkedNodes = categoryRef.value?.getCheckedNodes(true);
  1302. if (checkedNodes && checkedNodes.length > 0) {
  1303. categoryName = (checkedNodes[0].pathLabels || []).join(' / ');
  1304. }
  1305. // 加入临时 ID 列表
  1306. tempProductIds.value = tempProductIds.value ? `${tempProductIds.value},${idStr}` : idStr;
  1307. // 存储项目商品额外信息
  1308. tempProjectProductInfo[row.id] = {
  1309. negotiatedPrice: thirdPartyDialog.negotiatedPrice,
  1310. pricingRule: thirdPartyDialog.pricingRule,
  1311. categoryId,
  1312. categoryName,
  1313. minOrderQuantity: thirdPartyDialog.minOrderQuantity
  1314. };
  1315. thirdPartyDialog.visible = false;
  1316. proxy?.$modal.msgSuccess('添加成功');
  1317. await getList();
  1318. };
  1319. /** 获取分类名称 */
  1320. const getCategoryName = (row: BaseVO): string => {
  1321. if (row.bottomCategoryId) {
  1322. return getCategoryFullPath(row.bottomCategoryId);
  1323. }
  1324. return '-';
  1325. };
  1326. /** 判断商品是否已在入池清单中 */
  1327. const isProductSelected = (id: string | number): boolean => {
  1328. if (!tempProductIds.value) return false;
  1329. return tempProductIds.value.split(',').includes(String(id));
  1330. };
  1331. /** 获取当前产品池类型标签 */
  1332. const getPoolTypeLabel = (): string => {
  1333. const typeMap: Record<string, string> = {
  1334. '0': '自营池',
  1335. '1': '标准池',
  1336. '2': '协议池',
  1337. '3': '项目池',
  1338. '4': '营销池'
  1339. };
  1340. return typeMap[auditForm.type || ''] || '';
  1341. };
  1342. /** 协议价失焦校验 */
  1343. const handleAgreementPriceBlur = (row: BaseVO) => {
  1344. const price = tempAgreementPrices[row.id];
  1345. if (price === undefined || price === null || price <= 0) {
  1346. proxy?.$modal.msgWarning('协议价不能为0');
  1347. tempAgreementPrices[row.id] = undefined;
  1348. return;
  1349. }
  1350. const minPrice = Number(row.minSellingPrice) || 0;
  1351. const maxPrice = Number(row.memberPrice) || 0;
  1352. if (minPrice > 0 && price < minPrice) {
  1353. proxy?.$modal.msgWarning(`协议价不能低于最低售价(¥${minPrice})`);
  1354. tempAgreementPrices[row.id] = minPrice;
  1355. return;
  1356. }
  1357. if (maxPrice > 0 && price > maxPrice) {
  1358. proxy?.$modal.msgWarning(`协议价不能高于官网价(¥${maxPrice})`);
  1359. tempAgreementPrices[row.id] = maxPrice;
  1360. }
  1361. };
  1362. /** 移除商品(本地移除) */
  1363. const handleRemoveProduct = async (row: BaseVO) => {
  1364. await proxy?.$modal.confirm('确认要移除该商品吗?');
  1365. const idStr = String(row.id);
  1366. const idsArray = tempProductIds.value.split(',').filter((id) => id);
  1367. const idx = idsArray.indexOf(idStr);
  1368. if (idx !== -1) {
  1369. idsArray.splice(idx, 1);
  1370. tempProductIds.value = idsArray.join(',');
  1371. delete tempAgreementPrices[row.id];
  1372. }
  1373. proxy?.$modal.msgSuccess('移除成功');
  1374. await getList();
  1375. };
  1376. onMounted(async () => {
  1377. // 新增场景:有路由 type 但无 id,自动填充并加载选项
  1378. const hasId = route.params.id || route.query.id;
  1379. if (!hasId && typeDisabled.value && routeType.value) {
  1380. isInitializing.value = true;
  1381. auditForm.type = routeType.value;
  1382. isInitializing.value = false;
  1383. await loadSelectOptions(routeType.value);
  1384. }
  1385. loadAuditInfo();
  1386. getCategoryTree();
  1387. getSupplierList();
  1388. getList();
  1389. loadBrandOptions();
  1390. });
  1391. </script>
  1392. <style scoped lang="scss">
  1393. .add-product-dialog {
  1394. :deep(.el-form--inline .el-form-item) {
  1395. margin-right: 10px;
  1396. }
  1397. }
  1398. </style>