67287bef3ec9abbaab297ad732ae56cbcfc0f2e7..12f7304fe9e2bb56ec328e4429a654a0653928a9
2024-09-13 huangyinfeng
邮件列表联调
12f730 对比 | 目录
2024-09-13 huangyinfeng
邮件配置相关
2c1249 对比 | 目录
2个文件已删除
3个文件已添加
17个文件已修改
2085 ■■■■■ 已修改文件
src/App.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/email/userList.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Tinymce/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Tinymce/src/index.vue 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/email.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/http/axios/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/Edit/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/Inbox/index.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/Utils/convention.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/Utils/index.vue 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/Utils/mailboxManagement.vue 364 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/Utils/quickText.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/Editor.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/LeftNav.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/ListPage/drawerDetail.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/ListPage/list.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/ListPage/pageHeadLeft.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/ListPage/table.vue 219 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/SelectUser.vue 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/SelectUser/index.vue 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/email/components/pageHeadLeft.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
types/axios.d.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue
@@ -27,7 +27,7 @@
// 先检查proxy是否存在,再进行操作
if (proxy && proxy.$cookies) {
  proxy.$cookies.remove('JSESSIONID');
    proxy.$cookies.set('JSESSIONID', 'F0442747E3B63FB7C854454C925C1BD6.jvm_59_9010', '1d');
    proxy.$cookies.set('JSESSIONID', '6D5DE448FED8F63528B386BF5E78D3EE.jvm_59_9010', '1d');
} else {
    console.error('proxy对象未初始化或不包含$cookies属性');
}
src/api/email/userList.ts
@@ -20,7 +20,14 @@
  IS_EMAIL_VALID = '/crm/mail/account/isEmailValid.do',
  GET_EMAIL_MODULE = '/crm/mail/getEmailModule.do',
  ADD_SIGNATURE = '/crm/mail/signature/addSignature.do',
  UPDATE_SIGNATURE = '/crm/mail/signature/updateSignature.do',
  GET_SIGNATURE = '/crm/mail/signature/getSignature.do',
  ADD_QUICK_TEXT = '/crm/mail/quickText/addQuickText.do',
  UPDATE_QUICK_TEXT = '/crm/mail/quickText/updateQuickText.do',
  GET_QUICK_TEXT = '/crm/mail/quickText/getQuickText.do',
  DELETE_QUICK_TEXT = '/crm/mail/quickText/deleteQuickText.do',
  GET_MAIL_LIST = '/crm/mail/getMailList.do',
  GET_USER_INFO = '/crm/base/userInfo.do',
}
@@ -65,10 +72,57 @@
    url: Api.ADD_SIGNATURE,
    params,
  })
// 修改签名
export const updateSignatureApi = (params) =>
  defHttp.post({
    url: Api.UPDATE_SIGNATURE,
    params,
  })
  // 查询签名
export const getSignatureApi = (params) =>
  defHttp.get({
    url: Api.GET_SIGNATURE,
    params,
  })
  })
    // 查询快速文本
export const getQuickTextApi = (params) =>
  defHttp.get({
    url: Api.GET_QUICK_TEXT,
    params,
  })
      // 新增快速文本
export const addQuickTextApi = (params) =>
  defHttp.post({
    url: Api.ADD_QUICK_TEXT,
    params,
  })
      // 更新快速文本
export const updateQuickTextApi = (params) =>
  defHttp.post({
    url: Api.UPDATE_QUICK_TEXT,
    params,
  })
      // 删除快速文本
export const deleteQuickTextApi = (params) =>
  defHttp.post({
    url: Api.DELETE_QUICK_TEXT,
    params,
  })
        // 获取邮件列表
export const getMailListApi = (params) =>
  defHttp.get({
    url: Api.GET_MAIL_LIST,
    params,
  })
  // 获取企业同事列表
  export const getUserInfoApi = (params) =>
    defHttp.post({
      url: Api.GET_USER_INFO,
      params,
    })
src/components/Tinymce/index.ts
@@ -1,4 +1,8 @@
import { withInstall } from '@/utils';
import tinymce from './src/Editor.vue';
import tinymcePw from './src/index.vue';
export const Tinymce = withInstall(tinymce);
export const TinymcePw = withInstall(tinymcePw);
src/components/Tinymce/src/index.vue
@@ -2,16 +2,72 @@
  <div :class="prefixCls" :style="{ width: containerWidth }">
    <textarea
      :id="tinymceId"
      ref="elRef"
      ref="elPwRef"
      :style="{ visibility: 'hidden' }"
      v-if="!initOptions.inline"
    ></textarea>
    <slot v-else></slot>
    <div class="p-2 tox-statusbar">
      <a-upload
        v-if="isElse"
        v-model:file-list="fileListTemp"
        action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
        list-type="picture"
        class="upload-list-inline"
      >
        <a-button type="text" size="small" style="margin: 0 auto">
          <upload-outlined></upload-outlined>
          附件
        </a-button>
        <template #iconRender><PaperClipOutlined /></template>
        <template #itemRender="{ file, fileList, actions }">
          <a-space class="ant-upload-list-picture-card">
            <span style="display: flex; flex-wrap: wrap">
              <PaperClipOutlined style="margin-right: 4px" />
              <span v-if="!file.editor" :style="file.status === 'error' ? 'color: red' : ''">{{
                file.name
              }}</span>
              <span v-else>
                <a-input size="small" v-model:value="file.tempName"></a-input>
              </span>
            </span>
            <span v-if="!file.editor">
              <a href="javascript:;" @click="actions.preview">预览</a>
              <a href="javascript:;" @click="fnRename(file, fileList)">重命名</a>
              <a href="javascript:;" @click="actions.remove">删除</a>
            </span>
            <span v-else>
              <a href="javascript:;" @click="fnSaveRename(file, fileList)">保存</a>
              <a href="javascript:;" @click="fnOffRename(file, fileList)">取消</a>
            </span>
          </a-space>
        </template>
      </a-upload>
      <div :class="fileListTemp.length > 0 ? 'my-upload-list' : ''">
        <div style="display: flex">
          <ImgUpload
            :fullscreen="fullscreen"
            @uploading="handleImageUploading"
            @done="handleDone"
            v-if="isImg"
            v-show="editorRef"
            :title="'图片'"
            :disabled="disabled"
            :accept="'.jpg,.jpeg,.gif,.png,.webp'"
          />
          <a-button v-if="isText" type="text" size="small">
            <SnippetsOutlined />
            快速文本</a-button
          >
        </div>
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
  import type { Editor, RawEditorSettings } from 'tinymce';
  import { PaperClipOutlined, UploadOutlined, SnippetsOutlined } from '@ant-design/icons-vue';
  import tinymce from 'tinymce/tinymce';
  import 'tinymce/themes/silver';
  import 'tinymce/icons/default/icons';
@@ -88,12 +144,14 @@
    PropType,
    useAttrs,
  } from 'vue';
  import ImgUpload from './ImgUpload.vue';
  import {
    plugins as defaultPlugins,
    toolbar as defaultToolbar,
    toolbar_groups as defaultStyleFormats,
  } from './tinymce';
  import { buildShortUUID } from '@/utils/uuid';
  import { bindHandlers } from './helper';
  import { onMountedOrActivated } from '@vben/hooks';
  import { useDesign } from '@/hooks/web/useDesign';
  import { isNumber } from '@/utils/is';
@@ -129,7 +187,7 @@
    height: {
      type: [Number, String] as PropType<string | number>,
      required: false,
      default: 200,
      default: 400,
    },
    width: {
      type: [Number, String] as PropType<string | number>,
@@ -143,15 +201,27 @@
    fontsize: {
      type: String,
    },
    isElse: {
      type: Boolean,
      default: true,
    },
    isText: {
      type: Boolean,
      default: true,
    },
    isImg: {
      type: Boolean,
      default: true,
    },
  });
console.log('Editor', props);
  const emit = defineEmits(['change', 'update:modelValue', 'inited', 'init-error']);
  const attrs = useAttrs();
  const editorRef = ref<Editor | null>(null);
  const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
  const elRef = ref<HTMLElement | null>(null);
  const fullscreen = ref(false);
  const tinymceId = ref<string>(buildShortUUID('tiny-vue-pw'));
  const elPwRef = ref<HTMLElement | null>(null);
  const { prefixCls } = useDesign('tinymce-container');
@@ -187,13 +257,10 @@
      fontsize_formats: '10px 11px 12px 14px 16px 18px 24px 36px 48px 48px 56px 72px',
      image_advtab: true,
      importcss_append: true, // 允许样式生效
      toolbar:[],
      toolbar_sticky: true, // 粘性工具栏(或停靠工具栏),在向下滚动网页直到不再可见编辑器时,将工具栏和菜单停靠在屏幕顶部
      toolbar_mode: 'floating', //默认wrap不收缩工具栏,取值为floating或sliding时,将第一行放不下的工具栏按钮缩进抽屉(3个点的图标)里,scrolling则采用移动端的横线滚动方式。
      // style_formats_autohide: true,
      menubar: false,
      branding: false,
      elementpath: false,
      toolbar:[],
      // quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
      plugins,
      language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
