From 2ac7af9fc842c976d1c28058a4b4d8cfbcc41291 Mon Sep 17 00:00:00 2001
From: dap1 <15891557205@163.com>
Date: Tue, 13 Jun 2023 20:25:20 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E9=80=81=E9=82=AE=E4=BB=B6?=
 =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=9A=84=E6=9F=A5=E7=9C=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/enums/appEnum.ts                          |   3 +-
 src/views/system/mail/account/account.data.ts |  20 +++-
 src/views/system/mail/log/MailLogModal.vue    |  30 +++++
 src/views/system/mail/log/index.vue           |  38 ++++++-
 src/views/system/mail/log/mailLog.data.ts     | 106 ++++++++++++++++--
 .../system/mail/template/SendMailModal.vue    |  92 +++++++++------
 src/views/system/mail/template/index.vue      |   4 +-
 .../system/mail/template/template.data.ts     |  44 ++++++--
 8 files changed, 270 insertions(+), 67 deletions(-)
 create mode 100644 src/views/system/mail/log/MailLogModal.vue

diff --git a/src/enums/appEnum.ts b/src/enums/appEnum.ts
index b7c40e2f..de2bbd64 100644
--- a/src/enums/appEnum.ts
+++ b/src/enums/appEnum.ts
@@ -69,5 +69,6 @@ export enum IconEnum {
   ADD_FOLD = 'ant-design:folder-add-outlined',
   LOG = 'ant-design:exception-outlined',
   PASSWORD = 'ant-design:key-outlined',
-  SETTING = 'ant-design:setting-outlined'
+  SETTING = 'ant-design:setting-outlined',
+  SEND = 'ant-design:send-outlined'
 }
