企业需要在文件列表的右键菜单栏中,新增其它定制菜单项

如何在文件列表新增右键菜单项?

本定制方案仅适用于私有化WPS365版本

1. 方案概述

有时候,企业需要给不在组织通讯录中的成员共享文档,传统流程是把文档下载下来,通过邮件发送,易导致效率瓶颈。

现在,通过文档中心的扩展能力,可以直接在文件列表右键菜单新增自定义分享弹窗入口,简化协作流程。

这一功能不仅支持自定义分享弹窗,企业也可根据实际需要,在文件列表的右键菜单栏中,新增其它定制菜单项。

我们可以使用文档中心Web端(PC端)的开放能力,实现文件及文件夹的分享:

实现效果定制成功后,点击右键选择自定义分享弹窗,复制文档链接即可对外分享,也可以在高级设置中设置链接是否永久有效。用户获取链接后,可在无账号情况下查看或下载文件,并对免登录分享文件进行管理以及日志记录。VRQ7K6A6ADAGQ
支持端文档中心PC端
版本限制版本23-3a(含)以上
操作系统限制支持X86、XC

2. 使用指南

自定义文件列表右键菜单-分享弹窗

👉能力

sdk.kdrive.fileList.rightMenu

👉方法综述

名称描述版本号
onChange文件列表区域-右键菜单-改变事件(最近、星标、共享、我的文档、团队文档、我的团队)23.0309
onClick文件列表区域-右键菜单-点击事件拦截(最近、星标、共享、我的文档、团队文档、我的团队)23.0309

onChange

信息内容
支持版本23.0309
是否支持多插件true
是否支持异步false
兼容性说明

👉代码示例

// 云文档PCweb-自定义分享弹窗
kdrive.fileList.rightMenu.onChange((menus: Menus) => {
  const noShare: boolean = false;
  // 插入位置
  let insertIndex: number;

  // 区分类型
  switch (menus.record.type) {
    case 'file':
      if (noShareSuffix.some((name: string) => menus.record.name.toLowerCase().endsWith(name))) {
        return;
      }

      insertIndex = menus.operationList._data.findIndex((item) => item?.key === 'commonHistoryVersion');
      menus.operationList.add(
        { key: 'customShareModal', name: t('customShareModal'), disabled: noShare },
        insertIndex + 1
      );
      break;
    case 'folder':
      insertIndex = menus.operationList._data.findIndex((item) => item?.key === 'commonBatchDownload');
      menus.operationList.add(
        { key: 'customShareModal', name: t('customShareModal'), disabled: noShare },
        insertIndex - 1
      );
      break;
  }
});

onClick

信息内容
支持版本23.0309
是否支持多插件true
是否支持异步true
兼容性说明

👉代码示例

// 云文档PCweb-自定义分享弹窗-单击事件
kdrive.fileList.rightMenu.onClick(async (menuItem: any) => {
  if (menuItem.key === 'customShareModal') {
    // 获取分享权限
    // 正常开发是通过接口获取数据,现在写示例数据模拟调用接口
    const res = {
      data: {
        code: 200,
      },
    };
    if (res.data.code === 200) {
      const el = document.createElement('div');
      document.body.appendChild(el);
      const root = registerNode(el);
      root.render(
        <ShareModal
          destroy={() => unmount(el)}
          driveId={menuItem.data.driveId}
          fileId={menuItem.data.id}
          type={menuItem.data.type}
        />
      );
      return false;
    } else {
      message.error(t('serviceError'));
      return true;
    }
  }
});

完整代码及目录结构

👉目录结构

ecis-frontend-plugin-custom-frame/
├── src/
│   ├── plugin/
│   │   ├── components/
│   │   │   ├── shareModal/
│   │   │   │   ├── index.tsx   // 分享弹窗的主要组件文件,包含了弹窗的逻辑和UI
│   │   │   │   ├── index.module.less
│   │   ├── interface/
│   │   │   ├── shareModal.ts  // 定义了分享弹窗相关枚举
│   │   ├── plugin.tsx    // 插件的主入口文件,负责初始化和注册插件功能
│   ├── libs/
│   │   ├── sdk.ts  // 提供了与SDK相关的功能和方法
│   │   ├── i18n/
│   │   │   ├── locales/
│   │   │   │   ├── zh-CN.json  // 包含中文语言的翻译键值对
│   │   │   │   ├── zh-HK.json  // 包含繁体语言的翻译键值对
│   │   │   │   ├── en-US.json  // 包含英文语言的翻译键值对
│   ├── assets/
│   │   ├── hyperlink_type.svg  // 分享弹窗中使用的图标文件(根据实际需求替换)

👉完整代码

