init: userbase front/service/deploy source (CI builds front via Gitea Actions)

This commit is contained in:
ab
2026-08-04 21:24:50 +08:00
commit e02dcc23da
407 changed files with 48134 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
<template>
<div class="improt-wrap">
<div class="form-item">
<label>案例下载:</label>
<a @click="downTempFile" href="javascript:;">点击下载模板.xls</a>
</div>
<div class="upload-panel">
<Upload type="drag" :on-success="onSuccess" :on-error="onError" :data="uploadData" :headers="uploadHeader"
:with-credentials="true" :action="uploadUrl"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel">
<div class="upload-box">
<Icon type="ios-paper" size="52" style="color: #6281eb"></Icon>
<h4>点击或将文件拖拽到这里上传</h4>
<div class="info">文件大小不超过50M 支持扩展名: xls,xlsx</div>
</div>
</Upload>
</div>
<Alert v-if="!!errorMsg" type="error" show-icon><span v-html="errorMsg"></span></Alert>
<Alert v-else-if="!!successMsg" type="success" show-icon>{{ successMsg }}</Alert>
<Alert v-else style="margin-bottom: 0" type="warning" show-icon>
请按照案例模板内容进行编辑,否则将无法导入。
</Alert>
</div>
</template>
<script>
import Utils from "@/utils";
export default {
props: {
tempFileUrl: String,
uploadUrl: String,
uploadData: Object,
},
data() {
return {
errorMsg: null,
successMsg: null,
};
},
computed: {
uploadHeader() {
const roleInfo = Utils.getCurrentRoleInfo();
roleInfo.schoolId = roleInfo.schId;
roleInfo.bschapirequest = 1;
// roleInfo.userType = 2;
return roleInfo;
},
},
methods: {
downTempFile() {
window.top.location.href = this.tempFileUrl;
},
onSuccess(res, file) {
if (res.code == 0) {
this.errorMsg = null;
this.successMsg = "操作成功!";
this.$emit("on-success");
} else {
this.errorMsg = res.msg;
}
},
onError(err, file) {
this.errorMsg = JSON.stringify(err);
console.log(err, file);
},
},
};
</script>
<style lang="scss" scoped>
.improt-wrap {
padding: 16px;
.form-item {
margin-bottom: 10px;
}
}
.upload-panel {
.upload-box {
background: #f7f8fc;
padding: 20px 0;
h4 {
color: #505c7e;
margin: 3px 0;
}
}
.info {
font-size: 13px;
color: #666;
}
}
</style>
@@ -0,0 +1,276 @@
<template>
<div class="group-tree-search-panel">
<div v-if="searchVisible" class="search-bar mt-8 mb-8">
<Input v-model="searchKeyword" :placeholder="searchPlaceholder" suffix="ios-search" search clearable
class="search-input" @on-search="onSearch" @on-clear="onClear" />
<Button v-if="showCloseButton" icon="md-close" @click="closeSearch" />
</div>
<div class="tree-body">
<MyTree v-if="!treeLoading" ref="refMyTree" :autoClick="autoClick" :treeData="treeData"
:setting="treeSetting" @on-tree-click="treeLink" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, useTemplateRef } from "vue";
import { cloneDeep } from "lodash";
import { Message } from "view-ui-plus";
import MyTree from '@/components/MyTree.vue'
const GROUP_TYPE = {
NORMAL_GROUP: 1,
SCHOOL_GROUP: 2,
GRADE_GROUP: 3,
CLASS_GROUP: 4,
AREA_GROUP: 5,
STAGE_GROUP: 6,
TEACHER_GROUP: 7,
STUDENT_GROUP: 8,
} as const
const DEFAULT_CACHE_KEY = 'bsch.group-tree.selected-node'
const props = withDefaults(defineProps<{
treeApi: () => Promise<any[]>;
autoClick?: boolean;
defaultSearchVisible?: boolean;
showCloseButton?: boolean;
searchPlaceholder?: string;
cacheKey?: string;
}>(), {
autoClick: false,
defaultSearchVisible: true,
showCloseButton: false,
searchPlaceholder: '输入组名称搜索',
cacheKey: DEFAULT_CACHE_KEY,
})
const emit = defineEmits<{
'on-change': [treeNode: any]
'on-tree-data': [tree: any[]]
}>()
const curTreeNode = defineModel<any>('curTreeNode', { default: {} })
const refMyTree = useTemplateRef('refMyTree')
const treeLoading = ref(false)
const treeData = ref<any[]>([])
const searchKeyword = ref('')
const searchVisible = ref(props.defaultSearchVisible)
const treeSetting = ref({
view: { dblClickExpand: false, showIcon: false },
edit: { enable: false, showRemoveBtn: false, showRenameBtn: false, drag: false },
check: { enable: false },
callback: {},
})
function formatTree(list: any[], depth = 1): any[] {
return (list || []).map((node) => {
const children = node.children;
const hasChildren = Array.isArray(children) && children.length > 0;
const formatted: any = { ...node, isParent: hasChildren };
if (depth === 1) formatted.open = true;
if (hasChildren) {
formatted.children = formatTree(children, depth + 1);
} else {
delete formatted.children;
}
return formatted;
});
}
function findNodeById(list: any[], id?: number | null): any {
if (id == null) return null;
for (const node of list || []) {
if (Number(node.id) === Number(id)) return node;
const child = findNodeById(node.children || [], id);
if (child) return child;
}
return null;
}
function findFirstNodeByType(list: any[], type: number): any {
for (const node of list || []) {
if (Number(node.type) === type) return node;
const child = findFirstNodeByType(node.children || [], type);
if (child) return child;
}
return null;
}
function getCachedSelectedNodeId() {
try {
const raw = sessionStorage.getItem(props.cacheKey);
if (!raw) return null;
const cached = JSON.parse(raw);
const id = Number(cached?.id);
return Number.isFinite(id) ? id : null;
} catch {
return null;
}
}
function cacheSelectedNode(treeNode: any) {
if (!treeNode?.id) return;
sessionStorage.setItem(props.cacheKey, JSON.stringify({
id: Number(treeNode.id),
name: treeNode.name,
type: treeNode.type,
}));
}
function resolveInitialSelectId(list: any[], preferredId?: number | null) {
const preferred = findNodeById(list, preferredId);
if (preferred) return preferred.id;
const cached = findNodeById(list, getCachedSelectedNodeId());
if (cached) return cached.id;
const schoolNode = findFirstNodeByType(list, GROUP_TYPE.SCHOOL_GROUP);
if (schoolNode) return schoolNode.id;
if (props.autoClick && list.length) return list[0].id;
return list[0]?.id ?? null;
}
async function loadTree(preferredId?: number | null) {
treeLoading.value = true;
try {
const tree = await props.treeApi();
treeData.value = formatTree(tree || []);
emit('on-tree-data', cloneDeep(treeData.value));
} catch (e: any) {
Message.error(e?.message || '加载组织树失败');
} finally {
treeLoading.value = false;
}
if (!treeData.value.length) return;
await nextTick();
const selectId = resolveInitialSelectId(treeData.value, preferredId);
if (selectId != null) {
refMyTree.value?.reloadTree(selectId);
}
}
async function refreshTree(selectId?: number) {
await loadTree(selectId);
}
function treeLink(treeNode: any) {
const copyNode = cloneDeep(treeNode)
copyNode.children = [];
curTreeNode.value = copyNode;
cacheSelectedNode(copyNode);
emit('on-change', copyNode)
}
function onSearch() {
refMyTree.value?.search(searchKeyword.value);
}
function onClear() {
searchKeyword.value = '';
refMyTree.value?.search('');
}
function openSearch() {
searchVisible.value = true;
}
function closeSearch() {
searchVisible.value = false;
onClear();
}
function reloadTree(id?: number) {
refMyTree.value?.reloadTree(id);
}
function selectNode(id: number, isClick = true) {
refMyTree.value?.selectNode(id, isClick);
}
function removeNode(id: number) {
refMyTree.value?.removeNode(id);
}
function renameNode(id: number, name: string) {
refMyTree.value?.renameNode(id, name);
}
function getNodeById(id: number) {
return refMyTree.value?.getNodeById(id);
}
function addNote(pid: number, id: number, name: string, isParent?: boolean, children?: any[]) {
refMyTree.value?.addNote(pid, id, name, isParent, children);
}
function openTree(open: boolean) {
refMyTree.value?.openTree(open);
}
function searchByKeyword(keyword: string) {
searchKeyword.value = keyword;
onSearch();
}
function clearByKeyword() {
searchKeyword.value = '';
onClear();
}
loadTree()
defineExpose({
openSearch,
closeSearch,
onSearch,
onClear,
searchByKeyword,
clearByKeyword,
loadTree,
refreshTree,
reloadTree,
selectNode,
removeNode,
renameNode,
getNodeById,
addNote,
openTree,
})
</script>
<style lang="scss" scoped>
.group-tree-search-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.search-bar {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
}
.search-input {
width: 100%;
}
.tree-body {
flex: 1;
min-height: 0;
}
.tree-body :deep(.ztree) {
height: 100%;
overflow-y: auto;
}
</style>
+178
View File
@@ -0,0 +1,178 @@
<template>
<div class="module-authz-tree">
<div v-for="node in visibleNodes" :key="node.id" class="tree-node"
:style="{ paddingLeft: `${(node.level || 1) * 18 - 18}px` }"
:class="{ 'is-leaf': !node.children?.length, 'is-expanded': node.expand !== false }">
<div class="node-main">
<span v-if="node.children?.length" class="expand-icon" @click.stop="toggleExpand(node)">
<Icon :type="node.expand === false ? 'ios-add' : 'ios-remove'" />
</span>
<span v-else class="expand-placeholder"></span>
<Checkbox :model-value="!!node.checked"
:disabled="loadingId === node.id && loadingType === 'check'"
@on-change="(checked: boolean) => onCheckChange(node, checked)">
<span class="node-name" :title="node.name">{{ node.name }}</span>
</Checkbox>
</div>
<div class="node-inherit">
<Checkbox :model-value="!!node.inheritFlag"
:disabled="!node.checked || (loadingId === node.id && loadingType === 'inherit')"
@on-change="(checked: boolean) => onInheritChange(node, checked)">
</Checkbox>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive } from "vue";
export interface ModuleAuthzNode {
id: number;
name: string;
level?: number;
checked?: boolean;
inheritFlag?: boolean;
expand?: boolean;
children?: ModuleAuthzNode[];
}
const props = defineProps<{
nodes: ModuleAuthzNode[];
appId: number;
groupId: number;
loadingId?: number | null;
loadingType?: "check" | "inherit" | null;
}>();
const emit = defineEmits<{
(e: "module-change", payload: {
appId: number;
groupId: number;
moduleId: number;
checked: boolean;
inheritFlag: boolean;
type: "check" | "inherit";
}): void;
}>();
const expandMap = reactive<Record<number, boolean>>({});
function buildLocalNodes(nodes: ModuleAuthzNode[]): ModuleAuthzNode[] {
return (nodes || []).map((n) => ({
...n,
expand: expandMap[n.id] ?? true,
children: buildLocalNodes(n.children || []),
}));
}
const localNodes = computed<ModuleAuthzNode[]>(() => buildLocalNodes(props.nodes));
const visibleNodes = computed<ModuleAuthzNode[]>(() => {
const result: ModuleAuthzNode[] = [];
const walk = (list: ModuleAuthzNode[]) => {
for (const node of list) {
result.push(node);
if (node.children?.length && node.expand !== false) {
walk(node.children);
}
}
};
walk(localNodes.value);
return result;
});
function toggleExpand(node: ModuleAuthzNode) {
expandMap[node.id] = node.expand === false;
}
function onCheckChange(node: ModuleAuthzNode, checked: boolean) {
node.checked = checked;
if (!checked) {
node.inheritFlag = false;
}
emit("module-change", {
appId: props.appId,
groupId: props.groupId,
moduleId: node.id,
checked,
inheritFlag: !!node.inheritFlag,
type: "check",
});
}
function onInheritChange(node: ModuleAuthzNode, checked: boolean) {
node.inheritFlag = checked;
emit("module-change", {
appId: props.appId,
groupId: props.groupId,
moduleId: node.id,
checked: !!node.checked,
inheritFlag: checked,
type: "inherit",
});
}
</script>
<style lang="scss" scoped>
.module-authz-tree {
user-select: none;
}
.tree-node {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
min-height: 32px;
border-bottom: 1px dashed #f0f0f0;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f7f7f7;
}
}
.node-main {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
gap: 4px;
}
.expand-icon {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #999;
font-size: 16px;
flex-shrink: 0;
&:hover {
color: #2d8cf0;
}
}
.expand-placeholder {
width: 18px;
flex-shrink: 0;
}
.node-name {
font-size: 13px;
color: #333;
}
.node-inherit {
width: 80px;
text-align: center;
flex-shrink: 0;
}
</style>
@@ -0,0 +1,76 @@
<template>
<Modal v-model="isModal" @on-visible-change="showChange" :title="title">
<Alert type="warning" class="mb-1" show-icon>
<template #icon>
<Icon type="ios-bulb-outline" :class="{ 'mt-5': !info }" slot="icon" :size="28"></Icon>
</template>
<div v-if="msg" :class="['message-box', info ? '' : 'mt-10 mb-10 ml-10']">{{ msg }}</div>
<template #desc v-if="info" slot="desc">{{ info }}</template>
</Alert>
<template #footer>
<Button type="primary" :loading="okLoading" @click="onOkClick">{{ confirmText }}</Button>
<Button :loading="cancleLoading" @click="onCancleClick">{{ closeText }}</Button>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { Modal, Button, Alert, Icon } from "view-ui-plus";
const isModal = ref(false),
okLoading = ref(false),
cancleLoading = ref(false);
const props = withDefaults(
defineProps<{
title?: string;
msg: string;
info?: string;
isLoading?: boolean;
confirmText?: string;
closeText?: string;
confirmFn: any;
closeFn: any;
destroy: Function;
}>(),
{
title: "提示",
isLoading: true,
confirmText: "确认",
closeText: "取消",
}
);
onMounted(function () {
setTimeout(() => {
isModal.value = true;
}, 0);
});
const onOkClick = async () => {
if (!!props.isLoading) okLoading.value = true;
const { onOk, reslove } = props.confirmFn;
await Promise.resolve(onOk());
isModal.value = false;
okLoading.value = false;
reslove(true);
};
const onCancleClick = async () => {
if (!!props.isLoading) cancleLoading.value = true;
const { onCancel, reslove } = props.closeFn;
if (typeof onCancel !== "undefined") await Promise.resolve(onCancel());
cancleLoading.value = false;
isModal.value = false;
reslove(false);
};
const showChange = (state: boolean) => {
if (!state) {
props.destroy();
}
};
</script>
<style></style>
+42
View File
@@ -0,0 +1,42 @@
import { MyConfirm } from "@/types/Basics";
import confirm from "./Confirm.vue";
import { createApp } from "vue";
export default ({ title, msg, info, confirmText, closeText, isLoading, onOk, onCancel }: MyConfirm) => {
return new Promise<boolean>((reslove, reject) => {
const instance = createApp(confirm, {
title,
msg,
info,
confirmText,
closeText,
isLoading,
confirmFn: {
onOk,
reslove,
},
closeFn: {
onCancel,
reslove,
},
destroy: () => {
closeUnmount();
},
});
// 创建一个节点
const vnode = document.createElement("div");
// 添加到body元素上
document.body.appendChild(vnode);
// 实例挂载到节点上
instance.mount(vnode);
// 关闭状态
// 注册实例并移除节点
function closeUnmount() {
setTimeout(() => {
instance.unmount();
document.body.removeChild(vnode);
}, 100);
}
});
};
+214
View File
@@ -0,0 +1,214 @@
<template>
<div>
<div :id="domId" :ref="domId" class="ztree"></div>
</div>
</template>
<script>
export default {
props: {
treeData: {
type: Object,
},
setting: {
type: Object,
default: {},
},
autoClick: {
type: Boolean,
default: true,
},
isCheck: {
typeof: Boolean,
default: false,
},
},
data() {
return {
domId: "",
treeSetting: _.cloneDeep(this.setting),
treeObj: null,
highlightNodes: [],
};
},
created() {
this.$watch("treeData", val => {
if (val) {
this.$nextTick(() => {
this.initTree(this.treeData, this.domId);
});
}
});
},
mounted() {
this.domId = this.uuid();
if (this.treeData) {
this.$nextTick(() => {
this.initTree(this.treeData, this.domId);
});
}
},
methods: {
uuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
},
reloadTree(nodeId) {
this.initTree(this.treeData, this.domId);
if (nodeId != null) {
this.$nextTick(() => {
this.selectNode(nodeId, true); // 第二个参数 true 会 emit on-tree-click
});
}
},
initTree(_data, _domId) {
const _this = this;
_this.treeSetting.callback.onClick = (event, treeId, treeNode, clickFlag) => {
// console.log(event, treeId, treeNode, clickFlag);
if (_this.isCheck) {
var treeObj = $.fn.zTree.getZTreeObj(treeId);
treeObj.checkNode(treeNode, !treeNode.checked, true);
}
_this.$emit("on-tree-click", treeNode);
};
_this.treeSetting.callback.beforeDrop = (treeId, treeNodes, targetNode, moveType) => {
if (moveType == "inner") {
// 移除限制,允许叶子节点接受子节点
} else {
if (targetNode.pid === 0) return false;
}
return true;
};
_this.treeSetting.callback.beforeDrag = (treeId, treeNodes) => {
return treeNodes[0].drag !== false;
};
_this.treeSetting.callback.beforeClick = (treeId, treeNode, clickFlag) => {
return !(treeNode.click === false);
};
_this.treeSetting.callback.onDrop = (event, treeId, treeNodes, targetNode, moveType) => {
_this.$emit("on-tree-drag", { treeNodes, targetNode, moveType });
};
_this.treeSetting.callback.onCheck = (event, treeId, treeNode) => {
_this.$emit("on-tree-check", treeNode);
};
_this.treeObj = $.fn.zTree.init($("#" + _domId), _this.treeSetting, _data);
if (_this.autoClick) {
if (Array.isArray(this.treeData)) {
let id = this.treeData[0].id;
_this.selectNode(id);
} else {
let id = this.treeData.id;
_this.selectNode(id);
}
}
},
selectNode(id, isClick = true) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var node = treeObj.getNodesByFilter(node => node.id == id, true);
if (node == null) return false;
treeObj.selectNode(node);
treeObj.expandNode(node, true, false, false);
treeObj.selectNode(node);
if (isClick) this.$emit("on-tree-click", node);
},
openTree(open) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
treeObj.expandAll(open);
},
removeNode(id) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var node = treeObj.getNodesByFilter(node => node.id == id, true);
var pnode = node.getParentNode();
treeObj.removeNode(node);
pnode.isParent = true;
treeObj.updateNode(pnode);
this.selectNode(pnode.id);
},
getNodeById(id) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var node = treeObj.getNodesByFilter(node => node.id == id, true);
return node;
},
// 仅更新节点勾选状态,不级联、不触发 onCheck 回调
checkNodeById(id, checked) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
if (!treeObj) return;
var node = treeObj.getNodesByFilter(node => node.id == id, true);
if (node) treeObj.checkNode(node, !!checked, false);
},
renameNode(id, name) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var node = treeObj.getNodesByFilter(node => node.id == id, true);
node.name = name;
treeObj.updateNode(node);
},
addNote(pid, id, name, isParent, children) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var pnode = treeObj.getNodesByFilter(node => node.id == pid, true);
var newNode = { pid, id, name, isParent };
if (children && children.length) newNode.children = children;
treeObj.addNodes(pnode, -1, newNode);
},
setHighlight(node, on = true) {
const a = $('#' + node.tId + '_a');
if (on) a.addClass('ztree-search-highlight');
else a.removeClass('ztree-search-highlight');
},
clearHighlight() {
(this.highlightNodes || []).forEach(n => this.setHighlight(n, false));
this.highlightNodes = [];
},
search(keyword) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
if (!treeObj) return;
this.clearHighlight();
const kw = (keyword || '').trim().toLowerCase();
if (!kw) return;
const matchNodes = treeObj.getNodesByFilter(
(node) => node.name && node.name.toLowerCase().includes(kw),
false
) || [];
if (!matchNodes.length) {
if (this.$Message && this.$Message.info) this.$Message.info('未搜索到匹配组');
return;
}
matchNodes.forEach(n => {
let p = n;
while (p) {
treeObj.expandNode(p, true, false, false);
p = p.getParentNode();
}
});
this.highlightNodes = matchNodes;
matchNodes.forEach(n => this.setHighlight(n, true));
this.selectNode(matchNodes[0].id, true);
},
},
};
</script>
<style>
.ztree-search-highlight {
background-color: #ff6600 !important;
}
.ztree li a.curSelectedNode {
background-color: #ffe4bb !important;
}
</style>
+234
View File
@@ -0,0 +1,234 @@
<template>
<div style="height: 100%;">
<div class="tree-search-bar">
<Input v-model="keyword" placeholder="输入学校,回车查找" suffix="ios-search" clearable @on-enter="onSearch"
@on-clear="onSearch" style="width:100%;margin-bottom: 10px;" />
</div>
<div :id="domId" :ref="domId" class="ztree"></div>
</div>
</template>
<script>
export default {
props: {
treeData: {
type: Object,
},
setting: {
type: Object,
default: {
view: {
dblClickExpand: false,
showIcon: false,
},
edit: {
enable: true,
showRemoveBtn: false,
showRenameBtn: false,
drag: false,
},
check: {
enable: false,
},
callback: {},
},
},
autoClick: {
type: Boolean,
default: true,
},
isCheck: {
typeof: Boolean,
default: false,
},
},
data() {
return {
domId: "",
treeSetting: _.cloneDeep(this.setting),
treeObj: null,
keyword: '',
highlightNodes: []
};
},
created() {
this.$watch("treeData", val => {
console.log(val);
if (val) {
console.log(val);
this.$nextTick(() => {
this.initTree(this.treeData, this.domId);
});
}
});
},
mounted() {
this.domId = this.uuid();
},
methods: {
uuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
},
reloadTree() {
this.treeData.open = true;
this.initTree(this.treeData, this.domId);
},
initTree(_data, _domId) {
const _this = this;
_this.treeSetting.callback.onClick = (event, treeId, treeNode, clickFlag) => {
// console.log(event, treeId, treeNode, clickFlag);
if (_this.isCheck) {
var treeObj = $.fn.zTree.getZTreeObj(treeId);
treeObj.checkNode(treeNode, !treeNode.checked, true);
}
_this.$emit("on-tree-click", treeNode);
};
_this.treeSetting.callback.beforeDrop = (treeId, treeNodes, targetNode, moveType) => {
if (moveType == "inner") {
if (!targetNode.isParent) return false;
} else {
if (targetNode.pid === 0) return false;
}
};
_this.treeSetting.callback.beforeClick = (treeId, treeNode, clickFlag) => {
return !(treeNode.click === false);
};
_this.treeSetting.callback.onDrop = (event, treeId, treeNodes, targetNode, moveType) => {
_this.$emit("on-tree-drag", { treeNodes, targetNode, moveType });
};
_this.treeSetting.callback.onCheck = (event, treeId, treeNode) => {
_this.$emit("on-tree-check", treeNode);
};
_this.treeObj = $.fn.zTree.init($("#" + _domId), _this.treeSetting, _data);
if (_this.autoClick) {
if (Array.isArray(this.treeData)) {
let id = this.treeData[0].id;
_this.selectNode(id);
} else {
let id = this.treeData.id;
_this.selectNode(id);
}
}
},
selectNode(id, isClick = true) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var node = treeObj.getNodesByFilter(node => node.id == id, true);
if (node == null) return false;
treeObj.selectNode(node);
treeObj.expandNode(node, true, false, false);
treeObj.selectNode(node);
if (isClick) this.$emit("on-tree-click", node);
},
openTree(open) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
treeObj.expandAll(open);
},
removeNode(id) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var node = treeObj.getNodesByFilter(node => node.id == id, true);
var pnode = node.getParentNode();
treeObj.removeNode(node);
pnode.isParent = true;
treeObj.updateNode(pnode);
this.selectNode(pnode.id);
},
getNodeById(id) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var node = treeObj.getNodesByFilter(node => node.id == id, true);
return node;
},
renameNode(id, name) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var node = treeObj.getNodesByFilter(node => node.id == id, true);
node.name = name;
treeObj.updateNode(node);
},
addNote(pid, id, name, isParent) {
const _domId = this.domId;
var treeObj = $.fn.zTree.getZTreeObj(_domId);
var pnode = treeObj.getNodesByFilter(node => node.id == pid, true);
var newNode = { pid, id, name, isParent };
treeObj.addNodes(pnode, -1, newNode);
},
onSearch() {
this.clearHighlight();
if (!this.keyword) return;
this.doSearch(this.keyword);
},
doSearch(key) {
const treeObj = this.treeObj;
// 1. 模糊匹配
const matchNodes = treeObj.getNodesByFilter(node =>
node.type == 2 &&
node.name && node.name.toLowerCase().includes(key.toLowerCase())
);
if (!matchNodes.length) {
this.$Message.info('未搜索到匹配学校')
return;
}
// 2. 展开所有命中节点的父节点
matchNodes.forEach(n => {
let p = n.getParentNode();
while (p) {
treeObj.expandNode(p, true, false, false);
p = p.getParentNode();
}
});
// 3. 高亮
this.highlightNodes = matchNodes;
matchNodes.forEach(n => this.setHighlight(n, true));
// const box = document.getElementById('treeBox');
// const nodeEl = document.getElementById(matchNodes[0].tId);
// box.scrollTop = nodeEl.offsetTop - box.clientHeight / 2; // 居中
if (matchNodes.length) {
const treeObj = this.treeObj;
console.log(matchNodes[0]);
this.selectNode(matchNodes[0].id);
// treeObj.selectNode(matchNodes[0]); // 先选中,会自动滚入可视区
//this.$nextTick(() => treeObj.cancelSelectedNode()); // 取消选中,保持纯高亮
}
},
setHighlight(node, on = true) {
const a = $('#' + node.tId + '_a');
if (on) a.addClass('ztree-search-highlight');
else a.removeClass('ztree-search-highlight');
},
clearHighlight() {
this.highlightNodes.forEach(n => this.setHighlight(n, false));
this.highlightNodes = [];
}
},
};
</script>
<style>
.ztree {
height: calc(100% - 60px);
overflow-y: auto;
}
.ztree-search-highlight {
background-color: #ff6600 !important;
}
.ztree li a.curSelectedNode {
background-color: #ffe4bb !important;
}
</style>
+43
View File
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref, computed, withDefaults } from "vue";
interface Props {
prefix?: string;
name?: string;
color?: string;
hoverColor?: string;
size?: string | number;
}
const props = withDefaults(defineProps<Props>(), {
prefix: "icon",
name: "",
color: "#000",
});
const isHover = ref(false);
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
const svgStyle = computed(() => {
if (!!props.size) return { width: props.size + "px", height: props.size + "px" };
else return null;
});
const compColor = computed(() => {
if (!props.hoverColor) return props.color;
else {
return !!isHover.value ? props.hoverColor : props.color;
}
});
</script>
<template>
<svg
@mouseover="isHover = true"
@mouseout="isHover = false"
aria-hidden="true"
:color="compColor"
:style="svgStyle"
>
<use :xlink:href="symbolId" :fill="compColor" :color="compColor" />
</svg>
</template>
+119
View File
@@ -0,0 +1,119 @@
<template>
<!-- 表格 -->
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
class="link-table table-bottom-box" @on-selection-change="selectChange">
<template #loading>
<div class="table-spin-load">加载中</div>
</template>
</Table>
<!-- 分页 / 插槽 -->
<div class="page-panel">
<div class="fl ml-15 align-middle">
<slot :checkList="selectList" />
</div>
<Page class="page-box fr" :total="total" :model-value="page" :page-size="pageSize"
:page-size-opts="[10, 20, 30, 40]" show-total show-sizer placement="top" @on-change="onPageChange"
@on-page-size-change="onSizeChange" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { Message } from 'view-ui-plus'
import Utils from '@/utils'
import { OperatorEnum } from '@/types'
interface Props {
/** 必填:alova-gen 生成的 selectTextbook 方法(或任意同形方法) */
dataApi: any
columns: any[]
dataParams: any
tableHeight?: number
autoLoad?: boolean
dataFilter?: (resp: any) => any[],
formatResponse?: (res: any) => { total: number; list: any[] }
}
const props = withDefaults(defineProps<Props>(), {
autoLoad: true,
formatResponse: (res: any) => ({ total: res.total, list: res.content })
})
const emit = defineEmits<{
(e: 'selection-change', list: number[]): void
}>()
onMounted(() => {
})
/* ---------------- 表格高度 ---------------- */
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
/* ---------------- 选中行 ---------------- */
const selectList = ref<number[]>([])
function selectChange(rows: any[]) {
selectList.value = rows.map((r: any) => r.id)
emit('selection-change', selectList.value)
}
/* ---------------- 分页 + 请求 ---------------- */
const {
loading,
data: resp,
send: sendLoad,
page,
pageSize,
total,
reload
} = usePagination(
(current: number, size: number) =>
props.dataApi({
...props.dataParams, // 合并外部条件
pageIndex: current,
pageSize: size,
}),
{
// total: res => res.total,
// data: res => res.list,
total: res => props.formatResponse(res).total,
data: res => props.formatResponse(res).list,
// 默认分页参数
initialPage: 1,
initialPageSize: 10,
immediate: props.autoLoad,
watchingStates: [() => props.dataParams]
}
)
/* 列表数据 */
const list = computed(() => {
if (!resp.value) return []
return props.dataFilter ? props.dataFilter(resp.value) : resp.value
})
/* ---------------- 翻页事件 ---------------- */
const onPageChange = (p: number) => (page.value = p)
const onSizeChange = (s: number) => (pageSize.value = s)
/* ---------------- 外部可调用 ---------------- */
function refreshData() {
page.value = 1
reload()
}
function reloadData(type: OperatorEnum = OperatorEnum.sel) {
let p = page.value
const s = pageSize.value
if (type === OperatorEnum.add && p * s === total.value) p += 1
if (type === OperatorEnum.del && (p - 1) * s + 1 === total.value) p -= 1
if (p < 1) p = 1
page.value = p
sendLoad()
}
defineExpose({ list, refreshData, reloadData })
</script>
<style scoped>
/* 沿用旧样式即可 */
</style>
+105
View File
@@ -0,0 +1,105 @@
<template>
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
class="link-table table-bottom-box" @on-selection-change="selectChange">
<template #loading>
<div class="table-spin-load">加载中</div>
</template>
<template v-for="slotName in tableSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
</Table>
<div class="page-panel">
<div class="fl ml-15 align-middle">
<slot :checkList="selectList" />
</div>
<Page class="page-box fr" :total="total" :model-value="page" :page-size="pageSize"
:page-size-opts="[10, 20, 30, 40]" show-total show-sizer placement="top" @on-change="onPageChange"
@on-page-size-change="onSizeChange" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, useSlots } from 'vue'
import Utils from '@/utils'
import { OperatorEnum } from '@/types'
interface Props {
dataApi: any
columns: any[]
tableHeight?: number
autoLoad?: boolean
dataFilter?: (resp: any) => any[]
formatResponse?: (res: any) => { total: number; list: any[] }
}
const props = withDefaults(defineProps<Props>(), {
autoLoad: true,
formatResponse: (res: any) => ({ total: res.total, list: res.content })
})
const emit = defineEmits<{
(e: 'selection-change', list: number[]): void
}>()
const slots = useSlots()
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
const tableSlotNames = computed(() => Object.keys(slots).filter(name => !['default', 'loading'].includes(name)))
const selectList = ref<number[]>([])
function selectChange(rows: any[]) {
selectList.value = rows.map((r: any) => r.id)
emit('selection-change', selectList.value)
}
const {
loading,
data: resp,
send: sendLoad,
page,
pageSize,
total,
reload
} = usePagination(
(current: number, size: number) =>
props.dataApi({ pageIndex: current, pageSize: size }),
{
total: res => props.formatResponse(res).total,
data: res => props.formatResponse(res).list,
initialPage: 1,
initialPageSize: 10,
immediate: props.autoLoad,
}
)
const list = computed(() => {
if (!resp.value) return []
return props.dataFilter ? props.dataFilter(resp.value) : resp.value
})
const onPageChange = (p: number) => (page.value = p)
const onSizeChange = (s: number) => (pageSize.value = s)
function refreshData() {
page.value = 1
reload()
}
function reloadData(type: OperatorEnum = OperatorEnum.sel) {
let p = page.value
const s = pageSize.value
if (type === OperatorEnum.add && p * s === total.value) p += 1
if (type === OperatorEnum.del && (p - 1) * s + 1 === total.value) p -= 1
if (type === OperatorEnum.sel) p = 1
if (p < 1) p = 1
page.value = p
sendLoad()
}
defineExpose({ list, refreshData, reloadData })
</script>
<style scoped></style>
+169
View File
@@ -0,0 +1,169 @@
<template>
<div class="type_set-box">
<div class="type_name">
<i class="iconfont icon-stop fl mr-10" v-if="!isEdit && !active" style="color: #ff5959"></i>
<Input v-if="isEdit" v-model="type.name" ref="type_input" @on-blur="update" @on-enter="update" />
<span v-else class="fl">{{ type.name }}</span>
</div>
<div class="type_action flex">
<div class="action_item" @click="open">
<span v-if="active">停用</span>
<span v-else>启用</span>
</div>
<div class="action_edit" @click="edit">
<span>编辑</span>
</div>
<div class="action_delete" @click="remove">
<span>删除</span>
</div>
</div>
<div class="divider"></div>
<div class="divider2"></div>
</div>
</template>
<script>
export default {
name: "TypeSetBox",
props: {
type: {
name: { type: String, default: "" },
id: { type: String, default: "" },
},
active: { type: Boolean, default: true },
},
data() {
return {
isEdit: false,
eventType: "",
};
},
watch: {
type: {
deep: true,
handler(newVal) {
if (this.isEdit) {
this.isEdit = false;
}
},
},
"type.name"(newVal) {
if (this.isEdit) {
this.isEdit = false;
}
},
},
methods: {
open() {
this.$emit("active", this.type.id);
},
edit() {
this.isEdit = !this.isEdit;
if (this.isEdit) this.$nextTick(() => this.$refs["type_input"].focus());
},
update(e) {
if (!this.type.name) {
this.$Message.error("值不得为空!");
return;
}
if (this.eventType == "") {
this.eventType = e.type;
this.isEdit = false;
this.$emit("edit", this.type.id, this.type.name);
setTimeout(() => {
this.eventType = "";
}, 500);
}
},
remove() {
this.$emit("remove", this.type.id);
},
},
};
</script>
<style lang="scss" scoped>
.flex {
display: flex;
}
.type_set-box {
width: 222px;
background: #ffffff;
margin-right: 15px;
margin-bottom: 15px;
.type_name {
height: 66px;
border: 1px solid #e9e9e9;
border-bottom: none;
padding-left: 15px;
padding-right: 15px;
line-height: 66px;
display: flex;
justify-content: center;
align-items: center;
span {
height: 24px;
font-size: 15px;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 500;
color: #272829;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 222px;
display: inline-block;
}
.iconfont {
font-size: 26px;
}
}
.type_action {
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 222px;
height: 30px;
line-height: 30px;
background: #f7f7f7;
border-radius: 0px 0px 2px 2px;
border-top: none;
border: 1px solid #e9e9e9;
span {
font-size: 12px;
}
div {
cursor: pointer;
flex: 1;
text-align: center;
}
div:hover {
color: #6c91f0;
}
}
.divider {
// position: relative;
height: 14px;
width: 1px;
background: #e8e8e8;
// left: 73px;
// top: -23px;
float: left;
margin-left: 71px;
margin-top: -22px;
}
.divider2 {
// position: relative;
// left: 146px;
// top: -37px;
height: 14px;
width: 1px;
background: #e8e8e8;
float: left;
margin-left: 147px;
margin-top: -22px;
}
}
</style>
+106
View File
@@ -0,0 +1,106 @@
<template>
<div class="improt-wrap">
<slot></slot>
<div class="upload-panel">
<Upload type="drag" :on-success="onSuccess" :on-remove="onRemove" :on-error="onError"
:before-upload="handleUploadBefore" :headers="headers" :data="uploadData" :action="uploadUrl"
:with-credentials="true" :max-size="maxSize" :multiple="multiple">
<div class="upload-box">
<Icon type="ios-paper" size="52" style="color: #6281eb"></Icon>
<h4>点击或将文件拖拽到这里上传</h4>
<div class="info">文件大小不超过{{ $utils.getFileSize(maxSize) }}</div>
</div>
<Alert v-if="!!errorMsg" type="error" show-icon><span v-html="errorMsg"></span></Alert>
</Upload>
</div>
<!-- <slot name="footer"></slot> -->
</div>
</template>
<script>
export default {
model: {
event: "update:fileList",
prop: "fileList",
},
props: {
fileList: {
type: Array,
default: [],
},
uploadUrl: String,
uploadData: Object,
headers: Object,
multiple: {
type: Boolean,
default: false,
},
},
data() {
return {
maxSize: 200 * 1024 * 1024,
errorMsg: null,
};
},
methods: {
handleUploadBefore(file) {
var _this = this;
if (file.size > _this.maxSize) {
_this.$Message.error({
content: "请上传小于" + _this.$utils.getFileSize(_this.maxSize) + "的文件!",
duration: 3,
});
}
},
onSuccess(res, file) {
if (res.code == 0) {
this.errorMsg = null;
const { uid } = file;
this.fileList.push({ ...res.data, uid });
this.$emit("update:fileList", this.fileList);
} else {
let msg = res.data.error;
this.errorMsg = msg;
}
},
onRemove(file, fileList) {
const { uid } = file;
const findIndex = this.fileList.findIndex(item => item.uid === uid);
this.fileList.splice(findIndex, 1);
this.$emit("update:fileList", this.fileList);
console.log(file, fileList);
},
onError(err, file) {
this.errorMsg = JSON.stringify(err);
console.log(err, file);
},
},
};
</script>
<style lang="scss" scoped>
.improt-wrap {
padding: 0 12px;
.form-item {
margin-bottom: 10px;
}
}
.upload-panel {
.upload-box {
background: #f7f8fc;
padding: 30px 0;
h4 {
color: #505c7e;
margin: 3px 0;
}
}
.info {
font-size: 13px;
color: #666;
}
}
</style>
+132
View File
@@ -0,0 +1,132 @@
<template>
<div>
<ul class="public-upload-img fix">
<li v-for="(item, index) in uploadList">
<template v-if="item.status === 'finished'">
<img :src="item.url" alt="" />
<div class="upload-list-cover">
<Icon
type="ios-eye-outline"
size="24"
title="预览图片"
@click.native="handleView(item.url)"
></Icon>
<Icon
size="24"
class="ml-10"
title="删除图片"
type="ios-trash-outline"
@click.native="handleRemove(index)"
></Icon>
</div>
</template>
<template v-else>
<Progress v-if="item.showProgress" :percent="item.percentage" class="mt-24" hide-info></Progress>
</template>
</li>
<li class="add-li" v-show="uploadList.length < uploadCount">
<Upload
ref="upload"
:show-upload-list="false"
:on-success="handleSuccess"
:format="['jpg', 'jpeg', 'png', 'gif']"
accept="image/gif,image/jpeg,image/jpg,image/png"
:max-size="20480"
:on-format-error="handleFormatError"
:on-exceeded-size="handleMaxSize"
:default-file-list="defaultList"
:headers="uploadHeader"
type="drag"
:action="uploadUrl"
:data="data"
>
<div class="add-box">
<Icon type="ios-add" size="50"></Icon>
</div>
</Upload>
</li>
</ul>
<Modal title="图片预览" v-model="visible" footer-hide>
<img :src="imgName" v-if="visible" style="width: 100%" />
</Modal>
</div>
</template>
<script>
export default {
props: {
imgList: { type: Array, default: [] },
uploadUrl: { type: String, default: "" },
uploadCount: { type: Number, default: 1 },
data: { type: Object, default: null },
},
data() {
return {
imgName: "",
visible: false,
uploadList: [],
defaultList: [],
};
},
created() {
this.setDefaultList();
this.$watch("imgList", val => {
this.setDefaultList();
});
},
mounted() {
console.log(this.$refs.upload.fileList);
this.uploadList = this.$refs.upload.fileList;
},
computed: {
uploadHeader() {
const bschUtil = BschLoadUtil.getInstance();
if (!bschUtil.getIsSelf()) {
return {
ticket: sessionStorage.getItem("bschTicket"),
platform: "grzx",
};
} else return null;
},
},
methods: {
handleView(name) {
this.imgName = name;
this.visible = true;
},
handleRemove(index) {
this.$refs.upload.fileList.splice(index, 1);
this.imgList.splice(index, 1);
},
handleSuccess(res, file) {
console.log(res, file);
file.url = res.data.fileurl;
this.imgList.push(res.data.fileurl);
},
handleFormatError(file) {
this.$Notice.info({
title: "不支持的文件类型!",
desc: "请上传指定的文件类型",
});
},
handleMaxSize(file) {
this.$Notice.warning({
title: "图片过大",
desc: "图片最大上传大小为20M.",
});
},
setDefaultList() {
let arr = [];
this.imgList.forEach(element => {
arr.push({
name: "",
url: element,
});
});
this.defaultList = arr;
},
},
};
</script>
<style></style>
+259
View File
@@ -0,0 +1,259 @@
<script setup lang="ts">
import { ref, inject, onBeforeMount, watch, toRefs, nextTick, computed } from "vue";
import { LayoutData } from "@/types/Basics";
import { useRoute, useRouter } from "vue-router";
import { Message } from "view-ui-plus";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
let props = defineProps<{ layoutData: LayoutData }>();
const reload: Function = inject("reload") || new Function("console.error('empty')");
const appPanel = ref();
let route = useRoute();
const router = useRouter();
const isPanel = ref(false),
currPath = ref<string>(""),
navPath = ref<any[]>([]);
const { baseHost, ssoHost, appName } = props.layoutData;
const { appList, userInfo, isAlone, modList } = toRefs(props.layoutData);
const modArr = computed(() => modList.value)
const { $bus } = useCurrentInstance();
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
onBeforeMount(async () => {
// 初次加载时手动同步一次(参考 bschstandard created 中手动触发 routeChange)
routeChange(route);
});
// 路由变化时实时同步左侧高亮与面包屑
watch(
() => route.fullPath,
() => routeChange(route)
);
/**
* 将菜单树扁平化(参考 bschstandard.frame.layout 的 flaten)
* - 子节点携带父节点的 id 作为 pid,用于面包屑与高亮联动
*/
const flaten = (tree: any[], pid = 0): any[] => {
const arr: any[] = [];
tree.forEach(element => {
const { id, name, page, modId } = element;
arr.push({ id, name, page, modId, pid });
if (element.children && element.children.length) {
arr.push(...flaten(element.children, id));
}
});
return arr;
};
const menuArr = computed(() => flaten(modList.value || []));
/**
* 同步当前路由对应的左侧高亮项与面包屑
* - ① 命中菜单树 modId:高亮自身;分组下的子项面包屑为「父级 / 自身」
* - ② 未命中但带 meta.parentId 的深层子页(如 应用类别管理):
* 用 parentId 找父菜单 → 高亮父菜单、面包屑为「(祖先/)父级(可点) / 当前页(不可点)」
* —— 对应效果「应用管理 > 应用类别管理」(参考 bschstandard.frame.layout 的 parentId 回退)
* - ③ 未在菜单树中的旧路由(/subject 等)降级按路由表兜底,仅展示自身
*/
const routeChange = (to: any) => {
const meta: any = to.meta || {};
const { modId, title, parentId } = meta;
const navArr: any[] = [];
// ① 命中菜单节点
const find: any = menuArr.value.find(x => x.modId === modId);
if (find) {
currPath.value = find.page || "";
if (find.pid) {
const pfind = menuArr.value.find(x => x.id === find.pid);
if (pfind) navArr.push({ name: pfind.name });
}
navArr.push({ name: find.name || title, page: find.page });
navPath.value = navArr;
return;
}
// ② 深层子页:按 meta.parentId 找父菜单,高亮父菜单并展开多级面包屑
if (parentId) {
const parent = menuArr.value.find(x => x.modId === parentId);
if (parent) {
currPath.value = parent.page || ""; // 高亮父菜单(如「应用管理」)
if (parent.pid) {
const gp = menuArr.value.find(x => x.id === parent.pid);
if (gp) navArr.push({ name: gp.name }); // 祖先(若父菜单本身在分组下)
}
navArr.push({ name: parent.name, page: parent.page }); // 父级可点击
navArr.push({ name: title }); // 当前深层页,不可点击
navPath.value = navArr;
return;
}
}
// ③ 旧路由兜底:路由表中带 modId 的,仅展示自身
const r = router.getRoutes().find(rt => (rt.meta as any)?.modId === modId);
if (r) {
currPath.value = r.path;
navPath.value = [{ name: (r.meta as any).title, page: r.path }];
return;
}
currPath.value = "";
navPath.value = [];
};
const showPanel = async () => {
if (appList.value.length == 0) {
Message.info("暂无常用应用!");
return;
}
isPanel.value = false;
await nextTick();
isPanel.value = true;
await nextTick();
var showPanelClick = () => {
appPanel.value.onclick = (e: any) => {
e.stopPropagation();
};
document.addEventListener(
"click",
() => {
isPanel.value = false;
},
{ once: true }
);
};
setTimeout(showPanelClick, 0);
};
const linkMenu = (page: string) => {
const path = route.path;
if (page === path) reload();
else router.push(page);
};
const linkUserInfo = () => {
window.location.href = baseHost + "/v/userinfo";
};
const logout = () => {
let url = window.location.href;
url = "/logout?service=" + url;
window.location.href = ssoHost + url;
};
const childChange = (index: number) => {
modArr.value[index].childState = !modArr.value[index].childState;
};
function linkHomePage() {
//@ts-ignore
const bschUtil = BschLoadUtil.getInstance();
if (!bschUtil.getIsSelf()) {
bschUtil.goBackDesktop();
} else {
window.location.href = baseHost + "/BschHome/front/index.html";
}
}
</script>
<template>
<div class="layout-wrap">
<header class="header-container fix">
<div class="fl align-middle icon-box">
<div title="常用应用" :class="['icon-btn', isPanel ? 'active' : '']" @click="showPanel">
<i class="iconfont icon-apps"></i>
</div>
</div>
<div class="delimiter fl align-middle"></div>
<h4 class="fl align-middle">{{ appName }}</h4>
<div class="school-bg"></div>
<div class="header-aside fr">
<img :src="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 modArr">
<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>
{{ 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">
<a href="javascript:;" @click="linkMenu(citem.page)">
{{ citem.name }}
</a>
</li>
</div>
</transition>
</template>
<li v-else :class="item.page == currPath ? 'active' : ''" :key="'menu_li_' + index">
<a href="javascript:;" @click="linkMenu(item.page)">
{{ item.name }}
</a>
</li>
</template>
</ul>
<div class="clear-box"></div>
</div>
<div class="path-nav" :style="pathNavStyle">
<Breadcrumb separator=">">
<a href="javascript:;" @click="linkHomePage">
<BreadcrumbItem>
<i class="iconfont icon-category"></i>
首页
</BreadcrumbItem>
</a>
<BreadcrumbItem v-if="!layoutData.isAlone" to="/">{{ appName }}</BreadcrumbItem>
<template v-for="(item, index) in navPath">
<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" ref="appPanel" v-if="isPanel">
<div class="tips-box"></div>
<ul class="fix">
<li class="fl" v-for="item in appList">
<a :href="item.url" target="_bank" 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 + '/BschHome/front/index.html#/apps'" target="_bank" rel="noopener noreferrer">
全部应用
<i>→</i>
</a>
</div>
</div>
</transition>
</div>
</template>