view.vue 63 KB

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