sms-config-panel.vue 13.5 KB
<template>
  <div class="sms-config-panel">
    <ConfigPanelShell
      :modules="modules"
      :loading="loading"
      :show-save="false"
      storage-key="smsConfigActiveModule"
      alert-title="左侧切换「基础配置」与「模板管理」;基础配置单独保存,模板支持增删改与测试发送。"
      alert-type="info"
      default-module="credentials"
    >
      <template #credentials>
      <el-card shadow="never" class="config-section">
        <div slot="header" class="config-section__header">
          <span>短信基础配置</span>
          <span class="config-section__tag">阿里云短信</span>
        </div>
        <el-alert :title="configTip" type="info" :closable="false" show-icon class="config-alert" />
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="AccessKeyId" label-width="120px">
              <el-input v-model="configForm.accessKeyId" clearable placeholder="阿里云 AccessKeyId" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="AccessKeySecret" label-width="130px">
              <el-input
                v-model="configForm.accessKeySecret"
                show-password
                clearable
                autocomplete="new-password"
                :placeholder="configForm.hasAccessKeySecret ? `已配置 ${configForm.accessKeySecretMasked},留空不修改` : '阿里云 AccessKeySecret'"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="默认签名" label-width="120px">
              <el-input v-model="configForm.signName" clearable placeholder="绿纤聚财" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="厂商" label-width="120px">
              <el-select v-model="configForm.company" style="width: 100%">
                <el-option label="阿里云" value="1" />
                <el-option label="腾讯云(仅兼容历史)" value="2" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="Endpoint" label-width="120px">
              <el-input v-model="configForm.endpoint" clearable placeholder="dysmsapi.aliyuncs.com" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="生日防重发" label-width="120px">
              <el-switch v-model="configForm.preventDuplicate" :active-value="1" :inactive-value="0" />
              <span class="form-tip">同会员同日同场景不重复发送</span>
            </el-form-item>
          </el-col>
        </el-row>
        <div class="panel-actions">
          <el-button type="primary" size="small" :loading="configSaving" @click="saveConfig">保存短信配置</el-button>
        </div>
      </el-card>
    </template>

    <template #templates>
      <el-card shadow="never" class="config-section config-section--flush">
        <div class="panel-actions panel-actions--between">
          <div class="toolbar-left">
            <el-input
              v-model="templateQuery.name"
              size="small"
              clearable
              placeholder="名称"
              style="width: 140px; margin-right: 8px"
              @keyup.enter.native="searchTemplates"
            />
            <el-input
              v-model="templateQuery.sceneCode"
              size="small"
              clearable
              placeholder="场景编码"
              style="width: 140px; margin-right: 8px"
              @keyup.enter.native="searchTemplates"
            />
            <el-button size="small" icon="el-icon-search" @click="searchTemplates">查询</el-button>
          </div>
          <div class="toolbar-right">
            <el-button size="small" icon="el-icon-message" @click="openTestDialog">测试发送</el-button>
            <el-button size="small" icon="el-icon-document" @click="openLogDrawer">发送日志</el-button>
            <el-button type="primary" size="small" icon="el-icon-plus" @click="openTemplateDialog()">新增模板</el-button>
          </div>
        </div>

        <NCC-table v-loading="templateLoading" :data="templateList">
          <el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip>
            <template slot-scope="{ row }">
              <div class="table-cell-with-icon">
                <i class="el-icon-s-order cell-icon cell-icon--primary" />
                <span>{{ row.name || '无' }}</span>
              </div>
            </template>
          </el-table-column>
          <el-table-column prop="templateCode" label="CODE" min-width="140" show-overflow-tooltip>
            <template slot-scope="{ row }">{{ row.templateCode || '无' }}</template>
          </el-table-column>
          <el-table-column prop="signName" label="签名" min-width="110" show-overflow-tooltip>
            <template slot-scope="{ row }">{{ row.signName || '无' }}</template>
          </el-table-column>
          <el-table-column label="类型" width="100">
            <template slot-scope="{ row }">
              <el-tag size="mini" :type="templateTypeTag(row.templateType)" disable-transitions>
                {{ row.templateTypeName || templateTypeName(row.templateType) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="sceneCode" label="场景" min-width="110" show-overflow-tooltip>
            <template slot-scope="{ row }">{{ row.sceneCode || '无' }}</template>
          </el-table-column>
          <el-table-column label="启用" width="90" align="center">
            <template slot-scope="{ row }">
              <el-tag size="mini" :type="row.enabled === 1 ? 'success' : 'info'" disable-transitions>
                {{ row.enabled === 1 ? '启用' : '停用' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
            <template slot-scope="{ row }">{{ row.remark || '无' }}</template>
          </el-table-column>
          <el-table-column label="操作" width="220" fixed="right">
            <template slot-scope="{ row }">
              <el-button type="text" size="small" @click="openTemplateDialog(row)">编辑</el-button>
              <el-button type="text" size="small" @click="toggleEnabled(row)">
                {{ row.enabled === 1 ? '停用' : '启用' }}
              </el-button>
              <el-button type="text" size="small" class="NCC-table-delBtn" @click="removeTemplate(row)">删除</el-button>
            </template>
          </el-table-column>
        </NCC-table>
        <pagination
          :total="templateTotal"
          :page.sync="templateQuery.currentPage"
          :limit.sync="templateQuery.pageSize"
          @pagination="loadTemplates"
        />
      </el-card>
      </template>
    </ConfigPanelShell>

    <sms-template-dialog ref="templateDialog" @success="loadTemplates" />
    <sms-test-send-dialog ref="testDialog" />
    <sms-send-log-drawer ref="logDrawer" />
  </div>
</template>

<script>
import ConfigPanelShell from './config-panel-shell.vue'
import {
  getSmsConfig,
  saveSmsConfig,
  getSmsTemplateList,
  toggleSmsTemplateEnabled,
  deleteSmsTemplate
} from '@/api/extend/lqSms'
import SmsTemplateDialog from './sms-template-dialog.vue'
import SmsTestSendDialog from './sms-test-send-dialog.vue'
import SmsSendLogDrawer from './sms-send-log-drawer.vue'

const MODULES = [
  { key: 'credentials', label: '基础配置', desc: 'AccessKey 与签名', icon: 'el-icon-key', iconClass: 'module-icon--credentials' },
  { key: 'templates', label: '模板管理', desc: '场景模板与发送', icon: 'el-icon-message', iconClass: 'module-icon--templates' }
]

export default {
  name: 'SmsConfigPanel',
  components: {
    ConfigPanelShell,
    SmsTemplateDialog,
    SmsTestSendDialog,
    SmsSendLogDrawer
  },
  data() {
    return {
      modules: MODULES,
      loading: false,
      configSaving: false,
      templateLoading: false,
      configForm: {
        accessKeyId: '',
        accessKeySecret: '',
        accessKeySecretMasked: '',
        hasAccessKeySecret: false,
        credentialSource: '',
        signName: '绿纤聚财',
        company: '1',
        endpoint: 'dysmsapi.aliyuncs.com',
        preventDuplicate: 1
      },
      templateQuery: {
        currentPage: 1,
        pageSize: 10,
        name: '',
        sceneCode: ''
      },
      templateList: [],
      templateTotal: 0
    }
  },
  created() {
    this.init()
  },
  computed: {
    configTip() {
      const src = this.configForm.credentialSource === 'AliyunOSS'
        ? '当前 AccessKey 复用 appsettings 中 OSS(NCC_App:AliyunOSS),无需重复填写。'
        : (this.configForm.credentialSource === 'SmsConfig'
          ? '当前使用短信单独配置的 AccessKey。'
          : '未检测到可用 AccessKey,请确认 OSS 或短信配置。')
      return `主要用于会员生日短信(模板 SMS_503595047)。${src} Secret 脱敏回显;留空保存不修改单独覆盖项。`
    }
  },
  methods: {
    init() {
      this.loading = true
      Promise.all([
        this.loadConfig(),
        this.loadTemplates()
      ]).finally(() => {
        this.loading = false
      })
    },
    loadConfig() {
      return getSmsConfig().then(res => {
        const data = (res && res.data) || {}
        this.configForm = {
          accessKeyId: data.accessKeyId || '',
          accessKeySecret: '',
          accessKeySecretMasked: data.accessKeySecretMasked || '',
          hasAccessKeySecret: !!data.hasAccessKeySecret,
          credentialSource: data.credentialSource || '',
          signName: data.signName || '绿纤聚财',
          company: data.company || '1',
          endpoint: data.endpoint || 'dysmsapi.aliyuncs.com',
          preventDuplicate: Number(data.preventDuplicate) === 0 ? 0 : 1
        }
      }).catch(() => {})
    },
    saveConfig() {
      this.configSaving = true
      saveSmsConfig({
        accessKeyId: this.configForm.accessKeyId,
        accessKeySecret: this.configForm.accessKeySecret,
        signName: this.configForm.signName,
        company: this.configForm.company,
        endpoint: this.configForm.endpoint,
        preventDuplicate: this.configForm.preventDuplicate
      }).then(res => {
        this.$message.success((res && res.msg) || '保存成功')
        this.loadConfig()
      }).finally(() => {
        this.configSaving = false
      })
    },
    searchTemplates() {
      this.templateQuery.currentPage = 1
      this.loadTemplates()
    },
    loadTemplates() {
      this.templateLoading = true
      return getSmsTemplateList({ ...this.templateQuery }).then(res => {
        const page = (res && res.data) || {}
        this.templateList = page.list || []
        this.templateTotal = page.pagination && page.pagination.total != null
          ? page.pagination.total
          : (page.total || 0)
      }).catch(() => {
        this.templateList = []
        this.templateTotal = 0
      }).finally(() => {
        this.templateLoading = false
      })
    },
    openTemplateDialog(row) {
      this.$refs.templateDialog && this.$refs.templateDialog.open(row || null)
    },
    openTestDialog() {
      this.$refs.testDialog && this.$refs.testDialog.open()
    },
    openLogDrawer() {
      this.$refs.logDrawer && this.$refs.logDrawer.open()
    },
    toggleEnabled(row) {
      const enabled = row.enabled === 1 ? 0 : 1
      toggleSmsTemplateEnabled({ id: row.id, enabled }).then(res => {
        this.$message.success((res && res.msg) || '操作成功')
        this.loadTemplates()
      })
    },
    removeTemplate(row) {
      this.$confirm(`确认删除模板「${row.name}」?`, '提示', { type: 'warning' }).then(() => {
        return deleteSmsTemplate(row.id)
      }).then(res => {
        this.$message.success((res && res.msg) || '删除成功')
        this.loadTemplates()
      }).catch(() => {})
    },
    templateTypeName(type) {
      const map = { 1: '验证码', 2: '推广短信', 3: '通知短信' }
      return map[type] || '无'
    },
    templateTypeTag(type) {
      if (type === 1) return 'primary'
      if (type === 2) return 'warning'
      return 'info'
    }
  }
}
</script>

<style lang="scss" scoped>
.config-section {
  border: 1px solid #ebeef5;

  &--flush {
    border: none;
    box-shadow: none;

    ::v-deep .el-card__body {
      padding: 0;
    }
  }

  &__header {
    display: flex;
    align-items: center;
    gap: 8px;
    font-weight: 500;
  }

  &__tag {
    padding: 0 8px;
    height: 22px;
    line-height: 22px;
    border-radius: 4px;
    background: #f4f4f5;
    color: #909399;
    font-size: 12px;
    font-weight: normal;
  }
}

.module-icon--credentials { color: #409EFF; }
.module-icon--templates { color: #67C23A; }

.config-alert {
  margin-bottom: 12px;
}

.panel-actions {
  margin: 4px 0 0;
}

.panel-actions--between {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

.toolbar-left,
.toolbar-right {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}

.form-tip {
  margin-left: 12px;
  color: #909399;
  font-size: 12px;
  line-height: 1.5;
}

.ncc-content-dark {
  .config-section {
    border-color: rgba(255, 255, 255, 0.08);

    &__tag {
      background: rgba(255, 255, 255, 0.08);
      color: rgba(255, 255, 255, 0.55);
    }
  }

  .form-tip {
    color: rgba(255, 255, 255, 0.55);
  }
}
</style>