SpuTableSelect.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <template>
  2. <Dialog v-model="dialogVisible" :appendToBody="true" title="选择商品" width="70%">
  3. <ContentWrap>
  4. <el-form
  5. ref="queryFormRef"
  6. :inline="true"
  7. :model="queryParams"
  8. class="-mb-15px"
  9. label-width="68px"
  10. >
  11. <el-form-item label="商品名称" prop="name">
  12. <el-input
  13. v-model="queryParams.name"
  14. class="!w-240px"
  15. clearable
  16. placeholder="请输入商品名称"
  17. @keyup.enter="handleQuery"
  18. />
  19. </el-form-item>
  20. <el-form-item label="商品分类" prop="categoryId">
  21. <el-tree-select
  22. v-model="queryParams.categoryId"
  23. :data="categoryTreeList"
  24. :props="defaultProps"
  25. check-strictly
  26. class="!w-240px"
  27. node-key="id"
  28. placeholder="请选择商品分类"
  29. />
  30. </el-form-item>
  31. <el-form-item label="创建时间" prop="createTime">
  32. <el-date-picker
  33. v-model="queryParams.createTime"
  34. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  35. class="!w-240px"
  36. end-placeholder="结束日期"
  37. start-placeholder="开始日期"
  38. type="daterange"
  39. value-format="YYYY-MM-DD HH:mm:ss"
  40. />
  41. </el-form-item>
  42. <el-form-item>
  43. <el-button @click="handleQuery">
  44. <Icon class="mr-5px" icon="ep:search" />
  45. 搜索
  46. </el-button>
  47. <el-button @click="resetQuery">
  48. <Icon class="mr-5px" icon="ep:refresh" />
  49. 重置
  50. </el-button>
  51. </el-form-item>
  52. </el-form>
  53. <el-table v-loading="loading" :data="list" show-overflow-tooltip>
  54. <!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) -->
  55. <el-table-column width="55" v-if="multiple">
  56. <template #header>
  57. <el-checkbox
  58. v-model="isCheckAll"
  59. :indeterminate="isIndeterminate"
  60. @change="handleCheckAll"
  61. />
  62. </template>
  63. <template #default="{ row }">
  64. <el-checkbox
  65. v-model="checkedStatus[row.id]"
  66. @change="(checked: boolean) => handleCheckOne(checked, row, true)"
  67. />
  68. </template>
  69. </el-table-column>
  70. <!-- 2. 单选模式 -->
  71. <el-table-column label="#" width="55" v-else>
  72. <template #default="{ row }">
  73. <el-radio :label="row.id" v-model="selectedSpuId" @change="handleSingleSelected(row)">
  74. <!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
  75. &nbsp;
  76. </el-radio>
  77. </template>
  78. </el-table-column>
  79. <el-table-column key="id" align="center" label="商品编号" prop="id" min-width="60" />
  80. <el-table-column label="商品图" min-width="80">
  81. <template #default="{ row }">
  82. <el-image
  83. :src="row.picUrl"
  84. class="h-30px w-30px"
  85. :preview-src-list="[row.picUrl]"
  86. preview-teleported
  87. />
  88. </template>
  89. </el-table-column>
  90. <el-table-column label="商品名称" min-width="200" prop="name" />
  91. <el-table-column label="商品分类" min-width="100" prop="categoryId">
  92. <template #default="{ row }">
  93. <span>{{ categoryList?.find((c) => c.id === row.categoryId)?.name }}</span>
  94. </template>
  95. </el-table-column>
  96. </el-table>
  97. <!-- 分页 -->
  98. <Pagination
  99. v-model:limit="queryParams.pageSize"
  100. v-model:page="queryParams.pageNo"
  101. :total="total"
  102. @pagination="getList"
  103. />
  104. </ContentWrap>
  105. <template #footer v-if="multiple">
  106. <el-button type="primary" @click="handleEmitChange">确 定</el-button>
  107. <el-button @click="dialogVisible = false">取 消</el-button>
  108. </template>
  109. </Dialog>
  110. </template>
  111. <script lang="ts" setup>
  112. import { defaultProps, handleTree } from '@/utils/tree'
  113. import * as ProductCategoryApi from '@/api/mall/product/category'
  114. import * as ProductSpuApi from '@/api/mall/product/spu'
  115. import { propTypes } from '@/utils/propTypes'
  116. import { CHANGE_EVENT } from 'element-plus'
  117. type Spu = Required<ProductSpuApi.Spu>
  118. /**
  119. * 商品表格选择对话框
  120. * 1. 单选模式:
  121. * 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
  122. * 1.2 再次打开时,保持选中状态
  123. * 2. 多选模式:
  124. * 2.1 点击表格左侧的多选框时,记录选中的商品
  125. * 2.2 切换分页时,保持商品的选中的状态
  126. * 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
  127. * 2.4 再次打开时,保持选中状态
  128. */
  129. defineOptions({ name: 'SpuTableSelect' })
  130. defineProps({
  131. // 多选模式
  132. multiple: propTypes.bool.def(false)
  133. })
  134. // 列表的总页数
  135. const total = ref(0)
  136. // 列表的数据
  137. const list = ref<Spu[]>([])
  138. // 列表的加载中
  139. const loading = ref(false)
  140. // 弹窗的是否展示
  141. const dialogVisible = ref(false)
  142. // 查询参数
  143. const queryParams = ref({
  144. pageNo: 1,
  145. pageSize: 10,
  146. // 默认获取上架的商品
  147. tabType: 0,
  148. name: '',
  149. categoryId: null,
  150. createTime: []
  151. })
  152. /** 打开弹窗 */
  153. const open = (spuList?: Spu[]) => {
  154. // 重置
  155. checkedSpus.value = []
  156. checkedStatus.value = {}
  157. isCheckAll.value = false
  158. isIndeterminate.value = false
  159. // 处理已选中
  160. if (spuList && spuList.length > 0) {
  161. checkedSpus.value = [...spuList]
  162. checkedStatus.value = Object.fromEntries(spuList.map((spu) => [spu.id, true]))
  163. }
  164. dialogVisible.value = true
  165. resetQuery()
  166. }
  167. // 提供 open 方法,用于打开弹窗
  168. defineExpose({ open })
  169. /** 查询列表 */
  170. const getList = async () => {
  171. loading.value = true
  172. try {
  173. const data = await ProductSpuApi.getSpuPage(queryParams.value)
  174. list.value = data.list
  175. total.value = data.total
  176. // checkbox绑定undefined会有问题,需要给一个bool值
  177. list.value.forEach(
  178. (spu) => (checkedStatus.value[spu.id] = checkedStatus.value[spu.id] || false)
  179. )
  180. // 计算全选框状态
  181. calculateIsCheckAll()
  182. } finally {
  183. loading.value = false
  184. }
  185. }
  186. /** 搜索按钮操作 */
  187. const handleQuery = () => {
  188. queryParams.value.pageNo = 1
  189. getList()
  190. }
  191. /** 重置按钮操作 */
  192. const resetQuery = () => {
  193. queryParams.value = {
  194. pageNo: 1,
  195. pageSize: 10,
  196. // 默认获取上架的商品
  197. tabType: 0,
  198. name: '',
  199. categoryId: null,
  200. createTime: []
  201. }
  202. getList()
  203. }
  204. // 是否全选
  205. const isCheckAll = ref(false)
  206. // 全选框是否处于中间状态:不是全部选中 && 任意一个选中
  207. const isIndeterminate = ref(false)
  208. // 选中的商品
  209. const checkedSpus = ref<Spu[]>([])
  210. // 选中状态:key为商品ID,value为是否选中
  211. const checkedStatus = ref<Record<string, boolean>>({})
  212. // 选中的商品 spuId
  213. const selectedSpuId = ref()
  214. /** 单选中时触发 */
  215. const handleSingleSelected = (spu: Spu) => {
  216. emits(CHANGE_EVENT, spu)
  217. // 关闭弹窗
  218. dialogVisible.value = false
  219. // 记住上次选择的ID
  220. selectedSpuId.value = spu.id
  221. }
  222. /** 多选完成 */
  223. const handleEmitChange = () => {
  224. // 关闭弹窗
  225. dialogVisible.value = false
  226. emits(CHANGE_EVENT, [...checkedSpus.value])
  227. }
  228. /** 确认选择时的触发事件 */
  229. const emits = defineEmits<{
  230. change: [spu: Spu | Spu[] | any]
  231. }>()
  232. /** 全选/全不选 */
  233. const handleCheckAll = (checked: boolean) => {
  234. isCheckAll.value = checked
  235. isIndeterminate.value = false
  236. list.value.forEach((spu) => handleCheckOne(checked, spu, false))
  237. }
  238. /**
  239. * 选中一行
  240. * @param checked 是否选中
  241. * @param spu 商品
  242. * @param isCalcCheckAll 是否计算全选
  243. */
  244. const handleCheckOne = (checked: boolean, spu: Spu, isCalcCheckAll: boolean) => {
  245. if (checked) {
  246. checkedSpus.value.push(spu)
  247. checkedStatus.value[spu.id] = true
  248. } else {
  249. const index = findCheckedIndex(spu)
  250. if (index > -1) {
  251. checkedSpus.value.splice(index, 1)
  252. checkedStatus.value[spu.id] = false
  253. isCheckAll.value = false
  254. }
  255. }
  256. // 计算全选框状态
  257. if (isCalcCheckAll) {
  258. calculateIsCheckAll()
  259. }
  260. }
  261. // 查找商品在已选中商品列表中的索引
  262. const findCheckedIndex = (spu: Spu) => checkedSpus.value.findIndex((item) => item.id === spu.id)
  263. // 计算全选框状态
  264. const calculateIsCheckAll = () => {
  265. isCheckAll.value = list.value.every((spu) => checkedStatus.value[spu.id])
  266. // 计算中间状态:不是全部选中 && 任意一个选中
  267. isIndeterminate.value = !isCheckAll.value && list.value.some((spu) => checkedStatus.value[spu.id])
  268. }
  269. // 分类列表
  270. const categoryList = ref()
  271. // 分类树
  272. const categoryTreeList = ref()
  273. /** 初始化 **/
  274. onMounted(async () => {
  275. await getList()
  276. // 获得分类树
  277. categoryList.value = await ProductCategoryApi.getCategoryList({})
  278. categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId')
  279. })
  280. </script>