Pārlūkot izejas kodu

feat(短信模块): 添加短信管理模块

shizhongming 2 gadi atpakaļ
vecāks
revīzija
039a5cc44a

+ 62 - 0
src/modules/sms/views/SmsChannel/SmartSmsChannelManagerListView.api.ts

@@ -0,0 +1,62 @@
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+enum Api {
+  list = '/smart/sms/channel/list',
+  getById = '/smart/sms/channel/getById',
+  batchSaveUpdate = '/smart/sms/channel/saveUpdateBatch',
+  delete = '/smart/sms/channel/batchDeleteById',
+  setDefault = '/smart/sms/channel/setDefault',
+  sendTest = '/smart/sms/channel/sendTest',
+}
+
+export const listApi = (params) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_MESSAGE,
+    url: Api.list,
+    data: {
+      ...params,
+    },
+  });
+};
+
+export const batchSaveUpdateApi = (modelList: any[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_MESSAGE,
+    url: Api.batchSaveUpdate,
+    data: modelList,
+  });
+};
+
+export const deleteApi = (removeRecords: Recordable[]) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_MESSAGE,
+    url: Api.delete,
+    data: removeRecords.map((item) => item.id),
+  });
+};
+
+export const getByIdApi = (id: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_MESSAGE,
+    url: Api.getById,
+    data: id,
+  });
+};
+
+export const setDefaultApi = (id: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_MESSAGE,
+    url: Api.setDefault,
+    data: {
+      id,
+    },
+  });
+};
+
+export const sendTestApi = (data: Record<string, any>) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_MESSAGE,
+    url: Api.sendTest,
+    data,
+  });
+};

+ 261 - 0
src/modules/sms/views/SmsChannel/SmartSmsChannelManagerListView.config.ts