@@ -209,12 +276,22 @@
      skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
      content_css: publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
      ...options,
      readonly:true,
      setup: (editor: Editor) => {
        editorRef.value = editor;
        editor.on('init', (e) => initSetup(e));
      },
      // 只读模式
      readonly: true,
    };
  });
  const disabled = computed(() => {
    const { options } = props;
    const getdDisabled = options && Reflect.get(options, 'readonly');
    const editor = unref(editorRef);
    if (editor) {
      editor.setMode(getdDisabled ? 'readonly' : 'design');
    }
    return getdDisabled ?? false;
  });
  watch(
@@ -254,7 +331,7 @@
  }
  function initEditor() {
    const el = unref(elRef);
    const el = unref(elPwRef);
    if (el) {
      el.style.visibility = '';
    }
@@ -268,6 +345,18 @@
      });
  }
  function initSetup(e) {
    const editor = unref(editorRef);
    if (!editor) {
      return;
    }
    const value = props.modelValue || '';
    editor.setContent(value);
    bindModelHandlers(editor);
    bindHandlers(e, attrs, unref(editorRef));
  }
  function setValue(editor: Record<string, any>, val?: string, prevVal?: string) {
    if (
      editor &&
@@ -276,9 +365,123 @@
      val !== editor.getContent({ format: attrs.outputFormat })
    ) {
      editor.setContent(val);
    }else{
      editor.setContent('');
      console.log('-5-5-5555555555');
    }
  }
  function bindModelHandlers(editor: any) {
    const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
    const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
    watch(
      () => props.modelValue,
      (val, prevVal) => {
        setValue(editor, val, prevVal);
      },
    );
    watch(
      () => props.value,
      (val, prevVal) => {
        setValue(editor, val, prevVal);
      },
      {
        immediate: true,
      },
    );
    editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
      const content = editor.getContent({ format: attrs.outputFormat });
      emit('update:modelValue', content);
      const data = {
        content,
        fileUNID: fileListTemp.value,
      };
      emit('change', data);
    });
    editor.on('FullscreenStateChanged', (e) => {
      fullscreen.value = e.state;
    });
  }
  function handleImageUploading(name: string) {
    const editor = unref(editorRef);
    if (!editor) {
      return;
    }
    editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
    const content = editor?.getContent() ?? '';
    setValue(editor, content);
  }
  function handleDone(name: string, url: string) {
    const editor = unref(editorRef);
    if (!editor) {
      return;
    }
    const content = editor?.getContent() ?? '';
    const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
    setValue(editor, val);
  }
  function getUploadingImgName(name: string) {
    return `[uploading:${name}]`;
  }
  // 附件
  const fileListTemp = ref<UploadProps['fileList']>([
    // {
    //   uid: '-1',
    //   name: 'xxx.png',
    //   tempName: 'xxx',
    //   status: 'done',
    //   url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    //   thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    //   editor: false,
    // },
    // {
    //   uid: '-2',
    //   name: 'yyy.png',
    //   tempName: 'yyy',
    //   status: 'done',
    //   url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    //   thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    //   editor: false,
    // },
  ]);
  function fnRename(file, fileList) {
    console.log(file, fileList);
    fileListTemp.value = fileList.map((item) => {
      // item.tempName = item.name.split('.').slice(0,-1)
      if (file.uid == item.uid) {
        item.editor = true;
      } else {
        item.editor = false;
      }
      return item;
    });
  }
  function fnSaveRename(file, fileList) {
    fileListTemp.value = fileList.map((item) => {
      if (file.uid == item.uid) {
        item.name = item.tempName;
        item.editor = false;
      }
      return item;
    });
  }
  function fnOffRename(file, fileList) {
    fileListTemp.value = fileList.map((item) => {
      if (file.uid == item.uid) {
        item.editor = false;
      }
      return item;
    });
  }
</script>
<style lang="less" scope>
  @prefix-cls: ~'@{namespace}-tinymce-container';
@@ -293,9 +496,17 @@
    }
  }
  .my-upload-list {
    // 过渡
    position: absolute;
    left: 78px;
    transition: all 0.3s;
  }
  .tox-statusbar {
    display: flex;
    // position: absolute;
    min-height: 40px;
    border-bottom-right-radius: 8px;
    border-bottom-left-radius: 8px;
    background: #f0f2f5;
src/router/routes/modules/email.ts
@@ -42,21 +42,12 @@
            currentActiveMenu: '/email/index',
          },
        },
        {
          path: 'list/:id',
          name: 'EmailPage4',
          component: () => import('@/views/email/UnreadEmail/user.vue'),
          meta: {
            title: '1111',
          },
        },
      ],
    },
    {
      path: 'index',
      name: 'Index',
      component: () => import('@/views/email/UnreadEmail/index.vue'),
      meta: {
        title: '邮件',
        currentActiveMenu: '/email/index',
src/utils/http/axios/index.ts
@@ -51,11 +51,13 @@
      throw new Error(t('sys.api.apiRequestFailed'));
    }
    //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
    const { code, result } = data;
    const { msg:message } = data;
    const { code, result, state } = data;
    const { msg: message } = data;
    // 这里逻辑可以根据项目进行修改
    const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
    const isPass = code === ResultEnum.SUCCESS || state === ResultEnum.SUCCESS;
    const isReflect = Reflect.has(data, 'code') || Reflect.has(data, 'state');
    const hasSuccess = data && isReflect && isPass;
    if (hasSuccess) {
      let successMsg = message;
src/views/email/Edit/index.vue
@@ -240,7 +240,7 @@
  import { useMessage } from '@/hooks/web/useMessage';
  import { Tinymce } from '@/components/Tinymce';
  import { PlusCircleOutlined } from '@ant-design/icons-vue';
  import SelectUser from '../components/SelectUser.vue';
  import SelectUser from '../components/SelectUser/index.vue';
  import { Form } from 'ant-design-vue';
  const modelRef = reactive({
    sender: '',
src/views/email/Inbox/index.vue
@@ -1,13 +1,13 @@
<template>
  <div>
    <Page :pageList="pageList"></Page>
    <PageIndex :pageList="pageList"></PageIndex>
  </div>
</template>
<script lang="ts" setup>
  name: 'Inbox';
  import { ref, onMounted, computed } from 'vue';
  import Page from '@/views/email/components/ListPage/list.vue';
  import { ref, onMounted, computed, provide } from 'vue';
  import PageIndex from '@/views/email/components/ListPage/list.vue';
  import { useRoute } from 'vue-router';
  const route = useRoute();
@@ -19,6 +19,7 @@
        throw new Error('Invalid URL');
      }
      const basePart = url.split('?')[1];
      console.log(basePart, 'basePart');
      return basePart;
    } catch (error) {
      console.error('Error processing URL:', error);
@@ -26,19 +27,19 @@
      return '';
    }
  });
  import { receiveApi } from '@/api/email/userList';
  import { receiveApi, getMailListApi } from '@/api/email/userList';
  const pageList = ref([]);
  function getDataList() {
    receiveApi({ mail: routerId.value }).then((res) => {});
    getMailListApi({ mail: routerId.value }).then((res) => {
      if (res.code == 0) {
        pageList.value = res.data;
      }
    });
  }
  provide('getDataList',getDataList);
  onMounted(() => {
    getDataList();
  });
  const pageList = ref([
    {
      title: '收件箱',
      path: '/email/index',
      icon: 'ion:mail-outline',
    },
  ]);
</script>
<style scoped lang="less"></style>
src/views/email/Utils/convention.vue
@@ -1,10 +1,10 @@
<template>
  <div class="p-2" style="margin-left: 20px">
    <div class="title" style="font-size: 20px">常规</div>
    <h2 class="title" style="font-size: 20px">常规</h2>
    <a-form style="width: 60%" :labelCol="{ span: 6 }">
      <div>
        <div class="title">账号</div>
        <h2 class="title">账号</h2>
        <a-form-item label="默认邮箱">
          <a-select>
            <a-select-option v-for="(item, index) in emailList" :key="index" :value="item.email">
@@ -16,7 +16,7 @@
      </div>
      <div>
        <div class="title">签名</div>
        <h2 class="title">签名</h2>
        <a-form-item label="个性签名">
          <div style="display: flex">
            <a-select @change="fnSignatureChange" v-model:value="signature">
@@ -31,12 +31,12 @@
            <PlusCircleOutlined style="margin-left: 20px" @click="showModal('add')" />
          </div>
        </a-form-item>
        <a-form-item :labelCol="{ span: 6 }">
        <a-form-item :labelCol="{ span: 6 }" label="内容">
          <div style="height: 200px; overflow: hidden; border-bottom: 1px solid #d9d9d9">
            <Tinymce v-model="signContent"></Tinymce>
            <TinymcePw v-model="signContent"></TinymcePw>
          </div>
          <div style="margin-top: 20px; text-align: right">
            <a-button type="primary" size="small">编辑</a-button>
            <a-button type="primary" size="small" @click="showModal('update')">编辑</a-button>
            <a-button size="small" style="margin-left: 20px">删除</a-button>
          </div>
        </a-form-item>
@@ -52,7 +52,7 @@
          <a-input v-model:value="form.signName" placeholder="请输入名称" />
        </a-form-item>
        <a-form-item label="内容">
          <TinymceAdd v-model="form.signContent" :isElse="false" :isText="false"></TinymceAdd>
          <Tinymce v-model="form.signContent" :isElse="false" :isText="false"></Tinymce>
        </a-form-item>
      </a-form>
    </a-modal>