diff --git a/src/views/system/mail/account/account.data.ts b/src/views/system/mail/account/account.data.ts
index 69364b51..e15919b7 100644
--- a/src/views/system/mail/account/account.data.ts
+++ b/src/views/system/mail/account/account.data.ts
@@ -71,7 +71,16 @@ export const formSchema: FormSchema[] = [
     label: '邮箱',
     field: 'mail',
     required: true,
-    component: 'Input'
+    component: 'Input',
+    helpMessage: '填写发件邮箱地址',
+    rules: [
+      {
+        required: true,
+        message: '请输入正确的邮箱地址',
+        pattern: /^\w{3,}(\.\w+)*@[A-z0-9]+(\.[A-z]{2,5}){1,2}$/,
+        trigger: 'blur'
+      }
+    ]
   },
   {
     label: '用户名',
@@ -80,10 +89,11 @@ export const formSchema: FormSchema[] = [
     component: 'Input'
   },
   {
-    label: '密码',
+    label: '密码/授权码',
     field: 'password',
     required: true,
-    component: 'InputPassword'
+    component: 'InputPassword',
+    helpMessage: '填写邮件密码, 部分邮件商需要填写授权码'
   },
   {
     label: 'SMTP 服务器域名',
@@ -95,14 +105,14 @@ export const formSchema: FormSchema[] = [
     label: 'SMTP 服务器端口',
     field: 'port',
     required: true,
-    component: 'Input'
+    component: 'InputNumber'
   },
   {
     label: '是否开启 SSL',
     field: 'sslEnable',
     required: true,
     defaultValue: false,
-    component: 'Switch',
+    component: 'RadioButtonGroup',
     componentProps: {
       options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')
     }
diff --git a/src/views/system/mail/log/MailLogModal.vue b/src/views/system/mail/log/MailLogModal.vue
new file mode 100644
index 00000000..f740230d
--- /dev/null
+++ b/src/views/system/mail/log/MailLogModal.vue
@@ -0,0 +1,30 @@
+<template>
+  <BasicModal v-bind="$attrs" title="发送邮件详情" @register="registerModalInner" @ok="closeModal" width="800px">
+    <Description @register="registerDescription" />
+  </BasicModal>
+</template>
+
+<script setup lang="ts">
+import { BasicModal, useModalInner } from '@/components/Modal'
+import { Description, useDescription } from '@/components/Description/index'
+import { ref } from 'vue'
+import { logSchema } from './mailLog.data'
+
+defineOptions({ name: 'MailLogModal' })
+
+const logData = ref()
+const [registerModalInner, { closeModal }] = useModalInner((record: Recordable) => {
+  logData.value = record
+})
+
+const [registerDescription] = useDescription({
+  column: 1,
+  schema: logSchema,
+  data: logData,
+  labelStyle: {
+    width: '100px'
+  }
+})
+</script>
+
+<style scoped></style>
diff --git a/src/views/system/mail/log/index.vue b/src/views/system/mail/log/index.vue
index fd82ef2b..4652530b 100644
--- a/src/views/system/mail/log/index.vue
+++ b/src/views/system/mail/log/index.vue
@@ -1,15 +1,35 @@
 <template>
   <div>
-    <BasicTable @register="registerTable" />
+    <BasicTable @register="registerTable">
+      <template #bodyCell="{ column, record }">
+        <template v-if="column.key === 'action'">
+          <TableAction
+            :actions="[
+              {
+                icon: IconEnum.VIEW,
+                label: t('action.detail'),
+                onClick: handleShowInfo.bind(null, record)
+              }
+            ]"
+          />
+        </template>
+      </template>
+    </BasicTable>
+    <MailLogModal @register="registerModal" />
   </div>
 </template>
 <script lang="ts" setup>
-import { BasicTable, useTable } from '@/components/Table'
+import { useI18n } from '@/hooks/web/useI18n'
+import { IconEnum } from '@/enums/appEnum'
+import { BasicTable, useTable, TableAction } from '@/components/Table'
 import { getMailAccountPage } from '@/api/system/mail/log'
 import { columns, searchFormSchema } from './mailLog.data'
+import { useModal } from '@/components/Modal'
+import MailLogModal from './MailLogModal.vue'
 
 defineOptions({ name: 'SystemOperateLog' })
 
+const { t } = useI18n()
 const [registerTable] = useTable({
   title: '邮件日志列表',
   api: getMailAccountPage,
@@ -17,6 +37,18 @@ const [registerTable] = useTable({
   formConfig: { labelWidth: 120, schemas: searchFormSchema },
   useSearchForm: true,
   showTableSetting: true,
-  showIndexColumn: false
+  showIndexColumn: false,
+  actionColumn: {
+    width: 140,
+    title: t('common.action'),
+    dataIndex: 'action',
+    fixed: 'right'
+  }
 })
+
+const [registerModal, { openModal }] = useModal()
+
+function handleShowInfo(record: Recordable) {
+  openModal(true, record)
+}
 </script>
diff --git a/src/views/system/mail/log/mailLog.data.ts b/src/views/system/mail/log/mailLog.data.ts
index 7106cca1..65b901a5 100644
--- a/src/views/system/mail/log/mailLog.data.ts
+++ b/src/views/system/mail/log/mailLog.data.ts
@@ -1,6 +1,8 @@
 import { BasicColumn, FormSchema, useRender } from '@/components/Table'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 import { getSimpleMailAccountList } from '@/api/system/mail/account'
+import { DescItem } from '@/components/Description/index'
+import { h } from 'vue'
 
 export const columns: BasicColumn[] = [
   {
@@ -8,14 +10,6 @@ export const columns: BasicColumn[] = [
     dataIndex: 'id',
     width: 100
   },
-  {
-    title: '发送时间',
-    dataIndex: 'sendTime',
-    width: 180,
-    customRender: ({ text }) => {
-      return useRender.renderDate(text)
-    }
-  },
   {
     title: '接收邮箱',
     dataIndex: 'toMail',
@@ -42,7 +36,15 @@ export const columns: BasicColumn[] = [
   {
     title: '模板编号',
     dataIndex: 'templateId',
-    width: 180
+    width: 100
+  },
+  {
+    title: '发送时间',
+    dataIndex: 'sendTime',
+    width: 180,
+    customRender: ({ text }) => {
+      return useRender.renderDate(text)
+    }
   }
 ]
 
@@ -101,3 +103,89 @@ export const searchFormSchema: FormSchema[] = [
     colProps: { span: 8 }
   }
 ]
+
+export const logSchema: DescItem[] = [
+  {
+    field: 'sendStatus',
+    label: '发送状态',
+    labelMinWidth: 80,
+    render(value) {
+      return useRender.renderDict(value, DICT_TYPE.SYSTEM_MAIL_SEND_STATUS)
+    }
+  },
+  {
+    field: 'sendException',
+    label: '异常信息',
+    labelMinWidth: 80,
+    show: (data) => data && data.sendException && data.sendException.length > 0,
+    render(value) {
+      return h('span', { style: { fontWeight: 'bold' } }, value)
+    }
+  },
+  {
+    field: 'sendTime',
+    label: '发送时间',
+    render(value) {
+      return useRender.renderDate(value)
+    }
+  },
+  {
+    field: 'userId',
+    label: '用户类型',
+    render(_, data) {
+      const { userId, userType } = data
+      const uidTag = useRender.renderTag('uid: ' + userId)
+      const typeTag = useRender.renderDict(userType, DICT_TYPE.USER_TYPE)
+      return h('span', {}, [typeTag, uidTag])
+    }
+  },
+  {
+    field: 'fromMail',
+    label: '发件邮箱'
+  },
+  {
+    field: 'toMail',
+    label: '收件邮箱'
+  },
+  {
+    field: 'templateNickname',
+    label: '发件昵称'
+  },
+  {
+    field: 'templateTitle',
+    label: '邮件标题'
+  },
+  {
+    field: 'templateContent',
+    label: '邮件内容',
+    render(value) {
+      return h('div', { innerHTML: value })
+    }
+  },
+  {
+    field: 'templateParams',
+    label: '邮件参数',
+    render(value) {
+      return useRender.renderJsonPreview(value)
+    }
+  },
+  {
+    field: 'sendMessageId',
+    label: '返回ID'
+  },
+  {
+    field: 'templateCode',
+    label: '模板编码'
+  },
+  {
+    field: 'templateId',
+    label: '模板编号'
+  },
+  {
+    field: 'createTime',
+    label: '记录时间',
+    render(value) {
+      return useRender.renderDate(value)
+    }
+  }
+]
diff --git a/src/views/system/mail/template/SendMailModal.vue b/src/views/system/mail/template/SendMailModal.vue
index b0716bff..3b7ab712 100644
--- a/src/views/system/mail/template/SendMailModal.vue
+++ b/src/views/system/mail/template/SendMailModal.vue
@@ -1,39 +1,42 @@
 <template>
-  <BasicModal v-bind="$attrs" title="测试发送邮件" @register="innerRegister" @ok="submit">
-    <BasicForm @register="register" :schemas="reactiveSchemas" />
+  <BasicModal v-bind="$attrs" title="发送邮件" @register="innerRegister" @ok="submit" @cancel="resetForm" width="600px">
+    <BasicForm @register="register" />
   </BasicModal>
 </template>
 
 <script setup lang="ts">
 import { BasicModal, useModalInner } from '@/components/Modal'
 import { BasicForm, FormSchema, useForm } from '@/components/Form'
-import { reactive, ref } from 'vue'
 import { MailTemplate } from '@/api/system/mail/template'
 import { sendMail } from '@/api/system/mail/template'
 import { useMessage } from '@/hooks/web/useMessage'
-import { baseSendSchemas } from './template.data'
+import { baseSendSchemas, keyPrefix } from './template.data'
 
 defineOptions({ name: 'SendMailModal' })
 
-const { createMessage } = useMessage()
-let reactiveSchemas: FormSchema[] = reactive([])
-const templateCode = ref<string>('')
+const [register, { setFieldsValue, getFieldsValue, validateFields, resetFields, clearValidate, appendSchemaByField, removeSchemaByField }] =
+  useForm({
+    labelWidth: 120,
+    schemas: baseSendSchemas,
+    baseColProps: {
+      span: 24
+    },
+    showSubmitButton: false,
+    showResetButton: false
+  })
 
-const [register, { setFieldsValue, getFieldsValue, validateFields, resetFields, clearValidate, setProps }] = useForm({
-  labelWidth: 100,
-  baseColProps: {
-    span: 24
-  },
-  showSubmitButton: false,
-  showResetButton: false
-})
+// 存储动态生成的字段信息 后续需要进行移除
+let dyFields: string[] = []
 
-const [innerRegister, { changeLoading, closeModal }] = useModalInner((data: MailTemplate) => {
-  resetForm()
+const [innerRegister, { changeLoading, changeOkLoading, closeModal }] = useModalInner(async (data: MailTemplate) => {
+  // 打开时进行清空
+  await resetForm()
+  const dyschemas: FormSchema[] = []
   data.params.forEach((item) => {
+    // 这里加上前缀 防止和content/mail字段重名
+    const field = keyPrefix + item
     const dySchema: FormSchema = {
-      // 这里加上前缀 防止和content/mail字段重名
-      field: `key-${item}`,
+      field,
       label: `参数{${item}} `,
       component: 'Input',
       componentProps: {
@@ -41,48 +44,63 @@ const [innerRegister, { changeLoading, closeModal }] = useModalInner((data: Mail
       },
       required: true
     }
-    reactiveSchemas.push(dySchema)
+    dyschemas.push(dySchema)
+    dyFields.push(field)
   })
-  const { content, code } = data
-  setFieldsValue({ content })
-  templateCode.value = code
+  setFieldsValue(data)
+  // 添加动态参数到末尾
+  appendSchemaByField(dyschemas, undefined)
 })
 
+function modalLoading(status: boolean) {
+  changeOkLoading(status)
+  changeLoading(status)
+}
+
+/**
+ * 移除动态生成的表单元素
+ */
+async function removeDySchemas() {
+  await removeSchemaByField(dyFields)
+  dyFields = []
+}
+
+const { createMessage } = useMessage()
 const submit = async () => {
   try {
-    setProps({ disabled: true })
-    changeLoading(true)
+    modalLoading(true)
     await validateFields()
     const fields = getFieldsValue()
     const data = {
       mail: fields.mail,
-      templateCode: templateCode.value,
+      templateCode: fields.code,
       templateParams: {}
     }
     Object.keys(fields).forEach((key) => {
-      if (key === 'content' || key === 'mail') {
+      // 这几个是固定的字段 不用处理
+      const fixedKeys = ['mail', 'code', 'content']
+      if (fixedKeys.includes(key)) {
         return
       }
-      // 去掉 - 后的key
-      const realKey = key.split('-')[1]
+      // 去掉前缀后的key
+      const realKey = key.split(keyPrefix)[1]
       data.templateParams[realKey] = fields[key]
     })
     await sendMail(data)
     createMessage.success(`发送邮件到[${fields.mail}]成功`)
     closeModal()
+  } catch (e) {
   } finally {
-    setProps({ disabled: false })
-    changeLoading(false)
+    modalLoading(false)
   }
 }
 
-const resetForm = () => {
-  // 这里需要每次清空动态表单
-  reactiveSchemas.splice(0, reactiveSchemas.length)
-  reactiveSchemas.push(...baseSendSchemas)
+async function resetForm() {
+  // 这里需要清空动态表单
+  await removeDySchemas()
   // 清除上一次的表单校验和参数
-  resetFields()
-  clearValidate()
+  await resetFields()
+  await clearValidate()
 }
 </script>
 
diff --git a/src/views/system/mail/template/index.vue b/src/views/system/mail/template/index.vue
index 67bd9e72..4a701bd7 100644
--- a/src/views/system/mail/template/index.vue
+++ b/src/views/system/mail/template/index.vue
@@ -11,8 +11,8 @@
           <TableAction
             :actions="[
               {
-                icon: IconEnum.EDIT,
-                label: t('action.test'),
+                icon: IconEnum.SEND,
+                label: t('action.send'),
                 auth: 'system:mail-template:send-mail',
                 onClick: handleSend.bind(null, record)
               },
diff --git a/src/views/system/mail/template/template.data.ts b/src/views/system/mail/template/template.data.ts
index 8d750529..10a35fd7 100644
--- a/src/views/system/mail/template/template.data.ts
+++ b/src/views/system/mail/template/template.data.ts
@@ -1,6 +1,8 @@
 import { getSimpleMailAccountList } from '@/api/system/mail/account'
 import { BasicColumn, FormSchema, useRender } from '@/components/Table'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { ScrollContainer } from '@/components/Container'
+import { h } from 'vue'
 
 export const columns: BasicColumn[] = [
   {
@@ -65,7 +67,7 @@ export const searchFormSchema: FormSchema[] = [
     colProps: { span: 8 }
   },
   {
-    label: '邮箱账号',
+    label: '发件邮箱',
     field: 'accountId',
     component: 'ApiSelect',
     componentProps: {
@@ -112,10 +114,11 @@ export const formSchema: FormSchema[] = [
     label: '模板编码',
     field: 'code',
     required: true,
-    component: 'Input'
+    component: 'Input',
+    helpMessage: '建议使用下划线/数字/字母命名'
   },
   {
-    label: '邮箱账号',
+    label: '发件邮箱',
     field: 'accountId',
     required: true,
     component: 'ApiSelect',
@@ -132,19 +135,22 @@ export const formSchema: FormSchema[] = [
     label: '发送人名称',
     field: 'nickname',
     required: true,
-    component: 'Input'
+    component: 'Input',
+    helpMessage: '发件人的名称, 如:系统发件人'
   },
   {
     label: '模板标题',
     field: 'title',
     required: true,
-    component: 'Input'
+    component: 'Input',
+    helpMessage: '邮件的标题'
   },
   {
     label: '模板内容',
     field: 'content',
     component: 'Editor',
-    required: true
+    required: true,
+    helpMessage: '{}括号中的内容作为模板参数'
   },
   {
     label: '开启状态',
@@ -163,17 +169,35 @@ export const formSchema: FormSchema[] = [
 ]
 
 // 发送邮件
+
+// 这里加上前缀 防止和表单其他字段重名
+export const keyPrefix = 'key$-'
 export const baseSendSchemas: FormSchema[] = [
+  {
+    field: 'code',
+    label: '编码',
+    component: 'Input',
+    show: () => false
+  },
   {
     field: 'content',
     component: 'Editor',
     label: '模板内容 ',
     required: false,
     defaultValue: '',
-    componentProps: {
-      options: {
-        readonly: true
-      }
+    render({ model }) {
+      let content: string = model.content
+      Object.keys(model).forEach((key) => {
+        if (!key.startsWith(keyPrefix)) {
+          return
+        }
+        const realKey = key.split(keyPrefix)[1]
+        content = content.replace(`{${realKey}}`, model[key])
+      })
+      return h(ScrollContainer, {
+        innerHTML: content,
+        style: { border: '1px solid #e8e8e8', borderRadius: '4px', padding: '10px' }
+      })
     }
   },
   {