oo-preview.html 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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. function docType(ext){
  28. if(["xlsx","xls","csv","ods"].includes(ext)) return "cell";
  29. if(["pptx","ppt","odp"].includes(ext)) return "slide";
  30. return "word";
  31. }
  32. function stableKey(str){
  33. let h = 2166136261;
  34. for (const ch of str) {
  35. h ^= ch.codePointAt(0);
  36. h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
  37. }
  38. return (h >>> 0).toString(16);
  39. }
  40. function hasExt(name){ return /\.[^\/]+$/.test(name || ""); }
  41. function inferDirFromReferrer(){
  42. if(!document.referrer) return "";
  43. try {
  44. const r = new URL(document.referrer);
  45. let pth = decodeURIComponent(r.pathname || "");
  46. if (!pth.startsWith("/files/")) return "";
  47. let rel = pth.slice(7).replace(/^\/+|\/+$/g, "");
  48. if(!rel) return "";
  49. const arr = rel.split("/");
  50. if(arr.length && hasExt(arr[arr.length - 1])) arr.pop();
  51. return arr.join("/");
  52. } catch (e) {
  53. return "";
  54. }
  55. }
  56. function normalizeRawUrl(input, titleText) {
  57. try {
  58. const u = new URL(input, location.origin);
  59. let path = decodeURIComponent(u.pathname);
  60. const t = decodeURIComponent(titleText || "").replace(/^\/+/, "");
  61. const dup = path.match(/^(.*\/)([^\/]+\.[^\/]+)\/\2$/);
  62. if (dup) path = dup[1] + dup[2];
  63. const mFile = path.match(/^(\/raw\/files\/)([^\/]+\.[^\/]+)$/);
  64. if (mFile) {
  65. const base = mFile[2];
  66. let resolved = "";
  67. if (t.includes("/") && (t === base || t.endsWith("/" + base))) {
  68. resolved = t;
  69. }
  70. if (!resolved) {
  71. const refDir = inferDirFromReferrer();
  72. if (refDir) resolved = refDir + "/" + base;
  73. }
  74. if (resolved) path = mFile[1] + resolved;
  75. }
  76. u.pathname = path;
  77. return u.toString();
  78. } catch (e) {
  79. return input;
  80. }
  81. }
  82. if(!rawUrl){
  83. hintEl.innerHTML = '<span class="err">缺少参数: ?url=&lt;文件URL&gt;&type=&lt;扩展名&gt;&title=&lt;文件名&gt;</span>';
  84. throw new Error("missing file url");
  85. }
  86. const normalizedRawUrl = normalizeRawUrl(rawUrl, title);
  87. let fileUrl = normalizedRawUrl;
  88. try {
  89. fileUrl = encodeURI(decodeURIComponent(normalizedRawUrl));
  90. } catch (e) {
  91. fileUrl = encodeURI(normalizedRawUrl);
  92. }
  93. const nonce = `${Date.now()}-${Math.random().toString(16).slice(2,8)}`;
  94. const fileUrlWithNonce = fileUrl + (fileUrl.includes("?") ? "&" : "?") + `_v=${nonce}`;
  95. const docKey = `${stableKey(fileUrlWithNonce)}-${stableKey(title)}-${fileType}-${nonce}`.slice(0, 120);
  96. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='warn'>正在检测文件可达性...</span>`;
  97. async function precheck(){
  98. try {
  99. const r = await fetch(fileUrlWithNonce, { method: "GET", cache: "no-store" });
  100. if (!r.ok) {
  101. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='err'>源文件检测失败: HTTP ${r.status}</span>`;
  102. return false;
  103. }
  104. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='ok'>源文件检测通过</span>`;
  105. return true;
  106. } catch (e) {
  107. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='err'>源文件检测异常: ${String(e && e.message ? e.message : e)}</span>`;
  108. return false;
  109. }
  110. }
  111. function render(){
  112. try {
  113. new DocsAPI.DocEditor("editor", {
  114. documentType: docType(fileType),
  115. type: "desktop",
  116. width: "100%",
  117. height: "100%",
  118. document: {
  119. title: title,
  120. url: fileUrlWithNonce,
  121. fileType: fileType,
  122. key: docKey
  123. },
  124. editorConfig: {
  125. mode: "view",
  126. lang: "zh-CN",
  127. customization: { autosave: false, forcesave: false }
  128. },
  129. events: {
  130. onAppReady: function(){
  131. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='ok'>编辑器已启动</span>`;
  132. },
  133. onDocumentReady: function(){
  134. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='ok'>文档已加载</span>`;
  135. },
  136. onError: function(e){
  137. const code = e && e.data && e.data.errorCode;
  138. const desc = e && e.data && e.data.errorDescription;
  139. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='err'>OnlyOffice错误: code=${code||"unknown"} ${desc||""}</span>`;
  140. },
  141. onWarning: function(e){
  142. const code = e && e.data && e.data.warningCode;
  143. const desc = e && e.data && e.data.warningDescription;
  144. hintEl.innerHTML = `预览中: ${title} | <a href='${fileUrl}' target='_blank'>原文件</a> | <span class='warn'>OnlyOffice警告: code=${code||"unknown"} ${desc||""}</span>`;
  145. }
  146. }
  147. });
  148. } catch (e) {
  149. hintEl.innerHTML = `<span class='err'>预览初始化失败: ${String(e && e.message ? e.message : e)}</span>`;
  150. throw e;
  151. }
  152. }
  153. precheck().then(render);
  154. </script>
  155. </body>
  156. </html>