oo-preview.html 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>OnlyOffice 预览</title>
  7. <style>
  8. html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:#f5f7fa}
  9. .bar{padding:10px 14px;background:#111827;color:#fff;font-size:14px;word-break:break-all}
  10. .bar a{color:#93c5fd;text-decoration:none}
  11. .err{color:#fecaca}
  12. .ok{color:#86efac}
  13. .warn{color:#fde68a}
  14. #editor{height:calc(100% - 44px)}
  15. </style>
  16. <script src="/onlyoffice/web-apps/apps/api/documents/api.js"></script>
  17. </head>
  18. <body>
  19. <div class="bar" id="hint"></div>
  20. <div id="editor"></div>
  21. <script>
  22. const hintEl = document.getElementById("hint");
  23. const p = new URLSearchParams(location.search);
  24. const rawUrl = p.get("url") || "";
  25. const title = p.get("title") || (rawUrl ? rawUrl.split("/").pop() : "untitled.docx");
  26. const fileType = (p.get("type") || title.split(".").pop() || "docx").toLowerCase();
  27. const fileDocType = docType(fileType);
  28. const isCellDoc = fileDocType === "cell";
  29. const requestedMode = (p.get("mode") || "").toLowerCase();
  30. const forceReadOnly = (p.get("readonly") || "").toLowerCase() === "1";
  31. const editorMode = requestedMode || (isCellDoc ? "edit" : "view");
  32. function docType(ext){
  33. if(["xlsx","xls","csv","ods"].includes(ext)) return "cell";
  34. if(["pptx","ppt","odp"].includes(ext)) return "slide";
  35. return "word";
  36. }
  37. function stableKey(str){
  38. let h = 2166136261;
  39. for (const ch of str) {
  40. h ^= ch.codePointAt(0);
  41. h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
  42. }
  43. return (h >>> 0).toString(16);
  44. }
  45. function hasExt(name){ return /\.[^\/]+$/.test(name || ""); }
  46. function inferDirFromReferrer(){
  47. if(!document.referrer) return "";
  48. try {
  49. const r = new URL(document.referrer);
  50. let pth = decodeURIComponent(r.pathname || "");
  51. if (!pth.startsWith("/files/")) return "";
  52. let rel = pth.slice(7).replace(/^\/+|\/+$/g, "");
  53. if(!rel) return "";
  54. const arr = rel.split("/");
  55. if(arr.length && hasExt(arr[arr.length - 1])) arr.pop();
  56. return arr.join("/");
  57. } catch (e) {
  58. return "";
  59. }
  60. }
  61. function normalizeRawUrl(input, titleText) {
  62. try {
  63. const u = new URL(input, location.origin);
  64. let path = decodeURIComponent(u.pathname);
  65. const t = decodeURIComponent(titleText || "").replace(/^\/+/, "");
  66. const dup = path.match(/^(.*\/)([^\/]+\.[^\/]+)\/\2$/);
  67. if (dup) path = dup[1] + dup[2];
  68. const mFile = path.match(/^(\/raw\/files\/)([^\/]+\.[^\/]+)$/);
  69. if (mFile) {
  70. const base = mFile[2];
  71. let resolved = "";
  72. if (t.includes("/") && (t === base || t.endsWith("/" + base))) {
  73. resolved = t;
  74. }
  75. if (!resolved) {
  76. const refDir = inferDirFromReferrer();
  77. if (refDir) resolved = refDir + "/" + base;
  78. }
  79. if (resolved) path = mFile[1] + resolved;
  80. }
  81. u.pathname = path;
  82. return u.toString();
  83. } catch (e) {
  84. return input;
  85. }
  86. }
  87. if(!rawUrl){
  88. hintEl.innerHTML = '<span class="err">缺少参数: ?url=&lt;文件URL&gt;&type=&lt;扩展名&gt;&title=&lt;文件名&gt;</span>';
  89. throw new Error("missing file url");
  90. }
  91. const normalizedRawUrl = normalizeRawUrl(rawUrl, title);
  92. let fileUrl = normalizedRawUrl;
  93. try {
  94. fileUrl = encodeURI(decodeURIComponent(normalizedRawUrl));
  95. } catch (e) {
  96. fileUrl = encodeURI(normalizedRawUrl);
  97. }
  98. const nonce = `${Date.now()}-${Math.random().toString(16).slice(2,8)}`;
  99. const fileUrlWithNonce = fileUrl + (fileUrl.includes("?") ? "&" : "?") + `_v=${nonce}`;
  100. const docKey = `${stableKey(fileUrlWithNonce)}-${stableKey(title)}-${fileType}-${nonce}`.slice(0, 120);
  101. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='warn'>正在检测文件可达性...</span>`;
  102. async function precheck(){
  103. try {
  104. const r = await fetch(fileUrlWithNonce, { method: "GET", cache: "no-store" });
  105. if (!r.ok) {
  106. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='err'>源文件检测失败: HTTP ${r.status}</span>`;
  107. return false;
  108. }
  109. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='ok'>源文件检测通过</span>`;
  110. return true;
  111. } catch (e) {
  112. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='err'>源文件检测异常: ${String(e && e.message ? e.message : e)}</span>`;
  113. return false;
  114. }
  115. }
  116. function render(){
  117. const permissions = isCellDoc
  118. ? {
  119. edit: !forceReadOnly,
  120. review: false,
  121. comment: false,
  122. chat: false,
  123. copy: true,
  124. download: true,
  125. print: true
  126. }
  127. : {
  128. edit: false,
  129. review: false,
  130. comment: false,
  131. copy: true,
  132. download: true,
  133. print: true
  134. };
  135. const customization = {
  136. autosave: false,
  137. forcesave: false,
  138. compactHeader: false,
  139. toolbarNoTabs: false,
  140. statusBar: true
  141. };
  142. if (isCellDoc) {
  143. customization.showVerticalScroll = true;
  144. customization.showHorizontalScroll = true;
  145. customization.toolbar = true;
  146. customization.leftMenu = true;
  147. customization.rightMenu = false;
  148. customization.layout = {
  149. toolbar: true,
  150. leftMenu: true,
  151. rightMenu: false,
  152. statusBar: true
  153. };
  154. }
  155. try {
  156. new DocsAPI.DocEditor("editor", {
  157. documentType: fileDocType,
  158. type: "desktop",
  159. width: "100%",
  160. height: "100%",
  161. document: {
  162. title: title,
  163. url: fileUrlWithNonce,
  164. fileType: fileType,
  165. key: docKey,
  166. permissions: permissions
  167. },
  168. editorConfig: {
  169. mode: editorMode,
  170. lang: "zh-CN",
  171. callbackUrl: location.origin + "/onlyoffice-callback",
  172. user: { id: "preview-user", name: "preview-user" },
  173. customization: customization
  174. },
  175. events: {
  176. onAppReady: function(){
  177. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='ok'>编辑器已启动(模式: ${editorMode})</span>`;
  178. },
  179. onDocumentReady: function(){
  180. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='ok'>文档已加载</span>`;
  181. },
  182. onError: function(e){
  183. const code = e && e.data && e.data.errorCode;
  184. const desc = e && e.data && e.data.errorDescription;
  185. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='err'>OnlyOffice错误: code=${code||"unknown"} ${desc||""}</span>`;
  186. },
  187. onWarning: function(e){
  188. const code = e && e.data && e.data.warningCode;
  189. const desc = e && e.data && e.data.warningDescription;
  190. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='warn'>OnlyOffice警告: code=${code||"unknown"} ${desc||""}</span>`;
  191. }
  192. }
  193. });
  194. } catch (e) {
  195. hintEl.innerHTML = `<span class='err'>预览初始化失败: ${String(e && e.message ? e.message : e)}</span>`;
  196. throw e;
  197. }
  198. }
  199. precheck().then(render);
  200. </script>
  201. </body>
  202. </html>