@@ -0,0 +1,261 @@
+import type { SmartColumn, SmartSearchFormSchema } from '@/components/SmartTable';
+import type { FormSchema } from '@/components/Form';
+import { tableUseYnClass } from '@/components/SmartTable';
+
+/**
+ * 表格列表
+ */
+export const getTableColumns = (): SmartColumn[] => {
+  return [
+    {
+      type: 'checkbox',
+      width: 60,
+      align: 'center',
+      fixed: 'left',
+    },
+    {
+      field: 'channelCode',
+      fixed: 'left',
+      title: '{smart.sms.channel.title.channelCode}',
+      width: 160,
+    },
+    {
+      field: 'channelName',
+      fixed: 'left',
+      title: '{smart.sms.channel.title.channelName}',
+      width: 160,
+    },
+    {
+      field: 'channelType',
+      sortable: true,
+      title: '{smart.sms.channel.title.channelType}',
+      width: 120,
+    },
+    {
+      field: 'isDefault',
+      title: '{smart.sms.channel.title.isDefault}',
+      component: 'booleanTag',
+      width: 120,
+    },
+    {
+      field: 'channelProperties',
+      visible: false,
+      title: '{smart.sms.channel.title.channelProperties}',
+      width: 120,
+    },
+    {
+      field: 'seq',
+      sortable: true,
+      title: '{common.table.seq}',
+      width: 120,
+    },
+    {
+      field: 'remark',
+      title: '{common.table.remark}',
+      width: 120,
+    },
+    {
+      field: 'createTime',
+      sortable: true,
+      title: '{common.table.createTime}',
+      width: 170,
+    },
+    {
+      field: 'createBy',
+      title: '{common.table.createUser}',
+      width: 120,
+    },
+    {
+      field: 'updateTime',
+      sortable: true,
+      title: '{common.table.updateTime}',
+      width: 170,
+    },
+    {
+      field: 'updateBy',
+      title: '{common.table.updateUser}',
+      width: 120,
+    },
+    {
+      ...tableUseYnClass(),
+    },
+    {
+      title: '{common.table.operation}',
+      field: 'operation',
+      width: 200,
+      fixed: 'right',
+      slots: {
+        default: 'table-operation',
+      },
+    },
+  ];
+};
+
+/**
+ * 添加修改表单
+ */
+export const getFormSchemas = (t: Function): FormSchema[] => {
+  return [
+    {
+      field: 'id',
+      label: '',
+      component: 'Input',
+      componentProps: {},
+      show: false,
+    },
+    {
+      field: 'channelCode',
+      label: t('smart.sms.channel.title.channelCode'),
+      component: 'Input',
+      componentProps: {},
+      required: true,
+    },
+    {
+      field: 'channelName',
+      label: t('smart.sms.channel.title.channelName'),
+      component: 'Input',
+      componentProps: {},
+      required: true,
+    },
+    {
+      field: 'channelType',
+      label: t('smart.sms.channel.title.channelType'),
+      component: 'SmartApiSelectDict',
+      componentProps: {
+        dictCode: 'SMART_SMS_CHANNEL',
+      },
+      required: true,
+    },
+    {
+      field: 'seq',
+      label: t('common.table.seq'),
+      component: 'InputNumber',
+      componentProps: {},
+      required: true,
+      defaultValue: 1,
+    },
+    {
+      field: 'remark',
+      label: t('common.table.remark'),
+      component: 'Input',
+      componentProps: {},
+    },
+    {
+      field: 'useYn',
+      label: t('common.table.useYn'),
+      component: 'Switch',
+      componentProps: {},
+      defaultValue: true,
+    },
+    ...getAliyunFormSchemas(),
+    ...getTencentFormSchemas(),
+  ];
+};
+
+export const getSearchFormSchemas = (t: Function): SmartSearchFormSchema[] => {
+  return [
+    {
+      field: 'channelCode',
+      label: t('smart.sms.channel.title.channelCode'),
+      component: 'Input',
+      searchSymbol: 'like',
+    },
+    {
+      field: 'channelName',
+      label: t('smart.sms.channel.title.channelName'),
+      component: 'Input',
+      searchSymbol: 'like',
+    },
+    {
+      field: 'channelType',
+      label: t('smart.sms.channel.title.channelType'),
+      searchSymbol: '=',
+      component: 'SmartApiSelectDict',
+      componentProps: {
+        dictCode: 'SMART_SMS_CHANNEL',
+        style: {
+          width: '120px',
+        },
+      },
+    },
+  ];
+};
+
+const getAliyunFormSchemas = (): FormSchema[] => {
+  return [
+    {
+      field: 'channelProperties.ALIYUN.accessKey',
+      component: 'Input',
+      label: 'accessKey',
+      required: ({ model }) => model.channelType === 'ALIYUN',
+      show: ({ model }) => {
+        return model.channelType === 'ALIYUN';
+      },
+    },
+    {
+      field: 'channelProperties.ALIYUN.accessSecret',
+      component: 'Input',
+      label: 'accessSecret',
+      required: ({ model }) => model.channelType === 'ALIYUN',
+      show: ({ model }) => {
+        return model.channelType === 'ALIYUN';
+      },
+    },
+    {
+      field: 'channelProperties.ALIYUN.endpoint',
+      component: 'SmartApiSelectDict',
+      componentProps: {
+        dictCode: 'SMART_SMS_ALIYUN_ENDPOINT',
+      },
+      label: 'endpoint',
+      required: ({ model }) => model.channelType === 'ALIYUN',
+      show: ({ model }) => {
+        return model.channelType === 'ALIYUN';
+      },
+    },
+  ];
+};
+
+const getTencentFormSchemas = (): FormSchema[] => {
+  return [
+    {
+      field: 'channelProperties.TENCENT.accessKey',
+      component: 'Input',
+      label: 'accessKey',
+      required: ({ model }) => model.channelType === 'TENCENT',
+      show: ({ model }) => {
+        return model.channelType === 'TENCENT';
+      },
+    },
+    {
+      field: 'channelProperties.TENCENT.accessSecret',
+      component: 'Input',
+      label: 'accessSecret',
+      required: ({ model }) => model.channelType === 'TENCENT',
+      show: ({ model }) => {
+        return model.channelType === 'TENCENT';
+      },
+    },
+    {
+      field: 'channelProperties.TENCENT.appid',
+      component: 'Input',
+      label: 'appid',
+      required: ({ model }) => model.channelType === 'TENCENT',
+      show: ({ model }) => {
+        return model.channelType === 'TENCENT';
+      },
+    },
+    {
+      field: 'channelProperties.TENCENT.region',
+      component: 'SmartApiSelectDict',
+      componentProps: {
+        dictCode: 'SMART_SMS_TENCENT_REGION',
+      },
+      label: 'Region',
+      required: ({ model }) => model.channelType === 'TENCENT',
+      show: ({ model }) => {
+        return model.channelType === 'TENCENT';
+      },
+    },
+  ];
+};