plugin.tsx 插件的主入口文件,负责初始化和注册插件功能
import React, { useEffect } from 'react';
import { createRoot, Root } from 'react-dom/client';
import sdk from '../libs/sdk';
import { message } from 'antd';
import ShareModal from './components/shareModal';
import { noShareSuffix } from './interface/shareModal';
import { useTranslation } from 'react-i18next';

console.log('--- plugin start ---');

// 存储节点和对应的 root 实例
const nodeRootMap = new Map<HTMLElement, Root>();

// 卸载节点
function unmount(node: HTMLElement) {
  const root = nodeRootMap.get(node);
  if (root) {
    root.unmount();
    nodeRootMap.delete(node);
  }
  if (node && node.parentNode) {
    node.parentNode.removeChild(node);
  }
}

// 注册节点和 root 实例
function registerNode(node: HTMLElement) {
  const root = createRoot(node);
  nodeRootMap.set(node, root);
  return root;
}

interface MenuItem {
  key: string;
  data: {
    driveId: string;
    id: string;
    type: string;
  };
}

interface Menus {
  record: {
    type: string;
    name: string;
  };
  operationList: {
    _data: Array<{ key: string }>;
    add: (item: { key: string; name: string; disabled: boolean }, index: number) => void;
  };
}

const Plugin = () => {
  const { t } = useTranslation();
  const addEntrance = async () => {
    const kdrive = await sdk.kdrive;
    console.log('ecis: index start');
    kdrive.fileList.rightMenu.onChange((menus: Menus) => {
      const noShare: boolean = false;
      // 插入位置
      let insertIndex: number;

      // 区分类型
      switch (menus.record.type) {
        case 'file':
          if (noShareSuffix.some((name: string) => menus.record.name.toLowerCase().endsWith(name))) {
            return;
          }

          insertIndex = menus.operationList._data.findIndex((item) => item?.key === 'commonHistoryVersion');
          menus.operationList.add(
            { key: 'customShareModal', name: t('customShareModal'), disabled: noShare },
            insertIndex + 1
          );
          break;
        case 'folder':
          insertIndex = menus.operationList._data.findIndex((item) => item?.key === 'commonBatchDownload');
          menus.operationList.add(
            { key: 'customShareModal', name: t('customShareModal'), disabled: noShare },
            insertIndex - 1
          );
          break;
      }
    });

    kdrive.fileList.rightMenu.onClick(async (menuItem: MenuItem) => {
      if (menuItem.key === 'customShareModal') {
        // 获取分享权限
        const res = {
          data: {
            code: 200,
          },
        };
        if (res.data.code === 200) {
          const el = document.createElement('div');
          document.body.appendChild(el);
          const root = registerNode(el);
          root.render(
            <ShareModal
              destroy={() => unmount(el)}
              driveId={menuItem.data.driveId}
              fileId={menuItem.data.id}
              type={menuItem.data.type}
            />
          );
          return false;
        } else {
          message.error(t('serviceError'));
          return true;
        }
      }
    });
  };
  useEffect(() => {
    addEntrance();
  }, []);

  console.log('--- plugin end ---');
  return null;
};
export default React.memo(Plugin);
shareModal/index.tsx 分享弹窗的主要组件文件,包含了弹窗的逻辑和UI
import React, { useEffect, useState } from 'react';
import { Button, message, Modal, Switch } from 'antd';
import moment from 'moment';
import sdk from '../../../libs/sdk';
import copyLinkImg from '../../../assets/hyperlink_type.svg';
import { useTranslation } from 'react-i18next';
import styles from './index.module.less';

interface Props {
  destroy: () => void;
  driveId?: string;
  fileId?: string;
  type?: string;
}

