feat: 接入 UserHome 前端业务代码(含真实 src/依赖,保留 projman 标准 CI)
deploy-userbase-front / build-and-deploy (push) Failing after 1m54s
build / build (push) Successful in 3m36s

This commit is contained in:
chenxing
2026-09-09 22:37:59 +08:00
parent a53ce07228
commit 4279f99be1
270 changed files with 12479 additions and 25715 deletions
+439
View File
@@ -0,0 +1,439 @@
<template>
<div class="used-app-wrap">
<div class="used-app-list" ref="refApplist">
<template v-for="item in appList" :key="item.id + ''">
<ContextMenu
class="app-container"
:title="item.name"
@on-change="onMenuChange"
@select="onMenuSel"
:menu="[
{
label: '从桌面删除',
appId: item.id,
id: '1',
},
{
label: '新窗口打开',
appId: item.appId,
id: '2',
},
]"
>
<a @click="openLinkPage(item)">
<img v-if="item.type == 1" :src="$utils.appIconPath(item.icon)" />
<img
v-else-if="item.type == 2"
:src="$conf.fileHost + '/image/lightapp.default.png'"
v-real-img="$conf.serviceHost + item.icon"
/>
<div class="app-name">{{ item.name }}</div>
</a>
</ContextMenu>
</template>
</div>
<vue-drag-resize
v-if="isDragShowComp"
:class="{ 'widget-wrap__drag': dragObj.isDrag }"
contentClass="widget-wrap"
:x="dragPositionObj.xaxis"
:y="dragPositionObj.yaxis"
:w="dragPositionObj.width"
:h="dragPositionObj.height"
:parentW="dragObj.parentW"
:parentH="dragObj.parentH"
:minh="248"
:z="10"
:isActive="true"
:parentLimitation="true"
:isDraggable="dragObj.isDrag"
:isResizable="true"
:preventActiveBehavior="true"
:stickSize="5"
:sticks="['tm', 'bm']"
@clicked="onDragChange"
@dragstop="onDragResizestop($event, 'drag')"
@resizestop="onDragResizestop($event, 'resize')"
>
<div class="widget-header">
<Dropdown
placement="bottom-start"
:visible="dragObj.selVisible"
@on-click="onWidgetSelItemClick"
trigger="custom"
>
<div class="operate-btn" @click="showWidgetSelClick">
<SvgIcon class="fl" name="widget-add" size="24" color="#cbd5df" hoverColor="#fff" />
</div>
<template #list>
<DropdownMenu style="min-width: 120px; max-height: 200px; overflow-y: auto">
<DropdownItem :key="item.k" :name="item.k" v-for="item in widgetSel">
<div class="fix">
<div class="fl">{{ item.v }}</div>
<SvgIcon
v-if="!!item.checked"
class="fr mt-3"
name="widget-checked"
color="#2d8cf0"
size="14"
></SvgIcon>
</div>
</DropdownItem>
</DropdownMenu>
</template>
</Dropdown>
<div class="widget-header-drag" @mousedown="(e:any)=>dragObj.isDrag = e.buttons === 1"></div>
<div class="operate-btn" @click="onClickWidgetHide">
<SvgIcon class="fr" name="widget-sub" size="24" color="#cbd5df" hoverColor="#fff" />
</div>
</div>
<div class="widget-body" v-show="!dragObj.isDrag">
<vuescroll :ops="widgetOps" v-if="widgetAppList.length > 0">
<div class="widget-item pr" v-for="item in widgetAppList">
<div class="widget-app-header">
<span>{{ item.name }}</span>
<SvgIcon
@click="onWidgetSelItemClick(item.id)"
class="fr"
name="widget-close"
size="18"
color="#999999"
hoverColor="#646566"
/>
</div>
<Skeleton
style="padding: 15px"
:loading="item.loading"
:title="false"
:paragraph="{ rows: 4 }"
animated
/>
<iframe
v-show="!item.loading"
:style="{ height: item.height + 'px' }"
:id="'iframe_widget_' + item.id"
:data-id="item.id"
@load="onIframeLoad(item.id)"
:src="item.url"
class="widget-app-iframe"
></iframe>
</div>
</vuescroll>
<template v-if="widgetAppList.length == 0">
<div class="widget-empty-box">
<SvgIcon name="widget-empty" color="#2d8cf0" size="120"></SvgIcon>
<h4>单击 + 按钮可添加小工具。</h4>
</div>
</template>
</div>
</vue-drag-resize>
</div>
</template>
<script lang="ts" setup>
import ContextMenu from "@/components/ContextMenu.vue";
import { ref, reactive, onMounted, onUnmounted, computed, watch, watchEffect, nextTick } from "vue";
import BaseApi from "@/api/BaseApi";
// @ts-ignore
import VueDragResize from "vue-drag-resize/src";
import { useDefaultStore } from "@/store";
import SvgIcon from "@/components/SvgIcon.vue";
import { cloneDeep, debounce, difference, includes, once, pick, remove } from "lodash";
import UserSettingApi from "@/api/UserSettingApi";
import { bschLinkPage } from "@/utils/bschLinkPage";
import {
BschLoadTypeEnum,
BschTransferRoleInfo,
IframePageParmas,
LinkOpenTypeEnum,
StateEnum,
WidgetPosition,
WidgetPositionResp,
} from "@/types";
import { bschRoleEncode } from "@/utils/bschRoleEncode";
import { useRouter, useRoute } from "vue-router";
import mittBus from "@/utils/mitt";
import vuescroll from "vuescroll";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import { Message } from "view-ui-plus";
import { useDraggable } from "vue-draggable-plus";
const { $conf, $utils } = useCurrentInstance();
const store = useDefaultStore();
const route = useRoute();
const router = useRouter();
const refApplist = ref();
const isDragShow = ref(false),
isDesltopAppDefault = ref(false),
appList = ref<any[]>([]);
const dragObj = reactive({
isDrag: false,
selVisible: false,
parentW: 0,
parentH: 0,
});
const widgetOps = {
vuescroll: {},
scrollPanel: {},
rail: {},
bar: {
background: "#333",
},
};
const widgetAppList = ref<any[]>([]);
const dragPositionObj = reactive<Partial<WidgetPosition>>({});
const isDragShowComp = computed(() => {
const tempObj = cloneDeep(store.getWidgetDragObj);
return tempObj.isWidgetShow && isDragShow.value;
});
const compMenuId = computed(() => {
let menuList = store.curUserMenuList.find(x => x.url == route.path);
return menuList ? menuList.id : -1;
});
watchEffect(() => {
const tempObj = cloneDeep(store.getWidgetDragObj);
let isOutRangeX = false,
isOutRangeY = false; //是否超出范围
if (tempObj.xaxis == 0 && tempObj.yaxis === 0) {
tempObj.xaxis = dragObj.parentW - 540;
tempObj.yaxis = 88;
isOutRangeX = dragObj.parentW < 900;
} else {
isOutRangeX = dragObj.parentW < tempObj.width + tempObj.xaxis;
isOutRangeY = dragObj.parentH < tempObj.height + tempObj.yaxis;
}
if (isOutRangeX) tempObj.xaxis = dragObj.parentW - tempObj.width - 10;
if (isOutRangeY) tempObj.yaxis = dragObj.parentH - tempObj.height - 10;
Object.assign(dragPositionObj, tempObj);
reloadDragWrap();
});
const draggable = useDraggable(refApplist, appList, {
animation: 150,
onStart() {
// console.log("start");
},
async onUpdate() {
let ids = appList.value.map(x => x.id);
let appIds = appList.value.map(x => x.appId);
if (isDesltopAppDefault.value) {
await UserSettingApi.sortUserDesktopApp(appIds);
isDesltopAppDefault.value = false;
} else await UserSettingApi.sortUserDesktopApp(ids);
// console.log("update");
},
});
onMounted(async () => {
await getAppList();
dragObj.parentW = window.innerWidth;
dragObj.parentH = window.innerHeight;
window.addEventListener("resize", cancalDebounce);
window.addEventListener("message", iframeCallback);
});
onUnmounted(() => {
window.removeEventListener("resize", cancalDebounce);
window.removeEventListener("message", iframeCallback);
});
const widgetSel = computed(() => store.getWidgetSel);
watch(() => store.curRoleUid, getAppList);
watch(
() => store.getWidgetAppList,
val => {
widgetAppList.value = val.map(x => {
return { ...x, loading: true, height: 20 };
});
},
{
immediate: true,
}
);
function onMenuChange(curDom: any, state: boolean) {
$(curDom).toggleClass("app-container__focus", state);
}
async function onMenuSel(item: any) {
if (item.id == 1) {
try {
const find = appList.value.find(x => x.id == item.appId);
await UserSettingApi.deleteUserDesktopAppById(find.id, find.appId);
Message.success("操作成功");
await getAppList();
} catch (error) {
Message.error("内部服务错误");
}
}
if (item.id == 2) {
let find = appList.value.find(x => x.appId == item.appId);
bschLinkPage({ url: find.url, type: LinkOpenTypeEnum.Open });
}
}
function onDragChange(e: any) {
var t: Element = e.target;
if (!!t && !includes(t.classList, "widget-header-drag")) dragObj.isDrag = false;
}
function onDragResizestop(opt: any, type: "drag" | "resize") {
const { left, top, width, height } = opt;
if (type === "drag") {
dragObj.isDrag = false;
if (left === dragPositionObj.xaxis && top === dragPositionObj.yaxis) return false;
} else {
}
let positionInfo: WidgetPositionResp = {
xaxis: left,
yaxis: top,
width,
height,
};
store.updateWidgetInfo(positionInfo);
debounceSaveUserWidget(positionInfo);
}
const debounceSaveUserWidget = debounce(saveUserWidget, 1000);
async function saveUserWidget(info: WidgetPositionResp) {
info.id = store.curUserSettingInfo?.id;
info.schId = store.getCurSchId;
await UserSettingApi.saveUserWidget(info);
}
const cancalDebounce = debounce(setDragWindowInfo, 150);
function setDragWindowInfo() {
dragObj.parentW = window.innerWidth;
dragObj.parentH = window.innerHeight;
}
function reloadDragWrap() {
if (!store.getWidgetDragObj.isWidgetShow) return;
isDragShow.value = false;
requestAnimationFrame(() => {
isDragShow.value = true;
});
}
async function onWidgetSelItemClick(appId: any) {
const curAppIdStrs = store.curUserSettingInfo.widgetAppIds;
const curAppIds = store.getWidgetAppIds;
let find = store.getWidgetSel.find(x => x.k == appId);
if (!find) return false;
let checkAppIds: number[] = [];
if (!!find.checked) {
if (curAppIdStrs == null) checkAppIds = difference(curAppIds, [find.k]);
else {
let tempArr = cloneDeep(curAppIds);
checkAppIds = remove(tempArr, (x: number) => x !== appId);
}
} else {
checkAppIds = [...curAppIds, find.k];
}
console.log(checkAppIds);
store.curUserSettingInfo.widgetAppIds = checkAppIds.join(",");
const id = store.curUserSettingInfo?.id;
await UserSettingApi.saveUserAppIds(id, checkAppIds.join(","));
console.log("后续操作");
}
function hideWidgetSel() {
dragObj.selVisible = false;
}
function showWidgetSelClick() {
dragObj.selVisible = true;
window.removeEventListener("click", hideWidgetSel);
requestAnimationFrame(() => {
window.addEventListener("click", hideWidgetSel, { once: true });
});
}
async function onClickWidgetHide() {
const id = store.curUserSettingInfo?.id;
if (!!store.curUserSettingInfo) store.curUserSettingInfo.isWidgetShow = false;
await UserSettingApi.toggleWidgetShow(id);
}
function onIframeLoad(id: number) {
const ele: any = document.getElementById("iframe_widget_" + id);
if (!ele) return false;
const msgObj = {
roleInfo: store.curRoleInfo,
id: id,
};
const msgStr = bschRoleEncode(msgObj);
ele.contentWindow.postMessage(msgStr, "*");
}
function iframeCallback(ev: MessageEvent<any>) {
const { id, height } = JSON.parse(ev.data);
let find = widgetAppList.value.find(x => x.id == id);
if (!find) return;
find.height = height;
requestAnimationFrame(() => {
find.loading = false;
});
//console.log(find, id, height);
}
async function getAppList() {
appList.value = await UserSettingApi.getUserDesktopAppList();
let arr = cloneDeep(appList.value);
isDesltopAppDefault.value = arr.filter(x => x.id == null).length === arr.length;
}
async function openLinkPage(item: any) {
let options: Parameters<typeof bschLinkPage>[0];
options = {
url: item.url as string,
type: item.linkType,
title: item.name,
};
if (item.type == 1 && $conf.ifameAppIdArr.includes(item.appId)) {
options.type = LinkOpenTypeEnum.IFrame;
options.isHeader = true;
}
if (item.linkType == LinkOpenTypeEnum.Modal) {
if (!!item.layout) {
options.modalWidth = JSON.parse(item.layout).width;
options.modalHeight = JSON.parse(item.layout).height;
}
}
bschLinkPage(options);
//bschLinkPage
}
</script>
<style lang="scss" scoped>
@import url("@/assets/styles/componets/_widget.scss");
.used-app-wrap {
height: calc(100% - 88px);
}
.used-app-list {
margin-left: 20px;
display: flex;
flex-flow: column wrap;
justify-content: flex-start;
height: 100%;
align-items: stretch;
align-content: flex-start;
}
</style>
<style lang="scss">
.widget-wrap .vdr-stick-tm,
.vdr-stick-bm {
width: 380px !important;
left: 0 !important;
opacity: 0;
}
.vdr.active::before {
content: none;
}
</style>