+ 166 - 0
src/modules/sms/views/SmsChannel/SmartSmsChannelManagerListView.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="full-height page-container">
+    <SmartTable @register="registerTable" :size="getTableSize">
+      <template #table-operation="{ row }">
+        <SmartVxeTableAction :actions="getActions(row)" />
+      </template>
+    </SmartTable>
+
+    <SendTestModal @register="registerTestSendModal" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+  import { replace } from 'lodash-es';
+
+  import { createConfirm, successMessage } from '@/utils/message/SystemNotice';
+  import {
+    ActionItem,
+    SmartTable,
+    SmartVxeTableAction,
+    useSmartTable,
+  } from '@/components/SmartTable';
+
+  import {
+    getTableColumns,
+    getFormSchemas,
+    getSearchFormSchemas,
+  } from './SmartSmsChannelManagerListView.config';
+  import {
+    listApi,
+    deleteApi,
+    getByIdApi,
+    batchSaveUpdateApi,
+    setDefaultApi,
+  } from './SmartSmsChannelManagerListView.api';
+
+  import SendTestModal from './components/SendTestModal.vue';
+  import { useModal } from '@/components/Modal';
+
+  const { t } = useI18n();
+  const { getTableSize } = useSizeSetting();
+
+  const [registerTestSendModal, { openModal }] = useModal();
+
+  const getActions = (row: Recordable): ActionItem[] => {
+    return [
+      {
+        label: t('common.button.edit'),
+        onClick: () => editByRowModal(row),
+        auth: 'smart:sms:update',
+      },
+      {
+        label: t('smart.sms.channel.button.setDefault'),
+        auth: 'smart:sms:update',
+        disabled: row.isDefault === true,
+        onClick: () => {
+          createConfirm({
+            iconType: 'warning',
+            content: t('smart.sms.channel.message.setDefault'),
+            onOk: async () => {
+              await setDefaultApi(row.id);
+              successMessage(t('smart.sms.channel.message.setDefaultSuccess'));
+              await query();
+            },
+          });
+        },
+      },
+      {
+        label: t('smart.sms.channel.button.sendTest'),
+        onClick: () => {
+          openModal(true, { channelId: row.id });
+        },
+      },
+    ];
+  };
+
+  const [registerTable, { editByRowModal, query }] = useSmartTable({
+    id: 'smart-sms-smsChannel',
+    customConfig: { storage: true },
+    columns: getTableColumns(),
+    height: 'auto',
+    showOverflow: 'tooltip',
+    pagerConfig: true,
+    border: true,
+    useSearchForm: true,
+    sortConfig: {
+      remote: true,
+    },
+    columnConfig: { resizable: true },
+    searchFormConfig: {
+      schemas: getSearchFormSchemas(t),
+      searchWithSymbol: true,
+      colon: true,
+      layout: 'inline',
+      actionColOptions: {
+        span: undefined,
+      },
+      compact: true,
+    },
+    addEditConfig: {
+      formConfig: {
+        colon: true,
+        schemas: getFormSchemas(t),
+        baseColProps: { span: 24 },
+        labelCol: { span: 6 },
+        wrapperCol: { span: 17 },
+      },
+    },
+    proxyConfig: {
+      ajax: {
+        query: (params) => listApi(params.ajaxParameter),
+        save: ({ body: { insertRecords, updateRecords } }) => {
+          const saveDataList = [...insertRecords, ...updateRecords];
+          const formatDataList = saveDataList.map((item) => {
+            const result: Record<string, any> = {};
+            const channelProperties: Recordable = {};
+            const propertiesKeyPrefix = `channelProperties.${item.channelType}.`;
+            Object.keys(item).forEach((key) => {
+              if (!key.startsWith(propertiesKeyPrefix)) {
+                result[key] = item[key];
+              } else if (key.startsWith(propertiesKeyPrefix)) {
+                channelProperties[replace(key, propertiesKeyPrefix, '')] = item[key];
+              }
+            });
+            result.channelProperties = JSON.stringify(channelProperties);
+            return result;
+          });
+          return batchSaveUpdateApi(formatDataList);
+        },
+        delete: ({ body: { removeRecords } }) => deleteApi(removeRecords),
+        getById: async (params) => {
+          const result = await getByIdApi(params.id);
+          if (result.channelProperties) {
+            const channelProperties = JSON.parse(result.channelProperties);
+            const channelType = result.channelType;
+            const formatData: Recordable = {};
+            Object.keys(channelProperties).forEach((item) => {
+              formatData[`channelProperties.${channelType}.${item}`] = channelProperties[item];
+            });
+            console.log(formatData);
+            return {
+              ...result,
+              ...formatData,
+            };
+          }
+          return result;
+        },
+      },
+    },
+    toolbarConfig: {
+      zoom: true,
+      refresh: true,
+      column: { columnOrder: true },
+      buttons: [
+        {
+          code: 'ModalAdd',
+        },
+        {
+          code: 'delete',
+        },
+      ],
+    },
+  });
+</script>

