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-paperiframe标题栏文件名
原始900×424(margin 32px、圆角 12px、maxWidth 1200px)900×394显示
①+②1440×900(margin 0、圆角 0)1440×847隐藏
①+③1440×9001440×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)→ 保存。

机制(源码核对):

  1. src/redux/thunks/viewer.ts 的 openViewer():props.openInNew 为真且 viewer.type != "custom" 时, window.open(<当前 URL>?viewer=<viewerId>&open=<fileId>&size=…&version=…, "_blank"),不再在弹窗里打开。
  2. 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"这类结构性改动时才值得:

  1. 改 src/component/Viewers/Wopi.tsx(给 ViewerDialog 传 fullScreen、或包一层自定义壳)/ ViewerDialog.tsx(加 hideTitle 之类的开关)。
  2. 构建前端 → 产物目录重命名为 statics → 放到 Cloudreve 可执行文件同级的 data/ 下 → 重启, 日志出现 Folder with "statics" already exists, it will be used to serve static files. 即生效。
  3. 代价:升级后必须重新构建/覆盖;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 可靠):

  1. 打开任意一个 ViewerDialog 预览(例如新建 Text (.txt) 文件会自动开文本预览 —— 与 WOPI 共用同一套 ViewerDialog)。
  2. 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。
  3. 校验点:paper == viewport;div:has(.MuiTypography-subtitle2) 的 display 为 none;同弹窗里其它按钮不受影响; 另一个不含 #office_frame 的弹窗标题栏保持可见。
  4. 注意改动后同一次同步测量可能拿到过渡中的旧值(曾观察到 1080×506,稳定后为 1440×900)——隔一次再测。
  5. 「Save」之类 actions 按钮组在文件为空/不可保存时会被组件自己隐藏,别把它当成"CSS 误伤"。

10. 一句话决策树

  1. 要改的是文件徽标/文件名/边框那一层 → Cloudreve ViewerDialog → 用第 3 节 CSS。
  2. 要新标签页 → 后台 openInNew,然后仍需第 3 节 CSS 去掉标题栏。
  3. 要改 iframe 内部(logo 旁的文件名、OO 工具栏)→ 只能动 Document Server,先告诉对方"不受支持"再决定。
  4. 需要真·结构改动(去掉按钮、真 fullScreen prop)→ 第 7 节自定义前端,权衡升级成本。