view.vue 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793
  1. <template>
  2. <div class="app-container view-scroll-container">
  3. <el-card shadow="never" class="mb-3 view-header-card">
  4. <div class="flex items-center justify-between">
  5. <div class="flex items-center">
  6. <el-button icon="ArrowLeft" @click="handleBack">返回</el-button>
  7. <span class="ml-4 text-xl font-bold">查看商品</span>
  8. </div>
  9. </div>
  10. </el-card>
  11. <div class="product-wizard-page">
  12. <!-- 步骤内容 -->
  13. <div class="step-content" v-loading="loading">
  14. <!-- 步骤2: 填写商品信息 -->
  15. <el-card shadow="never" class="step-card">
  16. <template #header>
  17. <span class="text-lg font-bold">基本信息</span>
  18. </template>
  19. <el-form ref="productFormRef" :model="productForm" :rules="productRules" label-width="120px" class="product-info-form" disabled>
  20. <!-- 商品分类显示 -->
  21. <el-form-item label="商品分类:">
  22. <div class="category-display">
  23. <span class="category-text">{{ getCategoryPath() }}</span>
  24. </div>
  25. </el-form-item>
  26. <!-- 商品名称 -->
  27. <el-form-item label="商品名称:" prop="itemName" required>
  28. <el-input v-model="productForm.itemName" type="textarea" :rows="2" placeholder="请输入商品名称" maxlength="200" show-word-limit />
  29. </el-form-item>
  30. <!-- A10产品名称 -->
  31. <el-form-item label="A10产品名称:">
  32. <el-input
  33. :value="a10ProductNameComputed"
  34. type="textarea"
  35. :rows="2"
  36. disabled
  37. placeholder="自动拼接:品牌名 + 规格型号 + 产品分类 + 发票规格"
  38. />
  39. <div class="form-item-tip">A10产品名称由系统自动拼接:品牌名 + 规格型号 + 产品分类(三级分类)+ 发票规格,无需手动填写</div>
  40. </el-form-item>
  41. <!-- 规格型号 和 UPC(69)条码 -->
  42. <el-row :gutter="20">
  43. <el-col :span="12">
  44. <el-form-item label="规格型号:">
  45. <el-input v-model="productForm.specificationsCode" placeholder="请输入规格型号" maxlength="20" show-word-limit />
  46. </el-form-item>
  47. </el-col>
  48. <el-col :span="12">
  49. <el-form-item label="UPC(69)条码:">
  50. <el-input v-model="productForm.barCoding" placeholder="请输入UPC(69)条码" maxlength="20" show-word-limit @input="handleUpcInput" />
  51. </el-form-item>
  52. </el-col>
  53. </el-row>
  54. <!-- 发票名称 和 发票规格 -->
  55. <el-row :gutter="20">
  56. <el-col :span="12">
  57. <el-form-item label="发票名称:">
  58. <el-input v-model="productForm.invoiceName" placeholder="请输入发票名称" maxlength="20" show-word-limit />
  59. </el-form-item>
  60. </el-col>
  61. <el-col :span="12">
  62. <el-form-item label="发票规格:">
  63. <el-input v-model="productForm.invoiceSpecs" placeholder="请输入发票规格" maxlength="20" show-word-limit />
  64. </el-form-item>
  65. </el-col>
  66. </el-row>
  67. <el-row :gutter="20">
  68. <!-- 商品品牌 -->
  69. <el-col :span="12">
  70. <el-form-item label="商品品牌:" prop="brandId" required>
  71. <el-select
  72. v-model="productForm.brandId"
  73. placeholder="请输入品牌名称搜索"
  74. filterable
  75. remote
  76. clearable
  77. :remote-method="handleBrandSearch"
  78. :loading="brandLoading"
  79. class="w-full"
  80. >
  81. <el-option v-for="item in brandOptions" :key="item.id" :label="`${item.brandNo},${item.brandName}`" :value="item.id" />
  82. </el-select>
  83. </el-form-item>
  84. </el-col>
  85. <el-col :span="12">
  86. <el-form-item label="单位:">
  87. <el-select
  88. v-model="productForm.unitId"
  89. placeholder="请选择"
  90. clearable
  91. class="w-full"
  92. :disabled="productForm.productReviewStatus === 1"
  93. >
  94. <el-option v-for="option in unitOptions" :key="option.id" :label="`${option.unitNo},${option.unitName}`" :value="option.id" />
  95. </el-select>
  96. </el-form-item>
  97. </el-col>
  98. </el-row>
  99. <!-- 税率编码 、税率 和 币种 -->
  100. <el-row :gutter="20">
  101. <el-col :span="12">
  102. <el-form-item label="税率编码:">
  103. <el-input
  104. v-model="taxCodeNo"
  105. placeholder="点击选择税率编码"
  106. readonly
  107. class="w-full"
  108. />
  109. </el-form-item>
  110. </el-col>
  111. <el-col :span="12">
  112. <el-form-item label="税率:" required>
  113. <el-select v-model="productForm.taxRate" placeholder="请选择税率" clearable class="w-full">
  114. <el-option v-for="option in taxRateOptions" :key="option.id" :label="`${option.taxrateNo},${option.taxrateName}`" :value="option.taxrate" />
  115. </el-select>
  116. </el-form-item>
  117. </el-col>
  118. </el-row>
  119. <el-row :gutter="20">
  120. <el-col :span="12">
  121. <el-form-item label="币种:">
  122. <el-select v-model="productForm.currency" placeholder="请选择" class="w-full">
  123. <el-option label="人民币(RMB)" value="RMB" />
  124. <el-option label="美元(USD)" value="USD" />
  125. <el-option label="欧元(EUR)" value="EUR" />
  126. </el-select>
  127. </el-form-item>
  128. </el-col>
  129. </el-row>
  130. <!-- TaxCodeSelect 弹窗(查看模式下不启用) -->
  131. <!-- 销量人气 -->
  132. <el-row :gutter="20">
  133. <el-col :span="12">
  134. <el-form-item label="销量人气:">
  135. <el-input
  136. v-model="productForm.salesVolume"
  137. type="number"
  138. placeholder="请输入销量人气"
  139. :min="0"
  140. step="1"
  141. @input="handleSalesVolumeInput"
  142. />
  143. </el-form-item>
  144. </el-col>
  145. </el-row>
  146. <!-- 促销标题 -->
  147. <el-form-item label="促销标题:">
  148. <el-input v-model="productForm.promotionTitle" type="textarea" :rows="3" placeholder="请输入促销标题" maxlength="300" show-word-limit />
  149. </el-form-item>
  150. <!-- 商品简介 -->
  151. <el-form-item label="商品简介:">
  152. <el-input v-model="productForm.productDescription" type="textarea" :rows="3" placeholder="请输入商品简介" maxlength="500" show-word-limit />
  153. </el-form-item>
  154. <!-- 重量 和 体积 -->
  155. <el-row :gutter="20">
  156. <el-col :span="12">
  157. <el-form-item label="商品重量:">
  158. <el-input v-model="productForm.productWeight" placeholder="0" maxlength="10" show-word-limit>
  159. <template #append>
  160. <el-select v-model="productForm.weightUnit" placeholder="请选择" style="width: 100px">
  161. <el-option label="kg" value="kg" />
  162. <el-option label="g" value="g" />
  163. <el-option label="t" value="t" />
  164. </el-select>
  165. </template>
  166. </el-input>
  167. </el-form-item>
  168. </el-col>
  169. <el-col :span="12">
  170. <el-form-item label="商品体积:">
  171. <el-input v-model="productForm.productVolume" placeholder="0" maxlength="10" show-word-limit>
  172. <template #append>
  173. <el-select v-model="productForm.volumeUnit" placeholder="请选择" style="width: 80px">
  174. <el-option label="m³" value="m3" />
  175. <el-option label="cm³" value="cm3" />
  176. <el-option label="L" value="L" />
  177. </el-select>
  178. </template>
  179. </el-input>
  180. </el-form-item>
  181. </el-col>
  182. </el-row>
  183. <!-- 参考链接 -->
  184. <el-form-item label="参考链接">
  185. <el-input v-model="productForm.referenceLink" type="textarea" :rows="3" placeholder="请输入参考链接" />
  186. </el-form-item>
  187. <!-- 售后服务 -->
  188. <el-form-item label="售后服务:">
  189. <el-select v-model="productForm.afterSalesService" placeholder="请选择" clearable class="w-full">
  190. <el-option v-for="option in afterSalesOptions" :key="option.id" :label="option.afterSalesItems" :value="option.id" />
  191. </el-select>
  192. </el-form-item>
  193. <!-- 服务保障 -->
  194. <el-form-item label="服务保障:">
  195. <el-checkbox-group v-model="serviceGuarantees">
  196. <el-checkbox v-for="option in serviceGuaranteeOptions" :key="option.id" :label="option.ensureName" :value="option.id" />
  197. </el-checkbox-group>
  198. </el-form-item>
  199. <!-- 安装服务 -->
  200. <el-form-item label="安装服务:">
  201. <el-checkbox-group v-model="installationServices">
  202. <el-checkbox label="免费安装" value="freeInstallation" />
  203. </el-checkbox-group>
  204. </el-form-item>
  205. </el-form>
  206. </el-card>
  207. <!-- 销售价格 -->
  208. <el-card shadow="never" class="step-card mt-3">
  209. <template #header>
  210. <span class="text-lg font-bold">销售价格</span>
  211. </template>
  212. <el-form ref="priceFormRef" :model="productForm" :rules="productRules" label-width="120px" class="product-info-form" disabled>
  213. <el-row :gutter="20">
  214. <el-col :span="8">
  215. <el-form-item label="市场价:" prop="marketPrice" required>
  216. <el-input
  217. v-model="productForm.marketPrice"
  218. type="number"
  219. placeholder="请输入市场价"
  220. :min="0"
  221. @blur="formatPrice('marketPrice')"
  222. />
  223. </el-form-item>
  224. </el-col>
  225. <el-col :span="8">
  226. <el-form-item label="官网价:" prop="memberPrice" required>
  227. <el-input
  228. v-model="productForm.memberPrice"
  229. type="number"
  230. placeholder="请输入平台售价"
  231. :min="0"
  232. @blur="formatPrice('memberPrice')"
  233. />
  234. </el-form-item>
  235. </el-col>
  236. </el-row>
  237. <el-row :gutter="20">
  238. <el-col :span="8">
  239. <el-form-item label="最低起订量:" prop="minOrderQuantity" required>
  240. <el-input v-model="productForm.minOrderQuantity" min="1" type="number" placeholder="请输入最低起订量" />
  241. </el-form-item>
  242. </el-col>
  243. <el-col :span="8">
  244. <el-form-item label="备注:">
  245. <span class="currency-text">市场价>官网价</span>
  246. </el-form-item>
  247. </el-col>
  248. </el-row>
  249. </el-form>
  250. </el-card>
  251. <!-- 供应价格 -->
  252. <el-card shadow="never" class="step-card mt-3">
  253. <template #header>
  254. <span class="text-lg font-bold">供应价格</span>
  255. </template>
  256. <el-form label-width="120px" class="product-info-form" disabled>
  257. <el-row :gutter="20">
  258. <el-col :span="8">
  259. <el-form-item label="供应价:">
  260. <el-input
  261. v-model="productForm.supplyPrice"
  262. type="number"
  263. placeholder="请输入供应价"
  264. :min="0"
  265. @blur="formatPrice('supplyPrice')"
  266. />
  267. </el-form-item>
  268. </el-col>
  269. <el-col :span="8">
  270. <el-form-item label="供应有效时间:">
  271. <el-date-picker
  272. v-model="productForm.supplyValidityPeriod"
  273. type="date"
  274. placeholder="请选择供应有效时间"
  275. value-format="YYYY-MM-DD"
  276. class="w-full"
  277. />
  278. </el-form-item>
  279. </el-col>
  280. <el-col :span="8">
  281. <el-form-item label="是否包邮:">
  282. <el-radio-group v-model="productForm.supplyPostStatus">
  283. <el-radio :value="0">不包邮</el-radio>
  284. <el-radio :value="1">包邮</el-radio>
  285. </el-radio-group>
  286. </el-form-item>
  287. </el-col>
  288. </el-row>
  289. </el-form>
  290. </el-card>
  291. <!-- 自定义属性 -->
  292. <el-card shadow="never" class="step-card mt-3">
  293. <template #header>
  294. <div class="flex items-center justify-between">
  295. <span class="text-lg font-bold">自定义属性</span>
  296. </div>
  297. </template>
  298. <el-form label-width="0px" class="product-info-form" disabled>
  299. <div v-if="diyAttributesList.length === 0" class="text-center text-gray-400 py-4 text-sm">
  300. 暂无自定义属性
  301. </div>
  302. <el-row v-for="(item, index) in diyAttributesList" :key="index" :gutter="20" class="mb-2">
  303. <el-col :span="12">
  304. <el-input v-model="item.attributeKey" placeholder="属性名称" />
  305. </el-col>
  306. <el-col :span="12">
  307. <el-input v-model="item.attributeValue" placeholder="属性值" />
  308. </el-col>
  309. </el-row>
  310. </el-form>
  311. </el-card>
  312. <!-- 商品属性 -->
  313. <el-card shadow="never" class="step-card mt-3">
  314. <template #header>
  315. <span class="text-lg font-bold">商品属性</span>
  316. </template>
  317. <el-form ref="attributeFormRef" :model="productForm" label-width="120px" class="product-info-form" disabled>
  318. <div v-if="attributesList.length === 0" class="text-center text-gray-500 py-8">该分类暂无属性配置</div>
  319. <template v-else>
  320. <el-row :gutter="20" v-for="(row, rowIndex) in Math.ceil(attributesList.length / 2)" :key="rowIndex">
  321. <el-col :span="12" v-for="colIndex in 2" :key="colIndex">
  322. <template v-if="attributesList[rowIndex * 2 + colIndex - 1]">
  323. <el-form-item
  324. :label="attributesList[rowIndex * 2 + colIndex - 1].productAttributesName + ':'"
  325. :required="attributesList[rowIndex * 2 + colIndex - 1].required === '1'"
  326. >
  327. <!-- 下拉选择 -->
  328. <el-select
  329. v-if="attributesList[rowIndex * 2 + colIndex - 1].isOptional === '0'"
  330. v-model="productAttributesValues[attributesList[rowIndex * 2 + colIndex - 1].productAttributesName]"
  331. placeholder="请选择"
  332. clearable
  333. class="w-full"
  334. >
  335. <el-option
  336. v-for="option in parseAttributesList(attributesList[rowIndex * 2 + colIndex - 1].attributesList)"
  337. :key="option"
  338. :label="option"
  339. :value="option"
  340. />
  341. </el-select>
  342. <!-- 多选 -->
  343. <el-select
  344. v-else-if="attributesList[rowIndex * 2 + colIndex - 1].isOptional === '2'"
  345. v-model="productAttributesValues[attributesList[rowIndex * 2 + colIndex - 1].productAttributesName]"
  346. placeholder="请选择"
  347. multiple
  348. clearable
  349. class="w-full"
  350. >
  351. <el-option
  352. v-for="option in parseAttributesList(attributesList[rowIndex * 2 + colIndex - 1].attributesList)"
  353. :key="option"
  354. :label="option"
  355. :value="option"
  356. />
  357. </el-select>
  358. <!-- 文本输入 -->
  359. <el-input
  360. v-else
  361. v-model="productAttributesValues[attributesList[rowIndex * 2 + colIndex - 1].productAttributesName]"
  362. placeholder="请输入"
  363. clearable
  364. />
  365. </el-form-item>
  366. </template>
  367. </el-col>
  368. </el-row>
  369. </template>
  370. </el-form>
  371. </el-card>
  372. <!-- 商品详情 -->
  373. <el-card shadow="never" class="step-card mt-3">
  374. <template #header>
  375. <span class="text-lg font-bold">商品详情</span>
  376. </template>
  377. <el-form ref="detailFormRef" :model="productForm" label-width="120px" class="product-info-form" disabled>
  378. <!-- 商品主图 -->
  379. <el-form-item label="商品主图:" required>
  380. <div class="view-disabled-wrapper">
  381. <upload-image v-model="productForm.productImage" :limit="1" width="178px" height="178px" imageText="选择图片" />
  382. </div>
  383. </el-form-item>
  384. <!-- 商品轮播图 -->
  385. <el-form-item label="商品轮播图:" required>
  386. <div class="view-disabled-wrapper">
  387. <upload-image v-model="carouselImages" :limit="20" width="120px" height="120px" imageText="添加图片" />
  388. </div>
  389. </el-form-item>
  390. <!-- 商品详情 -->
  391. <el-form-item label="商品详情:" required>
  392. <el-tabs v-model="activeDetailTab" type="border-card">
  393. <el-tab-pane label="电脑端详情" name="pc">
  394. <div class="view-disabled-wrapper">
  395. <Editor v-model="productForm.pcDetail" :height="400" :readOnly="true" />
  396. </div>
  397. </el-tab-pane>
  398. <el-tab-pane label="移动端详情" name="mobile">
  399. <div class="view-disabled-wrapper">
  400. <Editor v-model="productForm.mobileDetail" :height="400" :readOnly="true" />
  401. </div>
  402. </el-tab-pane>
  403. </el-tabs>
  404. </el-form-item>
  405. </el-form>
  406. </el-card>
  407. <!-- 定制说明 -->
  408. <el-card shadow="never" class="step-card mt-3">
  409. <template #header>
  410. <span class="text-lg font-bold">定制说明</span>
  411. </template>
  412. <el-form ref="customFormRef" :model="customForm" label-width="120px" class="product-info-form" disabled>
  413. <!-- 可定制开关 -->
  414. <el-form-item label="可定制:">
  415. <el-switch v-model="customForm.isCustomize" />
  416. </el-form-item>
  417. <!-- 定制内容 -->
  418. <template v-if="customForm.isCustomize">
  419. <!-- 定制方式 -->
  420. <el-form-item label="定制方式:">
  421. <div class="custom-options">
  422. <el-button
  423. v-for="option in customMethodOptions"
  424. :key="option.value"
  425. :type="customForm.selectedMethods.includes(option.value) ? 'primary' : 'default'"
  426. disabled
  427. >
  428. {{ option.label }}
  429. </el-button>
  430. </div>
  431. </el-form-item>
  432. <!-- 定制工艺 -->
  433. <el-form-item label="定制工艺:">
  434. <div class="custom-options">
  435. <el-button
  436. v-for="craft in customCraftOptions"
  437. :key="craft.value"
  438. :type="customForm.selectedCrafts.includes(craft.value) ? 'primary' : 'default'"
  439. disabled
  440. >
  441. {{ craft.label }}
  442. </el-button>
  443. </div>
  444. </el-form-item>
  445. <!-- 定制方式表格 -->
  446. <el-form-item label="" label-width="120">
  447. <el-table :data="customForm.customDetails" border class="custom-table">
  448. <el-table-column label="装饰方法" width="120">
  449. <template #default="{ row }">
  450. <span>{{ row.decorationMethod }}</span>
  451. </template>
  452. </el-table-column>
  453. <el-table-column label="定制工艺" width="120">
  454. <template #default="{ row }">
  455. <span>{{ row.craft }}</span>
  456. </template>
  457. </el-table-column>
  458. <el-table-column label="起订数量" width="150">
  459. <template #default="{ row }">
  460. <el-input v-model="row.minOrderQty" placeholder="请输入" />
  461. </template>
  462. </el-table-column>
  463. <el-table-column label="起订价格" width="150">
  464. <template #default="{ row }">
  465. <el-input
  466. v-model="row.minOrderPrice"
  467. type="number"
  468. :min="0"
  469. placeholder="请输入"
  470. @blur="formatRowPrice(row, 'minOrderPrice')"
  471. />
  472. </template>
  473. </el-table-column>
  474. <el-table-column label="打样工期[天]" width="150">
  475. <template #default="{ row }">
  476. <el-input v-model="row.samplePeriod" placeholder="请输入" />
  477. </template>
  478. </el-table-column>
  479. <el-table-column label="生产周期[天]" width="150">
  480. <template #default="{ row }">
  481. <el-input v-model="row.productionPeriod" placeholder="请输入" />
  482. </template>
  483. </el-table-column>
  484. </el-table>
  485. </el-form-item>
  486. <!-- 定制说明 -->
  487. <el-form-item label="定制说明:">
  488. <el-input v-model="customForm.customDescription" type="textarea" :rows="5" placeholder="请输入定制说明" />
  489. </el-form-item>
  490. </template>
  491. </el-form>
  492. </el-card>
  493. </div>
  494. </div>
  495. </div>
  496. </template>
  497. <script setup lang="ts">
  498. import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue';
  499. import { useRoute, useRouter } from 'vue-router';
  500. import { ElMessage } from 'element-plus';
  501. import { Warning, ArrowRight, Check, Plus, CircleCheck, Search } from '@element-plus/icons-vue';
  502. import Editor from '@/components/Editor/index.vue';
  503. import UploadImage from '@/components/upload-image/index.vue';
  504. import TaxCodeSelect from '@/components/TaxCodeSelect/index.vue';
  505. import { categoryTreeVO, CategoryVO } from '@/api/product/category/types';
  506. import { BrandVO } from '@/api/product/brand/types';
  507. import { BaseForm } from '@/api/product/base/types';
  508. import { ClassificationDiyForm } from '@/api/product/classificationDiy/types';
  509. import { AttributesVO } from '@/api/product/attributes/types';
  510. import {
  511. addBase,
  512. updateBase,
  513. getBase,
  514. categoryTree,
  515. categoryList,
  516. categoryAttributeList,
  517. getAfterSaleList,
  518. getServiceList,
  519. getUnitList,
  520. getTaxRateList
  521. } from '@/api/product/base';
  522. import {
  523. addBaseAudit,
  524. updateBaseAudit,
  525. getBaseAudit,
  526. } from '@/api/product/baseAudit';
  527. import { BaseAuditVO, BaseAuditQuery, BaseAuditForm } from '@/api/product/baseAudit/types';
  528. import { getTaxCode } from '@/api/system/taxCode';
  529. import { listBrand, getBrand } from '@/api/product/brand';
  530. import { listInfo } from '@/api/customer/supplierInfo';
  531. import { InfoVO } from '@/api/customer/supplierInfo/types';
  532. import { listComStaff } from '@/api/system/comStaff';
  533. import { ComStaffVO } from '@/api/system/comStaff/types';
  534. const route = useRoute();
  535. const router = useRouter();
  536. const currentStep = ref(1);
  537. const loading = ref(false);
  538. const submitLoading = ref(false);
  539. const productFormRef = ref();
  540. // 服务保障和安装服务的多选框
  541. const serviceGuarantees = ref<(string | number)[]>([]);
  542. const installationServices = ref<string[]>([]);
  543. // 商品详情选项卡
  544. const activeDetailTab = ref('pc');
  545. // 轮播图URL数组(UI管理用)
  546. const carouselImages = ref<string[]>([]);
  547. // 税率选项
  548. const taxRateOptions = ref<any[]>([]);
  549. // 税率编码选择组件
  550. const taxCodeSelectRef = ref();
  551. // 已选的税率编码(显示用)
  552. const taxCodeNo = ref('');
  553. // 处理税率编码选择
  554. const handleTaxCodeSelect = async (row: any) => {
  555. (productForm as any).taxationId = row.id;
  556. try {
  557. const taxRes = await getTaxCode(row.id);
  558. if (taxRes.data) {
  559. taxCodeNo.value = `${taxRes.data.taxationNo},${taxRes.data.name}`;
  560. } else {
  561. taxCodeNo.value = row.taxationNo ? `${row.taxationNo},${row.name}` : (row.name || '');
  562. }
  563. } catch (e) {
  564. console.error('获取税率编码详情失败:', e);
  565. taxCodeNo.value = row.taxationNo ? `${row.taxationNo},${row.name}` : (row.name || '');
  566. }
  567. // 同时将显示值存入 form,方便编辑回显时直接读取
  568. (productForm as any).taxationNo = taxCodeNo.value;
  569. };
  570. // 定制说明表单
  571. const customForm = reactive({
  572. isCustomize: false,
  573. selectedMethods: [] as string[],
  574. selectedCrafts: [] as string[],
  575. customDetails: [] as Array<{
  576. decorationMethod: string;
  577. craft: string;
  578. minOrderQty: string;
  579. minOrderPrice: string;
  580. samplePeriod: string;
  581. productionPeriod: string;
  582. }>,
  583. customDescription: ''
  584. });
  585. // 定制方式选项
  586. const customMethodOptions = [
  587. { label: '包装定制', value: 'package' },
  588. { label: '商品定制', value: 'product' },
  589. { label: '开模定制', value: 'mold' }
  590. ];
  591. // 定制工艺选项
  592. const customCraftOptions = [
  593. { label: '丝印', value: 'silkScreen' },
  594. { label: '热转印', value: 'thermalTransfer' },
  595. { label: '激光', value: 'laser' },
  596. { label: '烤花', value: 'baking' },
  597. { label: '压印', value: 'embossing' }
  598. ];
  599. // 定制方式映射
  600. const customMethodMap: Record<string, string> = {
  601. 'package': '包装定制',
  602. 'product': '商品定制',
  603. 'mold': '开模定制'
  604. };
  605. // 定制工艺映射
  606. const customCraftMap: Record<string, string> = {
  607. 'silkScreen': '丝印',
  608. 'thermalTransfer': '热转印',
  609. 'laser': '激光',
  610. 'baking': '烤花',
  611. 'embossing': '压印'
  612. };
  613. // 服务保障选择不需要watch,在提交时直接转换为逗号分隔字符串
  614. // 监听安装服务复选框变化,同步到表单
  615. watch(
  616. installationServices,
  617. (newVal) => {
  618. productForm.freeInstallation = newVal.includes('freeInstallation') ? '1' : '0';
  619. },
  620. { deep: true }
  621. );
  622. // 监听定制方式和工艺选择变化,更新表格数据
  623. watch(
  624. [() => customForm.selectedMethods, () => customForm.selectedCrafts],
  625. ([newMethods, newCrafts]) => {
  626. const newDetails: typeof customForm.customDetails = [];
  627. // 遍历所有选中的定制方式和工艺组合
  628. newMethods.forEach((method) => {
  629. const decorationMethod = customMethodMap[method];
  630. newCrafts.forEach((craft) => {
  631. const craftName = customCraftMap[craft];
  632. // 查找是否已存在该组合的数据
  633. const existing = customForm.customDetails.find((item) => item.decorationMethod === decorationMethod && item.craft === craftName);
  634. newDetails.push(
  635. existing || {
  636. decorationMethod,
  637. craft: craftName,
  638. minOrderQty: '',
  639. minOrderPrice: '',
  640. samplePeriod: '',
  641. productionPeriod: ''
  642. }
  643. );
  644. });
  645. });
  646. customForm.customDetails = newDetails;
  647. },
  648. { deep: true }
  649. );
  650. // 切换定制方式选择
  651. const toggleMethod = (method: string) => {
  652. const index = customForm.selectedMethods.indexOf(method);
  653. if (index > -1) {
  654. customForm.selectedMethods.splice(index, 1);
  655. } else {
  656. customForm.selectedMethods.push(method);
  657. }
  658. };
  659. // 切换定制工艺选择
  660. const toggleCraft = (craft: string) => {
  661. const index = customForm.selectedCrafts.indexOf(craft);
  662. if (index > -1) {
  663. customForm.selectedCrafts.splice(index, 1);
  664. } else {
  665. customForm.selectedCrafts.push(craft);
  666. }
  667. };
  668. // 删除定制详情行
  669. const removeCustomDetail = (index: number) => {
  670. customForm.customDetails.splice(index, 1);
  671. };
  672. const pageTitle = computed(() => {
  673. return route.params.id ? '编辑商品' : '新增商品';
  674. });
  675. // 分类选择表单
  676. const categoryForm = reactive({
  677. topCategoryId: undefined as string | number | undefined,
  678. mediumCategoryId: undefined as string | number | undefined,
  679. bottomCategoryId: undefined as string | number | undefined
  680. });
  681. const autoCreateCategory = ref(false);
  682. const manualCategoryInput = ref('');
  683. // 商品信息表单
  684. const productForm = reactive<BaseForm>({
  685. id: undefined,
  686. productNo: undefined,
  687. itemName: undefined,
  688. brandId: undefined,
  689. topCategoryId: undefined,
  690. mediumCategoryId: undefined,
  691. bottomCategoryId: undefined,
  692. unitId: undefined,
  693. productImage: undefined,
  694. imageUrl: undefined,
  695. isSelf: 0,
  696. productReviewStatus: 0,
  697. homeRecommended: 0,
  698. categoryRecommendation: 0,
  699. cartRecommendation: 0,
  700. recommendedProductOrder: 0,
  701. isPopular: 0,
  702. isNew: 0,
  703. productStatus: '0',
  704. remark: undefined,
  705. a10ProductName: undefined,
  706. specificationsCode: undefined,
  707. barCoding: undefined,
  708. invoiceName: undefined,
  709. invoiceSpecs: undefined,
  710. packagingSpec: undefined,
  711. referenceLink: undefined,
  712. productWeight: undefined,
  713. weightUnit: 'kg',
  714. productVolume: undefined,
  715. volumeUnit: 'm3',
  716. productDescription: undefined,
  717. mainLibraryIntro: undefined,
  718. afterSalesService: undefined,
  719. serviceGuarantee: undefined, // 服务保障ID列表,逗号分隔
  720. freeInstallation: '0',
  721. marketPrice: undefined,
  722. memberPrice: undefined,
  723. minSellingPrice: undefined,
  724. purchasingPrice: undefined,
  725. maxPurchasePrice: undefined,
  726. supplyPrice: undefined,
  727. supplyValidityPeriod: undefined,
  728. supplyPostStatus: undefined,
  729. productNature: '1',
  730. purchasingPersonnel: '1',
  731. pcDetail: undefined,
  732. mobileDetail: undefined,
  733. taxRate: undefined,
  734. currency: 'RMB',
  735. minOrderQuantity: undefined,
  736. salesVolume: undefined
  737. });
  738. // 表单验证规则
  739. const productRules = {
  740. // productNo: [{ required: true, message: '商品编号不能为空', trigger: 'blur' }],
  741. itemName: [{ required: true, message: '商品名称不能为空', trigger: 'blur' }],
  742. brandId: [{ required: true, message: '商品品牌不能为空', trigger: 'change' }],
  743. // mainLibraryIntro: [{ required: true, message: '主供应商不能为空', trigger: 'change' }],
  744. marketPrice: [{ required: true, message: '市场价不能为空', trigger: 'blur' }],
  745. memberPrice: [{ required: true, message: '平台售价不能为空', trigger: 'blur' }],
  746. minSellingPrice: [{ required: true, message: '最低售价不能为空', trigger: 'blur' }],
  747. purchasingPrice: [{ required: true, message: '采购价不能为空', trigger: 'blur' }],
  748. productNature: [{ required: true, message: '产品经理不能为空', trigger: 'change' }],
  749. purchasingPersonnel: [{ required: true, message: '采购人员不能为空', trigger: 'change' }],
  750. taxRate: [{ required: true, message: '税率不能为空', trigger: 'change' }],
  751. minOrderQuantity: [{ required: true, message: '最低起订量不能为空', trigger: 'blur' }]
  752. };
  753. // 分类和品牌选项
  754. const categoryOptions = ref<categoryTreeVO[]>([]);
  755. const brandOptions = ref<BrandVO[]>([]);
  756. const brandLoading = ref(false);
  757. let brandSearchTimer: ReturnType<typeof setTimeout> | null = null;
  758. // 商品属性列表
  759. const attributesList = ref<AttributesVO[]>([]);
  760. const productAttributesValues = ref<Record<string | number, any>>({});
  761. // 售后服务和服务保障选项
  762. const afterSalesOptions = ref<any[]>([]);
  763. const serviceGuaranteeOptions = ref<any[]>([]);
  764. // 单位选项
  765. const unitOptions = ref<any[]>([]);
  766. // 主供应商选项
  767. const supplierOptions = ref<InfoVO[]>([]);
  768. // 自定义属性列表
  769. const diyAttributesList = ref<ClassificationDiyForm[]>([]);
  770. // 添加自定义属性行
  771. const addDiyAttribute = () => {
  772. diyAttributesList.value.push({ attributeKey: '', attributeValue: '' });
  773. };
  774. // 删除自定义属性行
  775. const removeDiyAttribute = (index: number) => {
  776. diyAttributesList.value.splice(index, 1);
  777. };
  778. // 采购人员选项
  779. const staffOptions = ref<ComStaffVO[]>([]);
  780. // 搜索关键词
  781. const searchLevel1 = ref('');
  782. const searchLevel2 = ref('');
  783. const searchLevel3 = ref('');
  784. // 三级分类下拉搜索
  785. const level3SearchValue = ref<string | number | null>(null);
  786. const level3SearchOptions = ref<CategoryVO[]>([]);
  787. const level3SearchLoading = ref(false);
  788. // 一级分类列表
  789. const level1Categories = computed(() => {
  790. return categoryOptions.value || [];
  791. });
  792. // 二级分类列表
  793. const level2Categories = ref<categoryTreeVO[]>([]);
  794. // 三级分类列表
  795. const level3Categories = ref<categoryTreeVO[]>([]);
  796. // 过滤后的一级分类列表
  797. const filteredLevel1Categories = computed(() => {
  798. if (!searchLevel1.value) {
  799. return level1Categories.value;
  800. }
  801. return level1Categories.value.filter((item) => item.label.toLowerCase().includes(searchLevel1.value.toLowerCase()));
  802. });
  803. // 过滤后的二级分类列表
  804. const filteredLevel2Categories = computed(() => {
  805. if (!searchLevel2.value) {
  806. return level2Categories.value;
  807. }
  808. return level2Categories.value.filter((item) => item.label.toLowerCase().includes(searchLevel2.value.toLowerCase()));
  809. });
  810. // 过滤后的三级分类列表
  811. const filteredLevel3Categories = computed(() => {
  812. if (!searchLevel3.value) {
  813. return level3Categories.value;
  814. }
  815. return level3Categories.value.filter((item) => item.label.toLowerCase().includes(searchLevel3.value.toLowerCase()));
  816. });
  817. // 搜索三级分类(调用接口)
  818. const handleLevel3Search = async (keyword: string) => {
  819. if (!keyword) {
  820. level3SearchOptions.value = [];
  821. return;
  822. }
  823. level3SearchLoading.value = true;
  824. try {
  825. const res = await categoryList({ classLevel: 3, categoryName: keyword, pageNum: 1, pageSize: 50 });
  826. level3SearchOptions.value = (res as any).data || (res as any).rows || [];
  827. } catch (error) {
  828. console.error('搜索三级分类失败:', error);
  829. } finally {
  830. level3SearchLoading.value = false;
  831. }
  832. };
  833. // 选择三级分类搜索结果后,自动在树中定位
  834. const handleLevel3SearchSelect = async (categoryId: string | number) => {
  835. if (!categoryId) return;
  836. const selectedCategory = level3SearchOptions.value.find((item) => String(item.id) === String(categoryId));
  837. if (!selectedCategory) return;
  838. // 在分类树中查找对应的二级节点(三级的父节点)
  839. const level2Node = findCategoryById(categoryOptions.value, selectedCategory.parentId);
  840. if (!level2Node) return;
  841. // 在一级列表中查找(二级的父节点)
  842. const level1Node = level1Categories.value.find((item) => String(item.id) === String(level2Node.parentId));
  843. if (!level1Node) return;
  844. // 依次选中一级、二级、三级
  845. categoryForm.topCategoryId = level1Node.id;
  846. selectedLevel1Name.value = level1Node.label;
  847. level2Categories.value = level1Node.children || [];
  848. await nextTick();
  849. categoryForm.mediumCategoryId = level2Node.id;
  850. selectedLevel2Name.value = level2Node.label;
  851. level3Categories.value = level2Node.children || [];
  852. await nextTick();
  853. // 精确查找三级节点
  854. const level3Node = level3Categories.value.find((item) => String(item.id) === String(selectedCategory.id));
  855. if (level3Node) {
  856. categoryForm.bottomCategoryId = level3Node.id;
  857. selectedLevel3Name.value = level3Node.label;
  858. await loadCategoryAttributes(level3Node.id);
  859. } else {
  860. categoryForm.bottomCategoryId = selectedCategory.id;
  861. selectedLevel3Name.value = selectedCategory.categoryName;
  862. await loadCategoryAttributes(selectedCategory.id);
  863. }
  864. // 清空搜索框
  865. level3SearchValue.value = null;
  866. level3SearchOptions.value = [];
  867. };
  868. // 选中的分类名称
  869. const selectedLevel1Name = ref('');
  870. const selectedLevel2Name = ref('');
  871. const selectedLevel3Name = ref('');
  872. // 选择一级分类
  873. const selectLevel1 = (item: categoryTreeVO) => {
  874. if (!item.children || item.children.length === 0) {
  875. ElMessage.warning('该分类无子分类,请选择含三级分类的类别');
  876. return;
  877. }
  878. categoryForm.topCategoryId = item.id;
  879. categoryForm.mediumCategoryId = undefined;
  880. categoryForm.bottomCategoryId = undefined;
  881. selectedLevel1Name.value = item.label;
  882. selectedLevel2Name.value = '';
  883. selectedLevel3Name.value = '';
  884. level2Categories.value = item.children || [];
  885. level3Categories.value = [];
  886. };
  887. // 选择二级分类
  888. const selectLevel2 = (item: categoryTreeVO) => {
  889. if (!item.children || item.children.length === 0) {
  890. ElMessage.warning('该分类无子分类,请选择含三级分类的类别');
  891. return;
  892. }
  893. categoryForm.mediumCategoryId = item.id;
  894. categoryForm.bottomCategoryId = undefined;
  895. selectedLevel2Name.value = item.label;
  896. selectedLevel3Name.value = '';
  897. level3Categories.value = item.children || [];
  898. };
  899. // 选择三级分类
  900. const selectLevel3 = async (item: categoryTreeVO) => {
  901. categoryForm.bottomCategoryId = item.id;
  902. selectedLevel3Name.value = item.label;
  903. // 加载该分类下的属性列表
  904. await loadCategoryAttributes(item.id);
  905. };
  906. // 获取分类路径
  907. const getCategoryPath = () => {
  908. const parts = [];
  909. if (selectedLevel1Name.value) parts.push(selectedLevel1Name.value);
  910. if (selectedLevel2Name.value) parts.push(selectedLevel2Name.value);
  911. if (selectedLevel3Name.value) parts.push(selectedLevel3Name.value);
  912. return parts.join(' > ') || '请选择分类';
  913. };
  914. // 清除分类
  915. const clearCategory = () => {
  916. categoryForm.topCategoryId = undefined;
  917. categoryForm.mediumCategoryId = undefined;
  918. categoryForm.bottomCategoryId = undefined;
  919. selectedLevel1Name.value = '';
  920. selectedLevel2Name.value = '';
  921. selectedLevel3Name.value = '';
  922. level2Categories.value = [];
  923. level3Categories.value = [];
  924. attributesList.value = [];
  925. productAttributesValues.value = {};
  926. };
  927. // 下一步
  928. const nextStep = async () => {
  929. if (currentStep.value === 0) {
  930. // 验证分类选择
  931. if (!categoryForm.topCategoryId) {
  932. ElMessage.warning('请选择一级分类');
  933. return;
  934. }
  935. if (!categoryForm.mediumCategoryId) {
  936. ElMessage.warning('请选择二级分类');
  937. return;
  938. }
  939. if (!categoryForm.bottomCategoryId) {
  940. ElMessage.warning('请选择三级分类');
  941. return;
  942. }
  943. // 将分类信息同步到商品表单
  944. productForm.topCategoryId = categoryForm.topCategoryId;
  945. productForm.mediumCategoryId = categoryForm.mediumCategoryId;
  946. productForm.bottomCategoryId = categoryForm.bottomCategoryId;
  947. currentStep.value++;
  948. } else if (currentStep.value === 1) {
  949. // 验证商品信息表单并提交
  950. try {
  951. await productFormRef.value?.validate();
  952. // 调用提交函数
  953. await handleSubmit();
  954. } catch (error) {
  955. ElMessage.warning('请完善商品信息');
  956. return;
  957. }
  958. }
  959. };
  960. // 上一步
  961. const prevStep = () => {
  962. if (currentStep.value > 0) {
  963. currentStep.value--;
  964. }
  965. };
  966. // 提交
  967. const handleSubmit = async () => {
  968. try {
  969. submitLoading.value = true;
  970. // 校验商品主图、轮播图、详情必填
  971. if (!productForm.productImage) {
  972. ElMessage.warning('请上传商品主图');
  973. submitLoading.value = false;
  974. return;
  975. }
  976. if (!carouselImages.value || carouselImages.value.length === 0) {
  977. ElMessage.warning('请上传商品轮播图');
  978. submitLoading.value = false;
  979. return;
  980. }
  981. if (!productForm.pcDetail) {
  982. ElMessage.warning('请填写电脑端商品详情');
  983. submitLoading.value = false;
  984. return;
  985. }
  986. // 校验价格关系:市场价 > 官网价 > 最低售价
  987. const midRange = parseFloat(String(productForm.marketPrice));
  988. const standard = parseFloat(String(productForm.memberPrice));
  989. const certificate = parseFloat(String(productForm.minSellingPrice));
  990. if (!isNaN(midRange) && !isNaN(standard) && !isNaN(certificate)) {
  991. if (!(midRange > standard)) {
  992. ElMessage.warning('市场价必须大于官网价');
  993. submitLoading.value = false;
  994. return;
  995. }
  996. if (!(standard > certificate)) {
  997. ElMessage.warning('官网价必须大于最低售价');
  998. submitLoading.value = false;
  999. return;
  1000. }
  1001. }
  1002. // 准备提交数据,包含定制信息(A10产品名称由前端自动拼接,不传后端)
  1003. const submitProductData: any = {
  1004. ...productForm,
  1005. // 将服务保障ID数组转换为逗号分隔字符串
  1006. serviceGuarantee: serviceGuarantees.value.map((id) => String(id)).join(','),
  1007. // 轮播图URL逗号分隔
  1008. imageUrl: carouselImages.value.join(','),
  1009. // 将商品属性值转换为JSON字符串
  1010. attributesList: JSON.stringify(productAttributesValues.value),
  1011. isCustomize: customForm.isCustomize ? 1 : 0,
  1012. customizedStyle: customForm.selectedMethods.join(','),
  1013. customizedCraft: customForm.selectedCrafts.join(','),
  1014. customDescription: customForm.customDescription,
  1015. customDetailsJson: JSON.stringify(customForm.customDetails),
  1016. diyAttributesList: diyAttributesList.value.filter((item) => item.attributeKey || item.attributeValue)
  1017. };
  1018. // A10产品名称不传后端
  1019. delete submitProductData.a10ProductName;
  1020. const auditData: BaseAuditForm = {
  1021. id: route.params.id as string,
  1022. productData: JSON.stringify(submitProductData),
  1023. type: 0,
  1024. auditStatus: 0
  1025. };
  1026. if (auditData.id) {
  1027. await updateBaseAudit(auditData);
  1028. ElMessage.success('修改成功');
  1029. } else {
  1030. await addBaseAudit(auditData);
  1031. ElMessage.success('新增成功');
  1032. }
  1033. // 跳转到完成页面(步骤3)
  1034. currentStep.value = 2;
  1035. } catch (error) {
  1036. console.error('提交失败:', error);
  1037. } finally {
  1038. submitLoading.value = false;
  1039. }
  1040. };
  1041. // 返回
  1042. const handleBack = () => {
  1043. router.back();
  1044. };
  1045. // 返回列表
  1046. const handleBackToList = () => {
  1047. router.push('/product/base');
  1048. };
  1049. // UPC(69)条码只允许输入数字
  1050. const handleUpcInput = () => {
  1051. if (productForm.barCoding) {
  1052. productForm.barCoding = productForm.barCoding.replace(/\D/g, '');
  1053. }
  1054. };
  1055. // 销量人气只允许输入整数
  1056. const handleSalesVolumeInput = (val: string) => {
  1057. if (val !== undefined && val !== null && val !== '') {
  1058. const intVal = parseInt(String(val).replace(/[^\d]/g, ''), 10);
  1059. productForm.salesVolume = !isNaN(intVal) ? intVal : undefined;
  1060. } else {
  1061. productForm.salesVolume = undefined;
  1062. }
  1063. };
  1064. // A10产品名称自动拼接(品牌名 + 规格型号 + 产品分类 + 发票规格)
  1065. const a10ProductNameComputed = computed(() => {
  1066. const brand = brandOptions.value.find((b) => Number(b.id) === Number(productForm.brandId));
  1067. const brandName = brand?.brandName || '';
  1068. const specificationsCode = productForm.specificationsCode || '';
  1069. const categoryName = selectedLevel3Name.value || '';
  1070. const invoiceSpecs = productForm.invoiceSpecs || '';
  1071. return [brandName, specificationsCode, categoryName, invoiceSpecs].filter((s) => s.trim()).join(' ');
  1072. });
  1073. // 格式化价格为两位小数(不允许负数)
  1074. const formatPrice = (field: string) => {
  1075. const val = (productForm as any)[field];
  1076. if (val !== undefined && val !== null && val !== '') {
  1077. let num = parseFloat(String(val));
  1078. if (!isNaN(num)) {
  1079. // 不允许负数
  1080. if (num < 0) num = 0;
  1081. (productForm as any)[field] = num.toFixed(2);
  1082. }
  1083. }
  1084. };
  1085. // 格式化表格行中的价格为两位小数(不允许负数)
  1086. const formatRowPrice = (row: any, field: string) => {
  1087. const val = row[field];
  1088. if (val !== undefined && val !== null && val !== '') {
  1089. let num = parseFloat(String(val));
  1090. if (!isNaN(num)) {
  1091. // 不允许负数
  1092. if (num < 0) num = 0;
  1093. row[field] = num.toFixed(2);
  1094. }
  1095. }
  1096. };
  1097. // 获取分类树
  1098. const getCategoryTree = async () => {
  1099. try {
  1100. const res = await categoryTree();
  1101. categoryOptions.value = res.data || [];
  1102. } catch (error) {
  1103. console.error('获取分类树失败:', error);
  1104. }
  1105. };
  1106. // 加载品牌选项(默认100条)
  1107. const loadBrandOptions = async (keyword?: string) => {
  1108. brandLoading.value = true;
  1109. try {
  1110. const res = await listBrand({ pageNum: 1, pageSize: 100, brandName: keyword });
  1111. const newList = res.rows || [];
  1112. // 编辑模式下保留当前选中的品牌,避免被新列表覆盖后 A10产品名称 computed 失效
  1113. if (productForm.brandId) {
  1114. const exists = newList.find((item) => Number(item.id) === Number(productForm.brandId));
  1115. if (!exists) {
  1116. const currentBrand = brandOptions.value.find((item) => Number(item.id) === Number(productForm.brandId));
  1117. if (currentBrand) {
  1118. newList.unshift(currentBrand);
  1119. }
  1120. }
  1121. }
  1122. brandOptions.value = newList;
  1123. } catch (error) {
  1124. console.error('加载品牌列表失败:', error);
  1125. } finally {
  1126. brandLoading.value = false;
  1127. }
  1128. };
  1129. // 品牌远程搜索(防抖)
  1130. const handleBrandSearch = (query: string) => {
  1131. if (brandSearchTimer) clearTimeout(brandSearchTimer);
  1132. brandSearchTimer = setTimeout(() => {
  1133. loadBrandOptions(query || undefined);
  1134. }, 300);
  1135. };
  1136. // 处理品牌下拉框显示/隐藏
  1137. const handleBrandVisibleChange = (visible: boolean) => {
  1138. if (visible && brandOptions.value.length === 0) {
  1139. loadBrandOptions();
  1140. }
  1141. };
  1142. // 获取售后服务列表
  1143. const getAfterSalesOptions = async () => {
  1144. try {
  1145. const res = await getAfterSaleList();
  1146. afterSalesOptions.value = res.data || [];
  1147. // 如果是新增模式且有选项,设置第一个为默认值
  1148. if (!route.params.id && afterSalesOptions.value.length > 0 && !productForm.afterSalesService) {
  1149. productForm.afterSalesService = afterSalesOptions.value[0].id;
  1150. }
  1151. } catch (error) {
  1152. console.error('获取售后服务列表失败:', error);
  1153. }
  1154. };
  1155. // 获取服务保障列表
  1156. const getServiceGuaranteeOptions = async () => {
  1157. try {
  1158. const res = await getServiceList();
  1159. serviceGuaranteeOptions.value = res.data || [];
  1160. // 如果是新增模式且有选项,设置第一个为默认选中
  1161. if (!route.params.id && serviceGuaranteeOptions.value.length > 0 && serviceGuarantees.value.length === 0) {
  1162. serviceGuarantees.value = [serviceGuaranteeOptions.value[0].id];
  1163. }
  1164. } catch (error) {
  1165. console.error('获取服务保障列表失败:', error);
  1166. }
  1167. };
  1168. // 获取单位列表
  1169. const getUnitOptions = async () => {
  1170. try {
  1171. const res = await getUnitList();
  1172. unitOptions.value = res.data || [];
  1173. // 如果是新增模式且有选项,设置第一个为默认值
  1174. if (!route.params.id && unitOptions.value.length > 0 && !productForm.unitId) {
  1175. productForm.unitId = unitOptions.value[0].id;
  1176. }
  1177. } catch (error) {
  1178. console.error('获取单位列表失败:', error);
  1179. }
  1180. };
  1181. // 获取主供应商列表
  1182. const getSupplierOptions = async () => {
  1183. try {
  1184. const res = await listInfo();
  1185. console.log('供应商接口返回:', res);
  1186. // 处理可能的数据结构: res.data 或 res.rows
  1187. const dataList = res.data || res.rows || [];
  1188. supplierOptions.value = dataList;
  1189. console.log('供应商列表:', supplierOptions.value);
  1190. // 如果有选项且当前没有选中值,设置第一个为默认值
  1191. if (supplierOptions.value.length > 0 && !productForm.mainLibraryIntro) {
  1192. productForm.mainLibraryIntro = String(supplierOptions.value[0].id);
  1193. }
  1194. } catch (error) {
  1195. console.error('获取主供应商列表失败:', error);
  1196. }
  1197. };
  1198. // 获取采购人员列表
  1199. const getStaffOptions = async () => {
  1200. try {
  1201. const res = await listComStaff();
  1202. console.log('采购人员接口返回:', res);
  1203. // 处理可能的数据结构: res.data 或 res.rows
  1204. const dataList = res.data || res.rows || [];
  1205. staffOptions.value = dataList;
  1206. console.log('采购人员列表:', staffOptions.value);
  1207. // 如果有选项且当前没有选中值,设置第一个为默认值
  1208. if (staffOptions.value.length > 0 && !productForm.purchasingPersonnel) {
  1209. productForm.purchasingPersonnel = String(staffOptions.value[0].staffId);
  1210. }
  1211. } catch (error) {
  1212. console.error('获取采购人员列表失败:', error);
  1213. }
  1214. };
  1215. // 获取税率列表
  1216. const getTaxRateOptions = async () => {
  1217. try {
  1218. const res = await getTaxRateList();
  1219. taxRateOptions.value = res.rows || [];
  1220. } catch (error) {
  1221. console.error('获取税率列表失败:', error);
  1222. }
  1223. };
  1224. // 加载分类属性列表
  1225. const loadCategoryAttributes = async (categoryId: string | number) => {
  1226. try {
  1227. const res = await categoryAttributeList(categoryId);
  1228. attributesList.value = res.data || [];
  1229. // 清空之前的属性值
  1230. productAttributesValues.value = {};
  1231. // 如果是新增模式,为有选项的属性设置默认值
  1232. if (!route.params.id) {
  1233. attributesList.value.forEach((attr) => {
  1234. if (attr.entryMethod === '1' && attr.attributesList) {
  1235. // 下拉选择
  1236. const options = parseAttributesList(attr.attributesList);
  1237. if (options.length > 0) {
  1238. productAttributesValues.value[attr.id] = options[0];
  1239. }
  1240. } else if (attr.entryMethod === '3' && attr.attributesList) {
  1241. // 多选
  1242. const options = parseAttributesList(attr.attributesList);
  1243. if (options.length > 0) {
  1244. productAttributesValues.value[attr.id] = [options[0]];
  1245. }
  1246. }
  1247. });
  1248. }
  1249. } catch (error) {
  1250. console.error('加载分类属性失败:', error);
  1251. attributesList.value = [];
  1252. }
  1253. };
  1254. // 解析属性值列表(JSON数组或逗号分隔字符串)
  1255. const parseAttributesList = (attributesListStr: string): string[] => {
  1256. if (!attributesListStr) return [];
  1257. try {
  1258. // 尝试解析为JSON数组
  1259. const parsed = JSON.parse(attributesListStr);
  1260. if (Array.isArray(parsed)) {
  1261. return parsed;
  1262. }
  1263. } catch (e) {
  1264. // 如果不是JSON,按逗号分隔
  1265. return attributesListStr
  1266. .split(',')
  1267. .map((item) => item.trim())
  1268. .filter((item) => item);
  1269. }
  1270. return [];
  1271. };
  1272. // 加载商品详情(编辑模式)
  1273. const loadProductDetail = async () => {
  1274. const id = route.params.id;
  1275. if (id) {
  1276. try {
  1277. loading.value = true;
  1278. const res = await getBaseAudit(id as string);
  1279. Object.assign(productForm, res.data.productBaseVo);
  1280. // 回显产品经理 - 确保转换为字符串类型以匹配下拉框的value
  1281. if (res.data.productBaseVo.productNature !== undefined && res.data.productBaseVo.productNature !== null) {
  1282. productForm.productNature = String(res.data.productBaseVo.productNature);
  1283. }
  1284. // 回显采购人员 - 确保转换为字符串类型以匹配下拉框的value
  1285. if (res.data.productBaseVo.purchasingPersonnel !== undefined && res.data.productBaseVo.purchasingPersonnel !== null) {
  1286. productForm.purchasingPersonnel = String(res.data.productBaseVo.purchasingPersonnel);
  1287. }
  1288. // 回显税率编码显示值
  1289. const rawData = res.data.productBaseVo as any;
  1290. // 通过 taxationId 调接口获取中文名称回显
  1291. if (rawData.taxationId) {
  1292. try {
  1293. const taxRes = await getTaxCode(rawData.taxationId);
  1294. if (taxRes.data) {
  1295. taxCodeNo.value = `${taxRes.data.taxationNo},${taxRes.data.name}`;
  1296. }
  1297. } catch (e) {
  1298. console.error('获取税率编码失败:', e);
  1299. }
  1300. }
  1301. // 回显税率 - 在税率选项中查找匹配的值(处理浮点数精度问题)
  1302. if (res.data.productBaseVo.taxRate !== undefined && res.data.productBaseVo.taxRate !== null) {
  1303. const apiTaxRate = Number(res.data.productBaseVo.taxRate);
  1304. // 使用精度容差比较浮点数
  1305. const matchedOption = taxRateOptions.value.find((opt) => Math.abs(Number(opt.taxrate) - apiTaxRate) < 0.0001);
  1306. if (matchedOption) {
  1307. productForm.taxRate = matchedOption.taxrate;
  1308. } else {
  1309. productForm.taxRate = apiTaxRate;
  1310. }
  1311. }
  1312. // 回显单位 - 确保类型与下拉选项的id一致(数字类型)
  1313. if (res.data.productBaseVo.unitId !== undefined && res.data.productBaseVo.unitId !== null) {
  1314. productForm.unitId = Number(res.data.productBaseVo.unitId);
  1315. }
  1316. // 回显品牌 - 先加载对应的品牌信息到选项列表中
  1317. if (res.data.productBaseVo.brandId) {
  1318. productForm.brandId = Number(res.data.productBaseVo.brandId);
  1319. try {
  1320. const brandRes = await getBrand(res.data.productBaseVo.brandId);
  1321. if (brandRes.data) {
  1322. // 检查品牌是否已在选项列表中
  1323. const existBrand = brandOptions.value.find((item) => Number(item.id) === Number(res.data.productBaseVo.brandId));
  1324. if (!existBrand) {
  1325. brandOptions.value.unshift(brandRes.data);
  1326. }
  1327. }
  1328. } catch (e) {
  1329. console.error('加载品牌信息失败:', e);
  1330. }
  1331. }
  1332. // 回显售后服务 - 确保类型与下拉选项的id一致(数字类型)
  1333. if (res.data.productBaseVo.afterSalesService !== undefined && res.data.productBaseVo.afterSalesService !== null) {
  1334. productForm.afterSalesService = Number(res.data.productBaseVo.afterSalesService);
  1335. }
  1336. // 回显轮播图
  1337. if (res.data.productBaseVo.imageUrl) {
  1338. carouselImages.value = res.data.productBaseVo.imageUrl.split(',').filter((url: string) => url.trim());
  1339. } else {
  1340. carouselImages.value = [];
  1341. }
  1342. // 回显分类选择
  1343. categoryForm.topCategoryId = res.data.productBaseVo.topCategoryId;
  1344. categoryForm.mediumCategoryId = res.data.productBaseVo.mediumCategoryId;
  1345. categoryForm.bottomCategoryId = res.data.productBaseVo.bottomCategoryId;
  1346. // 回显服务保障复选框 - 将逗号分隔的ID字符串转换为数组
  1347. if (res.data.productBaseVo.serviceGuarantee) {
  1348. serviceGuarantees.value = res.data.productBaseVo.serviceGuarantee.split(',').map((id: string) => {
  1349. // 尝试转换为数字,如果失败则保持字符串
  1350. const numId = Number(id.trim());
  1351. return isNaN(numId) ? id.trim() : numId;
  1352. });
  1353. } else {
  1354. serviceGuarantees.value = [];
  1355. }
  1356. // 回显安装服务复选框
  1357. const services: string[] = [];
  1358. if (res.data.productBaseVo.freeInstallation === '1') services.push('freeInstallation');
  1359. installationServices.value = services;
  1360. // 回显分类名称 - 使用nextTick确保DOM更新后再查找子分类
  1361. await restoreCategorySelection();
  1362. // 回显商品属性值(必须在restoreCategorySelection之后,避免loadCategoryAttributes清空属性值)
  1363. if (res.data.productBaseVo.attributesList) {
  1364. try {
  1365. const parsedAttributes = JSON.parse(res.data.productBaseVo.attributesList);
  1366. productAttributesValues.value = parsedAttributes;
  1367. } catch (e) {
  1368. console.error('解析商品属性失败:', e);
  1369. productAttributesValues.value = {};
  1370. }
  1371. }
  1372. // 回显自定义属性列表
  1373. const rawResData = res.data as any;
  1374. if (Array.isArray(rawResData.productBaseVo.diyAttributesList) && rawResData.productBaseVo.diyAttributesList.length > 0) {
  1375. diyAttributesList.value = rawResData.productBaseVo.diyAttributesList;
  1376. } else {
  1377. diyAttributesList.value = [];
  1378. }
  1379. // 回显定制说明相关字段
  1380. const baseVo = res.data.productBaseVo as any;
  1381. // 可定制开关
  1382. customForm.isCustomize = Number(baseVo.isCustomize) === 1;
  1383. // 定制方式(逗号分隔字符串 → 数组)
  1384. if (baseVo.customizedStyle) {
  1385. customForm.selectedMethods = String(baseVo.customizedStyle)
  1386. .split(',')
  1387. .map((s: string) => s.trim())
  1388. .filter((s: string) => s);
  1389. } else {
  1390. customForm.selectedMethods = [];
  1391. }
  1392. // 定制工艺(逗号分隔字符串 → 数组)
  1393. if (baseVo.customizedCraft) {
  1394. customForm.selectedCrafts = String(baseVo.customizedCraft)
  1395. .split(',')
  1396. .map((s: string) => s.trim())
  1397. .filter((s: string) => s);
  1398. } else {
  1399. customForm.selectedCrafts = [];
  1400. }
  1401. // 定制说明
  1402. customForm.customDescription = baseVo.customDescription || '';
  1403. // 定制详情表格(JSON 字符串 → 数组),必须在 selectedMethods/selectedCrafts 赋值后覆盖,避免 watch 重建覆盖填写值
  1404. await nextTick();
  1405. if (baseVo.customDetailsJson) {
  1406. try {
  1407. const parsedDetails = JSON.parse(baseVo.customDetailsJson);
  1408. if (Array.isArray(parsedDetails)) {
  1409. customForm.customDetails = parsedDetails;
  1410. }
  1411. } catch (e) {
  1412. console.error('解析定制详情失败:', e);
  1413. }
  1414. }
  1415. } catch (error) {
  1416. console.error('加载商品详情失败:', error);
  1417. ElMessage.error('加载商品详情失败');
  1418. } finally {
  1419. loading.value = false;
  1420. }
  1421. }
  1422. };
  1423. // 递归查找分类节点
  1424. const findCategoryById = (categories: categoryTreeVO[], id: string | number): categoryTreeVO | null => {
  1425. for (const category of categories) {
  1426. if (String(category.id) === String(id)) {
  1427. return category;
  1428. }
  1429. if (category.children && category.children.length > 0) {
  1430. const found = findCategoryById(category.children, id);
  1431. if (found) return found;
  1432. }
  1433. }
  1434. return null;
  1435. };
  1436. // 恢复分类选择状态
  1437. const restoreCategorySelection = async () => {
  1438. // 先保存原始的分类ID值
  1439. const originalTopCategoryId = categoryForm.topCategoryId;
  1440. const originalMediumCategoryId = categoryForm.mediumCategoryId;
  1441. const originalBottomCategoryId = categoryForm.bottomCategoryId;
  1442. console.log('回显分类 - 原始ID:', {
  1443. top: originalTopCategoryId,
  1444. medium: originalMediumCategoryId,
  1445. bottom: originalBottomCategoryId
  1446. });
  1447. if (!originalTopCategoryId) return;
  1448. // 查找一级分类
  1449. const level1 = level1Categories.value.find((item) => String(item.id) === String(originalTopCategoryId));
  1450. console.log('查找一级分类:', level1);
  1451. if (!level1) return;
  1452. // 设置一级分类选中状态
  1453. categoryForm.topCategoryId = level1.id;
  1454. selectedLevel1Name.value = level1.label;
  1455. level2Categories.value = level1.children || [];
  1456. await nextTick();
  1457. // 查找二级分类
  1458. if (originalMediumCategoryId) {
  1459. // 先在当前一级分类的children中查找
  1460. let level2 = level2Categories.value.find((item) => String(item.id) === String(originalMediumCategoryId));
  1461. // 如果找不到,尝试在整个分类树中查找(容错处理)
  1462. if (!level2) {
  1463. console.log('二级分类在当前一级下未找到,尝试全局查找...');
  1464. level2 = findCategoryById(categoryOptions.value, originalMediumCategoryId);
  1465. }
  1466. console.log('查找二级分类:', level2);
  1467. if (level2) {
  1468. categoryForm.mediumCategoryId = level2.id;
  1469. selectedLevel2Name.value = level2.label;
  1470. level3Categories.value = level2.children || [];
  1471. await nextTick();
  1472. // 查找三级分类
  1473. if (originalBottomCategoryId) {
  1474. // 先在当前二级分类的children中查找
  1475. let level3 = level3Categories.value.find((item) => String(item.id) === String(originalBottomCategoryId));
  1476. // 如果找不到,尝试在整个分类树中查找(容错处理)
  1477. if (!level3) {
  1478. console.log('三级分类在当前二级下未找到,尝试全局查找...');
  1479. level3 = findCategoryById(categoryOptions.value, originalBottomCategoryId);
  1480. }
  1481. console.log('查找三级分类:', level3, '原始ID:', originalBottomCategoryId);
  1482. if (level3) {
  1483. categoryForm.bottomCategoryId = level3.id;
  1484. selectedLevel3Name.value = level3.label;
  1485. console.log('设置三级分类名称:', selectedLevel3Name.value);
  1486. await loadCategoryAttributes(level3.id);
  1487. }
  1488. }
  1489. }
  1490. }
  1491. };
  1492. onMounted(async () => {
  1493. // 编辑模式下先直接跳到第二步,再加载数据,避免闪烁步骤一
  1494. if (route.params.id) {
  1495. currentStep.value = 1;
  1496. }
  1497. await getCategoryTree();
  1498. await getUnitOptions();
  1499. await getAfterSalesOptions();
  1500. await getServiceGuaranteeOptions();
  1501. await getTaxRateOptions();
  1502. // 先加载商品详情(如果是编辑模式)
  1503. await loadProductDetail();
  1504. // 再加载下拉选项,这样如果详情中没有值,会自动设置第一个
  1505. await getSupplierOptions();
  1506. await getStaffOptions();
  1507. loadBrandOptions();
  1508. });
  1509. </script>
  1510. <style scoped lang="scss">
  1511. .view-scroll-container {
  1512. height: calc(100vh - 84px);
  1513. overflow-y: auto;
  1514. overflow-x: hidden;
  1515. }
  1516. .view-header-card {
  1517. position: sticky;
  1518. top: 0;
  1519. z-index: 10;
  1520. }
  1521. .product-wizard-page {
  1522. .category-selection {
  1523. margin-top: 12px;
  1524. }
  1525. .category-box {
  1526. border: 1px solid #e4e7ed;
  1527. border-radius: 4px;
  1528. overflow: hidden;
  1529. .category-header {
  1530. background-color: #f5f7fa;
  1531. padding: 10px 12px;
  1532. font-weight: 600;
  1533. border-bottom: 1px solid #e4e7ed;
  1534. text-align: center;
  1535. font-size: 14px;
  1536. }
  1537. .category-search {
  1538. padding: 10px;
  1539. border-bottom: 1px solid #e4e7ed;
  1540. background-color: #fff;
  1541. }
  1542. .category-list {
  1543. height: 280px;
  1544. overflow-y: auto;
  1545. .category-item {
  1546. padding: 10px 12px;
  1547. cursor: pointer;
  1548. display: flex;
  1549. justify-content: space-between;
  1550. align-items: center;
  1551. border-bottom: 1px solid #f0f0f0;
  1552. transition: all 0.3s;
  1553. &:hover {
  1554. background-color: #f5f7fa;
  1555. }
  1556. &.active {
  1557. background-color: #ecf5ff;
  1558. color: #409eff;
  1559. font-weight: 600;
  1560. }
  1561. &:last-child {
  1562. border-bottom: none;
  1563. }
  1564. &.disabled {
  1565. cursor: not-allowed;
  1566. opacity: 0.45;
  1567. pointer-events: none;
  1568. }
  1569. }
  1570. }
  1571. }
  1572. .confirm-info {
  1573. margin-top: 12px;
  1574. text-align: left;
  1575. }
  1576. .product-info-form {
  1577. .category-display {
  1578. display: flex;
  1579. align-items: center;
  1580. .category-text {
  1581. color: #606266;
  1582. }
  1583. }
  1584. .form-item-tip {
  1585. font-size: 12px;
  1586. color: #909399;
  1587. line-height: 1.5;
  1588. margin-top: 4px;
  1589. }
  1590. .currency-text {
  1591. color: #303133;
  1592. font-size: 14px;
  1593. }
  1594. }
  1595. .custom-options {
  1596. display: flex;
  1597. gap: 10px;
  1598. flex-wrap: wrap;
  1599. }
  1600. .custom-table {
  1601. width: 100%;
  1602. margin-top: 10px;
  1603. }
  1604. .view-disabled-wrapper {
  1605. pointer-events: none;
  1606. opacity: 0.9;
  1607. width: 100%;
  1608. }
  1609. }
  1610. </style>