Files
UserhomeFront/src/views/AppList.vue
T
chenxing 2bd7ef3b80
build / build (push) Successful in 1m21s
refactor(header): 命令面板去背景联动,结果右侧标注应用类型
用户三点修订:
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,未触碰对方改动。
2026-10-08 19:10:14 +08:00

192 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>