Cloudreve WOPI 预览界面定制
Cloudreve v4 通过 WOPI 集成 OnlyOffice / Collabora / Office Online Server 时,页面上会出现 两个同名的文件标题。 先判断你要改的是哪一个,再动手 —— 这是本技能里最容易搞错、也最容易白费功夫的地方。
1. 先分清图层(决定"能不能改")
| 位置 | 由谁渲染 | 数据来源 | 可否定制 |
|---|---|---|---|
| 页面最顶部那条标题栏(文件类型图标 + 文件名 + 右上角全屏/关闭按钮,下方有分隔线) | Cloudreve 自己:src/component/Viewers/ViewerDialog.tsx(被 Wopi.tsx 调用,MUI Dialog) | Redux globalState.wopiViewer.file.name | ✅ 后台配置即可 |
再下面一行(ONLYOFFICE logo + 文件名;编辑器 iframe 内部) | Document Server 的 WOPI 页(hosting/wopi/...,跑在 iframe 里) | WOPI CheckFileInfo.BaseFileName,Cloudreve 后端 service/explorer/viewer.go 返回 | ❌ 见第 6 节 |
关键点:Cloudreve 在这套流程里是 WOPI host(pkg/wopi/、routers/controllers/wopi.go),它是把 DS 的 actionUrl
通过一个隐藏表单 #office_form POST(带 access_token)提交进 iframe#office_frame,所以:
- 编辑器始终包在 Cloudreve 的
ViewerDialog里 → 想"没有标题栏"必须处理 Cloudreve 这一层; - 没法直接把 OO 地址甩到新标签页(缺
access_token)。
2. 配置入口速查
| 需求 | 位置 | 配置键 |
|---|---|---|
| 注入 CSS / 自定义 HTML | 后台 → 参数设置 → 站点信息 → 页脚代码 | siteScript(原样替换到 index.html 的 {siteScript} 处,middleware/frontend.go) |
| 在新标签页打开 | 后台 → 参数设置 → 文件系统 → 应用列表 → 编辑该 WOPI 应用 → 「在新窗口直接打开」 | props.openInNew(Viewer 配置对象的 props;en 界面标签 “Open in new window”) |
| 导入 / 调整 WOPI 应用 | 后台 → 参数设置 → 文件系统 → 应用列表 → 添加应用 → WOPI 协议应用(填 <DS>/hosting/discovery) | file_viewers |
| 让某类文件默认用 WOPI 打开 | 后台 → 参数设置 → 文件系统 → 应用列表 → 默认打开方式(按扩展名指定应用) | viewer_default_apps |
「页脚代码」的中文说明是"在页面底部插入的自定义 HTML 代码。"(写进去的内容会原样替换到 index.html 末尾,可含 <style>/<script>)。
siteScript 是唯一能注入任意 HTML/CSS/JS 的官方入口(Appearance 里的 headless_footer_html / sidebar_bottom_html 只作用于登录页和侧边栏,够不到预览弹窗)。
3. 配方:默认全屏 + 去掉标题栏
写入后台「页脚代码」:
<style id="wopi-fullscreen">
/* ① 让预览弹窗铺满整个视口(等价于 MUI 自带的 fullScreen 变体) */
.MuiDialog-root:has(#office_frame) > .MuiDialog-container > .MuiDialog-paper {
margin: 0 !important;
width: 100% !important;
max-width: 100% !important;
height: 100% !important;
max-height: none !important;
border-radius: 0 !important;
}
/* ② 只隐藏「文件图标 + 文件名」那一组,右上角全屏/关闭按钮保留 */
.MuiDialog-root:has(#office_frame) #draggable-dialog-title > div:has(.MuiTypography-subtitle2) {
display: none !important;
}
/* ③ 连整条标题栏(含按钮)都不要时,用这两行替换 ②:编辑器占满 100% 视口 */
/* .MuiDialog-root:has(#office_frame) #draggable-dialog-title,
.MuiDialog-root:has(#office_frame) #draggable-dialog-title + .MuiDivider-root { display: none !important; } */
</style>
实测(Cloudreve 4.18.0,1440×900 视口,真实 ViewerDialog + 按 Wopi.tsx 同款样式注入的 iframe#office_frame):
| 场景 | .MuiDialog-paper | iframe | 标题栏文件名 |
|---|---|---|---|
| 原始 | 900×424(margin 32px、圆角 12px、maxWidth 1200px) | 900×394 | 显示 |
| ①+② | 1440×900(margin 0、圆角 0) | 1440×847 | 隐藏 |
| ①+③ | 1440×900 | 1440×900 | 隐藏 |
只要全屏、不动标题栏 → 只保留 ①。
选择器为什么这么写(踩过的坑)
#office_frame是 WOPI 弹窗的唯一稳定标记(Wopi.tsx里写死的 iframe id)。用.MuiDialog-root:has(#office_frame)做作用域: 从 OnlyOffice 里触发的「分享 / 历史版本」弹窗(Cloudreve 通过 postMessage →openShareDialog/setVersionControlDialog打开)是另外的.MuiDialog-root,不会被误伤。- 不要用
> div:first-child:ViewerDialog的标题栏结构是[actions?][文件图标+文件名][全屏/关闭按钮],actions只有部分预览组件(CodeViewer 等)会传,WOPI 不传 —— 但按位置写选择器迟早会被别的调用方打破。按内容定位:div:has(.MuiTypography-subtitle2)。 p.MuiTypography-subtitle2是错的:文件名由NoWrapTypography variant="subtitle2"渲染,实际元素是h6(<h6 class="MuiTypography-root MuiTypography-subtitle2">)。按p匹配会静默失效 —— 只写类名,别写标签名。- 需要
:has()(Chrome 105+ / Firefox 121+ / Safari 15.4+)。老浏览器请改用注入 JS 的方式(见第 7 节)。 !important必须带:要压过 MUI 生成的paperWidthLg(max-width:1200px)、paperFullWidth(width:calc(100% - 64px))、margin:32px、max-height:calc(100% - 64px)、圆角等运行时类。
4. 配方:在新标签页打开
后台 → 参数设置 → 文件系统 → 应用列表 → 编辑 WOPI 应用 → 打开 「在新窗口直接打开」(props.openInNew)→ 保存。
机制(源码核对):
src/redux/thunks/viewer.ts的openViewer():props.openInNew为真且viewer.type != "custom"时,window.open(<当前 URL>?viewer=<viewerId>&open=<fileId>&size=…&version=…, "_blank"),不再在弹窗里打开。src/redux/thunks/filemanager.ts的checkOpenViewerQuery()在新标签页里读这几个参数,history.replaceState清掉它们, 在已加载的目录列表里找到该文件,再openViewer(..., forceNotOpenInNew=true)—— 防止递归。
结论:新标签页里仍然是同一个 ViewerDialog(因为要 POST access_token),所以"没有整条标题栏"依旧靠第 3 节的 CSS;
好处是新标签页没有文件管理器边框,看起来更像原生应用。
5. 已核实的 DOM 钩子(Cloudreve 4.18.0 压缩产物)
iframe#office_frame / iframe[name=office_frame] Wopi.tsx 写入的编辑器 iframe(sandbox: allow-scripts allow-same-origin …)
form#office_form[target=office_frame] POST access_token 到 DS actionUrl 的隐藏表单
h2#draggable-dialog-title.MuiDialogTitle-root ViewerDialog 标题栏(styled(DialogTitle),flex + space-between)
├─ div (actions?) WOPI 无此节点
├─ div > svg(FileIcon) + h6.MuiTypography-subtitle2(文件名) + Chip(只读)
└─ div > IconButton(全屏) + IconButton(Dismiss)
hr.MuiDivider-root 标题栏下的分隔线
.MuiDialog-root > .MuiDialog-container > .MuiDialog-paper 弹窗层级链(MUI v6)
自查命令(不用跑前端,直接从发行版二进制里掏内置前端):
./cloudreve eject # 导出内置前端到 data/statics(会覆盖运行中的自定义 statics 来源)
grep -o '.\{0,40\}office_frame.\{0,60\}' data/statics/assets/*.js
grep -o '.\{280\}draggable-dialog-title.\{420\}' data/statics/assets/*.js # 标题栏 JSX 结构
grep -o 'at("MuiDialogTitle",\^*\]' data/statics/assets/*.js # MUI slot 类名生成
6. 改不了的:ONLYOFFICE 那一行标题
- 它由 DS 的 WOPI 页用
CheckFileInfo.BaseFileName渲染,Cloudreve 只能返回真实文件名(否则保存/重命名会坏)。 - WOPI 协议没有传
editorConfig的通道:官方wopi.*配置项里没有任何 customization 参数。 - 官方 API 参数
customization.toolbarHideFileName(且要求compactHeader+toolbarNoTabs同时为 true)只在自建 config 的 Docs API 模式生效,WOPI 模式读不到。 - 因此要真去掉只能改 Document Server 侧的模板/资源,属不受支持的改动;正常做法是接受它,或用第 3 节的 CSS 把 Cloudreve 那层做干净。
7. 备选:真的想改前端源码
只有需要"新标签页 + 真·全屏 prop + 去掉 actions"这类结构性改动时才值得:
- 改
src/component/Viewers/Wopi.tsx(给ViewerDialog传fullScreen、或包一层自定义壳)/ViewerDialog.tsx(加hideTitle之类的开关)。 - 构建前端 → 产物目录重命名为
statics→ 放到 Cloudreve 可执行文件同级的data/下 → 重启, 日志出现Folder with "statics" already exists, it will be used to serve static files.即生效。 - 代价:升级后必须重新构建/覆盖;
data/statics/version.json的Version与后端不一致时会打 version mismatch 错误(仅日志,不致命)。
不想改源码但又拿不到 :has() 时:同样可以把一小段 JS 放进「页脚代码」,用 MutationObserver 等 #office_frame 出现后给对应弹窗打 class。CSS 方案更简单,优先用它。
8. 生效性与缓存(务必提醒对方)
- Cloudreve 注册了 ServiceWorker,
index.html在sw.js的预缓存清单里并绑定了NavigationRoute: 已访问过的用户看不到新注入的 CSS,需要清一次站点数据(F12 → Application → Storage → Clear site data)或等静态资源版本更新提示后刷新;新访客直接生效。 - 后台「页脚代码」保存走正常设置接口,会自动失效服务端设置缓存;直接改数据库
settings表(本地测试用)需要重启进程才会读到新值。
9. 本地复现 / 验证配方
# 1. 起一个干净的实例(v4 默认 SQLite,端口 5212)
curl -LO https://github.com/cloudreve/cloudreve/releases/download/4.18.0/cloudreve_4.18.0_linux_amd64.tar.gz
tar xzf cloudreve_4.18.0_linux_amd64.tar.gz && ./cloudreve # 用 hub start,别用前台 bash
# 2. 造管理员:浏览器 /session/signup 注册首个账号(首个用户即管理员);
# 或纯 API:POST /api/v4/user 注册,POST /api/v4/session/token 拿 token
# (前端把会话存在 localStorage 键 cloudreve_session:{current, sessions:{<uid>:{user,token,settings}}, anonymousSettings})
# 3. 注入 CSS(本地测试可直接改库;settings.name='siteScript',注意 created_at 非空、name 唯一)
# SELECT id,value FROM settings WHERE name='siteScript';
# 4. 验证注入是否落到页面
curl -s http://localhost:5212/ | tail -c 300 # 应看到 <style id="…">…;/home 同样注入(SPA fallback 走同一模板)
浏览器里量尺寸的做法(浏览器实测比读 CSS 可靠):
- 打开任意一个 ViewerDialog 预览(例如新建
Text (.txt)文件会自动开文本预览 —— 与 WOPI 共用同一套ViewerDialog)。 - DevTools 里往
.MuiDialog-paper追加一个<iframe id="office_frame" style="width:100%;height:100%;min-height:calc(100vh - 200px);border:none">, 让:has(#office_frame)命中,然后测.MuiDialog-paper的getBoundingClientRect()、标题栏各子节点的display。 - 校验点:paper == viewport;
div:has(.MuiTypography-subtitle2)的 display 为none;同弹窗里其它按钮不受影响; 另一个不含#office_frame的弹窗标题栏保持可见。 - 注意改动后同一次同步测量可能拿到过渡中的旧值(曾观察到 1080×506,稳定后为 1440×900)——隔一次再测。
- 「Save」之类
actions按钮组在文件为空/不可保存时会被组件自己隐藏,别把它当成"CSS 误伤"。
10. 一句话决策树
- 要改的是文件徽标/文件名/边框那一层 → Cloudreve
ViewerDialog→ 用第 3 节 CSS。 - 要新标签页 → 后台
openInNew,然后仍需第 3 节 CSS 去掉标题栏。 - 要改 iframe 内部(logo 旁的文件名、OO 工具栏)→ 只能动 Document Server,先告诉对方"不受支持"再决定。
- 需要真·结构改动(去掉按钮、真 fullScreen prop)→ 第 7 节自定义前端,权衡升级成本。