| | |
| | | "echarts": "^5.5.1", |
| | | "exceljs": "^4.4.0", |
| | | "html2canvas": "^1.4.1", |
| | | "jsencrypt": "^3.3.2", |
| | | "lodash-es": "^4.17.21", |
| | | "mockjs": "^1.1.0", |
| | | "nprogress": "^0.2.0", |
| | |
| | | html2canvas: |
| | | specifier: ^1.4.1 |
| | | version: 1.4.1 |
| | | jsencrypt: |
| | | specifier: ^3.3.2 |
| | | version: 3.3.2 |
| | | lodash-es: |
| | | specifier: ^4.17.21 |
| | | version: 4.17.21 |
| | |
| | | peerDependenciesMeta: |
| | | canvas: |
| | | optional: true |
| | | |
| | | jsencrypt@3.3.2: |
| | | resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==} |
| | | |
| | | jsesc@2.5.2: |
| | | resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} |
| | |
| | | - supports-color |
| | | - utf-8-validate |
| | | |
| | | jsencrypt@3.3.2: {} |
| | | |
| | | jsesc@2.5.2: {} |
| | | |
| | | json-buffer@3.0.1: {} |
| | |
| | | // 先检查proxy是否存在,再进行操作 |
| | | if (proxy && proxy.$cookies) { |
| | | proxy.$cookies.remove('JSESSIONID'); |
| | | proxy.$cookies.set('JSESSIONID', '741E9E2AAD7578915B16287A5ECAE1DF.jvm_59_9010', '1d'); |
| | | proxy.$cookies.set('JSESSIONID', '071339196A579D1A2D374739F29D7521.jvm_59_9010', '1d'); |
| | | } else { |
| | | console.error('proxy对象未初始化或不包含$cookies属性'); |
| | | } |
| | |
| | | DELETE_BLACKLIST = '/crm/mail/blacklist/deleteBlackList.do', |
| | | GET_BLACKLIST = '/crm/mail/blacklist/getBlackList.do', |
| | | GET_EMAIL_MODULE_BELOW = '/crm/mail/getEmailModuleBelow.do', |
| | | ADD_LIAS_EMAIL = '/crm/mail/account/addAliasEmail.do', |
| | | } |
| | | // 获取邮件路由列表 |
| | | export const getEmailModuleApi = () => defHttp.get({ url: Api.GET_EMAIL_MODULE }); |
| | |
| | | // 修改邮箱配置 |
| | | export const updateAccountApi = (params) => defHttp.post<{}>({ url: Api.UPDATE_ACCOUNT, params }); |
| | | // 删除邮箱配置 |
| | | export const deleteAccountApi = (params) => defHttp.post<{}>({ url: Api.DELETE_ACCOUNT, params }); |
| | | export const deleteAccountApi = (query) => |
| | | defHttp.post<{}>({ url: Api.DELETE_ACCOUNT + '?accountId=' + query }); |
| | | |
| | | // 获取邮箱列表 |
| | | export const getAccountListApi = () => defHttp.get<{}>({ url: Api.GET_ACCOUNT_LIST }); |
| | |
| | | }); |
| | | // 设置完成时间 |
| | | export const updateHandleAPi = (params) => |
| | | defHttp.get({ |
| | | defHttp.post({ |
| | | url: Api.UPDATE_HANDLE, |
| | | params, |
| | | }); |
| | |
| | | params, |
| | | }); |
| | | |
| | | |
| | | // 新增黑名单 |
| | | export const addBlackListApi = (params) => |
| | | defHttp.post({ |
| | |
| | | params, |
| | | }); |
| | | |
| | | // 文件夹结构 |
| | | export const getEmailModuleBelowApi = () => |
| | | defHttp.get({ |
| | | url: Api.GET_EMAIL_MODULE_BELOW, |
| | | }); |
| | | |
| | | export const getEmailModuleBelowApi = () => |
| | | defHttp.get({ |
| | | url: Api.GET_EMAIL_MODULE_BELOW, |
| | | }); |
| | | |
| | | //邮箱别名 |
| | | export const addLiasEmailApi = (params) => |
| | | defHttp.get({ |
| | | url: Api.ADD_LIAS_EMAIL, |
| | | params, |
| | | }); |
New file |
| | |
| | | import { ref } from 'vue'; |
| | | import JSEncrypt from 'jsencrypt'; // 导入 JSEncrypt 库 |
| | | |
| | | export const useUploadHook = (baseUrl: string) => { |
| | | const computedUploadUrl = ref(baseUrl); |
| | | |
| | | // RSA 加密用的公钥 |
| | | const publicKey = `-----BEGIN PUBLIC KEY----- |
| | | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkRupiYcKVGGUtDBDoR1t/1zm3ZtZgnte39iTJW6hlqjdY0UagKjpNiIv7J6XjtgfX7SgsR4AWnivqQHAICIvdPKfGZZzIs62OQ19MqrDTMoB/LvK5teNWhClv23WMUfRbP+EHgprT6hTw8U5apw1IB6i/y57NkLav792wiYBYRU4X45NoTaT+aiTSLFEflbfm94EXnhSS3vFkBmrZGy5BRNI8gmzafroslGx2Hk90CqlNdeKYxgZQ6xtvj+u33yrszWvPT6F9fsJT8aMjtvH050iYKRVct+x6Q7VRJgCI4MgvAexnTKdxW54YzvXCuO5bDiy5la7CgerWkTAq9dzXwIDAQAB |
| | | -----END PUBLIC KEY-----`; |
| | | |
| | | // 固定的域名和ID(将来可从 Vuex 中获取) |
| | | const domain = 'https://img.onbus.cn'; |
| | | const dbid = 82; // 这里可以从 Vuex 获取 |
| | | const formid = 0; // 这里也可以从 Vuex 获取 |
| | | const uuid = ref(undefined); // 这里也可以从 Vuex 获取 |
| | | |
| | | // 使用 RSA 加密数据 |
| | | const encryptData = (data: Record<string, any>) => { |
| | | const encryptor = new JSEncrypt(); |
| | | encryptor.setPublicKey(publicKey); |
| | | |
| | | const encryptedData: Record<string, string> = {}; |
| | | for (const key in data) { |
| | | if (data.hasOwnProperty(key)) { |
| | | const encryptedValue = encryptor.encrypt(data[key]); |
| | | if (encryptedValue) { |
| | | encryptedData[key] = encodeURIComponent(encryptedValue); // URL 编码 |
| | | } |
| | | } |
| | | } |
| | | return encryptedData; |
| | | }; |
| | | |
| | | // 获取上传数据并加密 |
| | | const getUploadData = (uuid) => { |
| | | const data = { |
| | | dbid: dbid.toString(), |
| | | username: 'huang', |
| | | usercode: 'z9000137', |
| | | }; |
| | | |
| | | const newData = encryptData(data); |
| | | return { ...newData, type: 3, fieldid: '333', uuid: uuid }; |
| | | }; |
| | | |
| | | // 生成文件 URL |
| | | const generateFileUrl = (response: any) => { |
| | | const unid = response.uuid.split(';')[0] + '@P@' + response.uuid.split(';')[1]; |
| | | const fileExt = response.fileType; |
| | | |
| | | return `${domain}/uploads/attachment/${dbid}/${formid}/${unid}.${fileExt}`; |
| | | }; |
| | | |
| | | return { computedUploadUrl, getUploadData, generateFileUrl }; |
| | | }; |
New file |
| | |
| | | <template> |
| | | <div> |
| | | <Upload |
| | | v-if="type == 'png'" |
| | | name="file" |
| | | multiple |
| | | @change="handleUploadChange" |
| | | :action="computedUploadUrl" |
| | | :showUploadList="false" |
| | | :accept="accept" |
| | | :beforeUpload="beforeUpload" |
| | | > |
| | | <a-button type="text" size="small" v-bind="{ ...getButtonProps }"> |
| | | <PictureOutlined /> {{ title }} |
| | | </a-button> |
| | | </Upload> |
| | | |
| | | <a-upload |
| | | v-if="type == 'file'" |
| | | v-model:file-list="fileListTemp" |
| | | :action="computedUploadUrl" |
| | | list-type="picture" |
| | | class="upload-list-inline" |
| | | :before-upload="beforeUpload" |
| | | @change="handleFileUploadChange" |
| | | > |
| | | <a-button type="text" size="small" style="margin: 0 auto"> |
| | | <UploadOutlined /> |
| | | 附件 |
| | | </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="fnPreview(file)">预览</a> |
| | | <a href="javascript:;" @click="fnRename(file)">重命名</a> |
| | | <a href="javascript:;" @click="actions.remove">删除</a> |
| | | </span> |
| | | <span v-else> |
| | | <a href="javascript:;" @click="fnSaveRename(file)">保存</a> |
| | | <a href="javascript:;" @click="fnOffRename(file)">取消</a> |
| | | </span> |
| | | </a-space> |
| | | </template> |
| | | </a-upload> |
| | | <div id="previewContainer"></div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { computed, ref, watch } from 'vue'; |
| | | import { PictureOutlined, UploadOutlined, PaperClipOutlined } from '@ant-design/icons-vue'; |
| | | import { Upload } from 'ant-design-vue'; |
| | | import { useGlobSetting } from '@/hooks/setting'; |
| | | import { useUploadHook } from './hooks/useUploadHook'; |
| | | |
| | | defineOptions({ name: 'TinymceImageUpload' }); |
| | | |
| | | const props = defineProps({ |
| | | fullscreen: { |
| | | type: Boolean, |
| | | }, |
| | | disabled: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | accept: { |
| | | type: String, |
| | | default: '.jpg,.jpeg,.gif,.png,.webp', |
| | | }, |
| | | title: { |
| | | type: String, |
| | | default: '图片', |
| | | }, |
| | | type: { |
| | | type: String, |
| | | default: 'png', |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(['uploading', 'done', 'error', 'fileListChange']); |
| | | let uploading = false; |
| | | |
| | | const { uploadUrl: baseUrl } = useGlobSetting(); |
| | | const { computedUploadUrl, getUploadData, generateFileUrl } = useUploadHook(baseUrl); |
| | | |
| | | const fileListTemp = ref([]); // 用于附件的文件列表 |
| | | |
| | | const getButtonProps = computed(() => ({ |
| | | disabled: props.disabled, |
| | | })); |
| | | const uuid = ref(''); |
| | | |
| | | // beforeUpload 拦截器,用于动态修改 URL |
| | | const beforeUpload = () => { |
| | | const encryptedParams = getUploadData(uuid.value); |
| | | const queryString = new URLSearchParams(encryptedParams).toString(); |
| | | computedUploadUrl.value = `${baseUrl}?${queryString}`; |
| | | }; |
| | | // 处理上传变化 |
| | | const handleUploadChange = (info: Record<string, any>) => { |
| | | const file = info.file; |
| | | const status = file?.status; |
| | | |
| | | if (status === 'uploading') { |
| | | if (!uploading) { |
| | | emit('uploading', file.name); |
| | | uploading = true; |
| | | } |
| | | } else if (status === 'done') { |
| | | const newUrl = generateFileUrl(file.response); // 生成文件 URL |
| | | debugger; |
| | | if (!uuid.value) { |
| | | uuid.value = file.response.uuid.split(';')[0]; |
| | | } |
| | | emit('done', file.name, newUrl); |
| | | uploading = false; |
| | | } else if (status === 'error') { |
| | | emit('error'); |
| | | uploading = false; |
| | | } |
| | | }; |
| | | function handleFileUploadChange(info: Record<string, any>) { |
| | | const file = info.file; |
| | | const status = file?.status; |
| | | if (status === 'done') { |
| | | if (!uuid.value) { |
| | | uuid.value = file.response.uuid.split(';')[0]; |
| | | } |
| | | uploading = false; |
| | | } |
| | | } |
| | | // 重命名功能 |
| | | const fnRename = (file) => { |
| | | file.editor = true; |
| | | }; |
| | | |
| | | // 保存重命名 |
| | | const fnSaveRename = (file) => { |
| | | file.editor = false; |
| | | file.name = file.tempName; |
| | | }; |
| | | |
| | | // 取消重命名 |
| | | const fnOffRename = (file) => { |
| | | file.editor = false; |
| | | }; |
| | | |
| | | // 预览 |
| | | const fnPreview = (file) => { |
| | | if (!file || !file.response) { |
| | | console.error('Invalid file or response'); |
| | | return; |
| | | } |
| | | |
| | | // 生成安全的文件 URL |
| | | const safeUrl = generateFileUrl(file.response); |
| | | |
| | | // 获取文件类型(通过文件扩展名或 MIME 类型) |
| | | const fileExt = file.response.fileType || file.name.split('.').pop().toLowerCase(); // 获取文件扩展名 |
| | | const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExt); // 判断是否为图片 |
| | | |
| | | if (isImage) { |
| | | // 直接打开图片 |
| | | window.open(safeUrl, '_blank'); |
| | | } else { |
| | | // 非图片类型使用 Office Online Viewer 或其他工具预览 |
| | | const iframeSrc = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(safeUrl)}`; |
| | | window.open(iframeSrc, '_blank'); |
| | | } |
| | | }; |
| | | |
| | | watch( |
| | | () => fileListTemp.value, |
| | | (newValue) => { |
| | | emit('fileListChange', fileListTemp.value); |
| | | }, |
| | | ); |
| | | </script> |
| | | |
| | | <style lang="less" scoped> |
| | | @prefix-cls: ~'@{namespace}-tinymce-img-upload'; |
| | | |
| | | .@{prefix-cls} { |
| | | z-index: 20; |
| | | top: 4px; |
| | | right: 10px; |
| | | |
| | | &.fullscreen { |
| | | position: fixed; |
| | | z-index: 10000; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | ></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' : ''"> |
| | | <ImgUpload |
| | | ref="fileRef" |
| | | :fullscreen="fullscreen" |
| | | :fileListTemp="fileListTemp" |
| | | @uploading="handleImageUploading" |
| | | @done="handleDone" |
| | | @fileListChange="fileListChange" |
| | | v-if="isImg" |
| | | v-show="editorRef" |
| | | :title="'附件'" |
| | | :type="'file'" |
| | | :disabled="disabled" |
| | | /> |
| | | <div class="my-upload-list"> |
| | | <div style="display: flex"> |
| | | <ImgUpload |
| | | :fullscreen="fullscreen" |
| | |
| | | v-if="isImg" |
| | | v-show="editorRef" |
| | | :title="'图片'" |
| | | :type="'png'" |
| | | :disabled="disabled" |
| | | :accept="'.jpg,.jpeg,.gif,.png,.webp'" |
| | | /> |
| | |
| | | <script lang="ts" setup> |
| | | import type { Editor, RawEditorSettings } from 'tinymce'; |
| | | import { PaperClipOutlined, UploadOutlined, SnippetsOutlined } from '@ant-design/icons-vue'; |
| | | import { useGlobSetting } from '@/hooks/setting'; |
| | | |
| | | import tinymce from 'tinymce/tinymce'; |
| | | import 'tinymce/themes/silver'; |
| | | import 'tinymce/icons/default/icons'; |
| | |
| | | PropType, |
| | | useAttrs, |
| | | } from 'vue'; |
| | | import ImgUpload from './ImgUpload.vue'; |
| | | import ImgUpload from '@/components/MyUpload/index.vue'; |
| | | import { |
| | | plugins as defaultPlugins, |
| | | toolbar as defaultToolbar, |
| | |
| | | import { isNumber } from '@/utils/is'; |
| | | import { useLocale } from '@/locales/useLocale'; |
| | | import { useAppStore } from '@/store/modules/app'; |
| | | const { uploadUrl } = useGlobSetting(); |
| | | // console.log(uploadUrl,'uploadUrl'); |
| | | |
| | | defineOptions({ name: 'Tinymce', inheritAttrs: false }); |
| | | |
| | |
| | | editor.setContent(val); |
| | | } |
| | | } |
| | | const fileRef = ref(); |
| | | |
| | | function bindModelHandlers(editor: any) { |
| | | const modelEvents = attrs.modelEvents ? attrs.modelEvents : null; |
| | |
| | | } |
| | | |
| | | // 附件 |
| | | 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; |
| | | }); |
| | | const fileListTemp = ref([]); |
| | | function fileListChange(data) { |
| | | fileListTemp.value = data; |
| | | } |
| | | </script> |
| | | <style lang="less" scope> |
| | |
| | | meta: { |
| | | title: '全部发件', |
| | | }, |
| | | }] |
| | | }, |
| | | ], |
| | | }, |
| | | // { |
| | | // path: 'MassMailbox', |
| | |
| | | // currentActiveMenu: '/email/index', |
| | | // }, |
| | | // }, |
| | | { |
| | | path: 'folder', |
| | | name: 'Folder', |
| | | component: () => import('@/views/email/folder/index.vue'), |
| | | meta: { |
| | | title: '文件夹', |
| | | hideTab: true, |
| | | currentActiveMenu: '/email/index', |
| | | }, |
| | | }, |
| | | { |
| | | path: 'label', |
| | | name: 'Label', |
| | | component: () => import('@/views/email/label/index.vue'), |
| | | meta: { |
| | | title: '标签', |
| | | hideTab: true, |
| | | currentActiveMenu: '/email/index', |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | |
| | | |
| | | // Use error-handler-plugin |
| | | // 是否使用全局错误捕获 |
| | | useErrorHandle: true, |
| | | useErrorHandle: false, |
| | | |
| | | // Whether to open back to top |
| | | // 是否开启回到顶部 |
| | |
| | | }; |
| | | |
| | | function fnChangeContent(e) { |
| | | modelRef.content = e.content; |
| | | modelRef.attachmentList = e.attachmentList; |
| | | if (!isValidEvent(e)) { |
| | | console.error('Invalid event:', e); |
| | | return; |
| | | } |
| | | |
| | | console.log('Event:', e, '----------------'); |
| | | |
| | | modelRef.content = e.content; |
| | | modelRef.attachmentList = mergeArrayWithPrefix(fnBuildAttachmentList(e.fileUNID)); |
| | | |
| | | console.log('Updated modelRef:', modelRef); |
| | | } |
| | | |
| | | function isValidEvent(e) { |
| | | return e && typeof e === 'object' && typeof e.content === 'string'; |
| | | } |
| | | |
| | | function fnBuildAttachmentList(data) { |
| | | if (!Array.isArray(data)) { |
| | | console.error('Invalid argument: data must be an array'); |
| | | return []; |
| | | } |
| | | |
| | | return data.reduce((acc, item) => { |
| | | const uuid = item?.response?.uuid; |
| | | if (uuid) { |
| | | acc.push(uuid); |
| | | } else { |
| | | console.warn('Invalid item:', item); |
| | | } |
| | | return acc; |
| | | }, []); |
| | | } |
| | | |
| | | function mergeArrayWithPrefix(arr) { |
| | | if (!arr || arr.length === 0) return ''; |
| | | const [prefix] = arr[0].split(';'); |
| | | const suffixes = arr.map(item => item.split(';')[1]).filter(Boolean).join(';'); |
| | | |
| | | return `${prefix};${suffixes}`; |
| | | } |
| | | |
| | | function fnBuildingCommitData() { |
| | | return { |
| | |
| | | bcc: modelRef.bccRecipients, |
| | | subject: modelRef.subject, |
| | | content: modelRef.content, |
| | | attachmentList: '', |
| | | attachmentList: modelRef.attachmentList, |
| | | docCode: docCode.value, |
| | | }; |
| | | } |
| | |
| | | loading.value = false; |
| | | if (res.code === 0) { |
| | | createMessage.success(res.msg); |
| | | router.push('/email/list'); |
| | | router.push('/email/index'); |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | |
| | | |
| | | function fnGetList() { |
| | | getFolderApi({}).then((res) => { |
| | | console.log(res); |
| | | demo.tableData = convertToTableData(res.data); |
| | | console.log(demo.tableData, '3333333333333'); |
| | | }); |
| | | } |
| | | function convertToTableData(data, parentId = null) { |
| | | let tableData = []; |
| | | try { |
| | | let tableData = []; |
| | | |
| | | data.forEach((item) => { |
| | | let tableItem = { |
| | | folderId: item.folderId, |
| | | parentRowId: parentId, |
| | | rowId: item.rowId, |
| | | folderName: item.folderName, |
| | | treeControl: item.treeControl, |
| | | }; |
| | | data.forEach((item) => { |
| | | let tableItem = { |
| | | folderId: item.folderId, |
| | | parentRowId: parentId, |
| | | rowId: item.rowId, |
| | | folderName: item.folderName, |
| | | treeControl: item.treeControl, |
| | | }; |
| | | |
| | | if (item.list && item.list.length > 0) { |
| | | let children = convertToTableData(item.list, item.rowId); |
| | | tableData = tableData.concat(children); |
| | | } |
| | | if (item.list && item.list.length > 0) { |
| | | let children = convertToTableData(item.list, item.rowId); |
| | | tableData = tableData.concat(children); |
| | | } |
| | | |
| | | tableData.push(tableItem); |
| | | }); |
| | | return tableData; |
| | | tableData.push(tableItem); |
| | | }); |
| | | return tableData; |
| | | |
| | | } catch (error) { return [];} |
| | | } |
| | | |
| | | const inputRefs = ref<{ [key: number]: HTMLElement | null }>({}); |
| | |
| | | const $table = xTable.value; |
| | | const rid = Date.now(); |
| | | const record = { |
| | | folderName: `新数据${rid}`, |
| | | folderName: `新文件夹`, |
| | | id: rid, |
| | | }; |
| | | $table.insert(record).then(({ row }) => $table.setEditRow(row)); |
| | |
| | | |
| | | const { createMessage } = useMessage(); |
| | | function fnInputHandle(row) { |
| | | console.log(row, '----333'); |
| | | if (row.folderName == '') { |
| | | editRowEvent(row) |
| | | editRowEvent(row); |
| | | fnGetList(); |
| | | return createMessage.error('请输入文件夹名称'); |
| | | } |
| | | const data = |
| | |
| | | fnGetList(); |
| | | } else { |
| | | createMessage.error(res.msg); |
| | | fnGetList(); |
| | | } |
| | | }); |
| | | } |
| | |
| | | const $table = xTable.value; |
| | | const rid = Date.now(); |
| | | const record = { |
| | | folderName: `新数据${rid}`, |
| | | folderName: `新子文件夹`, |
| | | id: rid, |
| | | parentRowId: row.rowId, // 需要指定父节点,自动插入该节点中 |
| | | }; |
| | | console.log(record, '99999993'); |
| | | const { row: newRow } = await $table.insert(record); |
| | | |
| | | await $table.setTreeExpand(row, true); // 将父节点展开 |
| | | await $table.setEditRow(newRow); // 插入子节点 |
| | | setTimeout(() => { |
| | | inputRefs.value[rid].focus(); |
| | | }, 300); |
| | | } |
| | | function fnDelete(row) { |
| | | deleteFolderApi({ folderId: row.folderId }) |
| | |
| | | |
| | | function editRowEvent(row) { |
| | | const $table = xTable.value; |
| | | console.log(row, '---30494'); |
| | | row.opType = 'edit'; |
| | | $table.setEditRow(row); |
| | | } |
| | |
| | | :filter-config="{ showIcon: false }" |
| | | :row-config="{ isHover: true }" |
| | | :column-config="{ resizable: true }" |
| | | :edit-config="{ trigger: 'click', mode: 'cell' }" |
| | | > |
| | | <vxe-column width="60"> |
| | | <template #default> |
| | |
| | | <span style="margin-left: 10px; color: #3081fe; font-weight: 500">{{ row.email }}</span> |
| | | </template> |
| | | </vxe-column> |
| | | <vxe-column show-overflow field="companyName" title="显示名称" min-width="250"> |
| | | <vxe-column |
| | | show-overflow |
| | | field="aliasEmail" |
| | | title="显示名称" |
| | | min-width="250" |
| | | :edit-render="{}" |
| | | > |
| | | <template #default="{ row }"> |
| | | <span style="color: #999">{{ row.companyName }}</span> |
| | | <span style="color: #999">{{ row.aliasEmail }}</span> |
| | | </template> |
| | | <template #edit="{ row }"> |
| | | <vxe-input |
| | | ref="inputRef" |
| | | v-model="row.aliasEmail" |
| | | type="text" |
| | | @blur="fnInputHandle(row)" |
| | | ></vxe-input> |
| | | </template> |
| | | </vxe-column> |
| | | <vxe-column show-overflow field="status" title="邮箱状态" min-width="250"> |
| | | <template #default="{ row }"> |
| | | <div v-if="!isCheckAll"> |
| | | <a-tag color="orange" v-if="row.status === '正常'">正常</a-tag> |
| | | <a-tag color="success" v-if="row.mailStatus === '正常'">正常</a-tag> |
| | | <a-tag color="red" v-else>异常</a-tag> |
| | | </div> |
| | | <div v-else> |
| | |
| | | </template> |
| | | <a-input :disabled="typeAccount === 2" v-model:value="formData.email" /> |
| | | </a-form-item> |
| | | <a-form-item v-if="isCustom == 'custom'" name="password" label="邮箱密码"> |
| | | <!-- v-if="isCustom == 'custom'" --> |
| | | <a-form-item name="password" label="邮箱密码"> |
| | | <a-input-password |
| | | type="password" |
| | | v-model:value="formData.password" |
| | |
| | | deleteAccountApi, |
| | | getAccountListApi, |
| | | isEmailValidApi, |
| | | addLiasEmailApi, |
| | | } from '@/api/email/userList'; |
| | | const loading = ref(false); |
| | | import Sortable from 'sortablejs'; |
| | |
| | | aliasEmail: '', |
| | | biSyncFlag: false, |
| | | proxyFlag: true, |
| | | receiveProtocol: 'imap', |
| | | receiveProtocol: 'imaps', |
| | | receiveSSL: false, |
| | | receivePort: '', |
| | | receiveHost: '', |
| | | smtpSSL: false, |
| | | smtpSSL: true, |
| | | smtpPort: '', |
| | | smtpHost: '', |
| | | invalid: '', |
| | |
| | | const open = ref(false); |
| | | const fnHandleOk = () => { |
| | | formRef.value.validate().then(() => { |
| | | const data = formData.value; |
| | | const data = !isShow |
| | | ? formData.value |
| | | : { |
| | | email: formData.value.email, |
| | | password: formData.value.password, |
| | | }; |
| | | if (isShow.value == true) { |
| | | data.mailType = isCustom.value === 'onCustom' ? 1 : 2; |
| | | } |
| | |
| | | } |
| | | loading.value = false; |
| | | fnMailList(); |
| | | open.value = false; |
| | | }) |
| | | .catch((err) => { |
| | | loading.value = false; |
| | | open.value = false; |
| | | }); |
| | | open.value = false; |
| | | }); |
| | | }; |
| | | |
| | |
| | | } |
| | | function fnHandleDetailOk() { |
| | | openDrawerDetail.value = false; |
| | | deleteAccountApi({ accountId: accountId.value }) |
| | | deleteAccountApi(accountId.value ) |
| | | .then((res) => { |
| | | if (res.code === 0) { |
| | | createMessage.success(res.msg); |
| | |
| | | isEmailValidApi(email).then((res) => { |
| | | if (res.code == 0) { |
| | | isCheck.value = true; |
| | | checkStatus.value = res.data.status; |
| | | checkStatus.value = res.data.code == 0 ? true : false; |
| | | } |
| | | }); |
| | | } |
| | |
| | | isCheckAll.value = false; |
| | | }, 3000); |
| | | } |
| | | |
| | | function fnInputHandle(row) { |
| | | addLiasEmailApi({ aliasEmail: row.aliasEmail, accountId: row.accountId }).then((res) => {}); |
| | | } |
| | | </script> |
| | | <style scoped lang="less"> |
| | | .bullet { |
New file |
| | |
| | | <template> |
| | | <a-form :model="form" ref="formRef"> |
| | | <a-form-item |
| | | label="日期" |
| | | name="date" |
| | | :rules="[{ required: true, message: '请选择日期' }]" |
| | | > |
| | | <a-date-picker |
| | | format="YYYY/MM/DD" |
| | | :disabledDate="disabledDate" |
| | | v-model:value="form.date" |
| | | /> |
| | | </a-form-item> |
| | | <a-form-item |
| | | label="时间" |
| | | name="time" |
| | | :rules="[{ required: true, message: '请选择时间' }]" |
| | | > |
| | | <a-time-picker v-model:value="form.time" /> |
| | | </a-form-item> |
| | | <a-form-item> |
| | | <a-button @click="$emit('cancel')">取消</a-button> |
| | | <a-button |
| | | style="margin-left: 10px" |
| | | type="primary" |
| | | @click="$emit('submit')" |
| | | >确定</a-button> |
| | | </a-form-item> |
| | | </a-form> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { defineProps, defineEmits, reactive } from 'vue'; |
| | | |
| | | const props = defineProps(); |
| | | const emit = defineEmits(); |
| | | const form = reactive({ |
| | | date: '', |
| | | time: '', |
| | | }); |
| | | |
| | | const disabledDate = (currentDate) => { |
| | | return currentDate && currentDate < dayjs().startOf('day'); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* 添加自定义样式 */ |
| | | </style> |
New file |
| | |
| | | <template> |
| | | <div :style="style" class="custom-dropdown"> |
| | | <!-- <template #overlay> --> |
| | | <a-menu> |
| | | <a-menu-item key="reply" @click="handleMenuAction('reply')">回复</a-menu-item> |
| | | <a-menu-item key="replyAll" @click="handleMenuAction('replyAll')">回复全部</a-menu-item> |
| | | <a-menu-item key="replyWithAttachment" @click="handleMenuAction('replyWithAttachment')"> |
| | | 带附件回复 |
| | | </a-menu-item> |
| | | <a-menu-item key="replyAllWithAttachment" @click="handleMenuAction('replyAllWithAttachment')"> |
| | | 带附件回复全部 |
| | | </a-menu-item> |
| | | <a-menu-item key="forward" @click="handleMenuAction('forward')">转发</a-menu-item> |
| | | <a-menu-item key="forwardAsAttachment" @click="handleMenuAction('forwardAsAttachment')"> |
| | | 作为附件转发 |
| | | </a-menu-item> |
| | | <a-menu-item key="reEdit" @click="handleMenuAction('reEdit')">分发</a-menu-item> |
| | | <a-menu-item key="addNote" @click="handleMenuAction('addNote')">设置备注</a-menu-item> |
| | | <a-divider style="margin: 5px" /> |
| | | |
| | | <!-- <a-menu-item key="toProcess" @click="handleMenuAction('toProcess')"> |
| | | <div class="my-display"> |
| | | <a-dropdown> |
| | | <span>待处理 <FieldTimeOutlined /></span> |
| | | <template #overlay> |
| | | <a-card title="选择稍后处理时间:" style="width: 250px" size="small"> |
| | | <div |
| | | class="date p-1" |
| | | v-for="item in dateList" |
| | | :key="item.key" |
| | | @click="fnSelectDate(item)" |
| | | > |
| | | <div class="date-left">{{ item.name }}</div> |
| | | <div class="date-right"> |
| | | <span v-if="item.key !== 'today'">{{ item.dayOfWeek }}</span> |
| | | <span style="margin-left: 5px">{{ item.time }}</span> |
| | | </div> |
| | | </div> |
| | | <a-divider style="margin: 5px 0" /> |
| | | <div class="date p-1"> |
| | | <a-popover |
| | | :trigger="trigger" |
| | | title="自定义时间" |
| | | v-model:open="customTimeDropdownOpen" |
| | | @confirm="onSubmitCustomTime" |
| | | > |
| | | <template #content> |
| | | <CustomTimePicker |
| | | :form="form" |
| | | @cancel="cancelCustomTime" |
| | | @submit="submitCustomTime" |
| | | /> |
| | | </template> |
| | | |
| | | <div class="date-left" @click="toggleCustomTime">自定义时间</div> |
| | | </a-popover> |
| | | </div> |
| | | </a-card> |
| | | </template> |
| | | </a-dropdown> |
| | | </div> |
| | | </a-menu-item> --> |
| | | <a-menu-item key="markAs" @click="handleMenuAction('markAs')">{{ |
| | | `标记${row.readFlag ? '未' : '已'}读` |
| | | }}</a-menu-item> |
| | | <a-menu-item key="markAs" @click="handleMenuAction('markAs')">标记为</a-menu-item> |
| | | <a-divider style="margin: 5px" /> |
| | | <a-menu-item key="createRule" @click="handleMenuAction('createRule')" |
| | | >新建收发件规则</a-menu-item |
| | | > |
| | | <a-menu-item key="moveTo" @click="handleMenuAction('moveTo')">移动到</a-menu-item> |
| | | <a-divider style="margin: 5px" /> |
| | | <a-menu-item key="exportEmail" @click="handleMenuAction('exportEmail')">导出邮件</a-menu-item> |
| | | <a-menu-item key="trackCustomer" @click="handleMenuAction('trackCustomer')" |
| | | >建为客户跟进</a-menu-item |
| | | > |
| | | <a-menu-item key="createSchedule" @click="handleMenuAction('createSchedule')" |
| | | >新建日程</a-menu-item |
| | | > |
| | | <a-divider style="margin: 5px" /> |
| | | |
| | | <a-menu-item key="delete" @click="handleMenuAction('delete')">标为垃圾邮件</a-menu-item> |
| | | <a-menu-item key="delete" @click="handleMenuAction('delete')">删除</a-menu-item> |
| | | </a-menu> |
| | | <!-- </template> --> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { defineProps, defineEmits, computed, ref, reactive, inject } from 'vue'; |
| | | import CustomTimePicker from './CustomTimePicker.vue'; |
| | | const props = defineProps({ |
| | | style: Object, |
| | | selectedCell: Object, |
| | | }); |
| | | |
| | | const row = computed(() => props.selectedCell.row); |
| | | const emit = defineEmits(['close-menu']); |
| | | import { useRouter } from 'vue-router'; |
| | | const router = useRouter(); |
| | | import { updateReadApi, updateHandleAPi, deleteEmailAPi } from '@/api/email/userList'; |
| | | const getDataList = inject('getDataList'); |
| | | const handleMenuAction = (action) => { |
| | | const res = props.selectedCell.row; |
| | | // 根据不同的action执行对应的操作 |
| | | switch (action) { |
| | | case 'reply': |
| | | router.push({ path: '/email/edit', query: { docCode: res.docCode, type: 'reply' } }); |
| | | break; |
| | | case 'replyAll': |
| | | router.push({ path: '/email/edit', query: { docCode: res.docCode, type: 'reply' } }); |
| | | break; |
| | | case 'replyWithAttachment': |
| | | router.push({ path: '/email/edit', query: { docCode: res.docCode, type: 'reply' } }); |
| | | break; |
| | | case 'replyAllWithAttachment': |
| | | router.push({ path: '/email/edit', query: { docCode: res.docCode, type: 'reply' } }); |
| | | break; |
| | | case 'forward': |
| | | console.log('Forward action triggered'); |
| | | break; |
| | | case 'addNote': |
| | | console.log('Add Note action triggered'); |
| | | break; |
| | | case 'forwardAsAttachment': |
| | | console.log('Forward as Attachment action triggered'); |
| | | break; |
| | | case 'reEdit': |
| | | console.log('Re-edit action triggered'); |
| | | break; |
| | | case 'toProcess': |
| | | console.log('Add to Process action triggered'); |
| | | break; |
| | | case 'markAs': |
| | | const data = { |
| | | status: !res.readFlag, |
| | | list: [res.docCode], |
| | | }; |
| | | updateReadApi(data).then((res) => { |
| | | if (res.code == 0) { |
| | | getDataList({}); |
| | | } |
| | | }); |
| | | console.log('Mark as action triggered'); |
| | | break; |
| | | case 'createRule': |
| | | console.log('Create rule action triggered'); |
| | | break; |
| | | case 'moveTo': |
| | | console.log('Move to action triggered'); |
| | | break; |
| | | case 'exportEmail': |
| | | console.log('Export Email action triggered'); |
| | | break; |
| | | case 'trackCustomer': |
| | | console.log('Track Customer action triggered'); |
| | | break; |
| | | case 'createSchedule': |
| | | console.log('Create Schedule action triggered'); |
| | | break; |
| | | case 'delete': |
| | | deleteEmailAPi([res.docCode]).then((res) => { |
| | | if (res.code == 0) { |
| | | getDataList({}); |
| | | } |
| | | }); |
| | | break; |
| | | default: |
| | | console.warn('Unknown action:', action); |
| | | } |
| | | |
| | | // 操作完成后关闭菜单 |
| | | emit('close-menu'); |
| | | }; |
| | | |
| | | // 待处理 |
| | | import { FieldTimeOutlined, PushpinOutlined } from '@ant-design/icons-vue'; |
| | | function processDateList() { |
| | | const dateList = [ |
| | | { name: '今天稍晚', key: 'today' }, |
| | | { name: '明天', key: 'tomorrow' }, |
| | | { name: '本周稍晚', key: 'thisWeek' }, |
| | | { name: '本周末', key: 'thisWeekend' }, |
| | | { name: '下周', key: 'nextWeek' }, |
| | | ]; |
| | | |
| | | const now = new Date(); |
| | | |
| | | const dateArray = dateList.map((item) => { |
| | | let date = new Date(); // 初始化当前日期 |
| | | let dayOfWeekString = ''; // 初始化星期几 |
| | | let timeString = ''; // 初始化时间 |
| | | |
| | | switch (item.key) { |
| | | case 'today': |
| | | // 今天稍晚是今天的 16:00 |
| | | date.setHours(16, 0, 0, 0); |
| | | break; |
| | | case 'tomorrow': |
| | | // 明天 08:00 |
| | | date.setDate(now.getDate() + 1); |
| | | date.setHours(8, 0, 0, 0); |
| | | break; |
| | | case 'thisWeek': |
| | | // 本周稍晚:周五 08:00 |
| | | const dayOfWeek = now.getDay(); |
| | | const daysUntilFriday = (5 - dayOfWeek + 7) % 7; // 计算到周五的天数 |
| | | date.setDate(now.getDate() + daysUntilFriday); |
| | | date.setHours(8, 0, 0, 0); |
| | | break; |
| | | case 'thisWeekend': |
| | | // 本周末:周日 08:00 |
| | | const daysUntilSunday = (7 - now.getDay()) % 7; // 计算到周日的天数 |
| | | date.setDate(now.getDate() + daysUntilSunday); |
| | | date.setHours(8, 0, 0, 0); |
| | | break; |
| | | case 'nextWeek': |
| | | // 下周一 08:00 |
| | | const daysUntilNextMonday = ((1 - now.getDay() + 7) % 7) + 7; // 计算到下周一的天数 |
| | | date.setDate(now.getDate() + daysUntilNextMonday); |
| | | date.setHours(8, 0, 0, 0); |
| | | break; |
| | | default: |
| | | break; |
| | | } |
| | | |
| | | // 获取星期几的字符串表示 |
| | | dayOfWeekString = date.toLocaleDateString('zh-CN', { weekday: 'long' }); |
| | | |
| | | // 获取仅时间部分 |
| | | timeString = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); |
| | | |
| | | // 返回日期、星期几和时间信息 |
| | | return { |
| | | name: item.name, |
| | | key: item.key, |
| | | date: date, |
| | | dayOfWeek: dayOfWeekString, |
| | | time: timeString, // 保存仅时间部分 |
| | | }; |
| | | }); |
| | | |
| | | return dateArray; |
| | | } |
| | | const dateList = processDateList(); |
| | | const customTimeDropdownOpen = ref(false); |
| | | function onSubmitCustomTime() { |
| | | customTimeDropdownOpen.value = true; |
| | | } |
| | | |
| | | const disabledDate = (currentDate) => { |
| | | return currentDate && currentDate < dayjs().startOf('day'); |
| | | }; |
| | | const cancelCustomTime = () => { |
| | | customTimeDropdownOpen.value = false; |
| | | dropdownOpen.value = true; |
| | | }; |
| | | const submitCustomTime = () => { |
| | | formRef.value.validate().then((valid) => { |
| | | if (valid) { |
| | | customTimeDropdownOpen.value = false; |
| | | const date = form.date ? dayjs(form.date).format('YYYY-MM-DD') : ''; |
| | | const time = form.time ? dayjs(form.time).format('HH:mm') : ''; |
| | | const data = { |
| | | handleTime: date + ' ' + time, |
| | | docCode: props.docCodeS, |
| | | }; |
| | | pushUpdateHandle(data); |
| | | } else { |
| | | return false; |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const toggleCustomTime = () => { |
| | | customTimeDropdownOpen.value = !customTimeDropdownOpen.value; |
| | | dropdownOpen.value = false; |
| | | }; |
| | | // 表单数据 |
| | | const form = reactive({ |
| | | date: '', |
| | | time: '', |
| | | }); |
| | | |
| | | import { useMessage } from '@/hooks/web/useMessage'; |
| | | import { formatToDateDay } from '@/utils/dateUtil'; |
| | | |
| | | function fnSelectDate(item) { |
| | | dropdownOpen.value = false; |
| | | const date = formatToDateDay(new Date(item.date)); |
| | | const data = { |
| | | handleTime: date, |
| | | docCode: props.docCodeS, |
| | | }; |
| | | pushUpdateHandle(data); |
| | | } |
| | | function pushUpdateHandle(data) { |
| | | updateHandleAPi(data) |
| | | .then((res) => { |
| | | if (res.code == 0) { |
| | | createMessage.success(res.msg); |
| | | // getDataList({}); |
| | | } |
| | | }) |
| | | .catch((err) => {}); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .custom-dropdown { |
| | | position: absolute; |
| | | z-index: 9900; |
| | | width: 250px; |
| | | padding: 10px; |
| | | border: 1px solid #0505050f; |
| | | background-color: #fff; |
| | | } |
| | | |
| | | .custom-dropdown :deep(.ant-menu-vertical) { |
| | | border-inline-end: 0; |
| | | } |
| | | |
| | | .custom-dropdown :deep(.ant-menu-item) { |
| | | height: 32px; |
| | | line-height: 32px; |
| | | } |
| | | </style> |
New file |
| | |
| | | <template> |
| | | <div> |
| | | <a-sub-menu v-if="item.children.length > 0" :key="item.key"> |
| | | <template #title> |
| | | <div class="my-display"> |
| | | <span>{{ item.title }}</span> |
| | | </div> |
| | | </template> |
| | | <a-menu-item |
| | | v-for="child in item.children" |
| | | :key="child.key" |
| | | @click="$emit('click', child)" |
| | | style="display: flex; justify-content: space-between; padding-left: 28px" |
| | | > |
| | | <div class="my-display"> |
| | | <span>{{ child.title }}</span> |
| | | <span v-if="child.total > 0">{{ child.total }}</span> |
| | | </div> |
| | | </a-menu-item> |
| | | </a-sub-menu> |
| | | <a-menu-item v-else :key="item.key" @click.stop="$emit('click', item)"> |
| | | <div class="my-display"> |
| | | <span>{{ item.title }}</span> |
| | | <span v-if="item.total > 0" class="my-left">{{ item.total }}</span> |
| | | </div> |
| | | </a-menu-item> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | defineProps(['item']); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .my-display { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .my-left { |
| | | margin-left: 5px; |
| | | } |
| | | </style> |
New file |
| | |
| | | <template> |
| | | <div> |
| | | <a-sub-menu :key="item.key" :open="isOpen"> |
| | | <template #title> |
| | | <div> |
| | | <div v-if="!isSearch" class="my-display"> |
| | | <span>{{ item.title }}</span> |
| | | <div class="handle-icon"> |
| | | <SearchOutlined v-if="item.key == 'moduleBelowA'" @click.stop="handleSearch(item)" /> |
| | | <MoreOutlined /> |
| | | </div> |
| | | </div> |
| | | <a-select |
| | | v-if="isSearch" |
| | | ref="inputRef" |
| | | v-model:value="value" |
| | | style="width: 100%" |
| | | show-search |
| | | placeholder="请输入文件夹关键字搜索" |
| | | :options="options" |
| | | :field-names="{ label: 'title', value: 'key' }" |
| | | @change="handleChange" |
| | | @blur="blurSearch" |
| | | ></a-select> |
| | | </div> |
| | | </template> |
| | | <template v-for="child in item.children" :key="child.key"> |
| | | <template v-if="!child.children || child.children.length === 0"> |
| | | <a-menu-item :key="child.key" @click="$emit('click', child)"> |
| | | <span>{{ child.title }}</span> |
| | | <span v-if="child.total > 0" class="my-left">{{ child.total }}</span> |
| | | </a-menu-item> |
| | | </template> |
| | | <template v-else> |
| | | <!-- 递归调用自身 --> |
| | | <MyMenu :item="child" @click="$emit('click', child)" /> |
| | | </template> |
| | | </template> |
| | | </a-sub-menu> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { defineProps, defineEmits, ref, computed } from 'vue'; |
| | | import { SearchOutlined, MoreOutlined } from '@ant-design/icons-vue'; |
| | | |
| | | interface MenuItem { |
| | | title: string; |
| | | key: string; |
| | | total: number; |
| | | children?: MenuItem[]; |
| | | } |
| | | |
| | | const props = defineProps<{ item: MenuItem }>(); |
| | | const emit = defineEmits(); |
| | | const options = ref([]); |
| | | const isSearch = ref(false); |
| | | const inputRef = ref(); |
| | | const value = ref(); |
| | | |
| | | // 计算属性,判断当前菜单项是否应展开 |
| | | const isOpen = computed(() => { |
| | | return true; // 默认为展开状态 |
| | | }); |
| | | |
| | | function handleSearch(item: MenuItem) { |
| | | console.log('handleSearch', item); |
| | | options.value = flattenMenuItems(item.children); |
| | | console.log('options', options.value); |
| | | isSearch.value = true; |
| | | setTimeout(() => { |
| | | inputRef.value.focus(); |
| | | }, 100); |
| | | emit('handleSearch', item); |
| | | } |
| | | |
| | | function blurSearch() { |
| | | isSearch.value = false; |
| | | } |
| | | |
| | | const handleChange = (value: string) => { |
| | | console.log(`selected ${value}`, isSearch.value); |
| | | isSearch.value = false; |
| | | emit('updateSelectedKeys', value); |
| | | }; |
| | | |
| | | const flattenMenuItems = (items: MenuItem[]): MenuItem[] => { |
| | | let result: MenuItem[] = []; |
| | | const recursiveFlatten = (items: MenuItem[]) => { |
| | | for (const item of items) { |
| | | result.push({ ...item, children: undefined }); // 不保留 children |
| | | if (item.children && item.children.length > 0) { |
| | | recursiveFlatten(item.children); |
| | | } |
| | | } |
| | | }; |
| | | recursiveFlatten(items); |
| | | return result; |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .my-display { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .my-left { |
| | | margin-left: 5px; |
| | | } |
| | | |
| | | .handle-icon { |
| | | display: none; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .my-display:hover .handle-icon { |
| | | display: block; |
| | | } |
| | | </style> |
| | |
| | | v-model:open-keys="openKeys" |
| | | v-model:selected-keys="selectedKeys" |
| | | mode="inline" |
| | | :popupClassName="popupClassName" |
| | | > |
| | | <template v-for="item in menuItems" :key="item.key"> |
| | | <render-menu-item :item="item" @click="handleClick" /> |
| | | <EmailMenuItem :item="item" @click="handleClick" /> |
| | | </template> |
| | | </a-menu> |
| | | <a-divider /> |
| | | <a-menu |
| | | id="email-left-nav2" |
| | | v-model:open-keys="openKeys" |
| | | v-model:selected-keys="selectedKeys" |
| | | v-model:open-keys="openKeys2" |
| | | v-model:selected-keys="selectedKeys2" |
| | | mode="inline" |
| | | :popupClassName="popupClassName" |
| | | > |
| | | <template v-for="item in menuItems2" :key="item.key"> |
| | | <render-menu-item :item="item" @click="handleClick" /> |
| | | <template v-if="!item.children"> |
| | | <a-menu-item :key="item.key" @click="$emit('click', item)"> |
| | | <div class="my-display"> |
| | | <span>{{ item.title }}</span> |
| | | <span v-if="item.total > 0" class="my-left">{{ item.total }}</span> |
| | | </div> |
| | | </a-menu-item> |
| | | </template> |
| | | <template v-else> |
| | | <MyMenu |
| | | :item="item" |
| | | @click.stop="handleClickMyMenu(item)" |
| | | @updateSelectedKeys="updateSelectedKeys" |
| | | /> |
| | | </template> |
| | | </template> |
| | | </a-menu> |
| | | </div> |
| | |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { ref, onMounted, computed } from 'vue'; |
| | | import { PageWrapper } from '@/components/Page'; |
| | | import { MailOutlined, UserOutlined } from '@ant-design/icons-vue'; |
| | | import { getEmailModuleApi, getEmailModuleBelowApi } from '@/api/email/userList'; |
| | | import { useRouter } from 'vue-router'; |
| | | import { ref, onMounted } from 'vue'; |
| | | import { PageWrapper } from '@/components/Page'; |
| | | import { DingdingOutlined, MailOutlined, UserOutlined } from '@ant-design/icons-vue'; |
| | | import { getEmailModuleApi, getEmailModuleBelowApi } from '@/api/email/userList'; |
| | | import { useRouter } from 'vue-router'; |
| | | import MyMenu from './MyMenu.vue'; |
| | | import EmailMenuItem from './EmailMenuItem.vue'; |
| | | |
| | | interface MenuItem { |
| | | key: string; |
| | | title: string; |
| | | total?: number; |
| | | children?: MenuItem[]; |
| | | } |
| | | const selectedKeys = ref<string[]>(['Index']); |
| | | const openKeys = ref<string[]>(['Inbox']); |
| | | const openKeys2 = ref(['0049LM']); |
| | | const selectedKeys2 = ref([]); |
| | | |
| | | const selectedKeys = ref<string[]>(['Index']); |
| | | const openKeys = ref<string[]>(['Inbox']); |
| | | const items = ref<MenuItem[]>([]); |
| | | const items2 = ref<MenuItem[]>([]); |
| | | const menuItems = ref([]); |
| | | const menuItems2 = ref([]); |
| | | |
| | | const fetchEmailModules = async () => { |
| | | try { |
| | | const [res, res2] = await Promise.all([getEmailModuleApi(), getEmailModuleBelowApi()]); |
| | | items.value = convertRoutesToMenuItems(res.data); |
| | | items2.value = convertRoutesToMenuItems2(res2.data); |
| | | } catch (error) { |
| | | console.error('获取邮箱模块失败:', error); |
| | | const fetchEmailModules = async () => { |
| | | try { |
| | | const [res, res2] = await Promise.all([getEmailModuleApi(), getEmailModuleBelowApi()]); |
| | | menuItems.value = convertRoutesToMenuItems(res.data); |
| | | menuItems2.value = convertRoutesToMenuItems2(res2.data); |
| | | console.log('menuItems:', menuItems2.value); |
| | | } catch (error) { |
| | | console.error('获取邮箱模块失败:', error); |
| | | } |
| | | }; |
| | | |
| | | const convertRoutesToMenuItems = (routes: any[]) => { |
| | | return routes |
| | | .map((route) => ({ |
| | | key: route.key, |
| | | title: route.mailName, |
| | | total: route.total, |
| | | children: route.children ? convertRoutesToMenuItems(route.children) : undefined, |
| | | })) |
| | | .filter(Boolean); |
| | | }; |
| | | |
| | | const convertRoutesToMenuItems2 = (routes: any[], path) => { |
| | | return routes |
| | | .map((route) => ({ |
| | | key: route.key, |
| | | title: route.name, |
| | | total: route.number, |
| | | path: path ? path : route.key, |
| | | children: route.list |
| | | ? convertRoutesToMenuItems2(route.list, path ? path : route.key) |
| | | : undefined, |
| | | })) |
| | | .filter(Boolean); |
| | | }; |
| | | |
| | | onMounted(fetchEmailModules); |
| | | |
| | | const routesConfig = { |
| | | InboxPage1: '/email/index', |
| | | receiver: '/email/Inbox/list', |
| | | sender: '/email/outbox/list', |
| | | IndexPage1: '/email/outbox', |
| | | moduleBelowA: '/email/index', |
| | | moduleBelowB: '/email/index', |
| | | moduleBelowC: '/email/index', |
| | | }; |
| | | |
| | | const router = useRouter(); |
| | | // const handleClick = (item: any) => { |
| | | // selectedKeys2.value = []; |
| | | // debugger |
| | | // const routePath = |
| | | // routesConfig[item.key] || router.getRoutes().find((r) => r.name === item.key)?.path; |
| | | // if (routePath) { |
| | | |
| | | // router.push(routePath); |
| | | // } else { |
| | | // console.warn(`Unknown key: ${item.key}`); |
| | | // } |
| | | // }; |
| | | const handleClick = (e: any) => { |
| | | selectedKeys2.value = []; |
| | | |
| | | let matched = false; |
| | | const route = router.getRoutes() || []; |
| | | route.forEach((item) => { |
| | | if (item.name === e.key) { |
| | | router.push(item.path); |
| | | matched = true; |
| | | return; // 跳出当前循环 |
| | | } |
| | | |
| | | if (!matched) { |
| | | switch (e.key) { |
| | | case 'InboxPage1': |
| | | router.push(routesConfig[e.key]); |
| | | matched = true; |
| | | return; // 跳出当前循环 |
| | | case 'receiver': |
| | | router.push(`${routesConfig[e.key]}?${e.title}`); |
| | | matched = true; |
| | | return; // 跳出当前循环 |
| | | case 'sender': |
| | | router.push(`${routesConfig[e.key]}?${e.title}`); |
| | | matched = true; |
| | | return; // 跳出当前循环 |
| | | case 'IndexPage1': |
| | | router.push(`${routesConfig[e.key]}`); |
| | | matched = true; |
| | | return; // 跳出当前循环 |
| | | default: |
| | | // 处理默认情况,例如记录日志或抛出警告 |
| | | console.warn(`Unknown key: ${e.key}`); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const updateSelectedKeys = (keys) => { |
| | | openKeys2.value = findParentKeys(menuItems2.value[0], keys); |
| | | selectedKeys2.value = [keys]; |
| | | }; |
| | | |
| | | function findParentKeys(data, targetKey, path = []) { |
| | | path.push(data.key); |
| | | if (data.key === targetKey) { |
| | | return path; |
| | | } |
| | | |
| | | for (const item of data.children || []) { |
| | | const result = findParentKeys(item, targetKey, path.slice()); |
| | | if (result) { |
| | | return result; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | }; |
| | | |
| | | const convertRoutesToMenuItems = (routes: any[]): MenuItem[] => { |
| | | return routes |
| | | .map(route => ({ |
| | | key: route.key, |
| | | title: route.mailName, |
| | | total: route.total, |
| | | children: route.children ? convertRoutesToMenuItems(route.children) : undefined, |
| | | })) |
| | | .filter(Boolean) as MenuItem[]; |
| | | }; |
| | | |
| | | const convertRoutesToMenuItems2 = (routes: any[]): MenuItem[] => { |
| | | return routes |
| | | .map(route => ({ |
| | | key: route.key, |
| | | title: route.name, |
| | | total: route.number, |
| | | children: route.list ? convertRoutesToMenuItems2(route.list) : undefined, |
| | | })) |
| | | .filter(Boolean) as MenuItem[]; |
| | | }; |
| | | |
| | | onMounted(fetchEmailModules); |
| | | |
| | | const routesConfig = { |
| | | InboxPage1: '/email/index', |
| | | receiver: '/email/Inbox/list', |
| | | sender: '/email/outbox/list', |
| | | IndexPage1: '/email/outbox', |
| | | }; |
| | | |
| | | const router = useRouter(); |
| | | const handleClick = (item: MenuItem) => { |
| | | const routePath = routesConfig[item.key] || router.getRoutes().find(r => r.name === item.key)?.path; |
| | | if (routePath) { |
| | | router.push(routePath); |
| | | } else { |
| | | console.warn(`Unknown key: ${item.key}`); |
| | | } |
| | | }; |
| | | |
| | | const popupClassName = { |
| | | display: 'flex', |
| | | 'align-items': 'center', |
| | | 'justify-content': 'space-between', |
| | | }; |
| | | const handleClickMyMenu = (item: any) => { |
| | | selectedKeys.value = []; |
| | | // console.log('handleClickMyMenu:', item); |
| | | router.push({ path: '/email/folder' }); |
| | | console.log('updateSelectedKeys111111112333:', openKeys2.value); |
| | | }; |
| | | </script> |
| | | |
| | | <style lang="less" scoped> |
| | | .header-container { |
| | | height: 15vh; |
| | | padding: 20px 40px; |
| | | text-align: center; |
| | | } |
| | | .header-container { |
| | | height: 15vh; |
| | | padding: 20px 40px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .button-group { |
| | | display: flex; |
| | | justify-content: space-around; |
| | | } |
| | | .button-group { |
| | | display: flex; |
| | | justify-content: space-around; |
| | | } |
| | | |
| | | .write-button { |
| | | width: 100%; |
| | | margin-top: 10px; |
| | | padding: 0 30px; |
| | | } |
| | | .write-button { |
| | | width: 100%; |
| | | margin-top: 10px; |
| | | padding: 0 30px; |
| | | } |
| | | |
| | | .menu-container { |
| | | height: 70vh; |
| | | overflow: auto; |
| | | } |
| | | .menu-container { |
| | | height: 75vh; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .my-display { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | .my-display { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .my-left { |
| | | margin-left: 5px; |
| | | } |
| | | .my-left { |
| | | margin-left: 5px; |
| | | } |
| | | </style> |
| | |
| | | <a-divider style="margin: 5px 0" /> |
| | | <div class="date p-1"> |
| | | <a-popover |
| | | trigger="click" |
| | | :trigger="trigger" |
| | | title="自定义时间" |
| | | v-model:open="customTimeDropdownOpen" |
| | | @confirm="onSubmitCustomTime" |
| | |
| | | initialTooltipOpen: Boolean, // Tooltip 初始打开状态 |
| | | row: Object, // 当前行对象 |
| | | docCodeS: Array, |
| | | trigger: { |
| | | type: String, |
| | | default: 'click', |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(['updateHandleTime', 'completeAction', 'customTimeSubmit', 'tagRow']); |
| | |
| | | handleTime: date, |
| | | docCode: props.docCodeS, |
| | | }; |
| | | |
| | | pushUpdateHandle(data); |
| | | } |
| | | function pushUpdateHandle(data) { |
| | |
| | | |
| | | const onComplete = () => { |
| | | const data = { |
| | | handleTime: '', |
| | | docCode: props.docCodeS, |
| | | }; |
| | | pushUpdateHandle(data); |
| | | }; |
| | | const formRef = ref(); |
| | | const submitCustomTime = () => { |
| | | formRef.value.validate().then((valid) => { |
| | | if (valid) { |
| | | customTimeDropdownOpen.value = false; |
| | | const date = form.date ? dayjs(form.date).format('YYYY-MM-DD') : ''; |
| | | const time = form.time ? dayjs(form.time).format('HH:mm') : ''; |
| | | const data = { |
| | | handleTime: date + ' ' + time, |
| | | docCode: props.docCodeS, |
| | | }; |
| | | pushUpdateHandle(data); |
| | | } else { |
| | | return false; |
| | | } |
| | | }); |
| | | formRef.value.validate().then((valid) => { |
| | | if (valid) { |
| | | customTimeDropdownOpen.value = false; |
| | | const date = form.date ? dayjs(form.date).format('YYYY-MM-DD') : ''; |
| | | const time = form.time ? dayjs(form.time).format('HH:mm') : ''; |
| | | const data = { |
| | | handleTime: date + ' ' + time, |
| | | docCode: props.docCodeS, |
| | | }; |
| | | pushUpdateHandle(data); |
| | | } else { |
| | | return false; |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const cancelCustomTime = () => { |
| | |
| | | <span style="margin-right: 10px; font-size: 16px"> |
| | | <PushpinOutlined /> |
| | | </span> |
| | | <a-tag |
| | | v-if="tableRowData.attachmentPath?.length > 0" |
| | | style="margin-right: 10px; font-size: 12px" |
| | | > |
| | | <PaperClipOutlined style="margin-right: 4px; color: #ffac00" />{{ |
| | | tableRowData.attachmentPath?.length |
| | | }} |
| | | </a-tag> |
| | | </div> |
| | | <div class="right"> |
| | | <div class="tate">{{ formatToDateDay(tableRowData.receiveTime || tableRowData.updateTime) }}</div> |
| | | <div class="tate">{{ |
| | | formatToDateDay(tableRowData.receiveTime || tableRowData.updateTime) |
| | | }}</div> |
| | | <div> |
| | | <a-dropdown-button> |
| | | <div v-if="!isDrafts"> |
| | |
| | | <span>暂未查询到该客户的当地时间</span> |
| | | <!-- <span>2024-06-08 22:22</span> --> |
| | | </div> |
| | | |
| | | <div v-if="tableRowData.attachmentPath?.length>0" class="p-2 f-z-14" style="margin-top: 10px"> |
| | | <div style="display: flex; align-items: center"> |
| | | <span style="margin-right: 10px">{{ |
| | | `附件(${tableRowData.attachmentPath?.length})` |
| | | }}</span> |
| | | <a type="link" @click="fnAllDownload">全部下载</a> |
| | | </div> |
| | | <div class="my-d-f" style="width: 100%; margin-top: 10px"> |
| | | <div |
| | | class="file-item my-d-f" |
| | | v-for="item in tableRowData.attachmentPath" |
| | | :key="item" |
| | | > |
| | | <div class="icon"><FileExcelOutlined /></div> |
| | | <div class="name" @click="fnPreview(item)">{{ item.name }}</div> |
| | | <div class="size"> {{ item.size + 'k' }} </div> |
| | | <div class="download" @click="fnDownload(item)"><CloudDownloadOutlined /></div> |
| | | <!-- <div class="delete">删除</div> --> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="ct"> |
| | | <div v-if="tableRowData.content"> |
| | | <TinymcePw ref="TinymcePwRef" v-model="tableRowData.content" /> |
| | |
| | | CopyOutlined, |
| | | UserOutlined, |
| | | RollbackOutlined, |
| | | FileExcelOutlined, |
| | | CloudDownloadOutlined, |
| | | PaperClipOutlined, |
| | | } from '@ant-design/icons-vue'; |
| | | import pageHeadLeft from './pageHeadLeft.vue'; |
| | | import UserTips from '@/views/email/components/userTips/index.vue'; |
| | |
| | | function replyEmail(row, type) { |
| | | router.push({ path: '/email/edit', query: { docCode: row.docCode, type: type } }); |
| | | } |
| | | |
| | | const fnPreview = (item) => { |
| | | if (!item) { |
| | | console.error('Invalid file or response'); |
| | | return; |
| | | } |
| | | |
| | | // // 获取文件类型(通过文件扩展名或 MIME 类型) |
| | | const fileExt = item.fileType; // 获取文件扩展名 |
| | | const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExt); // 判断是否为图片 |
| | | |
| | | if (isImage) { |
| | | // 直接打开图片 |
| | | window.open(item.url, '_blank'); |
| | | } else { |
| | | const iframeSrc = `https://view.officeapps.live.com/op/view.aspx?src=${item.url}`; |
| | | window.open(iframeSrc, '_blank'); |
| | | } |
| | | }; |
| | | |
| | | const fnDownload = (item) => { |
| | | const link = document.createElement('a'); |
| | | link.href = item.url; |
| | | link.download = item.name; // 提取文件名 |
| | | document.body.appendChild(link); // 将链接添加到 DOM |
| | | link.click(); // 模拟点击下载 |
| | | document.body.removeChild(link); // 下载后移除链接 |
| | | }; |
| | | |
| | | const fnAllDownload = () => { |
| | | const urls = tableRowData.value.attachmentPath.map((item) => item.url); |
| | | const url = urls.join(','); |
| | | fnDownload(url); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="less"> |
| | |
| | | .f-z-14 { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .file-item { |
| | | width: 240px; |
| | | margin-right: 10px; |
| | | padding: 10px; |
| | | border-radius: 4px; |
| | | background-color: #f0f0f0; |
| | | |
| | | & .icon { |
| | | width: 20px; |
| | | margin-right: 5px; |
| | | } |
| | | |
| | | & .name { |
| | | flex: 1; |
| | | margin-right: 5px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | & .download { |
| | | display: none; |
| | | width: 20px; |
| | | } |
| | | } |
| | | |
| | | .file-item:hover .name { |
| | | transition: 0.3s; |
| | | color: #0960bd; |
| | | } |
| | | |
| | | .file-item:hover .size { |
| | | display: none; |
| | | transition: 0.3s; |
| | | } |
| | | |
| | | .file-item:hover .download { |
| | | display: block; |
| | | transition: 0.3s; |
| | | } |
| | | </style> |
| | |
| | | </div> |
| | | |
| | | <div class="right p-3" |
| | | >共<span style="padding: 0 5px">{{page.total}}</span>封 |
| | | >共<span style="padding: 0 5px">{{ page.total }}</span |
| | | >封 |
| | | <a-pagination |
| | | v-model:current="pageCurrent" |
| | | v-model:page-size='page.limit' |
| | | v-model:page-size="page.limit" |
| | | simple |
| | | :total="page.total" |
| | | style="margin-left: 10px" |
| | |
| | | </a-switch> |
| | | </div> |
| | | </div> |
| | | <div v-if="checked" style="height: 30px;" class="left-bt p-3"> |
| | | <div v-if="checked" style="height: 30px" class="left-bt p-3"> |
| | | 已选择此页面上所有 20 封邮件 , 选择全部 335 封邮件 |
| | | </div> |
| | | <div class="p-4" style="height: 90%; overflow: hidden"> |
| | | <a-tabs v-model:activeKey="activeKey"> |
| | | <a-tab-pane |
| | | v-for="item in tabsList" |
| | | :key="item.key" |
| | | :tab="`${item.label}${item.num ? '(' + item.num + ')' : ''}`" |
| | | style="height: 200px" |
| | | > |
| | | <Table |
| | | ref="tableRef" |
| | | :page="pageCurrent" |
| | | :pageList="newList" |
| | | :isDrafts="isDrafts" |
| | | @selectAll="fnSelectAll" |
| | | @updateSelectAll="updateSelectAll" |
| | | /> |
| | | </a-tab-pane> |
| | | </a-tabs> |
| | | <div class="p-4" style="height: 90%; overflow: auto"> |
| | | <div v-if="isTabs"> |
| | | <a-tabs v-model:activeKey="activeKey"> |
| | | <a-tab-pane |
| | | v-for="item in tabsList" |
| | | :key="item.key" |
| | | :tab="`${item.label}${item.num ? '(' + item.num + ')' : ''}`" |
| | | style="height: 200px" |
| | | > |
| | | <Table |
| | | ref="tableRef" |
| | | :page="pageCurrent" |
| | | :pageList="newList" |
| | | :isDrafts="isDrafts" |
| | | @selectAll="fnSelectAll" |
| | | @updateSelectAll="updateSelectAll" |
| | | /> |
| | | </a-tab-pane> |
| | | </a-tabs> |
| | | </div> |
| | | <div v-else> |
| | | <Table |
| | | ref="tableRef" |
| | | :page="pageCurrent" |
| | | :pageList="newList" |
| | | :isDrafts="isDrafts" |
| | | @selectAll="fnSelectAll" |
| | | @updateSelectAll="updateSelectAll" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </PageWrapper> |
| | |
| | | import pageHeadLeft from './pageHeadLeft.vue'; |
| | | import { PageWrapper } from '@/components/Page'; |
| | | |
| | | import { ref, watch, defineProps, defineEmits, computed, reactive, onMounted,inject } from 'vue'; |
| | | import { ref, watch, defineProps, defineEmits, computed, reactive, onMounted, inject } from 'vue'; |
| | | |
| | | // 定义属性 |
| | | interface Props { |
| | |
| | | pageData?: any; |
| | | mailType?: number; |
| | | isDrafts?: boolean; |
| | | isTabs?: boolean; |
| | | } |
| | | const props = defineProps<Props>(); |
| | | const props = defineProps({ |
| | | isTabs: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | pageList: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | pageData: { |
| | | type: Object, |
| | | default: () => {}, |
| | | }, |
| | | mailType: { |
| | | type: Number, |
| | | default: 0, |
| | | }, |
| | | isDrafts: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | }); |
| | | const newList = ref([]); |
| | | const selectAllRow = ref([]); |
| | | watch( |
| | |
| | | }, |
| | | ); |
| | | |
| | | const page = computed(() => props.pageData); |
| | | const page = computed(() => props.pageData); |
| | | const checked = computed(() => selectAllRow.value.length > 0); |
| | | const pageCurrent = ref(1); |
| | | const tableRef = ref(); |
| | |
| | | }); |
| | | |
| | | const getDataList = inject('getDataList'); |
| | | function handlePageChange(page, pageSize){ |
| | | getDataList(page) |
| | | |
| | | function handlePageChange(page, pageSize) { |
| | | getDataList(page); |
| | | } |
| | | </script> |
| | | <style scoped lang="less"> |
| | |
| | | align-items: center; |
| | | justify-content: space-flex-start; |
| | | width: 100%; |
| | | height: 100%;; |
| | | height: 100%; |
| | | |
| | | & .icon { |
| | | margin-right: 15px; |
| | |
| | | size="small" |
| | | min-height="40px" |
| | | :row-config="{ isCurrent: true, isHover: true }" |
| | | :menu-config="tableMenu" |
| | | @menu-click="contextMenuClickEvent" |
| | | :menu-config="{ enabled: true }" |
| | | @cell-click="cellClickEvent" |
| | | @checkbox-change="selectChangeEvent" |
| | | @cell-menu="onCellContextMenu" |
| | | > |
| | | <vxe-column type="checkbox" width="30"></vxe-column> |
| | | <vxe-column field="sender" title="发件人" data-index="sender" min-width="300px"> |
| | |
| | | <a-button type="link" size="small">往来邮件</a-button></div |
| | | > |
| | | </template> |
| | | <div class="title-dot" :class="fnIsItHighlighted(row) ? 'title-dot-color' : ''"> |
| | | <div :class="fnIsItHighlighted(row) ? '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> |
| | | </template> |
| | | </vxe-column> |
| | | <vxe-column |
| | | show-overflow |
| | | field="icon" |
| | | title="表题" |
| | | data-index="icon" |
| | | width="100" |
| | | align="right" |
| | | > |
| | | <template #default="{ row }"> |
| | | <a-tooltip placement="bottom"> |
| | | <template #title> |
| | | <span>有附件</span> |
| | | </template> |
| | | <span v-show="row.attachmentList?.length > 0"> |
| | | <PaperClipOutlined /> |
| | | </span> |
| | | </a-tooltip> |
| | | </template> |
| | | </vxe-column> |
| | | |
| | | <vxe-column |
| | | show-overflow |
| | | field="subject" |
| | |
| | | <span style="display: flex; justify-content: space-around"> |
| | | <span>{{ |
| | | row.mailType !== 0 |
| | | ? formatToDateDay(row.receiveTime) |
| | | ? formatToDateDay(row.receiveTime || row.createTime) |
| | | : formatToDateDay(row.createTime) |
| | | }}</span> |
| | | |
| | |
| | | </span> |
| | | </template> |
| | | </vxe-column> |
| | | </vxe-table> </div |
| | | </vxe-table> |
| | | <ContextMenu |
| | | v-if="showMenu" |
| | | :style="menuStyle" |
| | | :selected-cell="selectedCell" |
| | | @close-menu="showMenu = false" |
| | | /> </div |
| | | ></div> |
| | | |
| | | <div v-else style="display: flex; align-items: center; justify-content: center; height: 70vh"> |
| | |
| | | PushpinOutlined, |
| | | CopyOutlined, |
| | | DownOutlined, |
| | | PaperClipOutlined, |
| | | } from '@ant-design/icons-vue'; |
| | | |
| | | import { ref, watch, defineProps, defineEmits, computed, defineExpose, inject } from 'vue'; |
| | |
| | | } |
| | | |
| | | // 右键菜单 |
| | | const tableMenu = { |
| | | className: 'my-menus', |
| | | body: { |
| | | options: [ |
| | | [ |
| | | { |
| | | code: 'reply', |
| | | name: '回复', |
| | | }, |
| | | { code: 'replyAll', name: '回复全部' }, |
| | | { code: 'replyWithAttachment', name: '带附件回复' }, |
| | | { code: 'replyAllWithAttachment', name: '带附件回复全部' }, |
| | | { code: 'forward', name: '转发' }, |
| | | { code: 'forwardAsAttachment', name: '作为附件转发' }, |
| | | { code: 'distribute', name: '分发' }, |
| | | { code: 'setRemark', name: '设置备注' }, |
| | | ], |
| | | [ |
| | | { code: 'toHandle', name: '待处理' }, |
| | | { code: 'markAsUnread', name: '标为未读' }, |
| | | { code: 'labelAs', name: '标注为' }, |
| | | ], |
| | | [ |
| | | { code: 'newRule', name: '新建收发件规则' }, |
| | | { code: 'moveTo', name: '移动到' }, |
| | | ], |
| | | [ |
| | | { code: 'exportEmail', name: '导出邮件' }, |
| | | { code: 'createFollowUp', name: '建为客户跟进' }, |
| | | { code: 'createSchedule', name: '新建日程' }, |
| | | ], |
| | | [ |
| | | { code: 'markAsSpam', name: '标为垃圾邮件' }, |
| | | { code: 'delete', name: '删除' }, |
| | | ], |
| | | ], |
| | | }, |
| | | import ContextMenu from '@/views/email/components/ContextMenu/index.vue'; |
| | | const showMenu = ref(false); |
| | | const menuStyle = ref({}); |
| | | const selectedCell = ref(null); |
| | | const onCellContextMenu = ({ |
| | | type, |
| | | row, |
| | | rowIndex, |
| | | $rowIndex, |
| | | column, |
| | | columnIndex, |
| | | $columnIndex, |
| | | $event, |
| | | }) => { |
| | | $event.preventDefault(); // 阻止默认的浏览器右键菜单? |
| | | selectedCell.value = { row, column, columnIndex }; // 保存当前选中的单元格数据 |
| | | |
| | | // 计算菜单初始位置 |
| | | let menuX = $event.clientX; |
| | | let menuY = $event.clientY; |
| | | |
| | | // 获取菜单的宽度和高度 |
| | | const menuWidth = 200; // 假设菜单宽度为200px,可以根据实际情况调整 |
| | | const menuHeight = 800; // 假设菜单高度为150px,可以根据实际情况调整 |
| | | // 获取窗口宽度和高度 |
| | | const windowWidth = window.innerWidth; |
| | | const windowHeight = window.innerHeight; |
| | | |
| | | // 检查菜单是否超出窗口的右边界,如果是,则向左调整 |
| | | if (menuX + menuWidth > windowWidth) { |
| | | menuX = windowWidth - menuWidth; |
| | | } |
| | | |
| | | // 检查菜单是否超出窗口的下边界,如果是,则向上调整 |
| | | if (menuY + menuHeight > windowHeight) { |
| | | menuY = windowHeight - menuHeight; |
| | | } |
| | | menuStyle.value = { |
| | | position: 'fixed', |
| | | top: `${menuY}px`, |
| | | left: `${menuX}px`, |
| | | }; |
| | | showMenu.value = true; |
| | | }; |
| | | |
| | | function contextMenuClickEvent({ menu, row, column }) { |
| | | switch (menu.code) { |
| | | case 'copy': |
| | | if (row && column) { |
| | | } |
| | | break; |
| | | default: |
| | | } |
| | | } |
| | | // 监听全局点击事件,点击页面其他地方关闭菜单 |
| | | document.addEventListener('click', () => { |
| | | showMenu.value = false; |
| | | }); |
| | | |
| | | const vxeTableRef = ref(); |
| | | function fnIsItHighlighted(row) { |
| | | return row.readFlag && props.isDrafts; |
| | | if (props.isDrafts) { |
| | | return row.readFlag && props.isDrafts; |
| | | } else { |
| | | return row.readFlag; |
| | | } |
| | | } |
| | | function fnSelectAll(is) { |
| | | try { |
| | |
| | | |
| | | .span-title { |
| | | width: 100%; |
| | | height: 30px; |
| | | padding: 5px; |
| | | color: #000; |
| | | font-weight: 700; |
| | | line-height: 30px; |
| | | text-align: left; |
| | | } |
| | | |
New file |
| | |
| | | <template> |
| | | <div> |
| | | <a-spin :spinning="loading" class="p-1" style="height: 100%;"> |
| | | <PageIndex :pageList="pageList" :mailType="1" :pageData="pageData" :isTabs="false"> </PageIndex> |
| | | </a-spin> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | name: 'Inbox'; |
| | | import { ref, onMounted, computed, provide } from 'vue'; |
| | | import PageIndex from '@/views/email/components/ListPage/list.vue'; |
| | | import { useRoute } from 'vue-router'; |
| | | |
| | | const route = useRoute(); |
| | | // 获取当前完整 URL |
| | | const routerId = computed(() => { |
| | | try { |
| | | const url = window.location.href; |
| | | if (!url) { |
| | | throw new Error('Invalid URL'); |
| | | } |
| | | const basePart = url.split('?')[1]; |
| | | console.log(basePart, 'basePart'); |
| | | return basePart; |
| | | } catch (error) { |
| | | console.error('Error processing URL:', error); |
| | | // 返回默认值或空字符串 |
| | | return ''; |
| | | } |
| | | }); |
| | | import { getMailListApi } from '@/api/email/userList'; |
| | | const pageList = ref([]); |
| | | const loading = ref(false); |
| | | const pageData = ref({ |
| | | page: 1, |
| | | limit: 20, |
| | | total: 0, |
| | | }); |
| | | function getDataList(page:1) { |
| | | loading.value = true; |
| | | getMailListApi({ mail: routerId.value, mailType: 1,page }) |
| | | .then((res) => { |
| | | loading.value = false; |
| | | if (res.code == 0) { |
| | | pageList.value = res.data.list; |
| | | pageData.value.total = res.data.total; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | loading.value = false; |
| | | }); |
| | | } |
| | | |
| | | |
| | | provide('getDataList', getDataList); |
| | | onMounted(() => { |
| | | getDataList(); |
| | | }); |
| | | </script> |
| | | <style scoped lang="less"></style> |
New file |
| | |
| | | <template> |
| | | <div> |
| | | <a-spin :spinning="loading" class="p-1" style="height: 100%;"> |
| | | <PageIndex :pageList="pageList" :mailType="1" :pageData="pageData" :isTabs="false"> </PageIndex> |
| | | </a-spin> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | name: 'Inbox'; |
| | | import { ref, onMounted, computed, provide } from 'vue'; |
| | | import PageIndex from '@/views/email/components/ListPage/list.vue'; |
| | | import { useRoute } from 'vue-router'; |
| | | |
| | | const route = useRoute(); |
| | | // 获取当前完整 URL |
| | | const routerId = computed(() => { |
| | | try { |
| | | const url = window.location.href; |
| | | if (!url) { |
| | | throw new Error('Invalid URL'); |
| | | } |
| | | const basePart = url.split('?')[1]; |
| | | console.log(basePart, 'basePart'); |
| | | return basePart; |
| | | } catch (error) { |
| | | console.error('Error processing URL:', error); |
| | | // 返回默认值或空字符串 |
| | | return ''; |
| | | } |
| | | }); |
| | | import { getMailListApi } from '@/api/email/userList'; |
| | | const pageList = ref([]); |
| | | const loading = ref(false); |
| | | const pageData = ref({ |
| | | page: 1, |
| | | limit: 20, |
| | | total: 0, |
| | | }); |
| | | function getDataList(page:1) { |
| | | loading.value = true; |
| | | getMailListApi({ mail: routerId.value, mailType: 1,page }) |
| | | .then((res) => { |
| | | loading.value = false; |
| | | if (res.code == 0) { |
| | | pageList.value = res.data.list; |
| | | pageData.value.total = res.data.total; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | loading.value = false; |
| | | }); |
| | | } |
| | | |
| | | |
| | | provide('getDataList', getDataList); |
| | | onMounted(() => { |
| | | getDataList(); |
| | | }); |
| | | </script> |
| | | <style scoped lang="less"></style> |
| | |
| | | // secure: false |
| | | }, |
| | | '/upload': { |
| | | target: 'http://localhost:3300/upload', |
| | | target: 'http://yingchen.onbus.cn:9010/attachment/uploadAttachmentV2.do', |
| | | changeOrigin: true, |
| | | ws: true, |
| | | rewrite: (path) => path.replace(new RegExp(`^/upload`), ''), |