+ 91 - 0
src/modules/sms/views/SmsChannel/components/SendTestModal.vue

@@ -0,0 +1,91 @@
+<template>
+  <BasicModal
+    :title="t('smart.sms.channel.title.sendTest')"
+    @register="registerModal"
+    @ok="handleSendTest"
+    :okText="t('smart.sms.channel.button.sendTest')"
+  >
+    <BasicForm @register="registerForm" />
+  </BasicModal>
+</template>
+
+<script lang="ts" setup>
+  import { useModalInner, BasicModal } from '@/components/Modal';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { useForm, BasicForm } from '@/components/Form';
+  import { useMessage } from '@/hooks/web/useMessage';
+
+  import { sendTestApi } from '../SmartSmsChannelManagerListView.api';
+
+  const { t } = useI18n();
+  const { createSuccessModal } = useMessage();
+
+  let currentChannelId: number | null = null;
+
+  /**
+   * 发送测试
+   */
+  const handleSendTest = async () => {
+    try {
+      changeOkLoading(true);
+      const formModel = getFieldsValue();
+      const parameter: Record<string, any> = {
+        channelId: currentChannelId,
+        ...formModel,
+        phoneNumberList: formModel.phoneNumberListStr.split(','),
+      };
+      if (parameter.templateParameter) {
+        parameter.templateParameter = JSON.parse(parameter.templateParameter);
+      }
+      const sendResult = await sendTestApi(parameter);
+      createSuccessModal({
+        content: JSON.stringify(sendResult),
+      });
+      closeModal();
+    } finally {
+      changeOkLoading(false);
+    }
+  };
+
+  const [registerModal, { changeOkLoading, closeModal }] = useModalInner((data) => {
+    currentChannelId = data.channelId;
+  });
+
+  const [registerForm, { getFieldsValue }] = useForm({
+    showActionButtonGroup: false,
+    baseColProps: { span: 24 },
+    colon: true,
+    labelCol: { span: 4 },
+    wrapperCol: { span: 19 },
+    schemas: [
+      {
+        field: 'signName',
+        label: t('smart.sms.channel.title.signName'),
+        component: 'Input',
+        required: true,
+      },
+      {
+        field: 'template',
+        label: t('smart.sms.channel.title.template'),
+        component: 'Input',
+        required: true,
+      },
+      {
+        field: 'templateParameter',
+        label: 'Data',
+        component: 'InputTextArea',
+      },
+      {
+        field: 'phoneNumberListStr',
+        label: t('smart.sms.channel.title.phoneNumberList'),
+        component: 'InputTextArea',
+        required: true,
+        componentProps: {
+          placeholder: t('smart.sms.channel.message.phoneNumberList'),
+        },
+      },
+    ],
+  });
+</script>
+
+<style scoped></style>

