fix(frontend): bundle tdesign-icons sprite locally for offline deploys
The <t-icon> component from tdesign-vue-next lazily loads its SVG sprite from https://tdesign.gtimg.com/icon/<ver>/fonts/index.js at runtime. In air-gapped or intranet deployments this request fails and the whole UI loses its icons (agent cards, knowledge-base breadcrumbs/cards, upload buttons, etc.). Ship the sprite (tdesign-icons-vue-next@0.4.1) as a static asset under frontend/public/tdesign-icons/0.4.1/fonts/index.js and preload it from index.html so every <t-icon name="..."> resolves against local symbols. An offline guard injects stub <script>/<link> nodes that match tdesign's dedup selectors, so checkScriptAndLoad / checkLinkAndLoad short-circuit without ever hitting the CDN (covers both the 0.4.0 and 0.4.1 hardcoded URLs for forward-compat). Closes #867 Closes #897
This commit is contained in:
@@ -57,6 +57,12 @@
|
||||
})();
|
||||
</script>
|
||||
<script src="/config.js"></script>
|
||||
<!--
|
||||
离线部署兼容:提前本地加载 tdesign-icons SVG sprite,避免 tdesign-vue-next 的
|
||||
<t-icon> 运行时向 https://tdesign.gtimg.com 发起请求(见 issue #867 / #897)。
|
||||
升级 tdesign-icons-vue-next 时需同步更新 public/tdesign-icons/<version>/fonts/index.js。
|
||||
-->
|
||||
<script src="/tdesign-icons/0.4.1/fonts/index.js"></script>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
(function(){
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -10,6 +10,10 @@ import "@/assets/theme/theme.css";
|
||||
import "@/assets/dropdown-menu.less";
|
||||
import i18n from "./i18n";
|
||||
import { initTheme } from "@/composables/useTheme";
|
||||
import { installTDesignIconOfflineGuard } from "@/utils/tdesign-icon-offline";
|
||||
|
||||
// 必须在 Vue 组件挂载之前执行,避免 tdesign-icons 运行时请求 tdesign.gtimg.com
|
||||
installTDesignIconOfflineGuard();
|
||||
|
||||
initTheme();
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 阻断 tdesign-icons-vue-next 对外网 CDN 的 iconfont 请求。
|
||||
*
|
||||
* 背景(issue #867 / #897):
|
||||
* tdesign-icons-vue-next 的 Icon / IconFont 组件在 onMounted 时会通过
|
||||
* `checkScriptAndLoad` / `checkLinkAndLoad` 往 document 里注入指向
|
||||
* `https://tdesign.gtimg.com/icon/<version>/fonts/index.(js|css)` 的
|
||||
* <script> / <link>。在无外网环境下请求会失败,导致图标全部不渲染。
|
||||
*
|
||||
* 处理方式:在 Vue app 挂载之前预先插入符合 tdesign 匹配规则的占位节点,
|
||||
* 让 `checkScriptAndLoad` / `checkLinkAndLoad` 的去重判断命中、直接返回,
|
||||
* 不再追加指向 CDN 的真实节点。
|
||||
*
|
||||
* - 匹配选择器来源:tdesign-icons-vue-next/esm/utils/check-url-and-load.js
|
||||
* `.t-svg-js-stylesheet--unique-class[src="<url>"]`
|
||||
* `.t-iconfont-stylesheet--unique-class[href="<url>"]`
|
||||
* - 占位 <script> / <link> 使用非标准 type / rel,浏览器不会真正发起网络请求。
|
||||
*
|
||||
* SVG sprite 的符号则通过 index.html 中的 <script src="/tdesign-icons/.../index.js"></script>
|
||||
* 在本地提前注册,因此 <t-icon name="..."> 仍可正常渲染。
|
||||
*/
|
||||
|
||||
const SVG_SCRIPT_CLASS = "t-svg-js-stylesheet--unique-class";
|
||||
const ICONFONT_LINK_CLASS = "t-iconfont-stylesheet--unique-class";
|
||||
|
||||
// 对齐 tdesign-icons-vue-next 0.4.x 内部硬编码的地址;多版本号并存以兼容潜在升级。
|
||||
const BLOCKED_SCRIPT_URLS = [
|
||||
"https://tdesign.gtimg.com/icon/0.4.0/fonts/index.js",
|
||||
"https://tdesign.gtimg.com/icon/0.4.1/fonts/index.js",
|
||||
];
|
||||
|
||||
const BLOCKED_LINK_URLS = [
|
||||
"https://tdesign.gtimg.com/icon/0.4.0/fonts/index.css",
|
||||
"https://tdesign.gtimg.com/icon/0.4.1/fonts/index.css",
|
||||
];
|
||||
|
||||
let installed = false;
|
||||
|
||||
export function installTDesignIconOfflineGuard(): void {
|
||||
if (installed || typeof document === "undefined") return;
|
||||
installed = true;
|
||||
|
||||
const body = document.body;
|
||||
if (!body) {
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => installTDesignIconOfflineGuard(),
|
||||
{ once: true },
|
||||
);
|
||||
installed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
BLOCKED_SCRIPT_URLS.forEach((src) => {
|
||||
const exists = document.querySelector(
|
||||
`script.${SVG_SCRIPT_CLASS}[src="${src}"]`,
|
||||
);
|
||||
if (exists) return;
|
||||
const stub = document.createElement("script");
|
||||
stub.setAttribute("class", SVG_SCRIPT_CLASS);
|
||||
stub.setAttribute("src", src);
|
||||
// 非标准 MIME 类型会让浏览器跳过脚本的 fetch/执行阶段
|
||||
stub.setAttribute("type", "text/no-load");
|
||||
stub.setAttribute("data-weknora-blocked-cdn", "tdesign-icons");
|
||||
body.appendChild(stub);
|
||||
});
|
||||
|
||||
BLOCKED_LINK_URLS.forEach((href) => {
|
||||
const exists = document.querySelector(
|
||||
`link.${ICONFONT_LINK_CLASS}[href="${href}"]`,
|
||||
);
|
||||
if (exists) return;
|
||||
const stub = document.createElement("link");
|
||||
stub.setAttribute("class", ICONFONT_LINK_CLASS);
|
||||
stub.setAttribute("href", href);
|
||||
// 不声明 rel="stylesheet",浏览器不会发起样式表请求
|
||||
stub.setAttribute("rel", "preload-blocked");
|
||||
stub.setAttribute("data-weknora-blocked-cdn", "tdesign-icons");
|
||||
document.head.appendChild(stub);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user