build / build (push) Successful in 3m14s
落地(依据《事项库与办事大厅详细设计 v1》§7)
- 路由:办事事项 modId=21 / 服务字典 modId=22 + 子站镜像(/sch/{code}/site/{siteCode}/svc/*)
+ 3 条 legacyRedirect 旧链兼容;SchoolLayout 菜单图标兜底
- ServiceItemList:三维筛选/关键字/分页/发布下架删除/跳转编辑
- ServiceItemEdit:三段富文本 + 封面 + 指南附件上传回显(attList)+「我要办理外链」提示
+ 部门选择器不可达时提示"可留空保存"
- ServiceDictManage:object/category 双 Tab,被引用键编辑锁定 + 删除引用守护提示
- api/类型:SvcItemApi/SvcDictApi 与 types/cms.ts 契约补齐;站点设置补 svc_hall_base 键
审计修复(docs/18)
- D3 接线:列表页新增 ServiceItemSortModal 拖拽排序(顺序源 selList,保存按下标重排 sort_no)
与「同步部门名」按钮,此前后端 sort/syncDeptName 已备但前端无入口
- 附件场景拆分:封面与三个富文本编辑器改传 scene=svc_item_asset,指南附件保持 svc_item
(内容资产由后端按内容派生归属,不再依赖前端收集上传 id——删除只写不读的 uploadedIds 记账)
- 部门组树:SvcDeptVo 增 children,前端 flattenDepts 缩进扁平化(后端改嵌套下发,交互不变)
- RichTextEditor 补 articleId/slateMinHeight prop 并去掉死状态 editorLoading;
ArticleEdit 改为复用该组件(正文区最小高度按原口径传入,观感不变),消除双份 wangEditor 实现
测试与 mock
- mock 补 /api/svc/** 全量桩(事项分页/字典/部门树/排序/同步部门名/附件回显 + 站点 1、2 示例数据),
dev:mock 下三页可用(实测 curl 各项响应正确)
- cms.spec 补 save/syncDeptName/selList/dict save+delete 契约;siteRouter.spec 补事项三条路由
modId 与子站镜像 resolve 断言;新增 RichTextEditor.spec 5 例(上传参数三键/成功 emit/失败不 emit/工具栏排除项)
- vitest 127 用例全绿 + vite build 通过
498 lines
20 KiB
Vue
498 lines
20 KiB
Vue
<script setup lang="ts">
|
||
/* 学校专属管理壳(docs/09 §3.3,由 layout.vue 改造,二级壳 /sch/:schCode):
|
||
* header 左侧仅超管可见「↩返回学校列表」图标(校级账号不显示);
|
||
* 面包屑 = 学校列表(超管)> 校名 > 站点身份条 > 页面。
|
||
* 站点分层(docs/13 方案 C):
|
||
* URL 为站点上下文唯一事实源——主站 /sch/{code}/{page},子站 /sch/{code}/site/{siteCode}/{page};
|
||
* 站点身份条(SiteSwitcher)一步切换并停留当前功能页,上下文由 syncSiteFromRoute 同步;
|
||
* 菜单 page 保持主站形态,高亮剥段匹配、跳转按当前站点补段。
|
||
* 学校切换仍在壳外完成:/workbench 工作台选校。
|
||
* 菜单/高亮/子页面面包屑逻辑与原 layout 一致,布局数据来自 store.session。 */
|
||
import { ref, computed, watch, onBeforeMount } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { Message } from "view-ui-plus";
|
||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||
import { useCmsStore } from "@/store";
|
||
import { adminPathOf, stripSiteSeg, schoolSubPath, resolveRouteSite } from "@/utils/sitePath";
|
||
import SiteSwitcher from "@/components/SiteSwitcher.vue";
|
||
import config from "@/assets/config/host.config";
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const store = useCmsStore();
|
||
|
||
const isPanel = ref(false);
|
||
const currPath = ref<string>("");
|
||
const navPath = ref<any[]>([]);
|
||
|
||
const baseHost = computed(() => config.baseHost);
|
||
const ssoHost = computed(() => config.ssoHost);
|
||
const appName = computed(() => "云海CMS");
|
||
const userInfo = computed(() => ({
|
||
name: store.session.baseUserName || "管理员",
|
||
avatar: store.session.baseUserAvatar,
|
||
}));
|
||
const isAlone = computed(() => store.session.isAlone);
|
||
const modList = computed(() => store.session.modList || []);
|
||
|
||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||
|
||
/** 当前学校上下文(面包屑校名段) */
|
||
const school = computed(() => store.currentSchool);
|
||
const isSuper = computed(() => store.isSuperAdmin);
|
||
|
||
/** 当前落点是否院系子站(store 契约):决定身份条徽章与底边色带 */
|
||
const isSubSite = computed(() => store.isSubSite);
|
||
const pathNavClass = computed(() => (isSubSite.value ? "band-sub" : "band-main"));
|
||
|
||
/** 菜单/面包屑 page(主站形态完整路径)→ 当前站点形态:子站插入 /site/{code} 段(docs/13 §4)。
|
||
* 非 /sch/ 前缀(外链模块)原样返回,防拼坏 */
|
||
const withSiteSeg = (page: string): string => {
|
||
const code = school.value?.mainSiteCode;
|
||
if (!page || !code || !page.startsWith("/sch/")) return page;
|
||
return adminPathOf(code, store.siteSegCode, schoolSubPath(page));
|
||
};
|
||
|
||
const flaten = (tree: any[], pid = 0): any[] => {
|
||
const arr: any[] = [];
|
||
tree.forEach((element: any) => {
|
||
const { id, name, modId } = element;
|
||
// 平台配置的 path 格式不可控,统一解析为内部路由 path(名称/modId 兜底)。
|
||
// 解析结果回写原对象:模板高亮(citem.page == currPath)与 linkMenu 消费的是
|
||
// 原始 children,旧扁平路由下 raw page 与解析结果恰好相等,学校前缀后必须显式回写
|
||
const resolved = resolveMenuPage(element.page, element.modId ?? element.id, element.name);
|
||
if (resolved.path) {
|
||
element.page = resolved.path;
|
||
}
|
||
element.external = resolved.external;
|
||
arr.push({ id, name, modId, page: resolved.path, external: resolved.external, pid });
|
||
if (element.children && element.children.length) {
|
||
arr.push(...flaten(element.children, id));
|
||
}
|
||
});
|
||
return arr;
|
||
};
|
||
|
||
const menuArr = computed(() => flaten(modList.value));
|
||
|
||
const routeChange = (to: any) => {
|
||
const meta: any = to.meta || {};
|
||
const { modId, title, subOf } = meta;
|
||
const navArr: any[] = [];
|
||
// 菜单 page 为主站形态,匹配前剥掉站点中缀(docs/13 §4 剥段匹配)
|
||
const toLogic = stripSiteSeg(to.path || "");
|
||
|
||
// 侧边栏子菜单组自动展开:当前高亮落在某组子项时展开该组(改善直达体验)
|
||
const expandGroupOf = (page: string) => {
|
||
if (!page) return;
|
||
modList.value.forEach((m: any) => {
|
||
if (m.children?.some((c: any) => c.page === page)) {
|
||
m.childState = true;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 列表子页面(docs/09 §3.2,如新增/编辑文章):面包屑挂到所属列表下、
|
||
// 侧边栏高亮锁定列表项,优先级高于本页菜单匹配。
|
||
// subOf = 所属列表的业务子路径(/article);菜单解析后带学校前缀,按后缀匹配,modId 兜底
|
||
if (subOf) {
|
||
const parent =
|
||
menuArr.value.find((x: any) => x.page && (x.page === subOf || x.page.endsWith(subOf))) ||
|
||
menuArr.value.find((x: any) => x.modId === subOf);
|
||
if (parent) {
|
||
currPath.value = parent.page || "";
|
||
expandGroupOf(currPath.value);
|
||
if (parent.pid) {
|
||
const gp = menuArr.value.find((x: any) => x.id === parent.pid);
|
||
if (gp) navArr.push({ name: gp.name });
|
||
}
|
||
navArr.push({ name: parent.name, page: withSiteSeg(parent.page) });
|
||
navArr.push({ name: title });
|
||
navPath.value = navArr;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 优先按 path 匹配菜单(平台模块 id 与路由 meta.modId 不保证一致),
|
||
// 匹配不到再退回 modId 对照
|
||
const find: any =
|
||
menuArr.value.find((x: any) => x.page && x.page === toLogic) ||
|
||
menuArr.value.find((x: any) => x.modId === modId);
|
||
if (find) {
|
||
currPath.value = find.page || "";
|
||
expandGroupOf(currPath.value);
|
||
if (find.pid) {
|
||
const pfind = menuArr.value.find((x: any) => x.id === find.pid);
|
||
if (pfind) navArr.push({ name: pfind.name });
|
||
}
|
||
navArr.push({ name: find.name || title, page: withSiteSeg(find.page) });
|
||
navPath.value = navArr;
|
||
return;
|
||
}
|
||
|
||
if (modId != null) {
|
||
const r = router.getRoutes().find((rt: any) => (rt.meta as any)?.modId === modId);
|
||
if (r) {
|
||
currPath.value = stripSiteSeg(r.path);
|
||
navPath.value = [{ name: (r.meta as any).title, page: withSiteSeg(r.path) }];
|
||
return;
|
||
}
|
||
}
|
||
|
||
currPath.value = "";
|
||
navPath.value = [];
|
||
};
|
||
|
||
const linkMenu = (item: any) => {
|
||
const page: string = item?.page || "";
|
||
if (!page) {
|
||
// 解析不出有效页面:显式提示,而不是静默不动或被重定向吞到默认页
|
||
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
|
||
return;
|
||
}
|
||
// 平台配置的外部链接模块:整页跳转
|
||
if (item?.external) {
|
||
window.location.href = page;
|
||
return;
|
||
}
|
||
// 子站上下文下菜单目标补站点段(主站形态 → 当前站点形态)
|
||
const target = withSiteSeg(page);
|
||
if (target === route.path) {
|
||
router.go(0);
|
||
} else {
|
||
router.push(target);
|
||
}
|
||
};
|
||
|
||
const linkUserInfo = () => {
|
||
window.location.href = baseHost.value + "/v/userinfo";
|
||
};
|
||
const logout = () => {
|
||
let url = window.location.href;
|
||
url = "/logout?service=" + url;
|
||
window.location.href = ssoHost.value + url;
|
||
};
|
||
/** 返回首页:旧 BschHome 应用已下线(线上 /BschHome/front/index.html 返回 404),统一跳用户中心 userhome */
|
||
const linkHomePage = () => {
|
||
window.location.href = (baseHost.value || "") + "/userhome/front/index.html";
|
||
};
|
||
const childChange = (index: number) => {
|
||
const arr = modList.value || [];
|
||
if (arr[index]) {
|
||
arr[index].childState = !arr[index].childState;
|
||
}
|
||
};
|
||
|
||
/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖),
|
||
* 兜底保留 modId 对照。图标为 ysch-icons(CSS mask 类,见 styles/icons.css,与 Notice 同一套);
|
||
* 友情链接/附件库内无完全对应语义,分别取 cshxd(路牌指引)、sjlm(文档层叠)近似 */
|
||
const getMenuIcon = (item?: any): string => {
|
||
const page: string = item?.page || "";
|
||
if (page === "/article" || page.startsWith("/article")) return "icon-wenzhang";
|
||
if (page === "/category") return "icon-liebiao";
|
||
if (page === "/banner") return "icon-tupian";
|
||
if (page === "/link") return "icon-cshxd";
|
||
if (page === "/attachment") return "icon-sjlm";
|
||
if (page === "/site") return "icon-zhandian";
|
||
// 事项库 R2(§7.5):办事事项取"申请"图标,服务字典取列表图标(前缀分支,优先于 modId 对照)
|
||
if (page === "/svc/item" || page.startsWith("/svc/item")) return "icon-shenqing";
|
||
if (page === "/svc/dict") return "icon-liebiao";
|
||
switch (item?.modId ?? item?.id) {
|
||
case 1: return "icon-wenzhang";
|
||
case 2: return "icon-liebiao";
|
||
case 3: return "icon-tupian";
|
||
case 4: return "icon-cshxd";
|
||
case 5: return "icon-sjlm";
|
||
case 6: return "icon-zhandian";
|
||
case 21: return "icon-shenqing";
|
||
case 22: return "icon-liebiao";
|
||
default: return "";
|
||
}
|
||
};
|
||
|
||
/* ===== 学校上下文(docs/09 §3.3)===== */
|
||
function goWorkbench() {
|
||
router.push("/workbench");
|
||
}
|
||
|
||
/**
|
||
* URL 站点段 → store 同步(docs/13 §3:URL 是站点上下文唯一事实源)。
|
||
* 无 siteCode = 主站形态;有则按 code 匹配本校站点(parentSiteId 归属校验),
|
||
* 非法/跨校 siteCode 打回同页主站形态并告警。
|
||
* 启动引导期(user 未装载 → schoolSites 为空)跳过,由 user 就绪后的 watch 兜底重跑。
|
||
*/
|
||
function syncSiteFromRoute() {
|
||
const sc = school.value;
|
||
if (!sc || !store.user) return;
|
||
const sites = (store.schoolSites || []) as any[];
|
||
if (!sites.length) return;
|
||
const { site: target, invalid } = resolveRouteSite(sites, sc.mainSiteId, route.params.siteCode as string);
|
||
if (invalid) {
|
||
Message.warning("站点不存在或无权限,已返回主站");
|
||
router.replace({ path: stripSiteSeg(route.path), query: route.query, hash: route.hash });
|
||
return;
|
||
}
|
||
if (target && store.currentSite?.id !== target.id) {
|
||
store.setCurrentSite({
|
||
id: target.id,
|
||
code: target.code,
|
||
path: target.path || target.code,
|
||
name: target.name,
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 启动引导期守卫放行的深链:user 数据就绪后兜底校验/恢复学校上下文,再按 URL 同步站点 */
|
||
watch(
|
||
() => store.user,
|
||
u => {
|
||
if (!u) return;
|
||
const code = route.params.schCode as string;
|
||
if (route.path.startsWith("/sch/") && !school.value) {
|
||
const sites = u.sites || [];
|
||
const main = sites.find((s: any) => s.code === code);
|
||
if (!main) {
|
||
router.replace("/workbench");
|
||
return;
|
||
}
|
||
store.enterSchool(
|
||
{
|
||
schId: main.schId,
|
||
schName: main.schName,
|
||
mainSiteId: main.id,
|
||
mainSiteCode: main.code,
|
||
mainSiteName: main.name,
|
||
},
|
||
sites.filter((s: any) => s.id === main.id || s.parentSiteId === main.id)
|
||
);
|
||
}
|
||
syncSiteFromRoute();
|
||
routeChange(route);
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
onBeforeMount(() => {
|
||
routeChange(route);
|
||
});
|
||
|
||
watch(
|
||
() => route.fullPath,
|
||
() => {
|
||
syncSiteFromRoute();
|
||
routeChange(route);
|
||
}
|
||
);
|
||
|
||
// 深链启动期:学校上下文/菜单晚于首次 routeChange 到达,就绪后重算面包屑与高亮
|
||
watch(
|
||
() => [store.currentSchool?.mainSiteCode, store.session.modList.length],
|
||
() => {
|
||
syncSiteFromRoute();
|
||
routeChange(route);
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="layout-wrap">
|
||
<header class="header-container fix">
|
||
<div class="fl align-middle icon-box">
|
||
<div title="首页" class="icon-btn" @click="linkHomePage">
|
||
<i class="iconfont icon-shouye"></i>
|
||
</div>
|
||
</div>
|
||
<div class="delimiter fl align-middle"></div>
|
||
<!-- 返回学校列表:仅超管显示(校级账号固定本校,无需返回入口) -->
|
||
<a
|
||
v-if="isSuper"
|
||
class="fl align-middle back-workbench"
|
||
href="javascript:void(0);"
|
||
title="返回学校列表"
|
||
@click="goWorkbench"
|
||
>
|
||
<Icon type="ios-undo" :size="18" />
|
||
</a>
|
||
<div v-if="isSuper" class="delimiter fl align-middle"></div>
|
||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||
<div class="header-aside fr">
|
||
<img :src="userInfo.avatar" v-if="userInfo.avatar" class="fl align-middle" @click="linkUserInfo" />
|
||
<a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo">
|
||
{{ userInfo.name }}
|
||
</a>
|
||
<div class="delimiter fl align-middle"></div>
|
||
<a href="javascript:;" @click="logout" class="logout fl align-middle" title="退出登陆">
|
||
<i class="iconfont icon-logout"></i>
|
||
</a>
|
||
</div>
|
||
</header>
|
||
<div class="left-menu" v-if="!isAlone">
|
||
<div class="menu-top-box"></div>
|
||
<ul>
|
||
<template v-for="(item, index) in modList" :key="'menu_li_' + index">
|
||
<template v-if="item.children && item.children.length > 0">
|
||
<li>
|
||
<a href="javascript:;" @click="childChange(index)">
|
||
<i :class="item.childState ? 'arrow-down' : 'arrow-up'"></i>
|
||
<i v-if="getMenuIcon(item)" class="iconfont" :class="getMenuIcon(item)"></i>
|
||
{{ item.name }}
|
||
</a>
|
||
</li>
|
||
<transition name="fade_child_ul">
|
||
<div class="child-item-wrap" v-show="item.childState">
|
||
<li
|
||
:class="citem.page == currPath ? 'active child-item' : 'child-item'"
|
||
v-for="citem in item.children"
|
||
:key="citem.id"
|
||
>
|
||
<a href="javascript:;" @click="linkMenu(citem)">
|
||
{{ citem.name }}
|
||
</a>
|
||
</li>
|
||
</div>
|
||
</transition>
|
||
</template>
|
||
<li v-else :class="item.page == currPath ? 'active' : ''">
|
||
<a href="javascript:;" @click="linkMenu(item)">
|
||
<i v-if="getMenuIcon(item)" class="iconfont" :class="getMenuIcon(item)"></i>
|
||
{{ item.name }}
|
||
</a>
|
||
</li>
|
||
</template>
|
||
</ul>
|
||
<div class="clear-box"></div>
|
||
</div>
|
||
|
||
<div class="path-nav" :class="pathNavClass" :style="pathNavStyle">
|
||
<Breadcrumb separator=">">
|
||
<!-- 首页段(常驻第一段):旧 BschHome 已下线,统一跳用户中心 userhome -->
|
||
<a href="javascript:;" @click="linkHomePage">
|
||
<BreadcrumbItem>
|
||
<span class="crumb-home">
|
||
<i class="iconfont icon-category"></i>
|
||
首页
|
||
</span>
|
||
</BreadcrumbItem>
|
||
</a>
|
||
<!-- 超管:校名前多一段可点「学校列表」,与 header 返回图标同效(回工作台切换学校) -->
|
||
<BreadcrumbItem v-if="isSuper" to="/workbench">学校列表</BreadcrumbItem>
|
||
<!-- 面包屑第二段 = 学校上下文(常驻,不承载站点信息) -->
|
||
<BreadcrumbItem v-if="school">
|
||
<Icon type="ios-school" />
|
||
{{ school.schName || school.mainSiteName }}
|
||
</BreadcrumbItem>
|
||
<!-- 站点身份条(docs/13 §1):徽章+站点名▼,下拉一步切换;不传 to(否则 mixins-link 抢走点击) -->
|
||
<BreadcrumbItem v-if="store.currentSite">
|
||
<SiteSwitcher />
|
||
</BreadcrumbItem>
|
||
<template v-for="(item, index) in navPath" :key="index">
|
||
<BreadcrumbItem v-if="index != navPath.length - 1" :to="item.page">
|
||
{{ item.name }}
|
||
</BreadcrumbItem>
|
||
<BreadcrumbItem v-else>{{ item.name }}</BreadcrumbItem>
|
||
</template>
|
||
</Breadcrumb>
|
||
</div>
|
||
|
||
<transition name="fade_apps">
|
||
<div class="app-panel" v-if="isPanel">
|
||
<div class="tips-box"></div>
|
||
<ul class="fix">
|
||
<li class="fl" v-for="(item, index) in []" :key="index">
|
||
<a :href="item.url" target="_blank" rel="noopener noreferrer">
|
||
<div class="icon-box">
|
||
<img :src="item.icon" alt="" />
|
||
</div>
|
||
<p class="text-ellipsis">{{ item.name }}</p>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
<div class="link-panel">
|
||
<a :href="baseHost + '/userhome/front/index.html#/apps'" target="_blank" rel="noopener noreferrer">
|
||
全部应用
|
||
<i>→</i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<div class="base-wrap-main" :class="{ 'alone-page': isAlone }">
|
||
<div class="main-wrapper">
|
||
<router-view v-if="store.currentSite" />
|
||
<div v-else class="no-site-tip">当前学校暂无可管理站点,请联系平台管理员在学校工作台添加。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
/* 超管返回学校列表图标:header 常驻入口(校级账号不渲染) */
|
||
.back-workbench {
|
||
color: #fff;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
&:hover {
|
||
opacity: 0.85;
|
||
}
|
||
}
|
||
.no-site-tip {
|
||
height: 60vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #999;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 站点色带(docs/13 §1.4):::after 挂底边——.path-nav 高度 56px 固定且未设 border-box,
|
||
直接加 border-bottom 会改盒高(W2 高度链教训),绝对定位不参与布局 */
|
||
.path-nav::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
height: 3px;
|
||
}
|
||
.path-nav.band-main::after {
|
||
background: #577afb;
|
||
}
|
||
.path-nav.band-sub::after {
|
||
background: #ff9900;
|
||
}
|
||
|
||
/* W2(D6):左树高度链修复——UserBase App.vue:222-224 同款覆盖式。
|
||
layout.css 全局 .main-wrapper 只有 min-height:100% 无 height,
|
||
子级 .tree-wrapper{height:100%} 因父链高度 auto 解析失败,栏目树缩在一起。
|
||
scoped 生效前提:.base-wrap-main/.main-wrapper 是本组件模板内节点 */
|
||
.base-wrap-main .main-wrapper {
|
||
height: 100%;
|
||
}
|
||
|
||
/* 高度链顶层:layout.css 只定义到 .base-wrap-main{height:calc(100% - 56px)},
|
||
其百分比基于 .layout-wrap——本组件根节点,无高度定义则整条链解析失败
|
||
(UserBase 同名壳在 layout.vue 全局样式中补了这一层,此前 CMS 缺失) */
|
||
.layout-wrap {
|
||
height: 100%;
|
||
}
|
||
|
||
/* 左侧菜单图标与文字间距(ysch-icons mask 类,写法与 Notice layout.vue 一致) */
|
||
.left-menu a .iconfont {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
/* 面包屑「首页」垂直居中:layout.css 遗留的 .path-nav .icon-category{position:relative;top:1px}
|
||
是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏;
|
||
这里用 inline-flex 真居中并关掉该补偿(不动压缩产物 layout.css,覆盖收在本组件内)。
|
||
scoped 特异性 .crumb-home .iconfont[data-v-x](0,2,1)> .path-nav .icon-category(0,2,0),可覆盖 */
|
||
.crumb-home {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.crumb-home .iconfont {
|
||
position: static;
|
||
top: 0;
|
||
}
|
||
</style>
|