build / build (push) Successful in 1m21s
用户三点修订:
1. 面板已是模态框,去掉背景应用列表的同步过滤(解开 mittBus.searchChange 整条链)
2. 结果右侧灰色小字显示应用类型(来源 BoaAppType.name,与 /apps 分组标题同源)
3. 类型呈现暂按方案 A(右侧灰字);方案 B(类型分组)对照稿见 design/07
- Header.vue: watch(searchAppText) 不再 emit("searchChange");cmdkResult 由
getFlattenAppList(纯 flatten,会丢类型名)改为遍历 curUserAppList 分组展开并挂
appTypeName;模板加 .app-cmdk-type
- AppList.vue: 删 searchAppText / searchAppList / isSearch 与模板搜索态分支、
.no-search-box 样式、孤儿导入 flatten(lodash) 与 requireImg;删 mittBus 监听
- utils/mitt.ts: 删 searchChange 事件类型
- assets/styles/header.scss: 加 .app-cmdk-type 样式
- harness-main.ts: mock curUserAppList 补齐 BoaAppType 分组结构(id + name),
原 mock 丢了 name 导致类型名取不到
注:本仓工作区另有并行会话 WIP(StatsBarModal 迁入 Settings / 主题预览),
本提交只含本案 5 个文件与 hunks,未触碰对方改动。
192 lines
6.3 KiB
Vue
192 lines
6.3 KiB
Vue
<template>
|
||
<div class="app-list-wrapper base-mask">
|
||
<vuescroll :ops="scrollOps">
|
||
<template v-for="(item, index) in appList">
|
||
<h3 class="app-type-box">{{ item.name }}</h3>
|
||
<div
|
||
:class="{
|
||
'app-list-box': true,
|
||
'mb-25': index === appList.length - 1,
|
||
}"
|
||
>
|
||
<template v-for="citem in item.apps">
|
||
<ContextMenu
|
||
class="app-container app-container__mask"
|
||
:title="citem.name"
|
||
@on-change="onMenuChange"
|
||
@select="onMenuSel"
|
||
:menu="[
|
||
{
|
||
label: '添加到桌面',
|
||
appId: citem.id,
|
||
id: '1',
|
||
},
|
||
{
|
||
label: '新窗口打开',
|
||
appId: citem.id,
|
||
url: citem.webAddr,
|
||
id: '2',
|
||
},
|
||
]"
|
||
>
|
||
<a @click="linkPage(citem)">
|
||
<AppIcon v-if="vectorIconOf(citem)" :name="vectorIconOf(citem)" scene="tile"
|
||
:tint="citem.iconColor" />
|
||
<img v-else :src="$utils.appIconPath(citem.icon)" />
|
||
<div class="app-name">{{ citem.name }}</div>
|
||
</a>
|
||
</ContextMenu>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</vuescroll>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import ContextMenu from "@/components/ContextMenu.vue";
|
||
import AppIcon from "@/components/AppIcon.vue";
|
||
import vuescroll from "vuescroll";
|
||
import { ref, onMounted, onUnmounted, computed, watch } from "vue";
|
||
import BaseApi from "@/api/BaseApi";
|
||
import { useDefaultStore } from "@/store";
|
||
import mittBus from "@/utils/mitt";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { BschLoadTypeEnum, DesktopAppTypeEnum, LinkOpenTypeEnum } from "@/types";
|
||
import UserSettingApi from "@/api/UserSettingApi";
|
||
import { Message } from "view-ui-plus";
|
||
import { bschLinkPage, openAppPage } from "@/utils/bschLinkPage";
|
||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||
import { appiconKeyOf } from "@/constants/appIconRules";
|
||
|
||
/**
|
||
* 矢量渲染判定(方案 §5.4 回退链,与 index.vue / SearchPanel.vue 同口径):
|
||
* icon_key 命中 → 矢量;有位图 URL → 位图(存量零回归);URL 空且关键词命中 → 矢量;否则默认图。
|
||
*/
|
||
function vectorIconOf(app: any): string {
|
||
if (app?.iconKey) return app.iconKey;
|
||
if (app?.icon) return "";
|
||
return appiconKeyOf(app?.name || "");
|
||
}
|
||
|
||
// 仅在内容溢出时显示原生滚动条,避免空内容时也常驻滚动条
|
||
const scrollOps = {
|
||
vuescroll: { mode: "native", sizeStrategy: "percent" },
|
||
scrollPanel: { scrollingX: false },
|
||
bar: { showDelay: 500, onlyShowBarOnScroll: true, keepShow: false },
|
||
};
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
|
||
const store = useDefaultStore();
|
||
const { $conf, $utils } = useCurrentInstance();
|
||
|
||
const appList = ref<any[]>([]);
|
||
|
||
const compMenuId = computed(() => {
|
||
let menuList = store.curUserMenuList.find(x => x.url == route.path);
|
||
return menuList ? menuList.id : -1;
|
||
});
|
||
|
||
onMounted(() => {
|
||
reloadAppData();
|
||
mittBus.emit("searchReset");
|
||
});
|
||
onUnmounted(() => {
|
||
mittBus.emit("searchReset");
|
||
});
|
||
|
||
watch(() => store.curRoleUid, reloadAppData);
|
||
|
||
async function reloadAppData() {
|
||
store.setAppListLoading(true);
|
||
try {
|
||
const list = await BaseApi.getUserApp(store.getCurUserId, store.getCurSchId);
|
||
appList.value = list.filter(x => x.apps.length > 0);
|
||
// 写 store:Header 搜索命令面板与后台 WS 通知的应用名反查共用这一份(2026-10-08)
|
||
store.setCurUserAppList(appList.value);
|
||
} finally {
|
||
store.setAppListLoading(false);
|
||
}
|
||
}
|
||
|
||
function onMenuChange(curDom: any, state: boolean) {
|
||
$(curDom).toggleClass("app-container__focus", state);
|
||
}
|
||
|
||
function onMenuSel(item: any) {
|
||
if (item.id == 1) {
|
||
onLightAppSaveDesktop(item.appId);
|
||
}
|
||
if (item.id == 2) {
|
||
bschLinkPage({ url: item.url, type: LinkOpenTypeEnum.Open });
|
||
}
|
||
if (item.id == 3) {
|
||
router.push({
|
||
name: "BschAppDetails",
|
||
params: {
|
||
appId: item.appId,
|
||
menuId: compMenuId.value,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
async function onLightAppSaveDesktop(id: number) {
|
||
try {
|
||
await UserSettingApi.saveUserDesktopAppById(id, DesktopAppTypeEnum.Bsch);
|
||
Message.success("添加成功");
|
||
mittBus.emit("reloadUserSettings");
|
||
} catch (error) {
|
||
Message.error("内部服务错误");
|
||
}
|
||
}
|
||
|
||
function linkPage(item: any) {
|
||
openAppPage(item, $conf.ifameAppIdArr);
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.app-list-wrapper {
|
||
overflow-y: hidden;
|
||
height: calc(100% - var(--headerH));
|
||
padding: 12px 12px;
|
||
h3.app-type-box {
|
||
padding: 10px 0 10px 28px;
|
||
margin: 10px 0 10px;
|
||
color: #344449;
|
||
font-variant-numeric: lining-nums tabular-nums;
|
||
font-family: "PingFang SC";
|
||
font-size: 16px;
|
||
font-style: normal;
|
||
font-weight: 500;
|
||
line-height: 18px;
|
||
letter-spacing: 1px;
|
||
position: relative;
|
||
&::before {
|
||
position: absolute;
|
||
left: 15px;
|
||
top: 10px;
|
||
content: "";
|
||
display: block;
|
||
width: 5px;
|
||
height: 18px;
|
||
border-radius: 9px;
|
||
background: #5979ea;
|
||
}
|
||
}
|
||
// 首个分类标题去掉顶部 margin:该 10px margin 会塌陷进 vuescroll 的 __view,
|
||
// 使 scrollHeight 比可视高度大 10px,vuescroll 误判"内容溢出"而强制显示原生滚动条(overflow 为 scroll 而非 auto)
|
||
h3.app-type-box:first-child {
|
||
margin-top: 0;
|
||
}
|
||
.app-list-box {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
flex-wrap: wrap;
|
||
}
|
||
}
|
||
</style>
|