@@ -61,12 +61,11 @@
<script lang="ts" setup>
  name: 'convention';
  import { onMounted, ref } from 'vue';
  import { nextTick, onMounted, ref } from 'vue';
  import { getAccountListApi } from '@/api/email/userList';
  import { PlusCircleOutlined } from '@ant-design/icons-vue';
  import Tinymce from '@/components/Tinymce/src/index.vue';
  import TinymceAdd from '@/components/Tinymce/src/Editor.vue';
  import { addSignatureApi, getSignatureApi } from '@/api/email/userList';
  import { TinymcePw, Tinymce } from '@/components/Tinymce';
  import { addSignatureApi, getSignatureApi, updateSignatureApi } from '@/api/email/userList';
  interface EmailItem {
    email: string;
@@ -88,18 +87,34 @@
  });
  const open = ref(false);
  const signType = ref('add');
  const form = ref<Record<string, any>>({});
  const title = ref('新建');
  const showModal = (type) => {
    signType.value = type;
    open.value = true;
    form.value = {
      signName: '',
      signContent: '',
    };
    if (type == 'add') {
      form.value = {
        signName: '',
        signContent: '',
      };
    } else {
      title.value = '编辑';
      nextTick(() => {
        formRef.value.resetFields();
        getSign(signature.value);
      });
    }
  };
  function getSign(id) {
    const matchedItem = signatureList.value.find((item) => item.signId === id);
    if (matchedItem) {
      form.value = Object.assign(form.value, matchedItem);
    }
  }
  import { useMessage } from '@/hooks/web/useMessage';
  const { createMessage } = useMessage();
  const formRef = ref();
  const signContent = ref('<p>ddd</p>');
  const signContent = ref();
  function fnSignatureChange(e) {
    const matchedItem = signatureList.value.find((item) => item.signId === e);
    if (matchedItem) {
@@ -107,18 +122,15 @@
    } else {
      signContent.value = '';
    }
    console.log(signContent.value, '05050050505');
  }
  const signature = ref();
  const handleOk = (e: MouseEvent) => {
    console.log(form.value, '----4');
    formRef.value
      .validate()
      .then(() => {
        const data = form.value;
        // const api = signType.value !== 'add' ? updateAccountApi : addSignatureApi;
        const api = addSignatureApi;
        const api = signType.value == 'update' ? updateSignatureApi : addSignatureApi;
        // const api = addSignatureApi;
        api(data).then((res) => {
          if (res.code === 0) {
            createMessage.success(res.msg);
@@ -130,8 +142,6 @@
        console.log(e);
      });
  };
  const form = ref<Record<string, any>>({});
  const title = '新建';
</script>
<style scoped lang="less">
  .title {
src/views/email/Utils/index.vue
@@ -1,27 +1,28 @@
<template>
  <div class="p-2">
      <a-tabs v-model:activeKey="activeKey">
    <a-tab-pane key="1" tab="常规">
      <Convention v-if="activeKey==='1'"></Convention>
    </a-tab-pane>
    <a-tab-pane key="2" tab="邮箱管理" force-render>
      <MailboxManagement v-if="activeKey==='2'"></MailboxManagement>
    </a-tab-pane>
    <a-tab-pane key="3" tab="快速文本">Content of Tab Pane 3</a-tab-pane>
    <a-tab-pane key="3" tab="文件夹">Content of Tab Pane 3</a-tab-pane>
    <a-tab-pane key="3" tab="标签">Content of Tab Pane 3</a-tab-pane>
    <a-tab-pane key="3" tab="黑名单">Content of Tab Pane 3</a-tab-pane>
  </a-tabs>
    <a-tabs v-model:activeKey="activeKey">
      <a-tab-pane key="1" tab="常规">
        <Convention v-if="activeKey === '1'"></Convention>
      </a-tab-pane>
      <a-tab-pane key="2" tab="邮箱管理" force-render>
        <MailboxManagement v-if="activeKey === '2'"></MailboxManagement>
      </a-tab-pane>
      <a-tab-pane key="3" tab="快速文本"
        ><QuickText v-if="activeKey === '3'"></QuickText
      ></a-tab-pane>
      <a-tab-pane key="4" tab="文件夹">Content of Tab Pane 3</a-tab-pane>
      <a-tab-pane key="5" tab="标签">Content of Tab Pane 3</a-tab-pane>
      <a-tab-pane key="6" tab="黑名单">Content of Tab Pane 3</a-tab-pane>
    </a-tabs>
  </div>
</template>
<script lang="ts" setup>
  name: 'Utils';
  import { ref } from 'vue';
const activeKey = ref('1');
import MailboxManagement from './mailboxManagement.vue'
import Convention from './convention.vue'
  const activeKey = ref('1');
  import MailboxManagement from './mailboxManagement.vue';
  import Convention from './convention.vue';
  import QuickText from './quickText.vue';
</script>
<style scoped lang="less"></style>
src/views/email/Utils/mailboxManagement.vue
@@ -13,7 +13,7 @@
      ref="xTable"
      id="key"
      style="margin: 10px 0"
      :data="data"
      :data="demo.tableData"
      min-height="40px"
      row-key
      keep-source
@@ -21,6 +21,13 @@
      :row-config="{ isHover: true }"
      :column-config="{ resizable: true }"
    >
      <vxe-column width="60">
        <template #default>
          <span class="drag-btn">
            <HolderOutlined />
          </span>
        </template>
      </vxe-column>
      <vxe-column show-overflow field="email" title="邮箱账号" min-width="250">
        <template #default="{ row }">
          <!-- <HolderOutlined /> -->
@@ -59,48 +66,57 @@
      :confirmLoading="loading"
      @ok="fnHandleOk"
    >
      <a-form
        style="margin-top: 20px"
        layout="vertical"
        :model="formData"
        v-bind="{ span: 8 }"
        ref="formRef"
        :rules="rules"
      >
        <a-form-item v-if="isShow" label="收发件服务器验证">
          <a-select
            ref="select"
            v-model:value="isCustom"
            style="width: 120px"
            @change="fnHandleChange"
          >
            <a-select-option key="onCustom" value="onCustom"> 与邮箱相同 </a-select-option>
            <a-select-option key="custom" value="custom"> 自定义 </a-select-option>
          </a-select>
        </a-form-item>
        <a-form-item name="email">
          <template v-slot:label>
            邮箱账号
            <a-tooltip placement="right">
              <template #title>
                <span>工作邮箱是您处理公司事务时所使用的办公邮箱 建议您专用于办公目的</span>
              </template>
              <ExclamationCircleOutlined style="margin-left: 5px" />
            </a-tooltip>
          </template>
          <a-input :disabled="typeAccount === 2" v-model:value="formData.email" />
        </a-form-item>
        <a-form-item name="password" label="邮箱密码">
          <a-input type="password" v-model:value="formData.password" />
        </a-form-item>
        <a-form-item v-if="isShow" name="receiveProtocol" label="协议类型">
          <a-radio-group v-model:value="formData.receiveProtocol" name="radioGroup">
            <a-radio value="imap">IMAP</a-radio>
            <a-radio value="pop3">POP3(不推荐,无法同步发件)</a-radio>
            <!-- <a-radio value="exchange">Exchange</a-radio> -->
          </a-radio-group>
        </a-form-item>
        <!-- <a-form-item v-if="formData.receiveProtocol === 'exchange'" :name="['user', 'email']" label="Exchange服务器" :rules="[{ type: 'email' }]">
      <div class="p-2">
        <a-form
          style="margin-top: 20px"
          layout="vertical"
          :model="formData"
          v-bind="{ span: 8 }"
          ref="formRef"
          :rules="rules"
        >
          <a-form-item v-if="isShow" label="收发件服务器验证">
            <a-select
              ref="select"
              v-model:value="isCustom"
              style="width: 120px"
              @change="fnHandleChange"
            >
              <a-select-option key="onCustom" value="onCustom"> 与邮箱相同 </a-select-option>
              <a-select-option key="custom" value="custom"> 自定义 </a-select-option>
            </a-select>
          </a-form-item>
          <a-form-item name="email">
            <template v-slot:label>
              邮箱账号
              <a-tooltip placement="right">
                <template #title>
                  <span>工作邮箱是您处理公司事务时所使用的办公邮箱 建议您专用于办公目的</span>
                </template>
                <ExclamationCircleOutlined style="margin-left: 5px" />
              </a-tooltip>
            </template>
            <a-input :disabled="typeAccount === 2" v-model:value="formData.email" />
          </a-form-item>
          <a-form-item v-if="isCustom == 'custom'" name="password" label="邮箱密码">
            <a-input-password
              type="password"
              v-model:value="formData.password"
              placeholder="输入邮箱密码或者授权码"
            />
          </a-form-item>
          <a-form-item v-if="isShow" name="receiveProtocol" label="协议类型">
            <a-radio-group v-model:value="formData.receiveProtocol" name="radioGroup">
              <a-radio value="imap">IMAP</a-radio>
              <a-radio value="pop3">POP3(不推荐,无法同步发件)</a-radio>
              <!-- <a-radio value="exchange">Exchange</a-radio> -->
            </a-radio-group>
            <div style="color: red; font-size: 12px"
              >由于exchange协议支持问题,网易邮箱推荐使用IMAP协议。
              如需调整协议类型,请联系小满客服</div
            >
          </a-form-item>
          <!-- <a-form-item v-if="formData.receiveProtocol === 'exchange'" :name="['user', 'email']" label="Exchange服务器" :rules="[{ type: 'email' }]">
          <a-row style="display: flex; align-items: center">
            <a-col :span="18">
              <a-input v-model:value="formData.email" placeholder="Exchange服务器"
@@ -120,110 +136,113 @@
            </a-col>
          </a-row>
        </a-form-item> -->
        <a-form-item v-if="isShow" name="receiveHost" label="收邮件服务器">
          <a-row style="display: flex; align-items: center">
            <a-col :span="12">
              <a-input v-model:value="formData.receiveHost" placeholder="收邮件服务器" />
            </a-col>
            <a-col :span="1" style="margin-right: 10px; margin-left: 5px">:</a-col>
            <a-col :span="6" style="margin-right: 10px">
              <a-form-item-rest
                ><a-input width="50px" v-model:value="formData.receivePort" placeholder="端口"
              /></a-form-item-rest>
            </a-col>
            <a-col :span="3">
              <a-form-item-rest>
                <a-checkbox v-model:checked="formData.receiveSSL">SSL</a-checkbox></a-form-item-rest
              >
            </a-col>
          </a-row>
        </a-form-item>
        <a-form-item
          style="margin-top: 22px"
          v-if="isCustom == 'custom'"
          name="receiveEmail"
          label="收件账号"
        >
          <a-input v-model:value="formData.receiveEmail" />
        </a-form-item>
        <a-form-item v-if="isCustom == 'custom'" name="receivePassword" label="收件密码">
          <a-input v-model:value="formData.receivePassword" />
        </a-form-item>
        <a-form-item v-if="isShow" name="smtpHost" label="发邮件服务器">
          <a-row style="display: flex; align-items: center">
            <a-col :span="12">
              <a-input v-model:value="formData.smtpHost" placeholder="发邮件服务器" />
            </a-col>
            <a-col :span="1" style="margin-right: 10px; margin-left: 5px">:</a-col>
            <a-col :span="6" style="margin-right: 10px">
              <a-form-item-rest
                ><a-input width="50px" v-model:value="formData.smtpPort" placeholder="端口"
              /></a-form-item-rest>
            </a-col>
            <a-col :span="3">
              <a-form-item-rest
                ><a-checkbox v-model:checked="formData.smtpSSL">SSL</a-checkbox></a-form-item-rest
              >
            </a-col>
          </a-row>
        </a-form-item>
        <a-form-item
          style="margin-top: 22px"
          v-if="isCustom == 'custom'"
          name="smtpEmail"
          label="发件账号"
        >
          <a-input v-model:value="formData.smtpEmail" />
        </a-form-item>
        <a-form-item v-if="isCustom == 'custom'" name="smtpPassword" label="发件密码">
          <a-input v-model:value="formData.smtpPassword" />
        </a-form-item>
        <a-form-item v-if="isShow" name="proxyFlag">
          <template v-slot:label>
            <a-tooltip placement="right">
              <template #title>
                <span>开启后网络提速</span>
              </template>
              <QuestionCircleOutlined style="margin-right: 5px" />
            </a-tooltip>
            自定义代理
          </template>
          <a-radio-group v-model:value="formData.proxyFlag" name="radioGroup">
            <a-radio :value="true">开启</a-radio>
            <a-radio :value="false">关闭</a-radio>
          </a-radio-group>
        </a-form-item>
        <a-form-item v-if="isShow" name="biSyncFlag">
          <template v-slot:label>
            <a-tooltip placement="right">
              <template #title>
                <span
                  >开启后,对于新绑定的邮箱,全量同步文件夹及文件夹内的邮件。对于已经绑定的邮箱,全量同步文件夹及文件夹新收取的邮件,历史邮件不移动</span
          <a-form-item v-if="isShow" name="receiveHost" label="收邮件服务器">
            <a-row style="display: flex; align-items: center">
              <a-col :span="12">
                <a-input v-model:value="formData.receiveHost" placeholder="收邮件服务器" />
              </a-col>
              <a-col :span="1" style="margin-right: 10px; margin-left: 5px">:</a-col>
              <a-col :span="6" style="margin-right: 10px">
                <a-form-item-rest
                  ><a-input width="50px" v-model:value="formData.receivePort" placeholder="端口"
                /></a-form-item-rest>
              </a-col>
              <a-col :span="3">
                <a-form-item-rest>
                  <a-checkbox v-model:checked="formData.receiveSSL"
                    >SSL</a-checkbox
                  ></a-form-item-rest
                >
              </template>
              <QuestionCircleOutlined style="margin-right: 5px" />
            </a-tooltip>
            同步文件夹
          </template>
          <a-radio-group v-model:value="formData.biSyncFlag" name="radioGroup">
            <a-radio :value="true">开启</a-radio>
            <a-radio :value="false">关闭</a-radio>
          </a-radio-group>
        </a-form-item>
      </a-form>
      <a @click="fnIsShow" v-if="!isShow"> 手动配置</a>
      <a @click="fnIsShow" v-else> 收起手动配置</a>
              </a-col>
            </a-row>
          </a-form-item>
          <a-form-item
            style="margin-top: 22px"
            v-if="isCustom == 'custom'"
            name="receiveEmail"
            label="收件账号"
          >
            <a-input v-model:value="formData.receiveEmail" />
          </a-form-item>
          <a-form-item v-if="isCustom == 'custom'" name="receivePassword" label="收件密码">
            <a-input v-model:value="formData.receivePassword" />
          </a-form-item>
          <a-form-item v-if="isShow" name="smtpHost" label="发邮件服务器">
            <a-row style="display: flex; align-items: center">
              <a-col :span="12">
                <a-input v-model:value="formData.smtpHost" placeholder="发邮件服务器" />
              </a-col>
              <a-col :span="1" style="margin-right: 10px; margin-left: 5px">:</a-col>
              <a-col :span="6" style="margin-right: 10px">
                <a-form-item-rest
                  ><a-input width="50px" v-model:value="formData.smtpPort" placeholder="端口"
                /></a-form-item-rest>
              </a-col>
              <a-col :span="3">
                <a-form-item-rest
                  ><a-checkbox v-model:checked="formData.smtpSSL">SSL</a-checkbox></a-form-item-rest
                >
              </a-col>
            </a-row>
          </a-form-item>
          <a-form-item
            style="margin-top: 22px"
            v-if="isCustom == 'custom'"
            name="smtpEmail"
            label="发件账号"
          >
            <a-input v-model:value="formData.smtpEmail" />
          </a-form-item>
          <a-form-item v-if="isCustom == 'custom'" name="smtpPassword" label="发件密码">
            <a-input v-model:value="formData.smtpPassword" />
          </a-form-item>
          <a-form-item v-if="isShow" name="proxyFlag">
            <template v-slot:label>
              <a-tooltip placement="right">
                <template #title>
                  <span>开启后网络提速</span>
                </template>
                <QuestionCircleOutlined style="margin-right: 5px" />
              </a-tooltip>
              自定义代理
            </template>
            <a-radio-group v-model:value="formData.proxyFlag" name="radioGroup">
              <a-radio :value="true">开启</a-radio>
              <a-radio :value="false">关闭</a-radio>
            </a-radio-group>
          </a-form-item>
          <a-form-item v-if="isShow" name="biSyncFlag">
            <template v-slot:label>
              <a-tooltip placement="right">
                <template #title>
                  <span
                    >开启后,对于新绑定的邮箱,全量同步文件夹及文件夹内的邮件。对于已经绑定的邮箱,全量同步文件夹及文件夹新收取的邮件,历史邮件不移动</span
                  >
                </template>
                <QuestionCircleOutlined style="margin-right: 5px" />
              </a-tooltip>
              同步文件夹
            </template>
            <a-radio-group v-model:value="formData.biSyncFlag" name="radioGroup">
              <a-radio :value="true">开启</a-radio>
              <a-radio :value="false">关闭</a-radio>
            </a-radio-group>
          </a-form-item>
        </a-form>
        <a @click="fnIsShow" v-if="!isShow"> 手动配置</a>
        <a @click="fnIsShow" v-else> 收起手动配置</a>
      <a-divider style="margin-top: 50px" />
      <div style="font-size: 18px">帮助文档</div>
      <div
        ><a href="https://www.yuque.com/help.xiaoman/qwwqei/vkr8p7" target="_blank" rel="noopener"
          >1、查看绑定邮箱失败的常见原因及解决方案</a
        ></div
      >
      <div
        ><a href="https://www.yuque.com/help.xiaoman/qwwqei/sl9xuk" target="_blank" rel="noopener"
          >2、了解常见几类邮箱的具体绑定方法</a
        <a-divider style="margin-top: 50px" />
        <div style="font-size: 18px">帮助文档</div>
        <div
          ><a href="https://www.yuque.com/help.xiaoman/qwwqei/vkr8p7" target="_blank" rel="noopener"
            >1、查看绑定邮箱失败的常见原因及解决方案</a
          ></div
        >
        <div
          ><a href="https://www.yuque.com/help.xiaoman/qwwqei/sl9xuk" target="_blank" rel="noopener"
            >2、了解常见几类邮箱的具体绑定方法</a
          ></div
        ></div
      >
    </a-modal>
@@ -273,8 +292,7 @@
          <a-form-item label="邮箱密码">
            <div class="form-item">
              <div class="left" :class="checkStatus ? 'isColor' : 'isRed'"> ********</div>
              <div class="right"
                >
              <div class="right">
                <a-spin v-if="!isCheck" :indicator="indicator" />
                <CheckCircleOutlined v-else-if="checkStatus" class="isColor" />
                <CloseCircleOutlined v-else class="isRed" />
@@ -322,7 +340,7 @@
            <a-tooltip placement="right">
              <template #title>
                <span
                  >开启后,对于新绑定的邮箱,全量同步文件夹及文件夹内的邮件。对于已经绑定的邮箱,全量同步文件夹及文件夹新收取的邮件,历史邮件不移动</span
                  >开启后,对于新绑定的邮箱,全量同步文件夹及文件夹内的邮件。<br />对于已经绑定的邮箱,全量同步文件夹及文件夹新收取的邮件,历史邮件不移动</span
                >
              </template>
              <QuestionCircleOutlined style="margin-right: 5px" />
@@ -371,7 +389,7 @@
    CheckCircleOutlined,
    CloseCircleOutlined,
  } from '@ant-design/icons-vue';
  import { ref } from 'vue';
  import { ref, reactive, onUnmounted } from 'vue';
  import {
    addAccountApi,
    getAccountApi,
@@ -381,19 +399,52 @@
    isEmailValidApi,
  } from '@/api/email/userList';
  const loading = ref(false);
  import Sortable from 'sortablejs';
  let sortable: any;
  const demo = reactive({
    showHelpTip: false,
    tableData: [],
  });
  const xTable = ref();
  const rowDrop = () => {
    const $table = xTable.value;
    sortable = Sortable.create($table.$el.querySelector('.body--wrapper>.vxe-table--body tbody'), {
      handle: '.drag-btn',
      onEnd: (sortableEvent) => {
        const newIndex = sortableEvent.newIndex as number;
        const oldIndex = sortableEvent.oldIndex as number;
        const currRow = demo.tableData.splice(oldIndex, 1)[0];
        demo.tableData.splice(newIndex, 0, currRow);
      },
    });
  };
  const data = ref([]);
  let initTime: any;
  nextTick(() => {
    // 加载完成之后在绑定拖动事件
    initTime = setTimeout(() => {
      rowDrop();
    }, 500);
  });
  onUnmounted(() => {
    clearTimeout(initTime);
    if (sortable) {
      sortable.destroy();
    }
  });
  function fnMailList() {
    getAccountListApi()
      .then((res) => {
        if (res.code == 0) {
          data.value = res.data;
          demo.tableData = res.data;
        }
        console.log(res);
      })
      .catch((err) => {
        console.log(err);
        data.value = [];
        demo.tableData = [];
      });
  }
  fnMailList();
@@ -447,6 +498,10 @@
    password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
    receiveHost: [{ required: true, validator: checkReceivePort }],
    smtpHost: [{ required: true, validator: checkSmtpHost }],
    smtpPassword: [{ required: true, message: '请输入发件密码', trigger: 'blur' }],
    smtpEmail: [{ required: true, message: '请输入发件账号', trigger: 'blur' }],
    receiveEmail: [{ required: true, message: '请输入收件密码', trigger: 'blur' }],
    receivePassword: [{ required: true, message: '请输入收件账号', trigger: 'blur' }],
  });
  import { useMessage } from '@/hooks/web/useMessage';
  const { createMessage } = useMessage();
@@ -467,6 +522,7 @@
            createMessage.success(res.msg);
          }
          loading.value = false;
          fnMailList();
        })
        .catch((err) => {
          loading.value = false;
@@ -481,7 +537,7 @@
  }
  const title = ref('添加');
  const typeAccount = ref(1);
  function openAccount(type, row) {
  function openAccount(type, email) {
    formData.value = { ...defaultFormData };
    try {
      if (type == 'add') {
@@ -493,7 +549,7 @@
        title.value = '修改';
        typeAccount.value = 2;
        isShow.value = true;
        getAccountApi({ mail: '1244041895@qq.com' }).then((res) => {
        getAccountApi({ mail: email }).then((res) => {
          formData.value = res.data;
        });
      }
src/views/email/Utils/quickText.vue
New file
@@ -0,0 +1,202 @@
<template>
  <div class="p-2">
    <vxe-toolbar>
      <template #buttons>
        <div style="display: flex; align-items: flex-end">
          <span style="font-size: 1.25rem; font-weight: 600">快速文本</span
          ><span style="margin-left: 5px; padding-bottom: 4px; font-size: 12px"
            >作为模板使用,在写信时快速创建内容</span
          >
        </div>
      </template>
      <template #tools>
        <a-button type="primary" @click="showDrawer('add', '')">新建文本</a-button>
      </template>
    </vxe-toolbar>
    <vxe-table ref="xTable" style="margin: 10px 0" :data="demo.tableData" @mounted="onMounted">
      <vxe-column width="60">
        <template #default>
          <span class="drag-btn">
            <HolderOutlined />
          </span>
        </template>
      </vxe-column>
      <vxe-column field="userName" title="标题" width="250"></vxe-column>
      <vxe-column field="content" title="内容" min-width="250"></vxe-column>
      <vxe-column field="age" title="操作" width="150">
        <template #default="{ row }">
          <a style="margin-right: 10px" @click="showDrawer('update', row)">编辑</a>
          <a style="margin-right: 10px" @click="fnDelete(row)">删除</a>
        </template>
      </vxe-column>
    </vxe-table>
    <a-drawer :title="`${title}文本`" placement="right" :open="open" @close="onClose" width="600">
      <a-form ref="formRef" :model="form" style="margin-top: 20px">
        <a-form-item
          label="标题"
          name="textName"
          :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
        >
          <a-input v-model:value="form.textName" placeholder="请输入名称" />
        </a-form-item>
        <a-form-item
          label="内容"
          name="content"
          :rules="[{ required: true, message: '请输入内容', trigger: 'blur' }]"
        >
          <Tinymce v-model="form.content" :isElse="false" :isText="false" :isImg="false"></Tinymce>
        </a-form-item>
      </a-form>
      <template #footer>
        <div style="margin-top: 20px; text-align: center">
          <div style="margin-bottom: 20px"
            >写邮件时,输入
            <span style="color: red">&</span>标题关键词,联想出快速文本,快捷插入</div
          >
          <a-button style="margin-right: 8px" @click="onClose">取消</a-button>
          <a-button type="primary" @click="onOk">保存</a-button>
        </div>
      </template>
    </a-drawer>
  </div>
</template>
<script lang="ts" setup>
  import { ref, computed, onMounted, nextTick, onUnmounted, reactive } from 'vue';
  import { Tinymce } from '@/components/Tinymce';
  import {
    getQuickTextApi,
    addQuickTextApi,
    updateQuickTextApi,
    deleteQuickTextApi,
  } from '@/api/email/userList';
  // 排序
  import { HolderOutlined } from '@ant-design/icons-vue';
  import Sortable from 'sortablejs';
  let sortable: any;
  const demo = reactive({
    showHelpTip: false,
    tableData: [],
  });
  const xTable = ref();
  const rowDrop = () => {
    const $table = xTable.value;
    sortable = Sortable.create($table.$el.querySelector('.body--wrapper>.vxe-table--body tbody'), {
      handle: '.drag-btn',
      onEnd: (sortableEvent) => {
        const newIndex = sortableEvent.newIndex as number;
        const oldIndex = sortableEvent.oldIndex as number;
        const currRow:Record<string, any> = demo.tableData.splice(oldIndex, 1)[0];
        // demo.tableData.splice(newIndex, 0, currRow);
        updateQuickTextApi({
          textId: currRow.textId,
          textName: currRow.textName,
          content: currRow.content,
          sortId: newIndex,
        }).then(() => {
          fnGetList();
        }).catch(()=>{});
      },
    });
  };
  let initTime: any;
  nextTick(() => {
    // 加载完成之后在绑定拖动事件
    initTime = setTimeout(() => {
      rowDrop();
    }, 500);
  });
  onUnmounted(() => {
    clearTimeout(initTime);
    if (sortable) {
      sortable.destroy();
    }
  });
  const form = ref<Record<string, any>>({});
  const title = ref('新建');
  const open = ref<boolean>(false);
  const handleType = ref('add');
  const formRef = ref();
  const showDrawer = (type, row) => {
    handleType.value = type;
    open.value = true;
    if (type == 'add') {
      form.value = {
        textName: '',
        content: '',
      };
    } else {
      title.value = '编辑';
      nextTick(() => {
        formRef.value.resetFields();
        form.value = Object.assign(form.value, row);
      });
    }
  };
  const api = computed(() => {
    return handleType.value == 'add' ? addQuickTextApi : updateQuickTextApi;
  });
  function fnDelete(row) {
    deleteQuickTextApi({ textId: row.textId }).then((res) => {
      if (res.code == 0) {
        fnGetList();
      }
    });
  }
  function fnGetList() {
    getQuickTextApi({}).then((res) => {
      console.log(res);
      demo.tableData = res.data;
    });
  }
  const onClose = () => {
    open.value = false;
  };
  import { useMessage } from '@/hooks/web/useMessage';
  const { createMessage } = useMessage();
  const onOk = () => {
    formRef.value.validate().then(() => {
      const data: Record<string, any> = {
        textName: form.value.textName,
        content: form.value.content,
      };
      if (handleType.value != 'add') {
        data.textId = form.value.textId;
      }
      api
        .value(data)
        .then((res) => {
          if (res.code === 0) {
            createMessage.success(res.msg);
          }
          fnGetList();
          onClose();
        })
        .catch((e) => {
          createMessage.warning(e);
        });
    });
  };
  onMounted(() => {
    fnGetList();
  });
</script>
<style scoped>
  .sortable-row-demo .drag-btn {
    font-size: 12px;
    cursor: move;
  }
  .sortable-row-demo .vxe-body--row.sortable-ghost,
  .sortable-row-demo .vxe-body--row.sortable-chosen {
    background-color: #dfecfb;
  }
</style>
src/views/email/components/Editor.vue
@@ -229,7 +229,7 @@
  import { useMessage } from '@/hooks/web/useMessage';
  import { Tinymce } from '@/components/Tinymce';
  import { PlusCircleOutlined } from '@ant-design/icons-vue';
  import SelectUser from './SelectUser.vue';
  import SelectUser from './SelectUser/SelectUser.vue';
  import { Form } from 'ant-design-vue';
  const modelRef = reactive({
    sender: '',
@@ -366,23 +366,22 @@
            title: '提示',
            content: '邮件主题为空,确定要发送吗?',
            onOk() {
              pushSendingMail()
              pushSendingMail();
            },
            onCancel() {},
          });
        }else{pushSendingMail()}
        } else {
          pushSendingMail();
        }
      })
      .catch((error) => {
        createMessage.error('表单验证失败');
      });
  }
  function pushSendingMail(){
       const data = fnBuildingCommitData();
        sendingMailApi(data).then((res) => {
        });
  function pushSendingMail() {
    const data = fnBuildingCommitData();
    sendingMailApi(data).then((res) => {});
  }
  function fnPreview() {}
@@ -408,7 +407,7 @@
    value: string;
    email: string;
    title: string;
    name: string
    name: string;
  }
  const userParticulars = computed<Recipient | null>(() => {
src/views/email/components/LeftNav.vue
@@ -65,13 +65,12 @@
<script lang="ts" setup>
  import { ref, onMounted } from 'vue';
  import { PageWrapper } from '@/components/Page';
  import { Menu } from 'ant-design-vue';
  import { MailOutlined, UserOutlined } from '@ant-design/icons-vue';
  import { getEmailModuleApi } from '@/api/email/userList';
  import { useRouter } from 'vue-router';
  const selectedKeys = ref<string[]>(['Index']);
  const openKeys = ref<string[]>(['index']);
  const openKeys = ref<string[]>(['Inbox']);
  const items = ref([]); // 定义 items 类型
  const fnGetEmailModule = async () => {
@@ -111,7 +110,7 @@
  });
  const routesConfig = {
    InboxPage1: '/email/index',
    page: '/email/Inbox/list',
    receiver: '/email/Inbox/list',
  };
  // 点击事件处理
  const router = useRouter();
@@ -132,7 +131,7 @@
            router.push(routesConfig[e.key]);
            matched = true;
            return; // 跳出当前循环
          case 'page':
          case 'receiver':
            router.push(`${routesConfig[e.key]}?${e.title}`);
            matched = true;
            return; // 跳出当前循环
src/views/email/components/ListPage/drawerDetail.vue
@@ -113,7 +113,7 @@
  CopyOutlined,
  UserOutlined
  } from '@ant-design/icons-vue';
import pageHeadLeft from '../pageHeadLeft.vue';
import pageHeadLeft from './pageHeadLeft.vue';
  import Tinymce from '@/components/Tinymce/src/index.vue';
  // 定义属性
src/views/email/components/ListPage/list.vue
@@ -14,7 +14,9 @@
            ></a-checkbox>
            <!--更新  -->
            <SyncOutlined class="icon" v-show="!checked" />
            <pageHeadLeft :checked="checked"></pageHeadLeft>
            <pageHeadLeft :checked="checked" :selectAllRow="selectAllRow"
            :parentTableList='newList'
           ></pageHeadLeft>
          </div>
        </div>
@@ -69,6 +71,7 @@
            <Table
              ref="tableRef"
              :page="pageCurrent"
              :pageList="newList"
              @selectAll="fnSelectAll"
              @updateSelectAll="updateSelectAll"
            />
@@ -87,20 +90,24 @@
    FilterOutlined,
    PushpinOutlined,
  } from '@ant-design/icons-vue';
  import pageHeadLeft from '../pageHeadLeft.vue';
  import pageHeadLeft from './pageHeadLeft.vue';
  import { PageWrapper } from '@/components/Page';
  import { ref, watch, defineProps, defineEmits, computed, reactive, onMounted } from 'vue';
  // 定义属性
  interface Props {
    modelValue: boolean;
    title?: string;
    placement?: 'left' | 'right' | 'top' | 'bottom';
    idName?: string;
    pageList?: [];
    pageList: [];
  }
  const props = defineProps<Props>();
  const newList = ref([]);
  const selectAllRow = ref([]);
  watch(
    () => props.pageList,
    (newValue) => {
      newList.value = newValue;
    },
  );
  const checked = ref(false);
  const pageCurrent = ref(1);
