add.vue 54 KB

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