Sfoglia il codice sorgente

feat: emergency broadcast

Casper Dai 3 anni fa
parent
commit
aba960a447

+ 95 - 0
src/api/broadcast.js

@@ -0,0 +1,95 @@
+import request, { tenantRequest } from '@/utils/request'
+import {
+  messageSend,
+  confirmAndSend,
+  addTenant
+} from './base'
+
+export function addBroadcastTemplate (data) {
+  return request({
+    url: `/orchestration/broadcastTemplate`,
+    method: 'POST',
+    data
+  })
+}
+
+export function updateBroadcastTemplate (data) {
+  return request({
+    url: `/orchestration/broadcastTemplate`,
+    method: 'PUT',
+    data
+  })
+}
+
+export function delBroadcastTemplate (id) {
+  return request({
+    url: `/orchestration/broadcastTemplate/${id}`,
+    method: 'DELETE'
+  })
+}
+
+export function getBroadcastTemplate (id) {
+  return request({
+    url: `/orchestration/broadcastTemplate/${id}`,
+    method: 'GET'
+  })
+}
+
+export function getBroadcastTemplates (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: `/orchestration/broadcastTemplate/pageQuery`,
+    method: 'GET',
+    params: {
+      pageIndex,
+      pageSize,
+      ...params
+    }
+  })
+}
+
+export function publishBroadcast (data) {
+  return messageSend({
+    url: `/orchestration/broadcast`,
+    method: 'POST',
+    data: addTenant({
+      ...data
+    })
+  }, '发布', tenantRequest)
+}
+
+export function getBroadcasts (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: `/orchestration/broadcast/pageQuery`,
+    method: 'GET',
+    params: addTenant({
+      pageIndex,
+      pageSize,
+      ...params
+    })
+  })
+}
+
+export function disableBroadcast (id) {
+  return confirmAndSend('下架', '', {
+    url: `/orchestration/broadcast/disable/${id}`,
+    method: 'PUT'
+  })
+}
+
+export function disableDeviceRelease (data) {
+  return confirmAndSend('下架', '', {
+    url: `/orchestration/broadcast/deviceRelease/disable`,
+    method: 'PUT',
+    data
+  })
+}
+
+export function getDeviceReleases ({ releaseId }) {
+  return request({
+    url: `/orchestration/broadcast/${releaseId}/deviceRelease`,
+    method: 'GET'
+
+  })
+}

+ 14 - 0
src/icons/svg/bm.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 26 26" style="enable-background:new 0 0 26 26;" xml:space="preserve">
+<g>
+	<path d="M13,24c-0.8,0-1.5-0.3-2.1-0.8L6.6,19H4c-1.7,0-3-1.3-3-3v-6c0-1.7,1.3-3,3-3h2.6l4.3-4.1C11.8,2,13,1.8,14.2,2.3
+		C15.3,2.8,16,3.8,16,5V21c0,1.2-0.7,2.3-1.8,2.8C13.8,23.9,13.4,24,13,24z M4,9c-0.6,0-1,0.4-1,1v6c0,0.6,0.4,1,1,1h3.4l4.9,4.7
+		c0.4,0.4,0.9,0.3,1.1,0.2c0.1-0.1,0.6-0.3,0.6-0.9V5c0-0.6-0.5-0.9-0.6-0.9c-0.1-0.1-0.6-0.2-1.1,0.2L7.4,9H4z"/>
+	<path d="M21,6L21,6c-0.4,0.4-0.4,0.9-0.1,1.3c1.3,1.6,2,3.5,2,5.7c0,2.2-0.8,4.1-2,5.7c-0.3,0.4-0.3,1,0.1,1.3l0,0
+		c0.4,0.4,1.1,0.4,1.5-0.1c1.5-1.9,2.5-4.3,2.5-6.9c0-2.6-0.9-5.1-2.5-6.9C22.2,5.6,21.5,5.5,21,6z"/>
+	<path d="M18.2,8.8L18.2,8.8c-0.4,0.4-0.4,0.9-0.1,1.3c0.6,0.8,0.9,1.8,0.9,2.9c0,1.1-0.3,2.1-0.9,2.9c-0.3,0.4-0.3,1,0.1,1.3l0,0
+		c0.4,0.4,1.1,0.4,1.5-0.1c0.8-1.2,1.3-2.6,1.3-4.1c0-1.5-0.5-2.9-1.3-4.1C19.3,8.4,18.6,8.4,18.2,8.8z"/>
+</g>
+</svg>

