企业需要在文件列表的右键菜单栏中,新增其它定制菜单项
如何在文件列表新增右键菜单项?
本定制方案仅适用于私有化WPS365版本
1. 方案概述
有时候,企业需要给不在组织通讯录中的成员共享文档,传统流程是把文档下载下来,通过邮件发送,易导致效率瓶颈。
现在,通过文档中心的扩展能力,可以直接在文件列表右键菜单新增自定义分享弹窗入口,简化协作流程。
这一功能不仅支持自定义分享弹窗,企业也可根据实际需要,在文件列表的右键菜单栏中,新增其它定制菜单项。
我们可以使用文档中心Web端(PC端)的开放能力,实现文件及文件夹的分享:
| 实现效果 | 定制成功后,点击右键选择自定义分享弹窗,复制文档链接即可对外分享,也可以在高级设置中设置链接是否永久有效。用户获取链接后,可在无账号情况下查看或下载文件,并对免登录分享文件进行管理以及日志记录。![]() |
| 支持端 | 文档中心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”} |
最终演示效果

多语言效果对比
中

繁体

英

文件选择模式类型枚举介绍:
// 文件选择模式类型枚举
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", // 快捷方式类型
}