@@ -117,6 +124,7 @@
    checked.value = e.target.checked;
  }
  function updateSelectAll(data) {
    selectAllRow.value = data.records;
    if (!data.isAll) {
      state.indeterminate = true;
      state.checkAll = false;
@@ -160,13 +168,13 @@
  const activeKey = ref('1');
  const checked3 = ref(false);
  import Table from './table.vue';
  import { router } from '@/router';
  onMounted(() => {
    console.log('tableRef:', tableRef.value[0]);
  });
  function fnSelectAll() {
    console.log('44444444444');
  }
</script>
<style scoped lang="less">
  .head {
src/views/email/components/ListPage/pageHeadLeft.vue
New file
@@ -0,0 +1,134 @@
<template>
  <div class="left-box">
    <!-- 分发 -->
    <a-tooltip placement="bottom">
      <template #title>
        <span>分发</span>
      </template>
      <ExportOutlined v-show="checked" class="icon" />
    </a-tooltip>
    <!-- 时间 -->
    <ClockCircleOutlined class="icon" v-show="checked" />
    <!-- 删除 -->
    <a-tooltip placement="bottom">
      <template #title>
        <span>删除</span>
      </template>
      <DeleteOutlined v-show="checked" class="icon" />
    </a-tooltip>
    <!-- 收藏 -->
    <a-tooltip placement="bottom">
      <template #title>
        <span>标志</span>
      </template>
      <TagOutlined class="icon" v-show="checked" />
    </a-tooltip>
    <!-- 文件夹 -->
    <a-tooltip placement="bottom">
      <template #title>
        <span>文件夹</span>
      </template>
      <FolderOutlined class="icon" v-show="checked" />
    </a-tooltip>
    <!-- 更多 -->
    <a-tooltip placement="bottom">
      <template #title>
        <span>更多</span>
      </template>
      <a-dropdown :arrow="{ pointAtCenter: true }" placement="bottom" :trigger="['click']">
        <template #overlay>
          <a-menu>
            <a-menu-item key="2" @click="fnSelectAllRead(true)"> 标为已读</a-menu-item>
            <a-menu-item key="3" @click="fnSelectAllRead(false)"> 标为未读</a-menu-item>
            <a-menu-item key="4"> 设为置顶</a-menu-item>
            <a-divider style="margin: 2px; padding: 2px" />
            <a-menu-item key="5"> 标记为垃圾邮件</a-menu-item>
          </a-menu>
        </template>
        <MoreOutlined v-show="checked" class="icon" />
      </a-dropdown>
      <a-dropdown :arrow="{ pointAtCenter: true }" placement="bottom" :trigger="['click']">
        <template #overlay>
          <a-menu>
            <a-menu-item key="1" @click="fnAllRead">全部标记为已读</a-menu-item>
            <a-divider style="margin: 2px; padding: 2px" />
            <p style="color: #999; font-size: 12px">勾选邮件即可查看更多操作</p>
          </a-menu>
        </template>
        <MoreOutlined v-show="!checked" class="icon" />
      </a-dropdown>
    </a-tooltip>
  </div>
</template>
<script lang="ts" setup>
  import {
    ExportOutlined,
    ClockCircleOutlined,
    DeleteOutlined,
    TagOutlined,
    FolderOutlined,
    MoreOutlined,
  } from '@ant-design/icons-vue';
  import { ref, defineProps, defineEmits, computed, reactive, inject } from 'vue';
  interface Props {
    checked: boolean;
    handleId?: number;
    selectAllRow?: Array<any>;
    parentTableList: Array<any>;
  }
  const props = defineProps<Props>();
  const checked = computed(() => props.checked);
  import { updateReadApi } from '@/api/email/userList';
  const getDataList = inject('getDataList');
  function fnSelectAllRead(is) {
    const data = {
      status: is,
      list: getReadId(),
    };
    pushUpdateReadApi(data);
  }
  function pushUpdateReadApi(data) {
    updateReadApi(data).then((res) => {
      if (res.code == 0) {
        //
        getDataList({});
      }
    });
  }
  function fnAllRead() {
    fnGetTableList();
  }
  function fnGetTableList() {
    const data = {
      status: true,
      list:getReadId(),
    };
    pushUpdateReadApi(data);
  }
  function getReadId(){
    const ids = [];
    props.parentTableList.forEach((item: Record<string, any>) => {
      ids.push(item.docCode);
    });
    return ids
  }
</script>
<style lang="less" scoped>
  .left-box {
    display: flex;
    align-items: center;
    justify-content: space-flex-start;
    width: 100%;
    & .icon {
      margin-right: 15px;
      font-size: 16px;
    }
  }
</style>
src/views/email/components/ListPage/table.vue
@@ -15,11 +15,22 @@
        @cell-click="cellClickEvent"
        @checkbox-change="selectChangeEvent"
      >
        <vxe-column type="checkbox" width="60"></vxe-column>
        <vxe-column type="checkbox" width="30"></vxe-column>
        <vxe-column field="sender" title="发件人" data-index="sender" min-width="300px">
          <template #default="{ row }">
            <div style="display: flex; align-items: center">
              <a-avatar size="small" style="margin-right: 8px" :src="row.avatar" />
              <div
                class="dot"
                :class="row.readFlag ? 'dot-color' : ''"
                @click.stop="fnRowUpdateRead(row)"
              ></div>
              <a-tooltip placement="bottom">
                <template #title>
                  <span>陌生人</span>
                </template>
                <a-avatar size="small" style="margin-right: 8px" :src="row.avatar" />
              </a-tooltip>
              <a-popover placement="bottom">
                <template #content>
                  <div
@@ -31,7 +42,7 @@
                    "
                  >
                    <a-avatar size="small" style="margin-right: 8px" :src="row.avatar" />
                    <span style="color: #000; font-weight: 700">{{ row.email }}</span>
                    <span style="color: #000; font-weight: 700">{{ row.sender }}</span>
                    <CopyOutlined />
                  </div>
                  <div class="display-flex p-2">
@@ -65,10 +76,10 @@
                    <a-button type="link" size="small">往来邮件</a-button></div
                  >
                </template>
                <div>
                  <span style="font-weight: 700">{{ row.sender }}</span
                  ><span style="padding: 0 8px; color: #999">|</span>
                  <span style="color: #999; font-weight: 500">{{ row.email }}</span>
                <div class="title-dot" :class="row.readFlag ? 'title-dot-color' : ''">
                  <span style="font-weight: 700">{{ row.senderName }}</span
                  ><span style="padding: 0 8px">|</span>
                  <span style="font-weight: 500">{{ row.sender }}</span>
                </div>
              </a-popover>
            </div>
@@ -76,7 +87,13 @@
        </vxe-column>
        <vxe-column show-overflow field="subject" title="表题" data-index="subject" min-width="250">
          <template #default="{ row }">
            <span style="color: #3081fe; font-weight: 500">{{ row.subject }}</span> -
            <span
              class="title-dot"
              :class="row.readFlag ? 'title-dot-color' : ''"
              style="font-weight: 500"
              >{{ row.subject }}</span
            >
            -
            <span style="color: #999">{{ row.subject }}</span>
          </template>
        </vxe-column>
@@ -104,120 +121,24 @@
    DownOutlined,
  } from '@ant-design/icons-vue';
  import { ref, watch, defineProps, defineEmits, computed, defineExpose } from 'vue';
  import { ref, watch, defineProps, defineEmits, computed, defineExpose, inject } from 'vue';
  // 定义属性
  interface Props {
    page: number;
    pageList?: [];
    pageList: [];
  }
  // const props = defineProps<Props>();
  const props = defineProps<Props>();
  const dataSource = [
    {
      sender: '百旺金税云',
      email: 'gdbwjf.dzfp3@gdfapiao.com',
      subject: '您收到一张【佛山火炬创新创业园有限公司】开具的发票【发票号码: ...',
      date: '2024-09-08 20:19',
    },
    {
      sender: '百旺金税云',
      email: 'gdbwjf.dzfp3@gdfapiao.com',
      subject: '您收到一张【佛山火炬创新创业园有限公司】开具的发票【发票号码: ...',
      date: '2024-07-09 20:19',
    },
    {
      sender: '百旺金税云',
      email: 'gdbwjf.dzfp3@gdfapiao.com',
      subject: '您收到一张【佛山火炬创新创业园有限公司】开具的发票【发票号码: ...',
      date: '2024-09-06 20:19',
    },
    {
      sender: '百旺金税云',
      email: 'gdbwjf.dzfp3@gdfapiao.com',
      subject: '您收到一张【佛山火炬创新创业园有限公司】开具的发票【发票号码: ...',
      date: '2024-09-07 20:19',
    },
    {
      sender: '百旺金税云',
      email: 'gdbwjf.dzfp3@gdfapiao.com',
      subject: '您收到一张【佛山火炬创新创业园有限公司】开具的发票【发票号码: ...',
      date: '2024-09-07 20:19',
    },
    {
      sender: '百旺金税云',
      email: 'gdbwjf.dzfp3@gdfapiao.com',
      subject: '您收到一张【佛山火炬创新创业园有限公司】开具的发票【发票号码: ...',
      date: '2024-09-05 20:19',
    },
    {
      sender: '百旺金税云',
      email: 'gdbwjf.dzfp3@gdfapiao.com',
      subject: '您收到一张【佛山火炬创新创业园有限公司】开具的发票【发票号码: ...',
      date: '2024-09-05 20:19',
    },
    {
      sender: '微信支付小助手',
      email: 'weixinpay@tencent.com',
      subject: '自动提现失败通知 - 微信商户平台',
      date: '2024-09-05 11:46',
    },
    {
      sender: '网易外贸通',
      email: 'mianfei@qiye.163.com',
      subject: '开发外贸客户很难?这些方法开发精准客户效率直接提升3倍',
      date: '2024-09-04 10:13',
    },
    {
      sender: 'Apple Developer',
      email: 'developer@insideapple.app',
      subject: '开发者你好:2024年9月准备好迎接 Apple Intelligence...',
      date: '2024-09-04 01:13',
    },
    {
      sender: '微信支付小助手',
      email: 'weixinpay@tencent.com',
      subject: '自动提现失败通知 - 微信商户平台',
      date: '2024-09-03 11:45',
    },
    {
      sender: 'notice',
      email: 'notice@qiye.163.com',
      subject: '【反垃圾通知】您的邮箱共收到1封异常邮件',
      date: '2024-09-03 10:02',
    },
    {
      sender: '三星电子',
      email: 'memberclub.club@samsung.com',
      subject: '【三星电子】秋日畅享,以旧换“星”,等你来!',
      date: '2024-09-02 18:15',
    },
    {
      sender: '极光',
      email: 'support@amail.jpush.io',
      subject: '极光统计运营周报 - 运营周报 ERP 2024.08.26-2024.09.01...',
      date: '2024-09-02 12:00',
    },
    {
      sender: '微信支付小助手',
      email: 'weixinpay@tencent.com',
      subject: '自动提现失败通知 - 微信商户平台',
      date: '2024-09-02 12:00',
    },
    {
      sender: 'Salesforce Dreamforce',
      email: 'apacemarketing@salesforce.com',
      subject: 'Session lineup is live. Explore now on Salesforce+',
      date: '2024-09-02 11:01',
    },
    {
      sender: '网易外贸通',
      email: 'mianfei@qiye.163.com',
      subject: '更懂外贸的AI管家来了,帮您节省30%的人工成本',
      date: '2024-09-02 09:55',
    },
  ];
  const groupedEmails = ref<GroupedDataItem[]>([]);
  const dataSource = ref([]);
  watch(
    () => props.pageList,
    (newValue) => {
      dataSource.value = newValue;
      groupedEmails.value = groupEmailsByDate(newValue);
    },
  );
  import dayjs from 'dayjs';
  import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
  import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
@@ -313,8 +234,6 @@
    return result;
  }
  const groupedEmails: GroupedDataItem[] = groupEmailsByDate(dataSource);
  console.log(groupedEmails);
  // 右键菜单
  const tableMenu = {
@@ -370,15 +289,16 @@
    vxeTableRef.value.forEach((row) => {
      row.setAllCheckboxRow(is);
    });
    selectChangeEvent()
  }
function selectChangeEvent() {
  const isAll = getCheckboxRecords().length === dataSource.length;
  const data = {
    isAll,
    records: getCheckboxRecords(),
  }
    emit('updateSelectAll',data,)
  function selectChangeEvent() {
    const isAll = getCheckboxRecords().length === dataSource.value.length;
    const data = {
      isAll,
      records: getCheckboxRecords(),
    };
    emit('updateSelectAll', data);
  }
  function getCheckboxRecords() {
    const list = new Set();
@@ -409,7 +329,32 @@
    // collapseStore.toggle();
  };
  const emit = defineEmits(['selectAll',"updateSelectAll"]);
  // 更新祖父组件数据
  const getDataList = inject('getDataList');
  console.log(getDataList, '0000004');
  // function sendUpdate() {
  //   getDataList({ a: '33' });
  // }
  import { updateReadApi } from '@/api/email/userList';
  // 标志未读/经读
  function fnRowUpdateRead(row) {
    const data = {
      status: !row.readFlag,
      list: [row.docCode],
    };
    pushReadApi(data);
  }
  function pushReadApi(params) {
    updateReadApi(params).then((res) => {
      if (res.code == 0) {
        //
        getDataList({ a: '33' });
      }
    });
  }
  const emit = defineEmits(['selectAll', 'updateSelectAll']);
  defineExpose({
    fnSelectAll,
  });
@@ -471,4 +416,26 @@
  .my-menus {
    background-color: #f8f8f9;
  }
  // 圆点
  .dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 10px;
    border-radius: 50%;
    background-color: #0a6aff;
  }
  .dot-color {
    background-color: #d9d9d9;
    color: #d9d9d9;
  }
  .title-dot {
    color: #0a6aff;
  }
  .title-dot-color {
    color: #999;
  }
</style>
src/views/email/components/SelectUser.vue
File was deleted
src/views/email/components/SelectUser/index.vue
New file
@@ -0,0 +1,342 @@
<!-- Props:
modelValue: 控制抽屉的打开与关闭。
title: 抽屉标题,默认为Basic Drawer。
placement: 抽屉位置,默认为right。
Emits:
update:modelValue: 当内部状态改变时,更新外部绑定的modelValue。
计算属性:
title 和 placement 用于获取属性值或默认值。
内部状态:
drawerOpen 用于控制抽屉的状态。
方法:
showDrawer 用于显示抽屉。 -->
<template>
  <a-drawer
    v-model:open="drawerOpen"
    :title="title"
    :placement="placement"
    :width="1200"
    :body-style="{ paddingBottom: '80px' }"
    :footer-style="{ textAlign: 'right' }"
    @after-open-change="afterOpenChange"
    @close="afterOpenChange"
  >
    <a-tabs v-model:activeKey="activeKey">
      <a-tab-pane :key="item.key" :tab="item.title" v-for="item in tabsList">
        <a-form :model="form" :rules="rules" layout="vertical">
          <a-row :gutter="16">
            <a-col :span="12">
              <a-form-item label="关键字">
                <a-input v-model:value="form.keyword" placeholder="编号/联系人名称" />
              </a-form-item>
            </a-col>
          </a-row>
        </a-form>
        <div class="table-content">
          <div class="left">
            <a-card
              :bordered="false"
              :title="`联系人(${selectContactNum})`"
              style="height: 500px"
            >
              <template #extra>
                <a-checkbox v-model:checked="selectAllCurrentPage" @change="fnSelectAllCurrentPage"
                  >全选当前页</a-checkbox
                >
                <a-checkbox v-model:checked="selectEmail" @change="fnSelectEmail"
                  >选择前30个邮箱</a-checkbox
                ></template
              >
              <a-table
                :showHeader="false"
                :data-source="dataSource"
                :row-selection="{
                  selectedRowKeys: state.selectedRowKeys,
                  onChange: onSelectChange,
                }"
                :scroll="{ y: 300 }"
                size="small"
                rowKey="id"
                >
                <!-- :pagination="{ pageSize: 100 }" -->
                <a-table-column key="userName" title="name" data-index="name">
                  <template #default="{ record }">
                    <span>{{ record.userName }}</span
                    ><span>{{ `<${record.email}>` }}</span>
                    <span style="margin-left: 5px">{{ record.ccName }}</span>
                  </template>
                </a-table-column>
              </a-table>
            </a-card>
          </div>
          <div class="right">
            <a-card
              :bordered="false"
              :title="`已选联系人(${contactNum}/100)`"
              style="height: 500px"
            >
              <template #extra><a @click="fnClearSelect">清空选项</a></template>
              <a-table
                :showHeader="false"
                :data-source="tempDataSource"
                :pagination="false"
                :scroll="{ y: 355 }"
                size="small"
              >
                <a-table-column key="userName" title="name" data-index="name">
                  <template #default="{ record }">
                    <span>{{ record.userName }}</span
                    ><span>{{ `<${record.email}>` }}</span>
                    <!-- <span style="margin-left: 5px">{{ record.ccName }}</span> -->
                  </template>
                </a-table-column>
              </a-table>
            </a-card>
          </div>
        </div>
      </a-tab-pane>
    </a-tabs>
    <template #extra>
      <a-space>
        <a-button @click="afterOpenChange">取消</a-button>
        <a-button type="primary" @click="fnSaveOpenChange">保存</a-button>
      </a-space>
    </template>
  </a-drawer>
