SpuSelect.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <template>
  2. <Dialog v-model="dialogVisible" :appendToBody="true" :title="dialogTitle" width="70%">
  3. <ContentWrap>
  4. <el-row :gutter="20" class="mb-10px">
  5. <el-col :span="6">
  6. <el-input
  7. v-model="queryParams.name"
  8. class="!w-240px"
  9. clearable
  10. placeholder="请输入商品名称"
  11. @keyup.enter="handleQuery"
  12. />
  13. </el-col>
  14. <el-col :span="6">
  15. <el-tree-select
  16. v-model="queryParams.categoryId"
  17. :data="categoryList"
  18. :props="defaultProps"
  19. check-strictly
  20. class="w-1/1"
  21. node-key="id"
  22. placeholder="请选择商品分类"
  23. />
  24. </el-col>
  25. <el-col :span="6">
  26. <el-date-picker
  27. v-model="queryParams.createTime"
  28. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  29. class="!w-240px"
  30. end-placeholder="结束日期"
  31. start-placeholder="开始日期"
  32. type="daterange"
  33. value-format="YYYY-MM-DD HH:mm:ss"
  34. />
  35. </el-col>
  36. <el-col :span="6">
  37. <el-button @click="handleQuery">
  38. <Icon class="mr-5px" icon="ep:search" />
  39. 搜索
  40. </el-button>
  41. <el-button @click="resetQuery">
  42. <Icon class="mr-5px" icon="ep:refresh" />
  43. 重置
  44. </el-button>
  45. </el-col>
  46. </el-row>
  47. <el-table
  48. ref="spuListRef"
  49. v-loading="loading"
  50. :data="list"
  51. :expand-row-keys="expandRowKeys"
  52. row-key="id"
  53. @expand-change="expandChange"
  54. @selection-change="selectSpu"
  55. >
  56. <el-table-column v-if="isSelectSku" type="expand" width="30">
  57. <template #default>
  58. <SkuList
  59. v-if="isExpand"
  60. ref="skuListRef"
  61. :isComponent="true"
  62. :isDetail="true"
  63. :prop-form-data="spuData"
  64. :property-list="propertyList"
  65. @selection-change="selectSku"
  66. />
  67. </template>
  68. </el-table-column>
  69. <el-table-column type="selection" width="55" />
  70. <el-table-column key="id" align="center" label="商品编号" prop="id" />
  71. <el-table-column label="商品图" min-width="80">
  72. <template #default="{ row }">
  73. <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
  74. </template>
  75. </el-table-column>
  76. <el-table-column
  77. :show-overflow-tooltip="true"
  78. label="商品名称"
  79. min-width="300"
  80. prop="name"
  81. />
  82. <el-table-column align="center" label="商品售价" min-width="90" prop="price">
  83. <template #default="{ row }">
  84. {{ formatToFraction(row.price) }}
  85. </template>
  86. </el-table-column>
  87. <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
  88. <el-table-column align="center" label="库存" min-width="90" prop="stock" />
  89. <el-table-column align="center" label="排序" min-width="70" prop="sort" />
  90. <el-table-column
  91. :formatter="dateFormatter"
  92. align="center"
  93. label="创建时间"
  94. prop="createTime"
  95. width="180"
  96. />
  97. </el-table>
  98. <!-- 分页 -->
  99. <Pagination
  100. v-model:limit="queryParams.pageSize"
  101. v-model:page="queryParams.pageNo"
  102. :total="total"
  103. @pagination="getList"
  104. />
  105. </ContentWrap>
  106. <template #footer>
  107. <el-button type="primary" @click="confirm">确 定</el-button>
  108. <el-button @click="dialogVisible = false">取 消</el-button>
  109. </template>
  110. </Dialog>
  111. </template>
  112. <script lang="ts" setup>
  113. import { getPropertyList, PropertyAndValues, SkuList } from '@/views/mall/product/spu/components'
  114. import { ElTable } from 'element-plus'
  115. import { dateFormatter } from '@/utils/formatTime'
  116. import { createImageViewer } from '@/components/ImageViewer'
  117. import { formatToFraction } from '@/utils'
  118. import { defaultProps, handleTree } from '@/utils/tree'
  119. import * as ProductCategoryApi from '@/api/mall/product/category'
  120. import * as ProductSpuApi from '@/api/mall/product/spu'
  121. import { propTypes } from '@/utils/propTypes'
  122. defineOptions({ name: 'PromotionSpuSelect' })
  123. const props = defineProps({
  124. // 默认不需要(不需要的情况下只返回 spu,需要的情况下返回 选中的 spu 和 sku 列表)
  125. // 其它活动需要选择商品和商品属性导入此组件即可,需添加组件属性 :isSelectSku='true'
  126. isSelectSku: propTypes.bool.def(false), // 是否需要选择 sku 属性
  127. radio: propTypes.bool.def(false) // 是否单选 sku
  128. })
  129. const message = useMessage() // 消息弹窗
  130. const total = ref(0) // 列表的总页数
  131. const list = ref<any[]>([]) // 列表的数据
  132. const loading = ref(false) // 列表的加载中
  133. const dialogVisible = ref(false) // 弹窗的是否展示
  134. const dialogTitle = ref('') // 弹窗的标题
  135. const queryParams = ref({
  136. pageNo: 1,
  137. pageSize: 10,
  138. tabType: 0, // 默认获取上架的商品
  139. name: '',
  140. categoryId: null,
  141. createTime: []
  142. }) // 查询参数
  143. const propertyList = ref<PropertyAndValues[]>([]) // 商品属性列表
  144. const spuListRef = ref<InstanceType<typeof ElTable>>()
  145. const skuListRef = ref<InstanceType<typeof SkuList>>() // 商品属性选择 Ref
  146. const spuData = ref<ProductSpuApi.Spu>() // 商品详情
  147. const isExpand = ref(false) // 控制 SKU 列表显示
  148. const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
  149. //============ 商品选择相关 ============
  150. const selectedSpuId = ref<number>(0) // 选中的商品 spuId
  151. const selectedSkuIds = ref<number[]>([]) // 选中的商品 skuIds
  152. const selectSku = (val: ProductSpuApi.Sku[]) => {
  153. const skuTable = skuListRef.value?.getSkuTableRef()
  154. if (selectedSpuId.value === 0) {
  155. message.warning('请先选择商品再选择相应的规格!!!')
  156. skuTable?.clearSelection()
  157. return
  158. }
  159. if (val.length === 0) {
  160. selectedSkuIds.value = []
  161. return
  162. }
  163. if (props.radio) {
  164. // 只选择一个
  165. selectedSkuIds.value = [val.map((sku) => sku.id!)[0]]
  166. // 如果大于1个
  167. if (val.length > 1) {
  168. // 清空选择
  169. skuTable?.clearSelection()
  170. // 变更为最后一次选择的
  171. skuTable?.toggleRowSelection(val.pop(), true)
  172. return
  173. }
  174. } else {
  175. selectedSkuIds.value = val.map((sku) => sku.id!)
  176. }
  177. }
  178. const selectSpu = (val: ProductSpuApi.Spu[]) => {
  179. if (val.length === 0) {
  180. selectedSpuId.value = 0
  181. return
  182. }
  183. // 只选择一个
  184. selectedSpuId.value = val.map((spu) => spu.id!)[0]
  185. // 切换选择 spu 如果有选择的 sku 则清空,确保选择的 sku 是对应的 spu 下面的
  186. if (selectedSkuIds.value.length > 0) {
  187. selectedSkuIds.value = []
  188. }
  189. // 如果大于1个
  190. if (val.length > 1) {
  191. // 清空选择
  192. spuListRef.value?.clearSelection()
  193. // 变更为最后一次选择的
  194. spuListRef.value?.toggleRowSelection(val.pop(), true)
  195. return
  196. }
  197. expandChange(val[0], val)
  198. }
  199. // 计算商品属性
  200. const expandChange = async (row: ProductSpuApi.Spu, expandedRows?: ProductSpuApi.Spu[]) => {
  201. // 判断需要展开的 spuId === 选择的 spuId。如果选择了 A 就展开 A 的 skuList。如果选择了 A 手动展开 B 则阻断
  202. // 目的防止误选 sku
  203. if (selectedSpuId.value !== 0) {
  204. if (row.id !== selectedSpuId.value) {
  205. message.warning('你已选择商品请先取消')
  206. expandRowKeys.value = [selectedSpuId.value]
  207. return
  208. }
  209. // 如果已展开 skuList 则选择此对应的 spu 不需要重新获取渲染 skuList
  210. if (isExpand.value && spuData.value?.id === row.id) {
  211. return
  212. }
  213. }
  214. spuData.value = {}
  215. propertyList.value = []
  216. isExpand.value = false
  217. if (expandedRows?.length === 0) {
  218. // 如果展开个数为 0
  219. expandRowKeys.value = []
  220. return
  221. }
  222. // 获取 SPU 详情
  223. const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.Spu
  224. propertyList.value = getPropertyList(res)
  225. spuData.value = res
  226. isExpand.value = true
  227. expandRowKeys.value = [row.id!]
  228. }
  229. // 确认选择时的触发事件
  230. const emits = defineEmits<{
  231. (e: 'confirm', spuId: number, skuIds?: number[]): void
  232. }>()
  233. /**
  234. * 确认选择返回选中的 spu 和 sku (如果需要选择sku的话)
  235. */
  236. const confirm = () => {
  237. if (selectedSpuId.value === 0) {
  238. message.warning('没有选择任何商品')
  239. return
  240. }
  241. if (props.isSelectSku && selectedSkuIds.value.length === 0) {
  242. message.warning('没有选择任何商品属性')
  243. return
  244. }
  245. // 返回各自 id 列表
  246. props.isSelectSku
  247. ? emits('confirm', selectedSpuId.value, selectedSkuIds.value)
  248. : emits('confirm', selectedSpuId.value)
  249. // 关闭弹窗
  250. dialogVisible.value = false
  251. selectedSpuId.value = 0
  252. selectedSkuIds.value = []
  253. }
  254. /** 打开弹窗 */
  255. const open = () => {
  256. dialogTitle.value = '商品选择'
  257. dialogVisible.value = true
  258. }
  259. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  260. /** 查询列表 */
  261. const getList = async () => {
  262. loading.value = true
  263. try {
  264. const data = await ProductSpuApi.getSpuPage(queryParams.value)
  265. list.value = data.list
  266. total.value = data.total
  267. } finally {
  268. loading.value = false
  269. }
  270. }
  271. /** 搜索按钮操作 */
  272. const handleQuery = () => {
  273. getList()
  274. }
  275. /** 重置按钮操作 */
  276. const resetQuery = () => {
  277. queryParams.value = {
  278. pageNo: 1,
  279. pageSize: 10,
  280. tabType: 0, // 默认获取上架的商品
  281. name: '',
  282. categoryId: null,
  283. createTime: []
  284. }
  285. getList()
  286. }
  287. /** 商品图预览 */
  288. const imagePreview = (imgUrl: string) => {
  289. createImageViewer({
  290. zIndex: 99999999,
  291. urlList: [imgUrl]
  292. })
  293. }
  294. const categoryList = ref() // 分类树
  295. /** 初始化 **/
  296. onMounted(async () => {
  297. await getList()
  298. // 获得分类树
  299. const data = await ProductCategoryApi.getCategoryList({})
  300. categoryList.value = handleTree(data, 'id', 'parentId')
  301. })
  302. </script>