feat(theme): 接入云海子主题通道(A 类免插件,规范 v1.12 §2/§3)
build / build (push) Successful in 3m22s

- index.html 插 A 类 loader:唯一接入点,FOUC 门/manifest 预取/主题 link 追加与排序
  全由通道 yh-theme-init.js 承担,主题升级走 /static 通道,本应用无需重新构建
- vite.config 加 /static dev 代理(YH_THEME_PROXY_TARGET 可覆盖,默认线上通道)
- host.config 回写 __APP_CONFIG__.serviceHost(仅同源):init 的 appearance 请求有硬性
  同源限制,跨源会被直接跳过;本地 dev 保持空串走同源 /api 代理
- 顶栏头像接平台键 yh-avatar,监听 yh-appearance-updated,userInfo 兜底
- view-ui-plus 声明锁 1.3.24(对齐通道皮肤生成版本)
This commit is contained in:
lijing
2026-10-21 17:35:24 +08:00
parent 0ca8c08e8d
commit 64de9a0548
6 changed files with 78 additions and 3 deletions
+4
View File
@@ -5,6 +5,10 @@
# 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境。
# ⚠️ VITE_SERVICE_HOST 必须留空:host.config 设计约定 dev 模式走同源代理,
# 配置远端地址会导致浏览器直连被 CAS 过滤器拦截(无 CORS 头),全部请求失败
# 主题通道(A 类免插件接入 §2):index.html 的 loader 与 vite.config.ts 的 /static 代理已就位,
# 本地 dev 的 /static/** 由代理兜到线上通道;联调本地通道源时用环境变量覆盖代理目标:
# YH_THEME_PROXY_TARGET=http://localhost:9527 npm run dev
VITE_BASE_HOST=http://base.yunhaixingtong.com
VITE_SSO_HOST=http://sso.yunhaixingtong.com
VITE_SERVICE_HOST=
+20
View File
@@ -25,6 +25,26 @@
}
})();
</script>
<!-- 主题通道引导(A 类免插件接入,见 YschTheme《管理端主题远程化与公共静态通道方案》§11):
唯一接入点。不装 npm 依赖、不改构建配置;FOUC 门、manifest 预取、主题 <link> 追加与
排序(晚于 viewuiplus.css)全由通道 yh-theme-init.js 承担,主题升级走 /static 通道,
本应用无需重新构建。
origin 零写死,优先级四档:__APP_CONFIG__.staticHost(专用通道覆盖)
> __APP_CONFIG__.baseHost(env-config 在线维护,换域名改配置即生效)
> %VITE_THEME_HOST%(仅 dev 注入的跨源直连,需通道已开 CORS)
> 同源相对 "/static/…"(生产常态:本应用部署在 base 域 /<app>/front/,与通道同源;
本地 dev 由 vite.config 的 /static 代理兜到通道)。
document.write 是有意为之:保证 parser-blocking,让档位类与 FOUC 门先于首帧;
build 不读 .env,占位符留字面量 → 判首字符 % 置空 → 产物零环境值烧入。 -->
<script>
(function () {
var c = window.__APP_CONFIG__ || {};
var themeHost = "%VITE_THEME_HOST%";
if (themeHost.charAt(0) === "%") themeHost = "";
var host = String(c.staticHost || c.baseHost || themeHost).replace(/\/+$/, "");
document.write('<script src="' + host + '/static/js/yh-theme-init.js"><\/script>');
})();
</script>
<title><%- title %></title>
<!-- 业务模块组件的全局依赖(照搬 NoticeFront/index.html 的加载方式)。
★ zTree 是 jQuery 插件,**必须**在 jquery 之后、且在应用脚本之前加载:
+1 -1
View File
@@ -19,7 +19,7 @@
"mitt": "^3.0.1",
"pinia": "^2.1.4",
"sass": "^1.64.1",
"view-ui-plus": "^1.3.16",
"view-ui-plus": "1.3.24",
"vue": "^3.4.19",
"vue-router": "4"
},
+26
View File
@@ -78,4 +78,30 @@ if (ishttps) {
}
}
/**
* 回写 __APP_CONFIG__.serviceHost(仅同源场景)——云海子 A 类接入 §3 外观三键。
* 主题通道 yh-theme-init.js 的 appearance 请求有硬性同源限制:
* if (!url || !appearanceSameOrigin(url)) return; → 跨源会被直接跳过、连请求都不发。
* 本地 dev:serviceHost 是 VITE_SERVICE_HOST 的绝对地址(跨源),此时不能回写,
* 保持空串让 init 走同源 "/api/appearance",由 vite 代理转发到后端。
* 生产:serviceHost = 同域 ctx(https://base.../<contextPath>),同源,回写后 init 请求正确前缀。
* 已有 env-config 下发值时不动(以平台配置为准)。
*/
try {
const w = window as any;
w.__APP_CONFIG__ = w.__APP_CONFIG__ || {};
if (!w.__APP_CONFIG__.serviceHost) {
const candidate = conf.serviceHost || "";
let sameOrigin = !candidate;
try {
sameOrigin = new URL(candidate, window.location.href).origin === window.location.origin;
} catch {
sameOrigin = false;
}
if (sameOrigin) w.__APP_CONFIG__.serviceHost = candidate;
}
} catch {
/* 极端环境无 window:忽略,保持同源兜底 */
}
export default conf;
+21 -2
View File
@@ -8,7 +8,7 @@
* 点一下就等于发出一句完整请求
* 3. 使用提示 —— 常驻说明"信息不全也没关系",降低首次使用门槛
*/
import { computed, ref } from "vue";
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { Message } from "view-ui-plus";
import { baseApi } from "@/api/module/agent";
import conf from "@/assets/config/host.config";
@@ -23,6 +23,25 @@ const emit = defineEmits<{
(e: "new-session"): void;
}>();
/* 头像:平台键 yh-avatar(yh-theme-init 外观补齐段写入)优先,本仓用户信息兜底;
监听平台事件重读,不轮询、不手写清理(清键归 init,规范 §3) */
const yhAvatar = ref("");
const readYhAvatar = () => {
let v: string | null = null;
try {
v = localStorage.getItem("yh-avatar");
} catch {
v = null;
}
yhAvatar.value = v || "";
};
onMounted(() => {
readYhAvatar();
document.addEventListener("yh-appearance-updated", readYhAvatar);
});
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", readYhAvatar));
const avatarUrl = computed(() => yhAvatar.value || props.user?.profilePicture || "");
/**
* 退出登录(同时也是"换个账号登录"的唯一入口)。
*
@@ -196,7 +215,7 @@ function pick(m: { key: string; sample: string }) {
<p>写操作提交前可核对,确认无误再提交。</p>
</div>
<div class="sb__user">
<Avatar :src="user?.profilePicture" size="small" class="sb__avatar">
<Avatar :src="avatarUrl" size="small" class="sb__avatar">
{{ (user?.name || "访").slice(0, 1) }}
</Avatar>
<span class="sb__user-text">
+6
View File
@@ -85,6 +85,12 @@ export default defineConfig((): UserConfig => {
host: "localhost",
port: 8080,
proxy: {
// 主题通道本地联调(A 类接入 §2.2):默认走线上通道;联调本地通道源用
// YH_THEME_PROXY_TARGET=http://localhost:9527 npm run dev
"/static": {
target: process.env.YH_THEME_PROXY_TARGET || "http://base.yunhaixingtong.com",
changeOrigin: true,
},
// 同源代理:serviceHost 留空时前端走 /api/...,由这里转发到本地 AgentService(8093),
// 规避后端 CAS 过滤器对 /api/* 不回 CORS 头导致的浏览器直连跨域拦截。
// 如需连测试环境:target 换成 http://base.yunhaixingtong.com/agent