CombinationTableSelect.vue 11 KB

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