+ 28 - 0
src/router/index.js

@@ -320,6 +320,34 @@ export const asyncRoutes = [
       }
     ]
   },
+  {
+    path: '/bm',
+    component: Layout,
+    meta: { title: '应急广播', icon: 'bm' },
+    children: [
+      {
+        name: 'broadcast-template',
+        path: 'template',
+        component: () => import('@/views/broadcast/template/index'),
+        access: Access.MANAGE_TENANTS,
+        meta: { title: '模板管理' }
+      },
+      {
+        name: 'broadcast-deploy',
+        path: 'deploy',
+        component: () => import('@/views/broadcast/deploy/index'),
+        access: [Access.MANAGE_TENANTS, Access.MANAGE_TENANT],
+        meta: { title: '广播发布' }
+      },
+      {
+        name: 'broadcast-deploy-history',
+        path: 'history',
+        component: () => import('@/views/broadcast/history/index'),
+        access: [Access.MANAGE_TENANTS, Access.MANAGE_TENANT],
+        meta: { title: '发布历史' }
+      }
+    ]
+  },
   {
     path: '/l',
     component: Layout,

+ 286 - 0
src/views/broadcast/deploy/components/BroadcasDeployPanel.vue

@@ -0,0 +1,286 @@
+<template>
+  <div class="l-flex__auto l-flex--col">
+    <div class="l-flex__none l-flex--row c-step has-padding">
+      <button
+        class="l-flex__none c-sibling-item o-button"
+        :class="{ hidden: active === 0 }"
+        @click="onPresent"
+      >
+        上一步
+      </button>
+      <el-steps
+        :active="active"
+        class="l-flex__fill"
+        finish-status="success"
+        align-center
+      >
+        <el-step title="选择模板" />
+        <el-step title="选择设备" />
+        <el-step title="信息确认" />
+      </el-steps>
+      <button
+        class="l-flex__none c-sibling-item o-button"
+        :class="{ hidden: hideNext }"
+        @click="onNext"
+      >
+        {{ btnMsg }}
+      </button>
+    </div>
+    <div class="l-flex__auto l-flex--col has-padding">
+      <div
+        v-show="active === 0"
+        class="c-grid-form u-align-self--center"
+      >
+        <span class="c-grid-form__label required">位置</span>
+        <el-select v-model="position">
+          <el-option
+            v-for="option in positionOptions"
+            :key="option.value"
+            :label="option.label"
+            :value="option.value"
+          />
+        </el-select>
+        <span class="c-grid-form__label required">模板</span>
+        <schema-select
+          v-model="templateId"
+          :schema="templateSelectSchema"
+          placeholder="请选择"
+          @change="onTemplateChange"
+        />
+        <div
+          v-if="templateId && !templateContent"
+          class="c-grid-form__row u-text-center"
+        >
+          <i class="el-icon-loading" />
+        </div>
+        <template v-if="templateContent">
+          <span class="c-grid-form__label">失效时间</span>
+          <el-date-picker
+            v-model="endDateTime"
+            type="datetime"
+            placeholder="请选择失效时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            :picker-options="endDatePickerOptions"
+            @change="onEndDateTimeChange"
+          />
+          <span class="c-grid-form__label">内容</span>
+          <div class="l-flex--row c-grid-form__option c-grid-form__text">{{ templateContent }}</div>
+          <div
+            v-for="(value, key) in keywordMap"
+            :key="key"
+            class="c-grid-form__row"
+          >
+            <span class="c-grid-form__label required c-grid-form__text">{{ value.keywordName }}</span>
+            <el-input
+              v-model.trim="value.content"
+              placeholder="最多50个字符"
+              maxlength="50"
+              clearable
+            />
+          </div>
+        </template>
+      </div>
+      <device-group-tree
+        v-show="active === 1"
+        ref="tree"
+        class="l-flex__fill has-padding"
+        @change="onChange"
+      />
+      <div
+        v-if="active === 2"
+        class="c-grid-form u-align-self--center"
+      >
+        <span class="c-grid-form__label">位置:</span>
+        <div class="l-flex--row c-grid-form__option c-grid-form__text">{{ positionDesc }}</div>
+        <span class="c-grid-form__label">内容:</span>
+        <div class="l-flex--row c-grid-form__option c-grid-form__text">{{ targetContent }}</div>
+        <span class="c-grid-form__label">目标设备:</span>
+        <div class="l-flex--row c-grid-form__option c-grid-form__text">{{ selectedDeviceName }}</div>
+        <span class="c-grid-form__label">失效时间:</span>
+        <div class="l-flex--row c-grid-form__option c-grid-form__text">{{ endDateTime || '不限定' }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { parseTime } from '@/utils'
+import {
+  getBroadcastTemplates,
+  getBroadcastTemplate,
+  publishBroadcast
+} from '@/api/broadcast'
+
+export default {
+  name: 'BrodcastDeployPanel',
+  props: {
+    tenant: {
+      type: String,
+      default: ''
+    }
+  },
+  data () {
+    return {
+      active: 0,
+      position: 1,
+      positionOptions: [
+        { label: '屏幕顶部', value: 0 },
+        { label: '屏幕底部', value: 1 }
+      ],
+      templateSelectSchema: {
+        remote: getBroadcastTemplates,
+        pagination: true,
+        value: 'templateId',
+        label: 'templateName'
+      },
+      templateId: '',
+      templateContent: '',
+      targetContent: '',
+      endDateTime: '',
+      keywordMap: [],
+      selectedDevices: []
+    }
+  },
+  computed: {
+    minDate () {
+      const now = new Date()
+      return new Date(now.getFullYear(), now.getMonth(), now.getDate())
+    },
+    endDatePickerOptions () {
+      return {
+        disabledDate: this.isDisableDate
+      }
+    },
+    hideNext () {
+      switch (this.active) {
+        case 0:
+          return !this.templateContent || this.keywordMap.some(({ content }) => !content)
+        case 1:
+          return this.selectedDevices.length === 0
+        default:
+          return false
+      }
+    },
+    btnMsg () {
+      return this.active < 2 ? '下一步' : '发布'
+    },
+    positionDesc () {
+      return this.positionOptions[this.position].label
+    },
+    selectedDeviceName () {
+      return this.selectedDevices.map(device => device.name).join(', ')
+    }
+  },
+  methods: {
+    onEndDateTimeChange () {
+      if (this.endDateTime && new Date(this.endDateTime).getTime() < Date.now()) {
+        this.endDateTime = parseTime(Date.now(), '{y}-{m}-{d} {h}:{i}:{s}')
+      }
+    },
+    isDisableDate (date) {
+      return date < this.minDate
+    },
+    onTemplateChange (templateId) {
+      this.templateContent = ''
+      this.keywordMap = []
+      getBroadcastTemplate(templateId).then(({ data }) => {
+        const { templateName, templateContent, keywordMap, optionalNumbers } = data
+        const arr = []
+        let content = templateContent
+        for (let i = 0; i < optionalNumbers; i++) {
+          content = content.replace(new RegExp(`\\{${i}\\}`, 'g'), `{${keywordMap[i].keywordName}}`)
+          arr.push({
+            ...keywordMap[i],
+            content: ''
+          })
+        }
+        this.templateContent = content
+        this.$templateData = {
+          templateName,
+          templateContent,
+          optionalNumbers
+        }
+        this.keywordMap = arr
+      })
+    },
+    onPresent () {
+      if (this.active > 0) {
+        this.active -= 1
+      }
+    },
+    onNext () {
+      switch (this.active) {
+        case 0:
+          this.active += 1
+          break
+        case 1:
+          this.collectInfo()
+          this.active += 1
+          break
+        case 2:
+          this.publish().then(() => {
+            this.active = 0
+            this.position = 1
+            this.templateId = ''
+            this.templateContent = ''
+            this.keywordMap = []
+            this.endDateTime = ''
+            this.$refs.tree.reset()
+          })
+          break
+        default:
+          break
+      }
+    },
+    onChange (devices) {
+      this.selectedDevices = devices
+    },
+    collectInfo () {
+      const { templateContent, optionalNumbers } = this.$templateData
+      let content = templateContent
+      for (let i = 0; i < optionalNumbers; i++) {
+        content = content.replace(new RegExp(`\\{${i}\\}`, 'g'), this.keywordMap[i].content)
+      }
+      this.targetContent = content
+    },
+    publish () {
+      if (this.endDateTime && new Date(this.endDateTime).getTime() < Date.now()) {
+        this.$message({
+          type: 'warning',
+          message: '失效时间已过期'
+        })
+        return Promise.reject()
+      }
+      return this.$confirm(
+        `立即发布?`,
+        { type: 'warning' }
+      ).then(() => {
+        const keywordMap = {}
+        this.keywordMap.forEach(({ content }, key) => {
+          keywordMap[key] = { content }
+        })
+        return publishBroadcast({
+          tenant: this.tenant,
+          position: this.position,
+          name: this.$templateData.templateName,
+          templateId: this.templateId,
+          keywordMap,
+          startTime: parseTime(Date.now(), '{y}-{m}-{d} {h}:{i}:{s}'),
+          endTime: this.endDateTime,
+          deviceIds: this.selectedDevices.map(device => device.id)
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-step {
+  border-bottom: 1px solid $gray--light;
+
+  .hidden {
+    visibility: hidden;
+  }
+}
+</style>

+ 38 - 0
src/views/broadcast/deploy/index.vue

@@ -0,0 +1,38 @@
+<script>
+import { mapGetters } from 'vuex'
+import BroadcasDeployPanel from './components/BroadcasDeployPanel'
+
+export default {
+  name: 'BroadcastDeploy',
+  components: {
+    BroadcasDeployPanel
+  },
+  computed: {
+    ...mapGetters(['isSuperAdmin'])
+  },
+  render (h) {
+    return this.isSuperAdmin
+      ? h('TenantPage', {
+        scopedSlots: {
+          default ({ group }) {
+            return group
+              ? h('BroadcasDeployPanel', {
+                props: {
+                  tenant: group.path
+                }
+              })
+              : null
+          }
+        }
+      })
+      : h('Wrapper', {
+        props: {
+          fill: true,
+          margin: true,
+          padding: true,
+          background: true
+        }
+      }, [h('BroadcasDeployPanel')])
+  }
+}
+</script>

+ 112 - 0
src/views/broadcast/history/components/BroadcastHistoryTable.vue

@@ -0,0 +1,112 @@
+<template>
+  <schema-table
+    ref="table"
+    :schema="schema"
+  >
+    <table-dialog
+      ref="tableDialog"
+      title="关联设备"
+      :schema="deviceSchema"
+    />
+  </schema-table>
+</template>
+
+<script>
+import {
+  getBroadcasts,
+  disableBroadcast,
+  disableDeviceRelease,
+  getDeviceReleases
+} from '@/api/broadcast'
+
+export default {
+  name: 'BroadcastHistoryTable',
+  props: {
+    tenant: {
+      type: String,
+      default: ''
+    }
+  },
+  data () {
+    return {
+      schema: {
+        condition: { tenant: this.tenant },
+        list: getBroadcasts,
+        filters: [
+          {
+            key: 'releaseStatus',
+            type: 'select',
+            placeholder: '全部状态',
+            options: [
+              { value: 1, label: '已发布' },
+              { value: 0, label: '已失效' }
+            ]
+          }
+        ],
+        cols: [
+          { prop: 'name', label: '模板' },
+          { label: '位置', 'min-width': 60, render ({ position }) {
+            return ['顶部', '底部'][position]
+          } },
+          { prop: 'content', label: '内容', 'min-width': 120 },
+          { prop: 'startTime', label: '发布时间' },
+          { prop: 'endTime', label: '失效时间', render ({ endTime }) {
+            return endTime || '-'
+          } },
+          { label: '状态', type: 'tag', width: 100, render ({ releaseStatus }) {
+            return {
+              type: ['warning', 'success'][releaseStatus],
+              label: ['失效', '正常'][releaseStatus]
+            }
+          } },
+          { type: 'invoke', width: 160, render: [
+            { label: '设备列表', on: this.onViewDevice },
+            { label: '下架', render ({ releaseStatus }) {
+              return releaseStatus !== 0
+            }, on: this.onDisable }
+          ] }
+        ]
+      },
+      releaseId: ''
+    }
+  },
+  computed: {
+    deviceSchema () {
+      return {
+        condition: { releaseId: this.releaseId },
+        list: getDeviceReleases,
+        cols: [
+          { prop: 'deviceId', label: '设备id' },
+          { label: '状态', type: 'tag', render ({ publishStatus }) {
+            return {
+              type: ['warning', 'success'][publishStatus],
+              label: ['失效', '正常'][publishStatus]
+            }
+          } },
+          { type: 'invoke', render: [
+            { label: '下架', render ({ publishStatus }) {
+              return publishStatus !== 0
+            }, on: this.disableDeviceRelease }
+          ] }
+        ]
+      }
+    }
+  },
+  methods: {
+    onViewDevice ({ releaseId }) {
+      this.releaseId = releaseId
+      this.$refs.tableDialog.show()
+    },
+    onDisable (item) {
+      disableBroadcast(item.releaseId).then(() => {
+        item.releaseStatus = 0
+      })
+    },
+    disableDeviceRelease (item) {
+      disableDeviceRelease({ deviceId: item.deviceId, releaseId: this.releaseId }).then(() => {
+        item.status = 0
+      })
+    }
+  }
+}
+</script>

+ 38 - 0
src/views/broadcast/history/index.vue

@@ -0,0 +1,38 @@
+<script>
+import { mapGetters } from 'vuex'
+import BroadcastHistoryTable from './components/BroadcastHistoryTable'
+
+export default {
+  name: 'BroadcastHistory',
+  components: {
+    BroadcastHistoryTable
+  },
+  computed: {
+    ...mapGetters(['isSuperAdmin'])
+  },
+  render (h) {
+    return this.isSuperAdmin
+      ? h('TenantPage', {
+        scopedSlots: {
+          default ({ group }) {
+            return group
+              ? h('BroadcastHistoryTable', {
+                props: {
+                  tenant: group.path
+                }
+              })
+              : null
+          }
+        }
+      })
+      : h('Wrapper', {
+        props: {
+          fill: true,
+          margin: true,
+          padding: true,
+          background: true
+        }
+      }, [h('BroadcastHistoryTable')])
+  }
+}
+</script>

+ 240 - 0
src/views/broadcast/template/index.vue

@@ -0,0 +1,240 @@
+<template>
+  <wrapper
+    fill
+    margin
+    padding
+    background
+  >
+    <schema-table
+      ref="table"
+      :schema="schema"
+    />
+    <confirm-dialog
+      ref="editDialog"
+      :title="dialogTitle"
+      @confirm="onSave"
+    >
+      <div class="c-grid-form u-align-self--center">
+        <span class="c-grid-form__label required">名称</span>
+        <div class="c-grid-form__info">
+          <el-input
+            v-model.trim="template.templateName"
+            placeholder="最多50个字符"
+            maxlength="50"
+            clearable
+          />
+        </div>
+        <span class="c-grid-form__label required">内容</span>
+        <el-input
+          ref="textarea"
+          v-model.trim="template.templateContent"
+          type="textarea"
+          :rows="3"
+        />
+        <div class="c-grid-form__row">
+          <button
+            class="o-button"
+            @click="addPlaceholder"
+          >
+            <i class="o-button__icon el-icon-circle-plus-outline" />
+            新增占位符
+          </button>
+        </div>
+        <div
+          v-for="(value, key) in template.keywordMap"
+          :key="key"
+          class="c-grid-form__row"
+        >
+          <span class="c-grid-form__label required c-grid-form__text">{{ "{" + key + "}" }}名称</span>
+          <el-input
+            v-model.trim="value.keywordName"
+            placeholder="最多50个字符"
+            maxlength="50"
+            clearable
+          >
+            <template #append>
+              <i
+                class="c-sibling-item o-icon medium el-icon-plus has-active"
+                @click="onAddPlaceholder(key)"
+              />
+              <i
+                class="c-sibling-item o-icon medium el-icon-delete has-active"
+                @click="onDelPlaceholder(key)"
+              />
+            </template>
+          </el-input>
+        </div>
+      </div>
+    </confirm-dialog>
+  </wrapper>
+</template>
+
+<script>
+import {
+  getBroadcastTemplates,
+  addBroadcastTemplate,
+  updateBroadcastTemplate,
+  delBroadcastTemplate,
+  getBroadcastTemplate
+} from '@/api/broadcast'
+
+export default {
+  name: 'BroadcastTemplate',
+  data () {
+    return {
+      schema: {
+        list: getBroadcastTemplates,
+        buttons: [{ type: 'add', on: this.onAdd }],
+        filters: [
+          { key: 'templateName', type: 'search', placeholder: '模板名称' }
+        ],
+        cols: [
+          { prop: 'templateName', label: '模板名称' },
+          { prop: 'createTime', label: '创建时间' },
+          { type: 'invoke', render: [
+            { label: '编辑', on: this.onEdit },
+            { label: '删除', on: this.onDel }
+          ] }
+        ]
+      },
+      template: {}
+    }
+  },
+  computed: {
+    dialogTitle () {
+      return this.template.templateId ? '编辑模板' : '新增模板'
+    }
+  },
+  methods: {
+    getCursorIndex () {
+      return this.$refs.textarea.$el.children[0].selectionStart || this.template.templateContent.length
+    },
+    addTemplateContent (content) {
+      const arr = this.template.templateContent.split('')
+      arr.splice(this.getCursorIndex(), 0, content)
+      this.template.templateContent = arr.join('')
+    },
+    onAddPlaceholder (key) {
+      this.addTemplateContent(`{${key}}`)
+    },
+    onDelPlaceholder (delKey) {
+      this.template.optionalNumbers -= 1
+      const max = this.template.optionalNumbers
+      this.template.templateContent = this.template.templateContent.replace(
+        new RegExp(`\\{${delKey}\\}`, 'g'),
+        ''
+      )
+      for (let i = delKey + 1; i <= max; i++) {
+        this.template.templateContent = this.template.templateContent.replace(
+          new RegExp(`\\{${i}\\}`, 'g'),
+          `{${i - 1}}`
+        )
+      }
+      this.template.keywordMap.splice(delKey, 1)
+    },
+    addPlaceholder () {
+      this.template.keywordMap.push({
+        type: 0,
+        keywordName: ''
+      })
+      this.addTemplateContent(`{${this.template.optionalNumbers}}`)
+      this.template.optionalNumbers += 1
+    },
+    onAdd () {
+      this.template = {
+        type: 0,
+        templateName: '',
+        templateContent: '',
+        optionalNumbers: 0,
+        keywordMap: []
+      }
+      this.$refs.editDialog.show()
+    },
+    onEdit ({ templateId }) {
+      getBroadcastTemplate(templateId).then(({ data }) => {
+        const { templateId, type, templateName, templateContent, keywordMap, optionalNumbers } = data
+        const arr = []
+        for (let i = 0; i < optionalNumbers; i++) {
+          arr.push(keywordMap[i])
+        }
+        this.template = {
+          templateId, templateName, templateContent, type, optionalNumbers,
+          keywordMap: arr
+        }
+        this.$refs.editDialog.show()
+      })
+    },
+    onSave (done) {
+      const { templateId, templateName, templateContent, keywordMap, type, optionalNumbers } = this.template
+      if (!templateName) {
+        this.$message({
+          type: 'warning',
+          message: '请填写模板名称'
+        })
+        return
+      }
+      if (!templateContent) {
+        this.$message({
+          type: 'warning',
+          message: '请填写模板内容'
+        })
+        return
+      }
+      const map = {}
+      for (let i = 0; i < optionalNumbers; i++) {
+        const keyword = keywordMap[i]
+        if (!keyword.keywordName) {
+          this.$message({
+            type: 'warning',
+            message: `请填写占位符{${i}}名称`
+          })
+          return
+        }
+        if (!new RegExp(`\\{${i}\\}`).test(templateContent)) {
+          this.$message({
+            type: 'warning',
+            message: `模板内容缺少占位符{${i}}`
+          })
+          return
+        }
+        map[i] = keyword
+      }
+      if (templateId) {
+        updateBroadcastTemplate({
+          templateId,
+          type,
+          templateName,
+          templateContent,
+          optionalNumbers,
+          keywordMap: map
+        }).then(() => {
+          done()
+          this.$refs.table.pageTo()
+        })
+      } else {
+        addBroadcastTemplate({
+          type,
+          templateName,
+          templateContent,
+          optionalNumbers,
+          keywordMap: map
+        }).then(() => {
+          done()
+          this.$refs.table.resetCondition({ templateName })
+        })
+      }
+    },
+    onDel ({ templateId, templateName }) {
+      this.$confirm(`删除模板${templateName}?`, { type: 'warning' }).then(() => {
+        delBroadcastTemplate(templateId).then(() => {
+          this.$message({
+            type: 'success',
+            message: '删除成功'
+          })
+          this.$refs.table.decrease(1)
+        })
+      })
+    }
+  }
+}
+</script>