index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <template>
  2. <el-dialog v-model="dialogVisible" :title="dialogTitle" :fullscreen="isFullScreen" :show-close="false" width="70%"
  3. draggable class="dialog">
  4. <template #header="{ close }">
  5. <div class="my-header">
  6. <div class="my-header-left">{{dialogTitle}}</div>
  7. <div class="my-header-right">
  8. <span @click="fullScreen">
  9. <Icon :icon="isFullScreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" />
  10. </span>
  11. <span @click="close">
  12. <el-icon>
  13. <Close />
  14. </el-icon>
  15. </span>
  16. </div>
  17. </div>
  18. </template>
  19. <ContentWrap v-loading="formLoading">
  20. <div class="left">
  21. <SPuUploadImg v-model="formData.picUrl" :disabled="isDetail" />
  22. <el-tabs v-model="activeName" tab-position="left" class="child-tabs">
  23. <el-tab-pane label="基本信息" name="info" />
  24. <el-tab-pane label="价格/规格/型号" name="sku" />
  25. <el-tab-pane label="详情" name="description" />
  26. <el-tab-pane label="评价" name="comment" />
  27. <el-tab-pane label="客服" name="service" />
  28. <el-tab-pane label="售后" name="aftersale" />
  29. <el-tab-pane label="物流设置" name="delivery" />
  30. <el-tab-pane label="其它设置" name="other" />
  31. </el-tabs>
  32. </div>
  33. <div class="right">
  34. <div v-show="activeName == 'info'">
  35. <InfoForm ref="infoRef" :is-detail="isDetail" :propFormData="formData" />
  36. </div>
  37. <div v-show="activeName == 'sku'">
  38. <SkuForm ref="skuRef" :is-detail="isDetail" :propFormData="formData" />
  39. </div>
  40. <div v-show="activeName == 'delivery'">
  41. <DeliveryForm ref="deliveryRef" :is-detail="isDetail" :propFormData="formData" />
  42. </div>
  43. <div v-show="activeName == 'description'">
  44. <DescriptionForm ref="descriptionRef" :is-detail="isDetail" :propFormData="formData" />
  45. </div>
  46. <div v-show="activeName == 'other'">
  47. <OtherForm ref="otherRef" :is-detail="isDetail" :propFormData="formData" />
  48. </div>
  49. </div>
  50. <el-form style="clear: both;">
  51. <el-form-item style="float: right">
  52. <!-- 不在回收站的才可以停用 -->
  53. <el-button v-if="!isDetail && parentTabType!=4 && openType != 'create'" :loading="formLoading"
  54. type="danger" plain @click="handleStatus02Change(ProductSpuStatusEnum.RECYCLE.status)">
  55. 停用
  56. </el-button>
  57. <!-- 在回收站的可以选择恢复或者删除 -->
  58. <el-button v-if="!isDetail && parentTabType==4" :loading="formLoading"
  59. v-hasPermi="['product:spu:delete']" type="danger" plain @click="handleDelete(productId)">
  60. 删除
  61. </el-button>
  62. <el-button v-if="!isDetail && parentTabType==4" :loading="formLoading"
  63. v-hasPermi="['product:spu:update']" type="primary"
  64. @click="handleStatus02Change(ProductSpuStatusEnum.DISABLE.status)">
  65. 恢复
  66. </el-button>
  67. <el-button v-if="!isDetail && openType != 'create'" :loading="formLoading" type="primary"
  68. @click="handleStatusChange">
  69. {{parentRow.status == 0 ?"上架":"下架"}}
  70. </el-button>
  71. <el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
  72. 保存
  73. </el-button>
  74. </el-form-item>
  75. </el-form>
  76. </ContentWrap>
  77. </el-dialog>
  78. <!-- </div> -->
  79. </template>
  80. <script lang="ts" setup>
  81. import { cloneDeep } from 'lodash-es'
  82. import { useTagsViewStore } from '@/store/modules/tagsView'
  83. import * as ProductSpuApi from '@/api/mall/product/spu'
  84. import InfoForm from './InfoForm.vue'
  85. import DescriptionForm from './DescriptionForm.vue'
  86. import OtherForm from './OtherForm.vue'
  87. import SkuForm from './SkuForm.vue'
  88. import DeliveryForm from './DeliveryForm.vue'
  89. import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
  90. import {
  91. Close,
  92. FullScreen
  93. } from '@element-plus/icons-vue'
  94. import { Icon } from '@/components/Icon'
  95. import { ProductSpuStatusEnum } from '@/utils/constants'
  96. defineOptions({ name: 'ProductSpuForm' })
  97. const { t } = useI18n() // 国际化
  98. const message = useMessage() // 消息弹窗
  99. const { push, currentRoute } = useRouter() // 路由
  100. const { params, name } = useRoute() // 查询参数
  101. const { delView } = useTagsViewStore() // 视图操作
  102. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  103. const activeName = ref('info') // Tag 激活的窗口
  104. const isDetail = ref(false) // 是否查看详情
  105. const infoRef = ref() // 商品信息 Ref
  106. const skuRef = ref() // 商品规格 Ref
  107. const deliveryRef = ref() // 物流设置 Ref
  108. const descriptionRef = ref() // 商品详情 Ref
  109. const otherRef = ref() // 其他设置 Ref
  110. // SPU 表单数据
  111. const formData = ref<ProductSpuApi.Spu>({
  112. name: '', // 商品名称
  113. categoryId: undefined, // 商品分类
  114. keyword: '', // 关键字
  115. picUrl: '', // 商品封面图
  116. sliderPicUrls: [], // 商品轮播图
  117. introduction: '', // 商品简介
  118. deliveryTypes: [], // 配送方式数组
  119. deliveryTemplateId: undefined, // 运费模版
  120. brandId: undefined, // 商品品牌
  121. specType: false, // 商品规格
  122. subCommissionType: false, // 分销类型
  123. skus: [
  124. {
  125. price: 0, // 商品价格
  126. marketPrice: 0, // 市场价
  127. costPrice: 0, // 成本价
  128. barCode: '', // 商品条码
  129. picUrl: '', // 图片地址
  130. stock: 0, // 库存
  131. weight: 0, // 商品重量
  132. volume: 0, // 商品体积
  133. firstBrokeragePrice: 0, // 一级分销的佣金
  134. secondBrokeragePrice: 0 // 二级分销的佣金
  135. }
  136. ],
  137. description: '', // 商品详情
  138. sort: 0, // 商品排序
  139. giveIntegral: 0, // 赠送积分
  140. virtualSalesCount: 0 // 虚拟销量
  141. })
  142. const addFormData = ref<ProductSpuApi.Spu>({
  143. name: '', // 商品名称
  144. categoryId: undefined, // 商品分类
  145. keyword: '', // 关键字
  146. picUrl: '', // 商品封面图
  147. sliderPicUrls: [], // 商品轮播图
  148. introduction: '', // 商品简介
  149. deliveryTypes: [], // 配送方式数组
  150. deliveryTemplateId: undefined, // 运费模版
  151. brandId: undefined, // 商品品牌
  152. specType: false, // 商品规格
  153. subCommissionType: false, // 分销类型
  154. skus: [
  155. {
  156. price: 0, // 商品价格
  157. marketPrice: 0, // 市场价
  158. costPrice: 0, // 成本价
  159. barCode: '', // 商品条码
  160. picUrl: '', // 图片地址
  161. stock: 0, // 库存
  162. weight: 0, // 商品重量
  163. volume: 0, // 商品体积
  164. firstBrokeragePrice: 0, // 一级分销的佣金
  165. secondBrokeragePrice: 0 // 二级分销的佣金
  166. }
  167. ],
  168. description: '', // 商品详情
  169. sort: 0, // 商品排序
  170. giveIntegral: 0, // 赠送积分
  171. virtualSalesCount: 0 // 虚拟销量
  172. })
  173. const dialogVisible = ref(false) // 弹窗的是否展示
  174. const dialogTitle = ref('') // 弹窗的标题
  175. const parentTabType = ref(0)
  176. const productId = ref()
  177. const picUrl = ref("")
  178. const parentRow = ref()
  179. const openType = ref("")
  180. const parentNewStatus = ref(0)
  181. const isFullScreen = ref(false)
  182. // const handleError = (e) => {
  183. // e.target.src = "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"
  184. // }
  185. /** 打开弹窗 */
  186. const open = async (type : string, row : any, newStatus : number, tabType : number) => {
  187. parentTabType.value = tabType
  188. parentRow.value = row
  189. parentNewStatus.value = newStatus
  190. dialogVisible.value = true
  191. productId.value = row.id
  192. picUrl.value = row.picUrl
  193. isFullScreen.value = false
  194. await getDetail()
  195. activeName.value = 'info'
  196. openType.value = type
  197. dialogTitle.value = t('action.' + type)
  198. if (type == "view") {
  199. dialogTitle.value = "查看"
  200. } else if (type == "create") {
  201. formData.value = cloneDeep(addFormData.value)
  202. // console.log(formData.value)
  203. // console.log(addFormData.value)
  204. }
  205. // 判断打开状态,如果是view那就只能查看 否则可以编辑
  206. if ('view' == openType.value) {
  207. isDetail.value = true
  208. } else {
  209. isDetail.value = false
  210. }
  211. }
  212. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  213. //全屏/取消全屏弹窗
  214. const fullScreen = () => {
  215. isFullScreen.value = !isFullScreen.value
  216. }
  217. /** 获得详情 */
  218. const getDetail = async () => {
  219. console.log(productId.value)
  220. const id = productId.value as any as number
  221. if (id) {
  222. formLoading.value = true
  223. try {
  224. const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
  225. res.skus?.forEach((item) => {
  226. if (isDetail.value) {
  227. item.price = floatToFixed2(item.price)
  228. item.marketPrice = floatToFixed2(item.marketPrice)
  229. item.costPrice = floatToFixed2(item.costPrice)
  230. item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
  231. item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
  232. } else {
  233. // 回显价格分转元
  234. item.price = formatToFraction(item.price)
  235. item.marketPrice = formatToFraction(item.marketPrice)
  236. item.costPrice = formatToFraction(item.costPrice)
  237. item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice)
  238. item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice)
  239. }
  240. })
  241. formData.value = res
  242. } finally {
  243. formLoading.value = false
  244. }
  245. }
  246. }
  247. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  248. /** 添加到仓库 / 回收站的状态 */
  249. const handleStatus02Change = async (newStatus : number) => {
  250. try {
  251. // 二次确认
  252. const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
  253. await message.confirm(`确认要"${parentRow.value.name}"${text}吗?`)
  254. // 发起修改
  255. await ProductSpuApi.updateStatus({ id: parentRow.value.id, status: newStatus })
  256. message.success(text + '成功')
  257. // 刷新 tabs 数据
  258. // await getTabsCount()
  259. // 刷新列表
  260. close()
  261. emit('success')
  262. } catch { }
  263. }
  264. /** 删除按钮操作 */
  265. const handleDelete = async (id : number) => {
  266. try {
  267. // 删除的二次确认
  268. await message.delConfirm()
  269. // 发起删除
  270. await ProductSpuApi.deleteSpu(id)
  271. message.success(t('common.delSuccess'))
  272. close()
  273. emit('success')
  274. } catch { }
  275. }
  276. /** 更新上架/下架状态 */
  277. const handleStatusChange = async () => {
  278. console.log(parentRow.value.status)
  279. try {
  280. // 二次确认
  281. const text = !parentRow.value.status ? '上架' : '下架'
  282. const updateStatus = !parentRow.value.status ? 1 : 0
  283. await message.confirm(`确认要${text}"${parentRow.value.name}"吗?`)
  284. // 发起修改
  285. await ProductSpuApi.updateStatus({ id: parentRow.value.id, status: updateStatus })
  286. message.success(text + '成功')
  287. close()
  288. emit('success')
  289. } catch {
  290. }
  291. }
  292. /** 提交按钮 */
  293. const submitForm = async () => {
  294. // 提交请求
  295. formLoading.value = true
  296. try {
  297. // 校验各表单
  298. await unref(infoRef)?.validate()
  299. await unref(skuRef)?.validate()
  300. await unref(deliveryRef)?.validate()
  301. await unref(descriptionRef)?.validate()
  302. await unref(otherRef)?.validate()
  303. // 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
  304. const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
  305. deepCopyFormData.skus!.forEach((item) => {
  306. // 给sku name赋值
  307. item.name = deepCopyFormData.name
  308. // sku相关价格元转分
  309. item.price = convertToInteger(item.price)
  310. item.marketPrice = convertToInteger(item.marketPrice)
  311. item.costPrice = convertToInteger(item.costPrice)
  312. item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice)
  313. item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice)
  314. })
  315. // 处理轮播图列表
  316. const newSliderPicUrls : any[] = []
  317. deepCopyFormData.sliderPicUrls!.forEach((item : any) => {
  318. // 如果是前端选的图
  319. typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
  320. })
  321. deepCopyFormData.sliderPicUrls = newSliderPicUrls
  322. // 校验都通过后提交表单
  323. const data = deepCopyFormData as ProductSpuApi.Spu
  324. const id = productId.value as any as number
  325. if (!id) {
  326. await ProductSpuApi.createSpu(data)
  327. message.success(t('common.createSuccess'))
  328. } else {
  329. await ProductSpuApi.updateSpu(data)
  330. message.success(t('common.updateSuccess'))
  331. }
  332. close()
  333. emit('success')
  334. } finally {
  335. formLoading.value = false
  336. }
  337. }
  338. /** 关闭按钮 */
  339. const close = () => {
  340. dialogVisible.value = false
  341. }
  342. </script>
  343. <style type="text/css">
  344. .el-dialog__body {
  345. padding: unset;
  346. }
  347. .dialog .el-card__body {
  348. padding: 0 20px 20px 0;
  349. }
  350. .dialog .el-dialog__headerbtn {
  351. top: 0px;
  352. line-height: 60px;
  353. }
  354. .dialog .el-dialog__headerbtn:hover {
  355. background-color: #ef6b6b;
  356. }
  357. .dialog .el-dialog__headerbtn:hover .el-dialog__close {
  358. color: #fff;
  359. }
  360. .dialog .el-dialog__header {
  361. padding: 0;
  362. margin: 0
  363. }
  364. </style>
  365. <style lang="scss" scoped>
  366. ::v-deep .left {
  367. width: 106px;
  368. float: left;
  369. img {
  370. // width: 98%;
  371. // border-bottom: 2px solid #e4e7ee;
  372. // border-right: 2px solid #e4e7ee;
  373. // margin-bottom: -5px;
  374. }
  375. }
  376. .child-tabs {
  377. border-top: 2px solid #e4e7ef;
  378. margin-top: -7px;
  379. }
  380. ::v-deep .child-tabs .is-active {
  381. // border-left: 2px solid;
  382. }
  383. ::v-deep .child-tabs .el-tabs__active-bar {
  384. // background-color: #30fdff;
  385. // background-color: unset;
  386. }
  387. ::v-deep .child-tabs .el-tabs__item {
  388. width: 106px;
  389. justify-content: center;
  390. }
  391. ::v-deep .child-tabs .el-tabs__item {}
  392. .right {
  393. padding: 10px 0 60px;
  394. border-left: 2px solid #e4e7ee;
  395. margin-left: -2px;
  396. float: left;
  397. width: calc(100% - 120px);
  398. }
  399. .my-header {
  400. display: flex;
  401. justify-content: space-between;
  402. align-items: center;
  403. &-left {
  404. font-weight: bold;
  405. font-size: 18px;
  406. padding: 20px;
  407. padding-bottom: 10px
  408. }
  409. &-right {
  410. span {
  411. width: 55px;
  412. height: 55px;
  413. display: inline-block;
  414. line-height: 55px;
  415. text-align: center;
  416. cursor: pointer;
  417. }
  418. span:first-child:hover {
  419. background-color: #f6f6f6;
  420. // color: white
  421. }
  422. span:last-child:hover {
  423. background-color: #ef6b6b;
  424. color: white
  425. }
  426. }
  427. }
  428. </style>