export default function ShareModal(props: Props) {
  const { t } = useTranslation();
  // 分享弹窗显示
  const [shareVisible, setShareVisible] = useState<boolean>(true);
  // 是否开启
  const [isOpen, setIsOpen] = useState<boolean>(false);
  // 高级设置显示
  const [seniorVisible, setSeniorVisible] = useState<boolean>(false);
  // 开关状态
  const [switchChecked, setSwitchChecked] = useState<boolean>(false);
  // 复制链接
  const copyLink = () => {
    const content = `${window.location.origin}${sdk.utils.env.webpath}`;
    if (navigator.clipboard) {
      window.navigator.clipboard.writeText(content);
    } else {
      const textArea = document.createElement('textarea');
      textArea.value = content;
      document.body.appendChild(textArea);
      textArea.select();
      document.execCommand('copy');
      document.body.removeChild(textArea);
    }
    message.success(t('copySuccess'));
  };
  // 打开高级设置
  const openSetting = () => {
    setShareVisible(false);
    setSeniorVisible(true);
  };
  // 初始化查询
  const initShareLink = async () => {
    // 查询分享链接
    const res = {
      data: {
        code: 200,
        data: {
          id: 'dummy_id',
          status: 1,
          access_code: '自定义分享弹窗链接',
          expired_time: 'dummy_expired_time',
        },
      },
    };

    if (res.data.code === 200 && res.data.data) {
      // 判断链接当前状态
      if (
        res.data.data.status === 2 ||
        (res.data.data.expired_time && moment(new Date()).valueOf() > moment(res.data.data.expired_time).valueOf())
      ) {
        setIsOpen(false);
      } else {
        setIsOpen(true);
      }
    } else if (res.data.code !== 200) {
      message.error(t('queryLinkFailed'));
    }
  };

  useEffect(() => {
    initShareLink();
  }, []);

  return (
    <div>
      <Modal
        width={602}
        visible={shareVisible}
        title={t('customShareModalTitle')}
        footer={null}
        maskClosable={false}
        onCancel={() => props.destroy()}
      >
        <div className={styles['shareModal']}>
          <div className={styles['shareContent']}>
            <div className={styles['shareText']}>
              {isOpen && (
                <div>
                  <p>
                    <span style={{ color: '#2453DE' }}>{t('customShareModalContent')}</span>
                  </p>
                  <p>
                    <span style={{ color: '#0D0D0DA8' }}>
                      {t('customShareModalLink')}:{window.location.origin}
                      {sdk.utils.env.webpath}
                    </span>
                    <img className={styles['copyIcon']} onClick={copyLink} src={copyLinkImg} />
                  </p>
                </div>
              )}
            </div>
            <div className={styles['shareButton']}>
              {isOpen && (
                <p style={{ color: '#2453DE', cursor: 'pointer' }} onClick={copyLink}>
                  {t('copyLink')}
                </p>
              )}
            </div>
          </div>
          <div className={styles['seniorSetting']}>
            <Button
              disabled={!isOpen}
              style={{ color: '#0D0D0DA8', border: 'none', background: 'transparent' }}
              type="link"
              onClick={openSetting}
            >
              {t('advancedSettings')} {'>'}
            </Button>
          </div>
        </div>
      </Modal>
      <Modal
        title={t('advancedSettings')}
        visible={seniorVisible}
        footer={null}
        maskClosable={false}
        onCancel={() => {
          setSeniorVisible(false);
          setShareVisible(true);
        }}
      >
        <div className={styles['settingModal']}>
          <p className={styles['passwordAccess']}>
            <span>{t('linkPermanent')}</span>
            <Switch checked={switchChecked} onChange={setSwitchChecked} />
          </p>
        </div>
      </Modal>
    </div>
  );
}
shareModal/index.module.less
.shareModal {
  .shareContent {
    border: 1px solid lightgray;
    border-radius: 5px;
    display: flex;
    background-color: #f5f5f5;

    .shareText {
      width: 90%;
      display: flex;
      min-height: 40px;
      flex-direction: column;
      justify-content: center;
      background-color: white;
      padding: 10px 0px 10px 15px;
      border-top-left-radius: 5px;
      border-bottom-left-radius: 5px;

      .shareLink {
        color: #0D0D0DA8;
        width: auto;
        padding-left: 0px;
        max-width: 300px;
        text-overflow: ellipsis;
        display: inline-block;
        overflow-x: hidden;
        white-space: pre;
        vertical-align: middle;
      }

      .shareLink:hover {
        border: none;
      }

      .copyIcon {
        cursor: pointer;
        display: inline-flex;
        vertical-align: middle;
        color: '#2453DE';
        margin-left: 5px;
      }
    }

    .shareButton {
      width: 20%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
  }

  .seniorSetting {
    display: flex;
    flex-direction: row-reverse;
  }
}

.settingModal {
  padding: 10px 0px;

  .passwordAccess {
    display: flex;
    justify-content: space-between;
    margin-bottom: 15px;
  }
}
interface/shareModal.ts 定义了分享弹窗相关枚举
export const noShareSuffix = ['.otl', '.dbt', '.form', '.pom', '.pof'];
i18n/zh-CN.json + zh-HK.json + en-US.json
{“customShareModal”: “自定义分享弹窗”,”serviceError”: “服务异常”,”copySuccess”: “复制成功”,”queryLinkFailed”: “查询链接失败”,”customShareModalTitle”: “自定义分享弹窗标题”,”customShareModalContent”: “自定义分享弹窗内容”,”customShareModalLink”: “自定义分享弹窗跳转链接”,”copyLink”: “复制链接”,”advancedSettings”: “高级设置”,”linkPermanent”: “链接永久有效”}{“customShareModal”: “自定義分享彈框”,”serviceError”: “服務異常”,”copySuccess”: “複製成功”,”queryLinkFailed”: “查詢鏈接失敗”,”customShareModalTitle”: “自定義分享彈框標題”,”customShareModalContent”: “自定義分享彈框內容”,”customShareModalLink”: “自定義分享彈框跳轉鏈接”,”copyLink”: “複製鏈接”,”advancedSettings”: “高級設置”,”linkPermanent”: “鏈接永久有效”}{“customShareModal”: “Custom Share Modal”,”serviceError”: “Service Exception”,”copySuccess”: “Copy Success”,”queryLinkFailed”: “Query Link Failed”,”customShareModalTitle”: “Custom Share Modal Title”,”customShareModalContent”: “Custom Share Modal Content”,”customShareModalLink”: “Custom Share Modal Jump Link”,”copyLink”: “Copy Link”,”advancedSettings”: “Advanced Settings”,”linkPermanent”: “Link Permanent”}

最终演示效果

VRQ7K6A6ADAGQ

多语言效果对比

VRQ7K6A6ADAGQ

繁体

3E4PS6A6ACQB4

KYO7U6A6ABAGG

文件选择模式类型枚举介绍:

// 文件选择模式类型枚举
enum SELECT_MODE_TYPE {
  file = "file", // 选择文件模式
  folder = "folder", // 选择文件目录模式
}

// 左侧菜单类型枚举
enum MENU_TYPE {
  latest = "latest", // 最近列表
  tags = "tags", // 星标列表
  sharing = "sharing", // 共享列表
  space = "space", // 我的云文档列表
  group = "group", // 团队列表
  favorite = "favorite", // 常用列表
  devices = "devices", // 我的设备列表
}

// 文件类型过滤器枚举
enum FILE_FILTER_TYPE {
  IMG_ARRAY = "IMG_ARRAY", // 图片类型 ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg']
  ZIP_ARRAY = "ZIP_ARRAY", // 压缩包类型 ['zip', '7z', 'rar', 'iso', 'gz', 'tar']
  DOC_ARRAY = "DOC_ARRAY", // 文字类型 ['wps', 'wpt', 'doc', 'docx', 'dot', 'rtf', 'xml', 'docm', 'dotm', 'wdoc', 'uof', 'uot3', 'uott3']
  XLS_ARRAY = "XLS_ARRAY", // 表格类型 ['et', 'ett', 'xls', 'xlsx', 'xlsm', 'xlsb', 'xlam', 'xltx', 'xltm', 'xlt', 'xla', 'xlw', 'odc', 'uxdc', 'dbf', 'prn', 'wxls', 'csv']
  PPT_ARRAY = "PPT_ARRAY", // 演示类型 ['dps', 'dpt', 'pptx', 'ppt', 'pptm', 'ppsx', 'pps', 'ppsm', 'potx', 'pot', 'potm', 'wpd', 'wppt']
  TXT_ARRAY = "TXT_ARRAY", // 文本类型 ['txt', 'text']
  VIDEO_ARRAY = "VIDEO_ARRAY", // 视频类型 ['asf', 'avi', 'wm', 'wmp', 'wmv', 'ram', 'rm', 'rmvb', 'rp', 'rpm', 'rt', 'smi', 'smil', 'dat', 'm1v', 'm2p', 'm2t', 'm2ts', 'm2v', 'mp2v', 'mpe', 'mpeg', 'mpg', 'mpv2', 'pss', 'pva', 'tp', 'tpr', 'ts', 'm4b', 'm4p', 'm4v', 'mp4', 'mpeg4', '3g2', '3gp', '3gp2', '3gpp', 'mov', 'qt', 'f4v', 'flv', 'hlv', 'swf', 'ifo', 'vob', 'amv', 'bik', 'csf', 'divx', 'evo', 'ivm', 'mkv', 'mod', 'mts', 'ogm', 'pmp', 'scm', 'tod', 'vp6', 'webm', 'xlmv']
  AUDIO_ARRAY = "AUDIO_ARRAY", // 音频类型 ['aac', 'ac3', 'amr', 'ape', 'cda', 'dts', 'flac', 'm1a', 'm2a', 'm4a', 'mid', 'midi', 'mka', 'mp2', 'mp3', 'mpa', 'ogg', 'ra', 'tak', 'tta', 'wav', 'wma', 'wv']
}

// 路径类型枚举
enum PATH_TYPE {
  folder = "folder", // 文件夹
  whole = "whole", // 全员团队
  dept = "dept", // 部门团队
  normal = "normal", // 普通团队
}

// 文件类型枚举
enum FILE_TYPE {
  file = "file", // 文件类型
  folder = "folder", // 文件夹类型
  shortcut = "shortcut", // 快捷方式类型
}

相关新闻

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

立即咨询 立即试用 上门服务

请您留言

感谢您的关注,你可留下联系方式,我们将第一时间与您联系。