소스 검색

feat(mp): 新增公众号消息模板管理功能

- 新增消息模板、模板配置、发送记录的完整CRUD功能
- 实现消息模板同步、批量删除、消息推送等核心操作
- 支持模板变量动态提取与颜色配置
- 提供Excel导出和数据筛选功能
- 集成微信公众号模板消息发送能力
- 完善表单验证和用户交互体验
wuKong 3 달 전
부모
커밋
5242e6a228

+ 159 - 0
src/api/mp/template/index.ts

@@ -0,0 +1,159 @@
+import request from '@/config/axios'
+
+// 消息模板 VO
+export interface MsgTemplateVO {
+    id: number // 主键
+    appId: string // appid
+    templateId: string // 公众号模板ID
+    name: string // 模版名称
+    title: string // 标题
+    content: string // 模板内容
+    data: string // 消息内容
+    url: string // 链接
+    //miniprogram: string // 小程序信息
+    miniProgramAppId: string
+    miniProgramPagePath: string
+    isRemoved: number
+    configId: number
+    templateType: number
+    status: number // 是否有效
+}
+
+// 消息模板 API
+export const MsgTemplateApi = {
+    // 查询消息模板分页
+    getMsgTemplatePage: async (params: any) => {
+        return await request.get({ url: `/mp/template/page`, params })
+    },
+
+    // 查询消息模板详情
+    getMsgTemplate: async (id: number) => {
+        return await request.get({ url: `/mp/template/get?id=` + id })
+    },
+
+    // 新增消息模板
+    createMsgTemplate: async (data: MsgTemplateVO) => {
+        return await request.post({ url: `/mp/template/create`, data })
+    },
+
+    // 修改消息模板
+    updateMsgTemplate: async (data: MsgTemplateVO) => {
+        return await request.put({ url: `/mp/template/update`, data })
+    },
+
+    // 删除消息模板
+    deleteMsgTemplate: async (id: number) => {
+        return await request.delete({ url: `/mp/template/delete?id=` + id })
+    },
+
+    // 导出消息模板 Excel
+    exportMsgTemplate: async (params) => {
+        return await request.download({ url: `/mp/template/export-excel`, params })
+    },
+
+    // 同步公众号模板
+    syncMsgTemplate: async (params: any) => {
+        return await request.get({ url: `/mp/template/syncMsgTemplate`,params})
+    },
+
+    // 同步公众号模板
+    sendMsgBatch: async (data: any) => {
+        return await request.post({ url: `/mp/template/sendMsgBatch`,data})
+    },
+
+    deleteList: async (data: any) => {
+        return await request.delete({ url: `/mp/template/delete-list`,data})
+    },
+}
+
+// 模板消息配置 VO
+export interface MsgTemplateConfigVO {
+    id: number // 主键
+    appId: string // appId
+    templateId: string // 公众号模板ID
+    templateType: string // 模板类型
+    title: string // 标题
+    status: number // 是否有效 0有效,1无效
+    remark: string // 备注
+}
+
+// 模板消息配置 API
+export const MsgTemplateConfigApi = {
+    // 查询模板消息配置分页
+    getMsgTemplateConfigPage: async (params: any) => {
+        return await request.get({ url: `/mp/template/config/page`, params })
+    },
+
+    // 查询模板消息配置详情
+    getMsgTemplateConfig: async (id: number) => {
+        return await request.get({ url: `/mp/template/config/get?id=` + id })
+    },
+
+    // 新增模板消息配置
+    createMsgTemplateConfig: async (data: MsgTemplateConfigVO) => {
+        return await request.post({ url: `/mp/template/config/create`, data })
+    },
+
+    // 修改模板消息配置
+    updateMsgTemplateConfig: async (data: MsgTemplateConfigVO) => {
+        return await request.put({ url: `/mp/template/config/update`, data })
+    },
+
+    // 删除模板消息配置
+    deleteMsgTemplateConfig: async (id: number) => {
+        return await request.delete({ url: `/mp/template/config/delete?id=` + id })
+    },
+
+    // 导出模板消息配置 Excel
+    exportMsgTemplateConfig: async (params) => {
+        return await request.download({ url: `/mp/template/config/export-excel`, params })
+    },
+}
+
+// 微信模版消息发送记录 VO
+export interface MsgTemplateLogVO {
+    id: number // 主键
+    appId: string // appId
+    toUser: string // 用户openid
+    templateId: string // 公众号模板ID
+    data: string // 消息内容
+    url: string // 链接
+    miniProgramAppId: string // 小程序appid
+    miniProgramPagePath: string // 小程序页面路径
+    sendTime: Date // 发送时间
+    sendStatus: string // 发送状态 0成功,1失败
+    sendResult: string // 发送结果
+}
+
+// 微信模版消息发送记录 API
+export const MsgTemplateLogApi = {
+    // 查询微信模版消息发送记录分页
+    getMsgTemplateLogPage: async (params: any) => {
+        return await request.get({ url: `/mp/template/log/page`, params })
+    },
+
+    // 查询微信模版消息发送记录详情
+    getMsgTemplateLog: async (id: number) => {
+        return await request.get({ url: `/mp/template/log/get?id=` + id })
+    },
+
+    // 新增微信模版消息发送记录
+    createMsgTemplateLog: async (data: MsgTemplateLogVO) => {
+        return await request.post({ url: `/mp/template/log/create`, data })
+    },
+
+    // 修改微信模版消息发送记录
+    updateMsgTemplateLog: async (data: MsgTemplateLogVO) => {
+        return await request.put({ url: `/mp/template/log/update`, data })
+    },
+
+    // 删除微信模版消息发送记录
+    deleteMsgTemplateLog: async (id: number) => {
+        return await request.delete({ url: `/mp/template/log/delete?id=` + id })
+    },
+
+    // 导出微信模版消息发送记录 Excel
+    exportMsgTemplateLog: async (params) => {
+        return await request.download({ url: `/mp/template/log/export-excel`, params })
+    },
+}

