add.vue 76 KB

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