@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* 视觉取证 harness 入口(步骤 4 审计专用,不进 git)。
|
||||
* 复刻 App.vue 壳层(.app-desktop + base-mask + LeftMenu + Header + 真实路由页),
|
||||
* 用 store.$patch mock 三档登录态(?role=super|admin|user)驱动真实路由页渲染;
|
||||
* 本机无后端,接口失败均静默/走失败态展示(/msg 页取证失败态正是验收点之一)。
|
||||
* 页内断言器(#assert-out):留白四边 15px、msg-side 216px、主题 iframe 内外 data-theme/通道 link 分布。
|
||||
* $conf.baseHost 指向本地通道镜像(YschTheme serve:static 9527)——仅预览 iframe 的 init 走它。
|
||||
*/
|
||||
import { createApp, defineComponent, h } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import { createWebHashHistory, createRouter, RouterView } from "vue-router";
|
||||
import ViewUIPlus from "view-ui-plus";
|
||||
import "view-ui-plus/dist/styles/viewuiplus.css";
|
||||
import "@/assets/styles/main.scss";
|
||||
import vuescroll from "vuescroll";
|
||||
import VWave from "v-wave";
|
||||
import mitt from "mitt";
|
||||
|
||||
import routes from "@/router/routes";
|
||||
// 懒加载 chunk 在 headless virtual-time 下不可靠(fetch 挂起 → router-view 恒空注释),
|
||||
// 取证路由改静态 import 同构复刻(路径/name/meta 与 routes.ts 保持一致)
|
||||
import MsgPage from "@/views/msg/index.vue";
|
||||
import SettingsPage from "@/views/Settings.vue";
|
||||
import ThemePreviewPage from "@/views/ThemePreview.vue";
|
||||
// 默认头像轮取证新增:AppHeader(顶栏三态头像)+ UserInfoModal(120px 弹窗头像)
|
||||
import AppHeader from "@/components/AppHeader.vue";
|
||||
import UserInfoModal from "@/components/UserInfoModal.vue";
|
||||
import mittBus from "@/utils/mitt";
|
||||
|
||||
const harnessRoutes: any[] = [
|
||||
{ path: "/msg", name: "Msg", component: MsgPage, meta: { title: "消息" } },
|
||||
{ path: "/settings", name: "Settings", component: SettingsPage, meta: { title: "设置" } },
|
||||
{ path: "/theme-preview", name: "ThemePreview", component: ThemePreviewPage, meta: { title: "主题预览" } },
|
||||
{ path: "/appheader", name: "AppHeaderDemo", component: null as any, meta: { title: "顶栏头像取证" } },
|
||||
{ path: "/", name: "index", component: MsgPage, meta: { title: "我的桌面" } },
|
||||
];
|
||||
import { useDefaultStore } from "@/store";
|
||||
import LeftMenu from "@/components/LeftMenu.vue";
|
||||
import MyHeader from "@/components/Header.vue";
|
||||
import ThemePreview from "@/views/ThemePreview.vue";
|
||||
import directive from "@/utils/directive/index";
|
||||
import utils from "@/utils";
|
||||
import "virtual:svg-icons-register";
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const role = params.get("role") || "admin";
|
||||
|
||||
/** mock 三档登录态(照真实字段名):super 超管 type=1;admin 校管 roleId=2;user 普通教师 roleId=3。
|
||||
* ?avatar=img 追加本地 data-URI 头像(默认头像轮三态取证:有图 / ?yh-avatar=text 首字 / ?yh-avatar=silhouette 剪影) */
|
||||
function applyMockState() {
|
||||
const store = useDefaultStore();
|
||||
const profiles: Record<string, any> = {
|
||||
super: { userInfo: { id: 1, type: 1, name: "平台管理员", account: "admin", phone: "13800000001" }, roleId: 1 },
|
||||
admin: { userInfo: { id: 2, type: 0, name: "王老师", account: "T20240315", phone: "13800000002" }, roleId: 2 },
|
||||
user: { userInfo: { id: 3, type: 0, name: "李老师", account: "T20240316", phone: "13800000003" }, roleId: 3 },
|
||||
};
|
||||
const p = profiles[role] || profiles.admin;
|
||||
const userInfo = { ...p.userInfo };
|
||||
if (params.get("avatar") === "img") {
|
||||
userInfo.profilePicture =
|
||||
"data:image/svg+xml;utf8," +
|
||||
encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f6b352"/><stop offset="1" stop-color="#f68d52"/></linearGradient></defs><rect width="120" height="120" rx="60" fill="url(#g)"/><circle cx="60" cy="46" r="20" fill="#fff"/><path d="M22 104a38 38 0 0 1 76 0z" fill="#fff"/></svg>'
|
||||
);
|
||||
}
|
||||
store.$patch({
|
||||
userInfo,
|
||||
curRoleInfo: { roleId: p.roleId, roleName: "", schId: 1, schName: "云海一中", childId: 0, childName: "" },
|
||||
userRoleList: [],
|
||||
curUserMenuList: [
|
||||
{ id: 1, name: "消息", url: "/msg", isNative: true, isBottom: true, orderNum: 9998, icon: "" },
|
||||
{ id: 2, name: "设置", url: "/settings", isNative: true, isBottom: true, orderNum: 9999, icon: "" },
|
||||
],
|
||||
curUserAppList: [
|
||||
{
|
||||
apps: [
|
||||
{ appKey: "100019", appCode: "flow", name: "流程中心" },
|
||||
{ appKey: "100617", appCode: "notice", name: "通知公告" },
|
||||
{ appKey: "100611", appCode: "leave", name: "请假" },
|
||||
],
|
||||
},
|
||||
],
|
||||
unreadCount: 5,
|
||||
curUserSettingInfo: {
|
||||
wallpaperUrl:
|
||||
"data:image/svg+xml;utf8," +
|
||||
encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#dce7f2"/><stop offset="1" stop-color="#cfd9e6"/></linearGradient></defs><rect width="1600" height="900" fill="url(#g)"/></svg>'
|
||||
),
|
||||
} as any,
|
||||
});
|
||||
return store;
|
||||
}
|
||||
|
||||
const Shell = defineComponent({
|
||||
setup() {
|
||||
const store = applyMockState();
|
||||
|
||||
const wallpaper = store.curUserSettingInfo.wallpaperUrl;
|
||||
return () =>
|
||||
h("div", { class: "app-desktop app-desktop__animation", style: { backgroundImage: `url(${wallpaper})` } }, [
|
||||
h(LeftMenu),
|
||||
h("div", { class: "app-main-wrap base-mask" }, [
|
||||
h(MyHeader),
|
||||
h("div", { class: "harness-router" }, [h(RouterView)]),
|
||||
]),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
// /appheader:顶栏三态头像取证页(AppHeader + UserInfoModal 同屏;挂载后自动弹个人信息弹窗,一屏证 28px/120px 两处消费)
|
||||
const AvatarShell = defineComponent({
|
||||
setup() {
|
||||
applyMockState();
|
||||
setTimeout(() => mittBus.emit("showUserInfoModal", true), 800);
|
||||
return () =>
|
||||
h("div", { style: "position:relative;min-height:100vh;background:#f5f6f8" }, [
|
||||
h(AppHeader, { appName: "通知公告" }),
|
||||
h(UserInfoModal),
|
||||
]);
|
||||
},
|
||||
});
|
||||
harnessRoutes.find(r => r.path === "/appheader").component = AvatarShell;
|
||||
|
||||
// /theme-preview 直开(不经设置页)取证:走 App.vue 同款顶层独立分支语义(不进壳层、不依赖登录态)
|
||||
const PreviewShell = defineComponent({
|
||||
setup() {
|
||||
return () => h(ThemePreview);
|
||||
},
|
||||
});
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: harnessRoutes,
|
||||
});
|
||||
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
// 顶层分支必须用 setup 内的 useRoute()(响应式):原先捕获 currentRoute.value(非响应式解包),
|
||||
// 直开 /theme-preview 时首帧按初始路由 "/" 渲染了 Shell,预览被套进桌面壳里
|
||||
setup() {
|
||||
const rt = useRoute();
|
||||
return () =>
|
||||
h("div", [
|
||||
rt.path === "/theme-preview" ? h(PreviewShell) : rt.path === "/appheader" ? h(AvatarShell) : h(Shell),
|
||||
]);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(VWave as any);
|
||||
app.use(ViewUIPlus);
|
||||
app.use(vuescroll as any, { ops: { bar: { keepShow: true, background: "#595959" } } } as any);
|
||||
|
||||
app.config.globalProperties.$conf = {
|
||||
baseHost: "http://localhost:9527",
|
||||
ssoHost: "",
|
||||
serviceHost: "",
|
||||
fileHost: "",
|
||||
ifameAppIdArr: [],
|
||||
statsRealyConf: [],
|
||||
isMock: true,
|
||||
isDebug: true,
|
||||
};
|
||||
app.config.globalProperties.$utils = utils;
|
||||
app.config.globalProperties.$bus = mitt();
|
||||
directive(app);
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
// ===== 诊断探针(取证调试用):全局错误与路由就绪状态写进 DOM =====
|
||||
window.addEventListener("error", e => {
|
||||
const d = document.createElement("div");
|
||||
d.id = "js-error-" + (document.querySelectorAll("[id^=js-error]").length + 1);
|
||||
d.style.cssText = "position:fixed;top:0;right:0;z-index:99999;background:#fdd;color:#900;font-size:12px;padding:4px 8px;max-width:60%;";
|
||||
d.textContent = "JSERR:" + (e.message || String(e));
|
||||
document.body.appendChild(d);
|
||||
});
|
||||
window.addEventListener("unhandledrejection", e => {
|
||||
const d = document.createElement("div");
|
||||
d.id = "js-reject-" + (document.querySelectorAll("[id^=js-reject]").length + 1);
|
||||
d.style.cssText = "position:fixed;top:0;right:0;z-index:99999;background:#fed;color:#960;font-size:12px;padding:4px 8px;max-width:60%;";
|
||||
d.textContent = "REJ:" + String((e as any).reason || "").slice(0, 200);
|
||||
document.body.appendChild(d);
|
||||
});
|
||||
router.isReady().then(
|
||||
() => {
|
||||
const d = document.createElement("div");
|
||||
d.id = "router-ready";
|
||||
d.style.cssText = "position:fixed;top:0;left:0;z-index:99999;background:#dfd;color:#060;font-size:12px;padding:2px 8px;";
|
||||
d.textContent = "ROUTER-READY:" + router.currentRoute.value.fullPath;
|
||||
document.body.appendChild(d);
|
||||
},
|
||||
err => {
|
||||
const d = document.createElement("div");
|
||||
d.id = "router-ready";
|
||||
d.style.cssText = "position:fixed;top:0;left:0;z-index:99999;background:#fdd;color:#900;font-size:12px;padding:2px 8px;";
|
||||
d.textContent = "ROUTER-FAIL:" + String(err).slice(0, 300);
|
||||
document.body.appendChild(d);
|
||||
}
|
||||
);
|
||||
|
||||
// ===== 页内断言器(轮询等目标元素/iframe 就绪后输出,结果写 #assert-out,配 --dump-dom / 截图提取)=====
|
||||
function runAssert(attempt: number) {
|
||||
const gs = (el: Element | null, prop: string) => (el ? getComputedStyle(el)[prop as any] : "NOELEM");
|
||||
const shell = document.querySelector(".set-shell") || document.querySelector(".msg-shell") || document.querySelector(".theme-preview-root");
|
||||
const iframe = document.querySelector(".theme-iframe") as HTMLIFrameElement | null;
|
||||
// 等待条件:目标壳层已渲染;若页面含预览 iframe,还要等 iframe 应用加载出 router 内容
|
||||
let idoc: Document | null = null;
|
||||
try {
|
||||
idoc = iframe ? iframe.contentDocument : null;
|
||||
} catch (e) {
|
||||
/* 跨源(不应发生:同源) */
|
||||
}
|
||||
const iframeReady = !iframe || (idoc?.querySelector(".theme-preview-root") && idoc.querySelectorAll("link[data-yh-theme-css], script[data-yh-theme-init]").length > 0);
|
||||
if ((!shell || !iframeReady) && attempt < 120) {
|
||||
setTimeout(() => runAssert(attempt + 1), 500);
|
||||
return;
|
||||
}
|
||||
const out: string[] = [];
|
||||
out.push("role=" + role);
|
||||
out.push("wait-attempts=" + attempt);
|
||||
out.push("set-shell.padding=" + gs(document.querySelector(".set-shell"), "padding"));
|
||||
out.push("msg-shell.padding=" + gs(document.querySelector(".msg-shell"), "padding"));
|
||||
out.push("msg-side.width=" + gs(document.querySelector(".msg-side"), "width"));
|
||||
out.push("set-nav.width=" + gs(document.querySelector(".set-nav"), "width"));
|
||||
// 主题断言(§6 机器可验):预览 iframe document 有 data-theme 与通道 link;设置页本体无
|
||||
if (iframe) {
|
||||
out.push("iframe-present=true");
|
||||
out.push("iframe.data-theme=" + (idoc?.documentElement?.dataset?.theme ?? "NONE"));
|
||||
out.push("iframe.theme-css-links=" + (idoc ? idoc.querySelectorAll("link[data-yh-theme-css]").length : -1));
|
||||
out.push("iframe.init-script=" + (idoc ? idoc.querySelectorAll("script[data-yh-theme-init]").length : -1));
|
||||
out.push("iframe.theme-ready=" + (idoc?.documentElement?.classList?.contains("yh-theme-ready") ?? false));
|
||||
out.push("iframe.preview-root=" + (idoc ? !!idoc.querySelector(".theme-preview-root") : false));
|
||||
} else {
|
||||
out.push("iframe-present=false");
|
||||
}
|
||||
out.push("self.data-theme=" + (document.documentElement.dataset.theme ?? "NONE"));
|
||||
out.push("self.theme-css-links=" + document.querySelectorAll("link[data-yh-theme-css]").length);
|
||||
// 预览直开时断言主 document(即预览 document 自身)
|
||||
if (router.currentRoute.value.path === "/theme-preview") {
|
||||
out.push("preview.data-theme=" + (document.documentElement.dataset.theme ?? "NONE"));
|
||||
out.push("preview.theme-css-links=" + document.querySelectorAll("link[data-yh-theme-css]").length);
|
||||
out.push("preview.init-script=" + document.querySelectorAll("script[data-yh-theme-init]").length);
|
||||
out.push("preview.theme-ready=" + document.documentElement.classList.contains("yh-theme-ready"));
|
||||
}
|
||||
const pre = document.createElement("pre");
|
||||
pre.id = "assert-out";
|
||||
pre.style.cssText = "position:fixed;left:0;bottom:0;z-index:99999;background:#fff;color:#111;font-size:12px;margin:0;padding:8px 12px;white-space:pre;";
|
||||
pre.textContent = out.join("\n");
|
||||
document.body.appendChild(pre);
|
||||
// 完成标记:截图/外部等待循环看它
|
||||
(window as any).__HARNESS_ASSERT_DONE__ = true;
|
||||
document.title = "HARNESS-READY";
|
||||
}
|
||||
setTimeout(() => runAssert(0), 1500);
|
||||
@@ -708,6 +708,7 @@ function deptPathOf(u: any): string {
|
||||
.reverse();
|
||||
return names.length > 0 ? names.join("/") : "-";
|
||||
}
|
||||
const profileForm = reactive({ mobile: "", email: "" });
|
||||
const profileView = computed(() => {
|
||||
const u: any = store.userInfo || {};
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user