</template>
<script lang="ts" setup>
  import { ref, watch, defineProps, defineEmits, computed, reactive } from 'vue';
  import { getUserInfoApi } from '@/api/email/userList';
  // 定义组件名称
  defineOptions({ name: 'SelectUser' });
  // 定义属性
  interface Props {
    modelValue: boolean;
    title?: string;
    placement?: 'left' | 'right' | 'top' | 'bottom';
    idName?: string;
    selectIds?: number[];
  }
  const props = defineProps<Props>();
  const activeKey = ref('3');
  const tabsList = ref([
    {
      key: '1',
      title: '客户联系人',
    },
    {
      key: '2',
      title: '线索联系人',
    },
    {
      key: '3',
      title: '商机联系人',
    },
    {
      key: '4',
      title: '企业同事',
    },
    {
      key: '5',
      title: '个人通讯录',
    },
    {
      key: '6',
      title: '企业通讯录',
    },
  ]);
  // 定义事件
  const emit = defineEmits(['update:modelValue', 'updateData']);
  // 内部状态
  const drawerOpen = ref(props.modelValue);
  // 监听属性变化
  watch(
    () => props.modelValue,
    (newValue) => {
      drawerOpen.value = newValue;
      if (props.selectIds && props.selectIds.length > 0) {
        state.selectedRowKeys = props.selectIds;
        updateTempDataSource(props.selectIds);
      } else {
        state.selectedRowKeys = [];
        updateTempDataSource([]);
      }
    },
  );
  // 更新外部属性
  watch(drawerOpen, (newValue) => {
    emit('update:modelValue', newValue);
  });
  // 方法
  const afterOpenChange = (bool: boolean) => {
    if (bool) {
      fnGetUserList({});
    }
  };
  interface User {
    ccCode: string;
    ccName: string;
    userCode: string;
    userName: string;
    email: string;
  }
  const fnGetUserList = (params) =>
    getUserInfoApi(params).then((res) => {
      if (res && res.data && Array.isArray(res.data)) {
        dataSource.value = flattenAndDeduplicateData(res.data) || ([] as User[]);
        selectContactNum.value = dataSource.value.length;
      } else {
        console.error('Invalid response format:', res);
      }
    });
  function flattenAndDeduplicateData(data) {
    const result: Record<string, any>[] = [];
    const userCodeSet = new Set(); // 用于记录已经出现过的 userCode
    data.forEach((department) => {
      const { ccCode, ccName, subList } = department;
      if (subList && subList.length > 0) {
        subList.forEach((user) => {
          // 如果 userCode 不在 Set 中,才添加到结果中
          if (!userCodeSet.has(user.userCode)) {
            result.push({
              ccCode: user.ccCode || ccCode, // 使用子项或父项的 ccCode
              ccName: user.ccName || ccName, // 使用子项或父项的 ccName
              userCode: user.userCode,
              userName: user.userName,
              id: user.userCode,
              email: user.email || '', // 确保 email 不为 null
            });
            // 将 userCode 加入 Set,避免重复
            userCodeSet.add(user.userCode);
          }
        });
      }
    });
    return result;
  }
  // 计算属性
  const title = computed(() => props.title || 'Basic Drawer');
  const placement = computed(() => props.placement || 'right');
  const idName = computed(() => props.idName || 'recipients');
  // // 显示抽屉的方法
  // const showDrawer = () => {
  //   drawerOpen.value = true;
  // };
  const form = ref({
    keyword: '',
  });
  const rules = ref({
    keyword: [{ required: true, message: '关键字不能为空', trigger: 'blur' }],
  });
  const selectAllCurrentPage = ref(false);
  const selectEmail = ref(false);
  const fnSelectAllCurrentPage = (e) => {
    const temp = dataSource.value;
    if (selectAllCurrentPage.value) {
      state.selectedRowKeys = temp.map((item) => item.id);
    } else {
      state.selectedRowKeys = state.selectedRowKeys.filter(
        (item) => !temp.some((t) => t.id === item),
      );
    }
    updateTempDataSource(state.selectedRowKeys);
  };
  const fnSelectEmail = (e) => {
    const temp = dataSource.value.slice(0, 30);
    if (selectEmail.value) {
      state.selectedRowKeys = temp.map((item) => item.id);
    } else {
      state.selectedRowKeys = state.selectedRowKeys.filter(
        (item) => !temp.some((t) => t.id === item),
      );
    }
    updateTempDataSource(state.selectedRowKeys);
  };
  const updateTempDataSource = (selectedRowKeys: Key[]) => {
    tempDataSource.value = dataSource.value.filter((item) => selectedRowKeys.includes(item.id));
    contactNum.value = selectedRowKeys.length;
  };
  const contactNum = ref(0);
  const selectContactNum = ref(0);
  const dataSource = ref<Record<string, any>>([]);
  type Key = string | number;
  const state = reactive<{
    selectedRowKeys: Key[];
    loading: boolean;
  }>({
    selectedRowKeys: [], // Check here to configure the default column
    loading: false,
  });
  const tempDataSource = ref([]);
  const fnClearSelect = () => {
    state.selectedRowKeys = [];
    tempDataSource.value = [];
    contactNum.value = 0;
  };
  const onSelectChange = (selectedRowKeys: Key[]) => {
    console.log('selectedRowKeys changed: ', selectedRowKeys);
    state.selectedRowKeys = selectedRowKeys;
    tempDataSource.value = dataSource.value.filter((item) => selectedRowKeys.includes(item.id));
    contactNum.value = selectedRowKeys.length;
  };
  const fnSaveOpenChange = () => {
    console.log('selectedRowKeys changed: ', state.selectedRowKeys);
    drawerOpen.value = false;
    emit('update:modelValue', false);
    const data = {
      selectedRowKeys: state.selectedRowKeys,
      idName: idName.value,
    };
    emit('updateData', data);
  };
</script>
<style scoped lang="less">
  /* 样式可以在这里定义 */
  .table-content {
    display: flex;
    justify-content: space-between;
    border-top: 1px solid #f0f0f0;
    .left {
      width: 50%;
      height: 100%;
      border-right: 1px solid #f0f0f0;
    }
    .right {
      width: 50%;
      height: 100%;
    }
  }
  ::v-deep(.ant-table) {
    min-height: 355px !important;
  }
</style>
src/views/email/components/pageHeadLeft.vue
File was deleted
types/axios.d.ts
@@ -43,6 +43,8 @@
  result: T;
  msg: string;
  data: T;
  total: number;
  state: number;
}
// multipart/form-data: upload file