add.vue 78 KB

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