+ 333 - 0
src/views/mp/template/MsgTemplate.vue

@@ -0,0 +1,333 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+    >
+      <el-form-item label="公众号" prop="accountId">
+        <WxAccountSelect @change="onAccountChanged"/>
+      </el-form-item>
+      <!--      <el-form-item label="appId" prop="appId">
+              <el-input v-model="queryParams.appId" placeholder="请输入appId" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="公众号模板ID" prop="templateId">
+              <el-input v-model="queryParams.templateId" placeholder="请输入公众号模板ID" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="模版名称" prop="name">
+              <el-input v-model="queryParams.name" placeholder="请输入模版名称" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <el-form-item label="标题" prop="title">
+        <el-input
+            v-model="queryParams.title"
+            placeholder="请输入标题"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+        />
+      </el-form-item>
+      <!--      <el-form-item label="消息内容" prop="data">
+              <el-input v-model="queryParams.data" placeholder="请输入消息内容" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="链接" prop="url">
+              <el-input v-model="queryParams.url" placeholder="请输入链接" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="小程序appId" prop="miniProgramAppId">
+              <el-input v-model="queryParams.miniProgramAppId" placeholder="请输入小程序appId" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="小程序页面路径" prop="miniProgramPagePath">
+              <el-input v-model="queryParams.miniProgramPagePath" placeholder="请输入小程序页面路径" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="是否有效" prop="status">
+              <el-select v-model="queryParams.status" placeholder="请选择是否有效" clearable class="!w-240px">
+                <el-option label="请选择字典生成" value="" />
+              </el-select>
+            </el-form-item>-->
+      <!--      <el-form-item label="公众号是否已移除" prop="isRemoved">
+              <el-input v-model="queryParams.isRemoved" placeholder="请输入公众号是否已移除" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="创建时间" prop="createTime">
+              <el-date-picker
+                v-model="queryParams.createTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                class="!w-220px"
+              />
+            </el-form-item>-->
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px"/>
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px"/>
+          重置
+        </el-button>
+        <!--        <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:template:create']">
+                  <Icon icon="ep:plus" class="mr-5px" /> 新增
+                </el-button>-->
+        <el-button type="warning" plain @click="handleSync" :loading="syncLoading" v-hasPermi="['mp:template:sync']">
+          <Icon icon="ep:refresh" class="mr-5px"/>
+          同步模板
+        </el-button>
+        <el-button type="success" plain @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['mp:template:export']">
+          <Icon icon="ep:download" class="mr-5px"/>
+          导出
+        </el-button>
+        <el-button :disabled="multipleSelection.length !== 1" type="success" plain @click="sendMessage()"
+                   v-hasPermi="['mp:template:send']">
+          <el-icon>
+            <Promotion/>
+          </el-icon>
+          推送消息
+        </el-button>
+        <el-button :disabled="multipleSelection.length === 0" type="danger" plain @click="batchDelete()"
+                   v-hasPermi="['mp:template:delete']">
+          <el-icon>
+            <DeleteFilled/>
+          </el-icon>
+          批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+        @selection-change="handleSelectionChange"
+    >
+      <!--      <el-table-column label="主键" align="center" prop="id" />-->
+      <el-table-column type="selection" width="55"/>
+      <el-table-column label="appId" align="center" prop="appId"/>
+      <el-table-column label="公众号模板ID" align="center" prop="templateId"/>
+      <el-table-column label="模版名称" align="center" prop="name"/>
+      <el-table-column label="标题" align="center" prop="title"/>
+      <el-table-column label="模板内容" align="center" prop="content"/>
+      <el-table-column label="消息内容" align="center" prop="data"/>
+      <el-table-column label="链接" align="center" prop="url"/>
+      <el-table-column label="小程序appId" align="center" prop="miniProgramAppId"/>
+      <el-table-column label="小程序页面路径" align="center" prop="miniProgramPagePath"/>
+      <el-table-column label="是否有效" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IS_VALID" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="公众号是否已移除" align="center" prop="isRemoved">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IS_DELETE" :value="scope.row.isRemoved"/>
+        </template>
+      </el-table-column>
+      <el-table-column
+          label="创建时间"
+          align="center"
+          prop="createTime"
+          :formatter="dateFormatter"
+          width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+              link
+              type="primary"
+              @click="openForm('update', scope.row.id)"
+              v-hasPermi="['mp:template:update']"
+          >
+            配置
+          </el-button>
+          <el-button
+              link
+              type="danger"
+              @click="handleDelete(scope.row.id)"
+              v-hasPermi="['mp:template:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <MsgTemplateForm ref="formRef" @success="getList"/>
+  <MsgTemplateSend ref="sendRef" @success="getList"/>
+</template>
+
+<script setup lang="ts">
+import {DICT_TYPE} from '@/utils/dict'
+import {dateFormatter} from '@/utils/formatTime'
+import download from '@/utils/download'
+import {MsgTemplateApi, MsgTemplateVO} from '@/api/mp/template'
+import MsgTemplateForm from './MsgTemplateForm.vue'
+import MsgTemplateSend from './MsgTemplateSend.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+import {DeleteFilled, Promotion} from '@element-plus/icons-vue'
+
+/** 消息模板 列表 */
+defineOptions({name: 'MsgTemplate'})
+
+const message = useMessage() // 消息弹窗
+const {t} = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<MsgTemplateVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const multipleSelection = ref<MsgTemplateVO[]>([])
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  accountId: -1,
+  appId: undefined,
+  templateId: undefined,
+  name: undefined,
+  title: undefined,
+  content: undefined,
+  data: undefined,
+  url: undefined,
+  miniProgramAppId: undefined,
+  miniProgramPagePath: undefined,
+  status: undefined,
+  isRemoved: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+const syncLoading = ref(false) // 同步模板的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await MsgTemplateApi.getMsgTemplatePage(queryParams)
+    if (data) {
+      list.value = data.list
+      total.value = data.total
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+const handleSelectionChange = (val: MsgTemplateVO[]) => {
+  multipleSelection.value = val
+}
+
+/** 重置按钮操作 */
+const resetQuery = async () => {
+  // 暂存 accountId,并在 reset 后恢复
+  const accountId = queryParams.accountId
+  queryFormRef.value?.resetFields()
+  queryParams.accountId = accountId
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+/** 添加/修改操作 */
+const sendRef = ref()
+const sendMessage = () => {
+  let templateId = multipleSelection.value[0].templateId;
+  let data = multipleSelection.value[0].data;
+  if (!templateId) {
+    message.warning('消息模板无效')
+    return
+  }
+  if (!data || data.length === 0) {
+    message.warning('消息模板数据无效')
+    return
+  }
+  sendRef.value.open(queryParams.accountId, multipleSelection.value[0].appId, templateId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await MsgTemplateApi.deleteMsgTemplate(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await MsgTemplateApi.exportMsgTemplate(queryParams)
+    download.excel(data, '消息模板.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 同步公众号模板 */
+const handleSync = async () => {
+  try {
+    // 同步的二次确认
+    await message.confirm('确认要同步公众号模板吗?', '提示')
+    // 发起同步
+    syncLoading.value = true
+    await MsgTemplateApi.syncMsgTemplate(queryParams)
+    message.success('同步成功')
+    // 刷新列表
+    await getList()
+  } catch {
+  } finally {
+    syncLoading.value = false
+  }
+}
+
+/** 公众号选择变化 */
+const onAccountChanged = (accountId: number) => {
+  queryParams.accountId = accountId
+  queryParams.pageNo = 1
+  handleQuery()
+}
+
+const batchDelete = async () => {
+  let ids = multipleSelection.value.map((item) => item.id);
+  await message.confirm('确认要删除所选的公众号模板吗?', '提示')
+  await MsgTemplateApi.deleteList(ids)
+  message.success('删除成功')
+}
+
+/** 初始化 **/
+onMounted(() => {
+  // getList()
+})
+</script>

+ 335 - 0
src/views/mp/template/MsgTemplateForm.vue

@@ -0,0 +1,335 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px" v-loading="formLoading">
+      <!-- 其他表单项保持不变 -->
+      <el-form-item label="appId" prop="appId">
+        <el-input v-model="formData.appId" placeholder="请输入appId"/>
+      </el-form-item>
+      <el-form-item label="公众号模板ID" prop="templateId">
+        <el-input v-model="formData.templateId" placeholder="请输入公众号模板ID"/>
+      </el-form-item>
+      <el-form-item label="模版名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入模版名称"/>
+      </el-form-item>
+      <el-form-item label="标题" prop="title">
+        <el-input v-model="formData.title" placeholder="请输入标题"/>
+      </el-form-item>
+      <el-form-item label="链接" prop="url">
+        <el-input v-model="formData.url" placeholder="请输入链接"/>
+      </el-form-item>
+      <el-form-item label="小程序appId" prop="miniProgramAppId">
+        <el-input v-model="formData.miniProgramAppId" placeholder="请输入小程序appId"/>
+      </el-form-item>
+      <el-form-item label="小程序页面路径" prop="miniProgramPagePath">
+        <el-input v-model="formData.miniProgramPagePath" placeholder="请输入小程序页面路径"/>
+      </el-form-item>
+      <el-row :gutter="10">
+        <el-col :span="12">
+          <el-form-item label="是否有效" prop="status">
+            <el-radio-group v-model="formData.status">
+              <el-radio
+                  v-for="dict in getIntDictOptions(DICT_TYPE.IS_VALID)"
+                  :key="dict.value"
+                  :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <!--        <el-col :span="12">
+                  <el-form-item label="公众号是否已移除" prop="isRemoved">
+                    <el-radio-group v-model="formData.isRemoved">
+                      <el-radio
+                          v-for="dict in getIntDictOptions(DICT_TYPE.IS_DELETE)"
+                          :key="dict.value"
+                          :label="dict.value"
+                      >
+                        {{ dict.label }}
+                      </el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                </el-col>-->
+      </el-row>
+      <el-form-item label="模板内容" prop="content">
+        <el-input
+            readonly
+            type="textarea"
+            v-model="formData.content"
+            :rows="3"
+            autosize
+            placeholder="请输入模板内容,使用{{变量名.DATA}}格式定义变量"
+        />
+      </el-form-item>
+
+      <!-- 修改点:消息变量部分 -->
+      <el-form-item label="消息变量" v-if="templateVariables.length > 0">
+        <div class="variable-editor">
+          <div v-for="(item, index) in templateVariables" :key="item.name" class="variable-item">
+            <el-row :gutter="10">
+              <el-col :span="6">
+                <div class="variable-name">{{ item.name }}</div>
+              </el-col>
+              <el-col :span="14">  <!-- 修改点:增加输入框宽度 -->
+                <el-form-item
+                    :prop="`templateVariables.${index}.value`"
+                >
+                  <el-input v-model="item.value" placeholder="填充内容" clearable/>
+                </el-form-item>
+              </el-col>
+              <el-col :span="4">
+                <el-form-item>
+                  <el-color-picker v-model="item.color" :predefine="predefineColors" size="small"/>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </div>
+        </div>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import {ref, reactive, watch, nextTick} from 'vue'
+import {MsgTemplateApi, type MsgTemplateVO} from '@/api/mp/template'
+import {getIntDictOptions, DICT_TYPE} from '@/utils/dict'
+import type {FormInstance, FormRules} from 'element-plus'
+
+interface TemplateVariable {
+  name: string;
+  value: string;
+  color: string;
+}
+
+// 消息模板表单组件
+defineOptions({name: 'MsgTemplateForm'})
+
+const {t} = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const dialogVisible = ref(false) // 弹窗是否展示
+const dialogTitle = ref('') // 弹窗标题
+const formLoading = ref(false) // 表单加载状态
+const formType = ref('') // 表单类型:create/update
+const list = ref<any[]>([])
+
+// 表单数据保持不变
+const formData = reactive<MsgTemplateVO>({
+  id: undefined,
+  appId: '',
+  templateId: '',
+  name: '',
+  title: '',
+  content: '',
+  data: '',
+  url: '',
+  miniProgramAppId: '',
+  miniProgramPagePath: '',
+  status: 0,
+  isRemoved: 0
+})
+
+// 表单验证规则保持不变
+const formRules = reactive<FormRules<MsgTemplateVO>>({
+  appId: [{required: true, message: 'appId不能为空', trigger: 'blur'}],
+  templateId: [{required: true, message: '公众号模板ID不能为空', trigger: 'blur'}],
+  name: [{required: true, message: '模板名称不能为空', trigger: 'blur'}],
+  title: [{required: true, message: '标题不能为空', trigger: 'blur'}],
+  status: [{required: true, message: '请选择是否有效', trigger: 'change'}]
+})
+
+const templateVariables = ref<TemplateVariable[]>([]) // 模板变量
+const predefineColors = ref([
+  '#000000', // 黑色
+  '#173177', // 深蓝色
+  '#ff0000', // 红色
+  '#00b050', // 绿色
+  '#ff9900', // 橙色
+  '#800080', // 紫色
+]) // 预定义颜色
+
+const formRef = ref<FormInstance>() // 表单引用
+
+// 以下是新增的模板变量相关代码
+
+// 提取模板中的变量名
+const extractTemplateVariables = (content: string): string[] => {
+  if (!content) return []
+  const regex = /\{\{(\w+)\.DATA\}\}/g
+  const matches = content.match(regex) || []
+  const uniqueNames = [...new Set(matches.map(match =>
+      match.replace('{{', '').replace('.DATA}}', '')
+  ))]
+  return uniqueNames
+}
+
+// 当模板内容变化时更新变量列表
+watch(() => formData.content, (newContent) => {
+  const variableNames = extractTemplateVariables(newContent)
+
+  // 保留现有变量值
+  const currentVariables = [...templateVariables.value]
+
+  // 创建新的变量列表
+  const newVariables = variableNames.map(name => {
+    const existing = currentVariables.find(v => v.name === name)
+    return existing || {name, value: '', color: '#000000'}
+  })
+
+  templateVariables.value = newVariables
+})
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+
+  if (id) {
+    formLoading.value = true
+    try {
+      const res = await MsgTemplateApi.getMsgTemplate(id)
+      Object.assign(formData, res)
+
+      // 解析模板变量
+      if (res.data) {
+        try {
+          const parsedData = JSON.parse(res.data)
+          if (Array.isArray(parsedData)) {
+            templateVariables.value = parsedData
+          }
+        } catch (e) {
+          // 如果解析失败,从内容中提取变量
+          const variableNames = extractTemplateVariables(res.content || '')
+          templateVariables.value = variableNames.map(name => ({
+            name,
+            value: '',
+            color: '#000000'
+          }))
+        }
+      } else {
+        // 没有数据字段,从内容中提取变量
+        const variableNames = extractTemplateVariables(res.content || '')
+        templateVariables.value = variableNames.map(name => ({
+          name,
+          value: '',
+          color: '#000000'
+        }))
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+
+defineExpose({open}) // 暴露open方法
+
+/** 提交表单 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+  if (!formRef.value) return
+
+  // 使用 Element Plus 表单验证统一处理所有校验
+  try {
+    await formRef.value.validate()
+
+    // 验证模板变量是否都填写了
+    const isAllFilled = templateVariables.value.every(item => item.value.trim() !== '')
+    if (!isAllFilled) {
+      message.error('请填写所有消息变量的内容')
+      return
+    }
+
+    // 准备数据 - 修改点:将模板变量转为JSON字符串
+    const submitData = {
+      ...formData,
+      data: JSON.stringify(templateVariables.value)
+    }
+
+    formLoading.value = true
+    try {
+      if (formType.value === 'create') {
+        await MsgTemplateApi.createMsgTemplate(submitData)
+        message.success(t('common.createSuccess'))
+      } else {
+        await MsgTemplateApi.updateMsgTemplate(submitData)
+        message.success(t('common.updateSuccess'))
+      }
+      dialogVisible.value = false
+      emit('success')
+    } catch (error) {
+      message.error('操作失败,请重试')
+    } finally {
+      formLoading.value = false
+    }
+  } catch (error) {
+    // 表单验证失败
+    message.error('请完善表单信息')
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formRef.value?.resetFields()
+  Object.assign(formData, {
+    id: undefined,
+    appId: '',
+    templateId: '',
+    name: '',
+    title: '',
+    content: '',
+    data: '',
+    url: '',
+    miniProgramAppId: '',
+    miniProgramPagePath: '',
+    status: 0,
+    isRemoved: 0
+  })
+  // 新增:重置模板变量
+  templateVariables.value = []
+}
+</script>
+
+<style scoped>
+/* 新增的样式 */
+.variable-editor {
+  width: 100%;
+  border: 1px solid #EBEEF5;
+  border-radius: 4px;
+  padding: 12px;
+  background-color: var(--el-bg-color);
+}
+
+.variable-item {
+  padding: 8px 0;
+}
+
+.variable-item + .variable-item {
+  border-top: 1px dashed #EBEEF5;
+  padding-top: 12px;
+}
+
+.variable-name {
+  line-height: 32px;
+  font-weight: bold;
+  color: var(--el-text-color);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+:deep(.el-color-picker) {
+  vertical-align: middle;
+  margin-left: 8px;
+}
+
+/* 增加输入框宽度 */
+.variable-editor :deep(.el-input) {
+  width: 100%;
+}
+</style>

+ 214 - 0
src/views/mp/template/MsgTemplateLog.vue

@@ -0,0 +1,214 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px">
+      <!--      <el-form-item label="appId" prop="appId">
+              <el-input v-model="queryParams.appId" placeholder="请输入appId" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <el-form-item label="用户openid" prop="toUser">
+        <el-input v-model="queryParams.toUser" placeholder="请输入用户openid" clearable @keyup.enter="handleQuery"
+                  class="!w-240px"/>
+      </el-form-item>
+      <!--      <el-form-item label="公众号模板ID" prop="templateId">
+              <el-input v-model="queryParams.templateId" placeholder="请输入公众号模板ID" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="消息内容" prop="data">
+              <el-input v-model="queryParams.data" placeholder="请输入消息内容" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="链接" prop="url">
+              <el-input v-model="queryParams.url" placeholder="请输入链接" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="小程序appid" prop="miniProgramAppId">
+              <el-input v-model="queryParams.miniProgramAppId" placeholder="请输入小程序appid" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="小程序页面路径" prop="miniProgramPagePath">
+              <el-input v-model="queryParams.miniProgramPagePath" placeholder="请输入小程序页面路径" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="发送时间" prop="sendTime">
+              <el-date-picker
+                v-model="queryParams.sendTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                class="!w-220px"
+              />
+            </el-form-item>-->
+      <!--      <el-form-item label="发送状态 0成功,1失败" prop="sendStatus">
+              <el-select v-model="queryParams.sendStatus" placeholder="请选择发送状态 0成功,1失败" clearable class="!w-240px">
+                <el-option label="请选择字典生成" value="" />
+              </el-select>
+            </el-form-item>-->
+      <!--      <el-form-item label="发送结果" prop="sendResult">
+              <el-input v-model="queryParams.sendResult" placeholder="请输入发送结果" clearable @keyup.enter="handleQuery" class="!w-240px"/>
+            </el-form-item>-->
+      <!--      <el-form-item label="创建时间" prop="createTime">
+              <el-date-picker
+                v-model="queryParams.createTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                class="!w-220px"
+              />
+            </el-form-item>-->
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px"/>
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px"/>
+          重置
+        </el-button>
+        <!--        <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:template-log:create']">
+                  <Icon icon="ep:plus" class="mr-5px" /> 新增
+                </el-button>-->
+        <el-button type="success" plain @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['mp:template-log:export']">
+          <Icon icon="ep:download" class="mr-5px"/>
+          导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <!--      <el-table-column label="主键" align="center" prop="id" />-->
+      <el-table-column label="appId" align="center" prop="appId"/>
+      <el-table-column label="用户openid" align="center" prop="toUser"/>
+      <el-table-column label="公众号模板ID" align="center" prop="templateId"/>
+      <el-table-column label="消息内容" align="center" prop="data"/>
+      <el-table-column label="链接" align="center" prop="url"/>
+      <el-table-column label="小程序appid" align="center" prop="miniProgramAppId"/>
+      <el-table-column label="小程序页面路径" align="center" prop="miniProgramPagePath"/>
+      <el-table-column label="发送时间" align="center" prop="sendTime" :formatter="dateFormatter" width="180px"/>
+      <el-table-column label="发送状态" align="center" prop="sendStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SEND_STATUS" :value="scope.row.sendStatus"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="发送结果" align="center" prop="sendResult"/>
+      <el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px"/>
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <!--          <el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['mp:template-log:update']">
+                      编辑
+                    </el-button>-->
+          <el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['mp:template-log:delete']">
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
+                @pagination="getList"/>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <MsgTemplateLogForm ref="formRef" @success="getList"/>
+</template>
+
+<script setup lang="ts">
+import {DICT_TYPE} from '@/utils/dict'
+import {dateFormatter} from '@/utils/formatTime'
+import download from '@/utils/download'
+import {MsgTemplateLogApi, MsgTemplateLogVO} from '@/api/mp/template'
+import MsgTemplateLogForm from './MsgTemplateLogForm.vue'
+
+/** 微信模版消息发送记录 列表 */
+defineOptions({name: 'MsgTemplateLog'})
+
+const message = useMessage() // 消息弹窗
+const {t} = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<MsgTemplateLogVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  appId: undefined,
+  toUser: undefined,
+  templateId: undefined,
+  data: undefined,
+  url: undefined,
+  miniProgramAppId: undefined,
+  miniProgramPagePath: undefined,
+  sendTime: [],
+  sendStatus: undefined,
+  sendResult: undefined,
+  createTime: [],
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await MsgTemplateLogApi.getMsgTemplateLogPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await MsgTemplateLogApi.deleteMsgTemplateLog(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await MsgTemplateLogApi.exportMsgTemplateLog(queryParams)
+    download.excel(data, '微信模版消息发送记录.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 144 - 0
src/views/mp/template/MsgTemplateLogForm.vue

@@ -0,0 +1,144 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="100px"
+        v-loading="formLoading"
+    >
+      <el-form-item label="appId" prop="appId">
+        <el-input v-model="formData.appId" placeholder="请输入appId"/>
+      </el-form-item>
+      <el-form-item label="用户openid" prop="toUser">
+        <el-input v-model="formData.toUser" placeholder="请输入用户openid"/>
+      </el-form-item>
+      <el-form-item label="公众号模板ID" prop="templateId">
+        <el-input v-model="formData.templateId" placeholder="请输入公众号模板ID"/>
+      </el-form-item>
+      <el-form-item label="消息内容" prop="data">
+        <el-input v-model="formData.data" placeholder="请输入消息内容"/>
+      </el-form-item>
+      <el-form-item label="链接" prop="url">
+        <el-input v-model="formData.url" placeholder="请输入链接"/>
+      </el-form-item>
+      <el-form-item label="小程序appid" prop="miniProgramAppId">
+        <el-input v-model="formData.miniProgramAppId" placeholder="请输入小程序appid"/>
+      </el-form-item>
+      <el-form-item label="小程序页面路径" prop="miniProgramPagePath">
+        <el-input v-model="formData.miniProgramPagePath" placeholder="请输入小程序页面路径"/>
+      </el-form-item>
+      <el-form-item label="发送时间" prop="sendTime">
+        <el-date-picker
+            v-model="formData.sendTime"
+            type="date"
+            value-format="x"
+            placeholder="选择发送时间"
+        />
+      </el-form-item>
+      <el-form-item label="发送状态 0成功,1失败" prop="sendStatus">
+        <el-radio-group v-model="formData.sendStatus">
+          <el-radio value="1">请选择字典生成</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="发送结果" prop="sendResult">
+        <el-input v-model="formData.sendResult" placeholder="请输入发送结果"/>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import {MsgTemplateLogApi, MsgTemplateLogVO} from '@/api/mp/template'
+
+/** 微信模版消息发送记录 表单 */
+defineOptions({name: 'MsgTemplateLogForm'})
+
+const {t} = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  appId: undefined,
+  toUser: undefined,
+  templateId: undefined,
+  data: undefined,
+  url: undefined,
+  miniProgramAppId: undefined,
+  miniProgramPagePath: undefined,
+  sendTime: undefined,
+  sendStatus: undefined,
+  sendResult: undefined,
+})
+const formRules = reactive({
+  appId: [{required: true, message: 'appId不能为空', trigger: 'blur'}],
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await MsgTemplateLogApi.getMsgTemplateLog(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as MsgTemplateLogVO
+    if (formType.value === 'create') {
+      await MsgTemplateLogApi.createMsgTemplateLog(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await MsgTemplateLogApi.updateMsgTemplateLog(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    appId: undefined,
+    toUser: undefined,
+    templateId: undefined,
+    data: undefined,
+    url: undefined,
+    miniProgramAppId: undefined,
+    miniProgramPagePath: undefined,
+    sendTime: undefined,
+    sendStatus: undefined,
+    sendResult: undefined,
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 82 - 0
src/views/mp/template/MsgTemplateSend.vue

@@ -0,0 +1,82 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <div style="font-size: 16px;margin: 10px 0;">已选择{{
+        multipleSelection.length > 0 ? multipleSelection
+            .length : total
+      }}个用户
+    </div>
+    <mpUser ref="mpUserRef" @change="mpUserChange"/>
+    <template #footer>
+      <el-button type="primary" @click="submit" :disabled="multipleSelection.length==0&&total==0" :loading="loading">推 送</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import {ref, nextTick} from "vue";
+import mpUser from '@/views/mp/user/index.vue'
+import {MsgTemplateApi} from "@/api/mp/template";
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗是否展示
+const dialogTitle = ref('') // 弹窗标题
+const multipleSelection = ref<any[]>([])
+const total = ref(0)
+const loading = ref(false)
+
+let dataForm = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  accountId: -1,
+  openid: '',
+  nickname: '',
+  unionId: '',
+  openidList: [] as string[],
+  appId: '',
+  templateId: ''
+})
+
+/** 打开mpUser弹窗 */
+const mpUserRef = ref()
+const open = async (accountId: number, appId: string, templateId: string) => {
+  // 清空dataForm
+  Object.keys(dataForm).forEach((key) => {
+    dataForm[key] = ''
+  })
+  loading.value = false
+  dialogVisible.value = true
+  dialogTitle.value = "批量推送消息"
+  await nextTick()
+  mpUserRef.value.open(accountId)
+  dataForm.accountId = accountId
+  dataForm.appId = appId
+  dataForm.templateId = templateId
+}
+defineExpose({open}) // 暴露open方法
+
+const submit = async () => {
+  try {
+    await message.confirm(`已选择${multipleSelection.value.length > 0 ? multipleSelection.value.length : total.value}个用户,请确认是否推送?`)
+    loading.value = true
+    await MsgTemplateApi.sendMsgBatch(dataForm)
+    message.success('推送成功')
+    dialogVisible.value = false
+    loading.value = false
+  } catch (e) {
+    console.error(e);
+  }
+}
+
+const mpUserChange = (changeEventData: any) => {
+  total.value = changeEventData.total;
+  multipleSelection.value = changeEventData.multipleSelection;
+  dataForm = {...dataForm, ...changeEventData.queryParams};
+  dataForm.openidList = changeEventData.multipleSelection.map((item: any) => item.openid);
+}
+
+</script>
+
+<style scoped lang="scss"></style>

+ 44 - 7
src/views/mp/user/index.vue

@@ -11,7 +11,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <WxAccountSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" :modelValue="queryParams.accountId" />
       </el-form-item>
       <el-form-item label="用户标识" prop="openid">
         <el-input
@@ -49,12 +49,13 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list">
+    <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" v-if="isDialog"/>
       <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="用户标识" align="center" prop="openid" width="260" />
       <el-table-column label="用户头像" min-width="80px" prop="headImageUrl">
         <template #default="scope">
-          <el-avatar :src="scope.row.headImageUrl" />
+          <el-avatar :src="scope.row.headImageUrl"/>
         </template>
       </el-table-column>
       <el-table-column label="昵称" align="center" prop="nickname" />
@@ -105,20 +106,24 @@
   <UserForm ref="formRef" @success="getList" />
 </template>
 <script lang="ts" setup>
-import { dateFormatter } from '@/utils/formatTime'
+import {dateFormatter} from '@/utils/formatTime'
 import * as MpUserApi from '@/api/mp/user'
 import * as MpTagApi from '@/api/mp/tag'
 import WxAccountSelect from '@/views/mp/components/wx-account-select'
-import type { FormInstance } from 'element-plus'
+import type {FormInstance} from 'element-plus'
 import UserForm from './UserForm.vue'
+import {ref} from "vue";
 
 defineOptions({ name: 'MpUser' })
 
+const { t } = useI18n() // 国际化
 const message = useMessage() // 消息
 
+const isDialog = ref(false) // 是不是弹窗调用
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref<any[]>([]) // 列表的数据
+const multipleSelection = ref<String[]>([])
 
 const queryParams = reactive({
   pageNo: 1,
@@ -140,6 +145,7 @@ const onAccountChanged = (id: number) => {
 /** 查询列表 */
 const getList = async () => {
   try {
+    multipleSelection.value = []
     loading.value = true
     const data = await MpUserApi.getUserPage(queryParams)
     list.value = data.list
@@ -153,6 +159,9 @@ const getList = async () => {
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
+  if(isDialog.value){
+    emitChange()
+  }
 }
 
 /** 重置按钮操作 */
@@ -176,8 +185,36 @@ const handleSync = async () => {
     await MpUserApi.syncUser(queryParams.accountId)
     message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询')
     await getList()
-  } catch {
-    //
+  } catch {}
+}
+
+/** Expose*/
+defineExpose({
+  open: (accountId: number) => {
+    onAccountChanged(accountId)
+    isDialog.value = true
+  }
+});
+
+/** Emits*/
+interface Emits {
+  (e: 'change', data: {
+    multipleSelection: any[]
+    total: number
+    queryParams: object
+  }): void
+  // (e: 'select', user: any): void
+  // (e: 'cancel'): void
+}
+const emit = defineEmits<Emits>()
+const emitChange = () => {
+  emit('change', {multipleSelection: multipleSelection.value, total: total.value, queryParams})
+}
+
+const handleSelectionChange = (val: any[]) => {
+  multipleSelection.value = val
+  if (isDialog.value) {
+    emitChange()
   }
 }