oo-preview.html 8.6 KB

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