+ 35 - 0
src/modules/sms/views/SmsChannel/lang/en_US.ts

@@ -0,0 +1,35 @@
+/**
+ * 短信通道表 国际化信息
+ */
+export default {
+  trans: true,
+  key: 'smart.sms.channel',
+  data: {
+    title: {
+      channelCode: 'Channel code',
+      channelName: 'Channel name',
+      channelType: 'Channel type',
+      isDefault: 'Default',
+      channelProperties: 'Properties',
+    },
+    validate: {
+      channelCode: 'Please enter channel code',
+      channelName: 'Please enter channel name',
+      channelType: 'Please select channel type',
+    },
+    rules: {},
+    search: {
+      channelCode: 'Please enter channel code',
+      channelName: 'Please enter channel name',
+      channelType: 'Please select channel type',
+    },
+    button: {
+      setDefault: 'Set default',
+    },
+    message: {
+      setDefault:
+        'There can only be one default channel. Are you sure you want to set it as the default?',
+      setDefaultSuccess: 'Set default successfully',
+    },
+  },
+};

+ 40 - 0
src/modules/sms/views/SmsChannel/lang/zh_CN.ts

@@ -0,0 +1,40 @@
+/**
+ * 短信通道表 国际化信息
+ */
+export default {
+  trans: true,
+  key: 'smart.sms.channel',
+  data: {
+    title: {
+      channelCode: '通道编号',
+      channelName: '通道名称',
+      channelType: '通道类型',
+      isDefault: '是否是默认的',
+      channelProperties: '通道参数',
+      sendTest: '发送测试',
+      signName: '签名',
+      template: '模板',
+      phoneNumberList: '手机号',
+    },
+    validate: {
+      channelCode: '请输入通道编号',
+      channelName: '请输入通道名称',
+      channelType: '请输入通道类型',
+    },
+    rules: {},
+    search: {
+      channelCode: '请输入通道编号',
+      channelName: '请输入通道名称',
+      channelType: '请输入通道类型',
+    },
+    button: {
+      setDefault: '设为默认',
+      sendTest: '发送测试',
+    },
+    message: {
+      setDefault: '只能有一个默认的通道,确定要设为默认吗?',
+      setDefaultSuccess: '设置默认成功',
+      phoneNumberList: '多个手机号以逗号分隔',
+    },
+  },
+};

+ 24 - 0
src/modules/sms/views/SmsLog/SmartSmsLogListView.api.ts

@@ -0,0 +1,24 @@
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+enum Api {
+  list = '/smart/sms/log/listAll',
+  getById = '/smart/sms/log/getById',
+}
+
+export const listApi = (params) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_MESSAGE,
+    url: Api.list,
+    data: {
+      ...params,
+    },
+  });
+};
+
+export const getByIdApi = (id: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_MESSAGE,
+    url: Api.getById,
+    data: id,
+  });
+};

+ 91 - 0
src/modules/sms/views/SmsLog/SmartSmsLogListView.config.ts

@@ -0,0 +1,91 @@
+import type { SmartColumn, SmartSearchFormSchema } from '@/components/SmartTable';
+
+/**
+ * 表格列表
+ */
+export const getTableColumns = (): SmartColumn[] => {
+  return [
+    {
+      type: 'checkbox',
+      width: 60,
+      align: 'center',
+      fixed: 'left',
+    },
+    {
+      field: 'id',
+      visible: false,
+      title: '',
+      width: 120,
+    },
+    {
+      field: 'channelId',
+      title: '{smart.sms.log.title.channelId}',
+      width: 120,
+    },
+    {
+      field: 'isSuccess',
+      title: '{smart.sms.log.title.isSuccess}',
+      width: 120,
+    },
+    {
+      field: 'sendParameter',
+      visible: false,
+      title: '{smart.sms.log.title.sendParameter}',
+      width: 120,
+    },
+    {
+      field: 'sendResult',
+      visible: false,
+      title: '{smart.sms.log.title.sendResult}',
+      width: 120,
+    },
+    {
+      field: 'errorMessage',
+      visible: false,
+      title: '{smart.sms.log.title.errorMessage}',
+      width: 120,
+    },
+    {
+      field: 'createTime',
+      title: '{common.table.createTime}',
+      width: 120,
+    },
+    {
+      field: 'createBy',
+      title: '{common.table.createUser}',
+      width: 120,
+    },
+    {
+      title: '{common.table.operation}',
+      field: 'operation',
+      width: 120,
+      fixed: 'right',
+      slots: {
+        default: 'table-operation',
+      },
+    },
+  ];
+};
+
+export const getSearchFormSchemas = (t: Function): SmartSearchFormSchema[] => {
+  return [
+    {
+      field: 'channelId',
+      label: t('smart.sms.log.title.channelId'),
+      component: 'Select',
+      searchSymbol: '=',
+    },
+    {
+      field: 'isSuccess',
+      label: t('smart.sms.log.title.isSuccess'),
+      component: 'Select',
+      searchSymbol: '=',
+    },
+    {
+      field: 'createTime',
+      label: t('common.table.createTime'),
+      component: 'DatePicker',
+      searchSymbol: '=',
+    },
+  ];
+};

+ 71 - 0
src/modules/sms/views/SmsLog/SmartSmsLogListView.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="full-height page-container">
+    <SmartTable @register="registerTable" :size="getTableSize">
+      <template #table-operation="{ row }">
+        <SmartVxeTableAction :actions="getActions(row)" />
+      </template>
+    </SmartTable>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+
+  import {
+    ActionItem,
+    SmartTable,
+    SmartVxeTableAction,
+    useSmartTable,
+  } from '@/components/SmartTable';
+
+  import { getTableColumns, getSearchFormSchemas } from './SmartSmsLogListView.config';
+  import { listApi, getByIdApi } from './SmartSmsLogListView.api';
+
+  const { t } = useI18n();
+  const { getTableSize } = useSizeSetting();
+
+  const getActions = (row: Recordable): ActionItem[] => {
+    return [
+      {
+        label: t('common.button.edit'),
+        onClick: () => editByRowModal(row),
+      },
+    ];
+  };
+
+  const [registerTable, { editByRowModal }] = useSmartTable({
+    id: 'smart-sms-smsLog',
+    customConfig: { storage: true },
+    columns: getTableColumns(),
+    height: 'auto',
+    border: true,
+    sortConfig: {
+      remote: true,
+    },
+    pagerConfig: true,
+    useSearchForm: true,
+    searchFormConfig: {
+      schemas: getSearchFormSchemas(t),
+      searchWithSymbol: true,
+      colon: true,
+      compact: true,
+      layout: 'inline',
+      actionColOptions: {
+        span: undefined,
+      },
+    },
+    proxyConfig: {
+      ajax: {
+        query: (params) => listApi(params.ajaxParameter),
+        getById: (params) => getByIdApi(params.id),
+      },
+    },
+    toolbarConfig: {
+      zoom: true,
+      refresh: true,
+      column: { columnOrder: true },
+      buttons: [],
+    },
+  });
+</script>

+ 22 - 0
src/modules/sms/views/SmsLog/lang/zh_CN.ts

@@ -0,0 +1,22 @@
+/**
+ * 短息发送日志 国际化信息
+ */
+export default {
+  trans: true,
+  key: 'smart.sms.log',
+  data: {
+    title: {
+      channelId: '短信通道',
+      isSuccess: '是否发送成功',
+      sendParameter: '发送参数',
+      sendResult: '发送结果',
+      errorMessage: '错误信息',
+    },
+    validate: {},
+    rules: {},
+    search: {
+      channelId: '请输入短信通道ID',
+      isSuccess: '请输入是否发送成功',
+    },
+  },
+};