feat: 接入 UserHome 前端业务代码(含真实 src/依赖,保留 projman 标准 CI)
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<header class="header-container fix">
|
||||
<a class="logout fl align-middle ml-24 mr-20" title="返回" @click="linkBack">
|
||||
<SvgIcon name="back" class="fl" color="#fff" size="24" />
|
||||
</a>
|
||||
|
||||
<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.profilePicture" @click="showUserInfo" class="fl align-middle" />
|
||||
<a class="name fl align-middle text-ellipsis" @click="showUserInfo" href="javascript:void(0);">
|
||||
Hi
|
||||
<span class="ml-4">{{ userInfo.name }}</span>
|
||||
</a>
|
||||
<!-- <div class="delimiter fl align-middle"></div> -->
|
||||
|
||||
<!-- <a href="javascript:;" class="logout fl align-middle" title="返回桌面" @click="linkBack">
|
||||
<i class="iconfont icon-shouye"></i>
|
||||
|
||||
</a> -->
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { computed } from "vue";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import mittBus from "@/utils/mitt";
|
||||
import { StateEnum } from "@/types";
|
||||
import { useRouter } from "vue-router";
|
||||
const store = useDefaultStore();
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
appName: {
|
||||
type: String,
|
||||
default: () => "",
|
||||
},
|
||||
});
|
||||
|
||||
const emitEvents = defineEmits(["on-back-click"]);
|
||||
|
||||
const userInfo = computed(() => store.userInfo);
|
||||
|
||||
function linkBack() {
|
||||
emitEvents("on-back-click");
|
||||
// router.push({ name: "index" });
|
||||
// setTimeout(() => {
|
||||
// mittBus.emit("updateState", StateEnum.Desktop);
|
||||
// }, 10);
|
||||
}
|
||||
|
||||
function showUserInfo() {
|
||||
mittBus.emit("showUserInfoModal", true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import url("@/assets/styles/componets/_app_header.scss");
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="draggable-wrapper" ref="containerRef" :style="{ left: compX + 'px', top: compY + 'px' }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDraggable } from "@/utils/hook/useDraggable";
|
||||
import { ref } from "vue";
|
||||
|
||||
interface Props {
|
||||
x?: number;
|
||||
y?: number;
|
||||
el?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
x: 100,
|
||||
y: 120,
|
||||
});
|
||||
|
||||
const containerRef = ref(null);
|
||||
const { x: compX, y: compY, isDraggable } = useDraggable(containerRef, props.x, props.y);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.draggable-wrapper {
|
||||
position: fixed;
|
||||
position: 20;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="app-bsch-iframe tc base-mask" v-if="isLoadding">
|
||||
<img class="align-middle" style="width: 100px" :src="requireImg('page-loading.svg')" alt="" />
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'app-bsch-iframe base-mask': true,
|
||||
'app-bsch-iframe__header': isHeader,
|
||||
}"
|
||||
v-if="!!url"
|
||||
v-show="!isLoadding"
|
||||
>
|
||||
<AppHeader :app-name="appName" v-if="!!isHeader" @on-back-click="onBackClick"></AppHeader>
|
||||
<iframe id="app_bsch_iframe" :src="url" @load="onIframeLoad"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { BaseLoadEnum, BschIframeType, IframePageParmas, StateEnum } from "@/types";
|
||||
import { computed, ref, nextTick, onMounted, onUnmounted } from "vue";
|
||||
import AppHeader from "./AppHeader.vue";
|
||||
import mittBus from "@/utils/mitt";
|
||||
import { bschRoleEncode, bschRoleParamsEncode } from "@/utils/bschRoleEncode";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const isLoadding = ref(true),
|
||||
isHeader = ref(false),
|
||||
appUrl = ref<string>(),
|
||||
appName = ref<string>();
|
||||
const store = useDefaultStore();
|
||||
const url = computed(() => {
|
||||
if (!!isHeader.value) {
|
||||
if (!appUrl.value || !isValidUrl(appUrl.value)) return null;
|
||||
let uri = new URL(appUrl.value);
|
||||
if (!!isHeader.value) {
|
||||
let { childId, roleId, schId } = store.curRoleInfo;
|
||||
uri.searchParams.set("role", bschRoleParamsEncode(JSON.stringify({ childId, roleId, schId })));
|
||||
}
|
||||
return uri.href;
|
||||
} else return appUrl.value;
|
||||
});
|
||||
|
||||
const isValidUrl = (urlString: string) => {
|
||||
try {
|
||||
return Boolean(new URL(urlString));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
async function openPage(params: IframePageParmas) {
|
||||
isLoadding.value = true;
|
||||
appUrl.value = "";
|
||||
await nextTick();
|
||||
isHeader.value = params.isHeader ?? false;
|
||||
appUrl.value = params.url;
|
||||
appName.value = params.appName;
|
||||
|
||||
setTimeout(() => {
|
||||
isLoadding.value = false;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function onIframeLoad() {
|
||||
const ele: any = document.getElementById("app_bsch_iframe");
|
||||
if (!ele || !isHeader.value) return false;
|
||||
const msgObj = {
|
||||
roleInfo: store.curRoleInfo,
|
||||
};
|
||||
const msgStr = bschRoleEncode(msgObj);
|
||||
ele.contentWindow.postMessage(msgStr, "*");
|
||||
// isLoadding.value = true;
|
||||
}
|
||||
|
||||
onMounted(function () {
|
||||
window.addEventListener("message", iframeCallback);
|
||||
});
|
||||
|
||||
onUnmounted(function () {
|
||||
window.removeEventListener("message", iframeCallback);
|
||||
});
|
||||
|
||||
function iframeCallback(ev: MessageEvent<any>) {
|
||||
const { id, code } = JSON.parse(ev.data);
|
||||
if (Number(code) == BaseLoadEnum.GoBack) {
|
||||
router.push({ name: "index" });
|
||||
setTimeout(() => {
|
||||
mittBus.emit("updateState", StateEnum.Desktop);
|
||||
}, 10);
|
||||
} else if (Number(code) == BaseLoadEnum.UserInfo) {
|
||||
showUserInfo();
|
||||
}
|
||||
}
|
||||
function showUserInfo() {
|
||||
mittBus.emit("showUserInfoModal", true);
|
||||
}
|
||||
|
||||
function onBackClick() {
|
||||
if (route.name === "AppDetails") router.push({ name: "index" });
|
||||
setTimeout(() => {
|
||||
mittBus.emit("updateState", StateEnum.Desktop);
|
||||
}, 10);
|
||||
}
|
||||
defineExpose({ openPage });
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,95 +0,0 @@
|
||||
<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.yschapirequest = 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,87 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="context-container">
|
||||
<slot></slot>
|
||||
<Teleport to="body">
|
||||
<Transition @beforeEnter="handleBeforeEnter" @enter="handleEnter" @afterEnter="handleAfterEnter">
|
||||
<div class="context-menu" :style="{ left: x + 'px', top: y + 'px' }" v-if="visible">
|
||||
<div @click="handleClick(item)" class="menu-item" v-for="item in menu" :key="item.label">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useContextMenu } from "@/utils/hook/useContextMenu";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
interface Menu {
|
||||
label: string;
|
||||
[keys: string]: any;
|
||||
}
|
||||
const props = defineProps({
|
||||
menu: {
|
||||
type: Array<Menu>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
const containerRef = ref(null);
|
||||
const { x, y, visible } = useContextMenu(containerRef);
|
||||
|
||||
const emit = defineEmits(["select", "on-change"]);
|
||||
|
||||
watch(visible, val => emit("on-change", containerRef.value, val));
|
||||
|
||||
function handleClick(item: Menu) {
|
||||
emit("select", item);
|
||||
}
|
||||
|
||||
function handleBeforeEnter(el: any) {
|
||||
el.style.height = 0;
|
||||
}
|
||||
|
||||
function handleEnter(el: any) {
|
||||
el.style.height = "auto";
|
||||
const h = el.clientHeight;
|
||||
el.style.height = 0;
|
||||
requestAnimationFrame(() => {
|
||||
el.style.height = h + "px";
|
||||
el.style.transition = ".3s";
|
||||
});
|
||||
}
|
||||
|
||||
function handleAfterEnter(el: any) {
|
||||
el.style.transition = "none";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.context-menu {
|
||||
background: #fff;
|
||||
position: fixed;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 20;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
line-height: 20px;
|
||||
border-radius: 2px;
|
||||
padding: 7px 16px;
|
||||
color: #515a6e;
|
||||
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: #edf4ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, withDefaults, nextTick } from "vue";
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
color?: string;
|
||||
hoverColor?: string;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
name: "",
|
||||
color: "#000",
|
||||
});
|
||||
const refFetchSvg = ref<any>(null);
|
||||
|
||||
const isHover = ref(false),
|
||||
svgData = ref(),
|
||||
curViewBox = ref<string>("");
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const svgResponse = await fetch(props.src);
|
||||
const svgText = await svgResponse.text();
|
||||
const divDom: HTMLDivElement = document.createElement("div");
|
||||
divDom.innerHTML = svgText;
|
||||
const svgDom = divDom.querySelector("svg");
|
||||
if (svgDom == null) return;
|
||||
svgDom.setAttribute("aria-hidden", "true");
|
||||
svgDom.setAttribute("color", compColor.value);
|
||||
svgDom.setAttribute("fill", compColor.value);
|
||||
if (svgStyle.value != null) {
|
||||
svgDom.setAttribute("width", svgStyle.value.width);
|
||||
svgDom.setAttribute("height", svgStyle.value.height);
|
||||
}
|
||||
curViewBox.value = svgDom.getAttribute("viewBox") || "0 0 1024 1024";
|
||||
svgData.value = svgDom?.innerHTML;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
@mouseover="isHover = true"
|
||||
@mouseout="isHover = false"
|
||||
aria-hidden="true"
|
||||
:color="compColor"
|
||||
:fill="compColor"
|
||||
:style="svgStyle"
|
||||
:width="svgStyle?.width"
|
||||
:height="svgStyle?.height"
|
||||
:viewBox="curViewBox"
|
||||
ref="refFetchSvg"
|
||||
v-html="svgData"
|
||||
></svg>
|
||||
</template>
|
||||
@@ -1,294 +0,0 @@
|
||||
<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" :isCheck="checkable" @on-tree-click="treeLink"
|
||||
@on-tree-check="(node: any) => emit('on-tree-check', node)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick, useTemplateRef } from "vue";
|
||||
import { cloneDeep } from "lodash";
|
||||
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;
|
||||
autoSelect?: boolean;
|
||||
defaultSearchVisible?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
cacheKey?: string;
|
||||
checkable?: boolean;
|
||||
}>(), {
|
||||
autoClick: false,
|
||||
autoSelect: true,
|
||||
defaultSearchVisible: true,
|
||||
showCloseButton: false,
|
||||
searchPlaceholder: '输入组名称搜索',
|
||||
cacheKey: DEFAULT_CACHE_KEY,
|
||||
checkable: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'on-change': [treeNode: any]
|
||||
'on-tree-data': [tree: any[]]
|
||||
'on-tree-check': [treeNode: 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: props.checkable, chkboxType: { Y: '', N: '' } },
|
||||
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) {
|
||||
console.warn(e?.message || '加载组织树失败');
|
||||
} finally {
|
||||
treeLoading.value = false;
|
||||
}
|
||||
|
||||
if (!treeData.value.length) return;
|
||||
|
||||
await nextTick();
|
||||
// 默认自动选中首个可用节点(触发 on-change);移动组等场景传 autoSelect=false 关闭,避免打开即误触"不能移动到自身"
|
||||
if (props.autoSelect) {
|
||||
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 getCheckedNodes() {
|
||||
return refMyTree.value?.getCheckedNodes() || [];
|
||||
}
|
||||
|
||||
function checkNodeById(id: number, checked: boolean) {
|
||||
refMyTree.value?.checkNodeById(id, checked);
|
||||
}
|
||||
|
||||
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,
|
||||
getCheckedNodes,
|
||||
checkNodeById,
|
||||
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>
|
||||
@@ -0,0 +1,602 @@
|
||||
<template>
|
||||
<header :class="headerClass">
|
||||
<img v-if="!!logoUrl" :src="baseUrl + '/image/default-logo.png'" v-real-img="logoUrl" class="logo-box" alt="" />
|
||||
<ul class="operate-container fix">
|
||||
<!-- <li>
|
||||
<Button @click="settingsMenu">test</Button>
|
||||
</li> -->
|
||||
<li v-if="isWidgetIconShow" :class="{ active: isWidgetShow }">
|
||||
<div class="icon-box" v-wave @click="toggleWidgetShow">
|
||||
<SvgIcon name="header-4" color="#333333" />
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="isSearchIconShow">
|
||||
<Transition name="fade1" :duration="{ enter: 1000, leave: 0 }">
|
||||
<div v-show="!isSearching" @click="isSearching = true" class="icon-box" v-wave>
|
||||
<SvgIcon name="header-1" color="#333333" />
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="search" :duration="{ enter: 300, leave: 300 }">
|
||||
<div v-show="isSearching" class="search-box">
|
||||
<input ref="refSearchInput" v-model="searchAppText" type="text" placeholder="请输入应用名称搜索" />
|
||||
<SvgIcon @click="isSearching = false" class="icon" name="header-1" size="20" color="#c4c4c4" />
|
||||
</div>
|
||||
</Transition>
|
||||
</li>
|
||||
<li>
|
||||
<Poptip class="fl" popper-class="message-wrap reset-panel" :width="360" trigger="click"
|
||||
placement="bottom-end" v-model="isMsgShow" :transfer="true">
|
||||
<div class="icon-box" v-wave>
|
||||
<SvgIcon name="header-2" color="#333333" />
|
||||
</div>
|
||||
<template #content>
|
||||
<h3 class="title">查看消息</h3>
|
||||
<vuescroll>
|
||||
<ul class="msg-ul">
|
||||
<li class="no-msg-box" v-if="userMsgList.length == 0">暂无消息</li>
|
||||
<template v-else v-for="(item, index) in userMsgList">
|
||||
<li class="fix">
|
||||
<h4 :class="{
|
||||
'text-ellipsis': true,
|
||||
read: item.read,
|
||||
}">
|
||||
<span class="app-name-box">{{ item.name }}</span>
|
||||
<span class="content" :title="item.msg"
|
||||
@click="linkMessage(item.id, item.appPath, item.read)">
|
||||
{{ item.msg }}
|
||||
</span>
|
||||
</h4>
|
||||
<a href="javascript:;" @click="linkMessage(item.id, item.appPath, item.read)"
|
||||
title="查看详情">
|
||||
<SvgIcon name="arrow-right" color="#bbbbbb" />
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</vuescroll>
|
||||
<div class="footer fix">
|
||||
<a href="javascript:;" class="fl link-all" @click="showMsgList">查看全部</a>
|
||||
<a href="javascript:;" class="fr read-all" v-if="userMsgList.length > 0"
|
||||
@click="readMsgAll(1)">
|
||||
全部已读
|
||||
</a>
|
||||
|
||||
<!-- <a class="tzql_r" @click="clearMsg" href="javascript:;" title="清空消息"
|
||||
><img src="image/msg-clear.png"
|
||||
/></a> -->
|
||||
</div>
|
||||
</template>
|
||||
</Poptip>
|
||||
</li>
|
||||
<li>
|
||||
<Poptip class="fl" popper-class="roleinfo-wrap reset-panel" :width="240" trigger="click"
|
||||
placement="bottom-end" v-model="isRoleInfoShow" :transfer="true">
|
||||
<div class="icon-box" v-wave>
|
||||
<SvgIcon name="header-3" color="#333333" />
|
||||
</div>
|
||||
<template #content>
|
||||
<h3 class="title">
|
||||
<SvgIcon name="switch-role" color="#272829" />
|
||||
<span class="ml-8">切换身份</span>
|
||||
</h3>
|
||||
<vuescroll>
|
||||
<ul class="roles-ul">
|
||||
<template v-for="(item, index) in userRole">
|
||||
<li :class="{ acitve: getIsCheckRoles(item) }" @click="changeRole(item)">
|
||||
<SvgIcon v-if="getIsCheckRoles(item)" color="#45B972" name="role-checked" />
|
||||
<SvgIcon v-else name="role-check" color="#dcdee0" />
|
||||
<div class="user-wrap">
|
||||
<div class="user-box fix">
|
||||
<span v-if="item.roleId != 3" class="fl f16 mr-10">
|
||||
{{ store.userInfo.name }}
|
||||
</span>
|
||||
<span v-else class="fl f16 mr-10">
|
||||
{{ item.childName }}
|
||||
</span>
|
||||
<span v-if="!!item.roleName" class="role-info">
|
||||
{{ item.roleName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="schname-box">
|
||||
{{ item.schName }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</vuescroll>
|
||||
</template>
|
||||
</Poptip>
|
||||
</li>
|
||||
<li>
|
||||
<Poptip class="fl" popper-class="reset-panel" :width="220" trigger="click" placement="bottom-end"
|
||||
v-model="isUserInfoShow" :transfer="true">
|
||||
<div class="icon-box" v-wave>
|
||||
<SvgIcon name="header-5" color="#333333" />
|
||||
</div>
|
||||
<template #content>
|
||||
<ul class="userinfo-ul">
|
||||
<!-- <li class="header border" @click="onUserinfoClickRoles">
|
||||
<span class="name">{{ store.userInfo?.name }}</span>
|
||||
<span v-if="!!store.curRoleInfo?.roleName" class="role-info mr-10">
|
||||
{{ store.curRoleInfo?.roleName }}
|
||||
</span>
|
||||
<Icon :size="18" color="#333" type="ios-arrow-forward" />
|
||||
</li> -->
|
||||
<li class="border" @click="showUserInfo">
|
||||
<img :src="requireImg('userinfo/icon1.png')" alt="" />
|
||||
<span>个人信息</span>
|
||||
</li>
|
||||
<li v-if="!!store.getIsAdmin" @click="linkSysSetting">
|
||||
<img :src="requireImg('userinfo/icon5.png')" alt="" />
|
||||
<span>系统设置</span>
|
||||
</li>
|
||||
<li v-if="!!store.getIsAdmin" @click="linkLogoSetting">
|
||||
<img :src="requireImg('userinfo/icon3.png')" alt="" />
|
||||
<span>设置LOGO</span>
|
||||
</li>
|
||||
<li @click="settingsMenu" v-if="!!store.getIsAdmin">
|
||||
<img :src="requireImg('userinfo/icon6.png')" alt="" />
|
||||
<span>边栏设置</span>
|
||||
</li>
|
||||
<li @click="settingsStats" v-if="!!store.getIsAdmin">
|
||||
<img :src="requireImg('userinfo/icon7.png')" alt="" />
|
||||
<span>统计设置</span>
|
||||
</li>
|
||||
<!--<li @click="settingsTeacherCopilot" v-if="!!store.getIsAdmin">
|
||||
<img :src="requireImg('userinfo/icon8.png')" alt="" />
|
||||
<span>教师助手设置</span>
|
||||
</li>-->
|
||||
|
||||
<li @click="settingsWallpaper" class="border">
|
||||
<img :src="requireImg('userinfo/icon2.png')" alt="" />
|
||||
<span>设置壁纸</span>
|
||||
</li>
|
||||
<li @click="logout">
|
||||
<img :src="requireImg('userinfo/icon4.png')" alt="" />
|
||||
<span>退出登陆</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</Poptip>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<Modal v-model="isMsgListShow" width="900" :mask="true" title="消息" class-name="msg-modal" :transfer="false">
|
||||
<div class="fix mb-10">
|
||||
<Button type="primary" @click="resetMsgPage" size="small" class="fl ml-10 width-10" ghost>刷新</Button>
|
||||
<Button type="primary" @click="readMsgAll(2)" size="small" class="fl ml-10 width-10" ghost>全部已读</Button>
|
||||
</div>
|
||||
<TablePage :table-height="360" ref="refTablePage" page-type="small" :dataApi="tableApi" :pageParams="pageParams"
|
||||
:columns="columns" :auto-load="false" />
|
||||
<template #footer>
|
||||
<Button type="primary" ghost @click="isMsgListShow = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal v-model="isWallpaperShow" class-name="wallpaper-modal" width="960" :mask="false" title="设置壁纸"
|
||||
:transfer="false">
|
||||
<vuescroll>
|
||||
<div class="wallpaper-wrap">
|
||||
<ul class="wallpaper-ul">
|
||||
<template v-for="(item, index) in wallpaperList">
|
||||
<li :class="(index + 1) % 6 == 0 ? '' : 'mr-16'" @click="onClickWallpaper(item.url)">
|
||||
<div class="check-box">
|
||||
<SvgIcon size="16" v-if="getIsWallpaper(item.url)" color="#45B972"
|
||||
name="role-checked" />
|
||||
<SvgIcon size="16" v-else name="role-check" color="#dcdee0" />
|
||||
</div>
|
||||
|
||||
<label v-if="item.type == 0">系统</label>
|
||||
<label v-if="item.type == 1">校</label>
|
||||
<ContextMenu v-if="item.isEdit" @select="onWallpaperSelect" :menu="[
|
||||
{
|
||||
label: '删除',
|
||||
id: item.id,
|
||||
},
|
||||
]">
|
||||
<img :src="item.url" alt="" />
|
||||
</ContextMenu>
|
||||
<template v-else>
|
||||
<img :src="item.url" alt="" />
|
||||
</template>
|
||||
|
||||
<div class="mask-box">{{ index + 1 }}</div>
|
||||
</li>
|
||||
</template>
|
||||
<li>
|
||||
<Upload class="add-wrap tc" :on-success="wallpaperSuccess" :show-upload-list="false"
|
||||
:action="$conf.serviceHost + '/api/wallpaperUpload'"
|
||||
accept="image/jpeg, image/jpg, image/png" :with-credentials="true"
|
||||
:headers="uploadHeaders">
|
||||
<div class="add-box">
|
||||
<SvgIcon name="upload-add" color="#939393" style="width: 24px; height: 24px" />
|
||||
<p>点击上传图片</p>
|
||||
</div>
|
||||
</Upload>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</vuescroll>
|
||||
|
||||
<template #footer>
|
||||
<Button type="primary" ghost @click="isWallpaperShow = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="isLogoShow" class-name="logo-modal" width="720" :mask="false" title="设置LOGO" :transfer="false">
|
||||
<vuescroll>
|
||||
<div class="wallpaper-wrap">
|
||||
<ul class="wallpaper-ul">
|
||||
<template v-for="(item, index) in logoList">
|
||||
<li :class="(index + 1) % 4 == 0 ? '' : 'mr-16'">
|
||||
<label v-if="item.type == 0">系统</label>
|
||||
<label v-else>校</label>
|
||||
<ContextMenu v-if="item.isEdit" @select="onLogoSelect" :menu="[
|
||||
{
|
||||
label: '删除',
|
||||
id: item.id,
|
||||
},
|
||||
]">
|
||||
<img :src="item.url" alt="" />
|
||||
</ContextMenu>
|
||||
<template v-else>
|
||||
<img :src="item.url" alt="" />
|
||||
</template>
|
||||
</li>
|
||||
</template>
|
||||
<li>
|
||||
<Upload class="add-wrap tc" :on-success="logoSuccess" :show-upload-list="false"
|
||||
:action="$conf.serviceHost + '/api/logoUpload'"
|
||||
accept="image/jpeg, image/jpg, image/png, image/gif, image/webp"
|
||||
:with-credentials="true" :headers="uploadHeaders">
|
||||
<div class="add-box">
|
||||
<SvgIcon name="upload-add" color="#939393" style="width: 24px; height: 24px" />
|
||||
<p>点击上传图片</p>
|
||||
</div>
|
||||
</Upload>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</vuescroll>
|
||||
|
||||
<template #footer>
|
||||
<Button type="primary" ghost @click="isLogoShow = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<MenuBarModal ref="refMenuBarModal" />
|
||||
<StatsBarModal ref="refStatsBarModal" />
|
||||
<TeacherCopilotModal ref="refTeacherCopilotModal" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import vuescroll from "vuescroll";
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, inject, watch } from "vue";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
import ContextMenu from "@/components/ContextMenu.vue";
|
||||
import MenuBarModal from "@/components/MenuBarModal.vue";
|
||||
import StatsBarModal from "@/components/StatsBarModal.vue";
|
||||
import TeacherCopilotModal from "@/components/TeacherCopilotModal.vue";
|
||||
import BaseApi from "@/api/BaseApi";
|
||||
import TablePage from "@/components/TablePage.vue";
|
||||
import { IframePageParmas, RoleInfo } from "@/types";
|
||||
import { isEqual, orderBy } from "lodash";
|
||||
import { PageParams } from "@/types/Basics";
|
||||
import UserSettingApi from "@/api/UserSettingApi";
|
||||
import mittBus from "@/utils/mitt";
|
||||
import MenuBarApi from "@/api/MenuBarApi";
|
||||
import Utils from "@/utils";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useDefaultStore();
|
||||
const refTablePage: any = ref(null);
|
||||
const { $conf } = useCurrentInstance();
|
||||
const baseUrl = computed(() => $conf.fileHost);
|
||||
|
||||
const refSearchInput = ref<any>();
|
||||
const refMenuBarModal = ref<any>();
|
||||
const refStatsBarModal = ref<any>();
|
||||
const refTeacherCopilotModal = ref<any>();
|
||||
|
||||
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
|
||||
|
||||
const logoUrl = computed(() => {
|
||||
const schLogo = logoList.value.find(x => x.type == 1);
|
||||
if (schLogo) return schLogo.url;
|
||||
const sysLogo = logoList.value.find(x => x.type == 0);
|
||||
return sysLogo ? sysLogo.url : "";
|
||||
});
|
||||
const isMsgShow = ref(false),
|
||||
isMsgListShow = ref(false),
|
||||
isUserInfoShow = ref(false),
|
||||
isRoleInfoShow = ref(false),
|
||||
isWallpaperShow = ref(false),
|
||||
isLogoShow = ref(false),
|
||||
isSearching = ref(false),
|
||||
searchAppText = ref<string | null>(null),
|
||||
wallpaperList = ref<any[]>([]),
|
||||
logoList = ref<any[]>([]),
|
||||
userMsgList = ref<any[]>([]);
|
||||
|
||||
const headerClass = computed(() => {
|
||||
return { "default-header": true, "base-mask": route.name !== "index" };
|
||||
});
|
||||
|
||||
const isWidgetIconShow = computed(() => {
|
||||
return route.name === "index";
|
||||
});
|
||||
|
||||
const isSearchIconShow = computed(() => {
|
||||
return route.name === "apps";
|
||||
});
|
||||
const isWidgetShow = computed(() => {
|
||||
return store.getWidgetDragObj.isWidgetShow;
|
||||
});
|
||||
|
||||
const userRole = computed(() => store.userRoleList);
|
||||
|
||||
const toggleSearch: any = inject("toggleSearch");
|
||||
|
||||
const pageParams: PageParams = reactive({ page: 1, count: 10 });
|
||||
const tableApi = (params: PageParams) => BaseApi.getMessagePageList(params);
|
||||
const columns = ref<any[]>([
|
||||
{
|
||||
type: "index",
|
||||
title: "序号",
|
||||
width: 70,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "应用",
|
||||
width: 150,
|
||||
tooltip: true,
|
||||
align: "center",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "消息内容",
|
||||
tooltip: true,
|
||||
key: "msg",
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
width: 100,
|
||||
render(h: any, params: any) {
|
||||
return h("span", params.row.read ? "已读" : "未读");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "日期",
|
||||
align: "center",
|
||||
width: 200,
|
||||
key: "date",
|
||||
},
|
||||
]);
|
||||
onMounted(async () => {
|
||||
setTimeout(() => {
|
||||
reloadMsg();
|
||||
// isWallpaperShow.value = true;
|
||||
}, 200);
|
||||
loadLogoList();
|
||||
mittBus.on("searchReset", () => {
|
||||
isSearching.value = false;
|
||||
searchAppText.value = null;
|
||||
});
|
||||
});
|
||||
onUnmounted(() => {
|
||||
mittBus.off("searchReset");
|
||||
});
|
||||
watch(isSearching, val => {
|
||||
if (val) {
|
||||
setTimeout(() => refSearchInput.value?.focus(), 300);
|
||||
} else {
|
||||
// searchAppText.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(searchAppText, val => {
|
||||
mittBus.emit("searchChange", val);
|
||||
});
|
||||
function linkMessage(msgId: number, path: string, isread: boolean) {
|
||||
if (!isread) BaseApi.readMessage(msgId);
|
||||
var index = userMsgList.value.findIndex(x => x.id == msgId);
|
||||
if (index !== -1) userMsgList.value[index].read = true;
|
||||
window.open(path);
|
||||
}
|
||||
|
||||
function getIsCheckRoles(item: RoleInfo) {
|
||||
return isEqual(item, store.curRoleInfo);
|
||||
}
|
||||
|
||||
async function changeRole(role: RoleInfo) {
|
||||
if (!!getIsCheckRoles(role)) return;
|
||||
setTimeout(() => {
|
||||
isRoleInfoShow.value = false;
|
||||
}, 150);
|
||||
store.setCurRoleInfo(role);
|
||||
await BaseApi.savUserRole(role);
|
||||
mittBus.emit("switchRole", role);
|
||||
await getUserWallpaperList();
|
||||
await loadLogoList();
|
||||
}
|
||||
|
||||
function onUserinfoClickRoles() {
|
||||
isUserInfoShow.value = false;
|
||||
setTimeout(() => {
|
||||
isRoleInfoShow.value = true;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async function readMsgAll(type: 1 | 2) {
|
||||
await BaseApi.readMessageAll();
|
||||
await reloadMsg();
|
||||
if (type == 2) resetMsgPage();
|
||||
}
|
||||
|
||||
async function reloadMsg() {
|
||||
const result = await BaseApi.getMessageList();
|
||||
userMsgList.value = result.list;
|
||||
}
|
||||
|
||||
function resetMsgPage() {
|
||||
refTablePage.value.reloadData();
|
||||
}
|
||||
function showMsgList() {
|
||||
isMsgShow.value = false;
|
||||
isMsgListShow.value = true;
|
||||
refTablePage.value.reloadData();
|
||||
}
|
||||
|
||||
async function wallpaperSuccess(response: any, file: any, fileListt: any) {
|
||||
await getUserWallpaperList();
|
||||
}
|
||||
|
||||
async function getUserWallpaperList() {
|
||||
const list = await UserSettingApi.getUserWallpaperList(store.getCurSchId);
|
||||
// list.forEach(element => {
|
||||
// if (element.type == 1) element.url = $conf.serviceHost + element.url;
|
||||
// });
|
||||
wallpaperList.value = orderBy(list, ["type", "id"], ["asc", "asc"]);
|
||||
}
|
||||
async function settingsWallpaper() {
|
||||
await getUserWallpaperList();
|
||||
isUserInfoShow.value = false;
|
||||
isWallpaperShow.value = true;
|
||||
}
|
||||
|
||||
async function settingsMenu() {
|
||||
isUserInfoShow.value = false;
|
||||
refMenuBarModal.value.show();
|
||||
}
|
||||
|
||||
async function settingsStats() {
|
||||
isUserInfoShow.value = false;
|
||||
refStatsBarModal.value.show();
|
||||
}
|
||||
|
||||
async function settingsTeacherCopilot() {
|
||||
isUserInfoShow.value = false;
|
||||
refTeacherCopilotModal.value.show();
|
||||
}
|
||||
|
||||
async function onClickWallpaper(url: string) {
|
||||
await UserSettingApi.checkWallpaper(store.getCurSchId, url);
|
||||
const userSetting = store.curUserSettingInfo;
|
||||
if (!!userSetting) {
|
||||
userSetting.wallpaperUrl = url;
|
||||
store.setCurUserSettingsInfo(userSetting);
|
||||
}
|
||||
isWallpaperShow.value = false;
|
||||
}
|
||||
|
||||
async function onWallpaperSelect(item: any) {
|
||||
await UserSettingApi.deleteWallById(item.id);
|
||||
await getUserWallpaperList();
|
||||
}
|
||||
|
||||
async function loadLogoList() {
|
||||
const list = await UserSettingApi.getLogoList();
|
||||
logoList.value = list || [];
|
||||
}
|
||||
|
||||
async function logoSuccess(response: any, file: any, fileListt: any) {
|
||||
await loadLogoList();
|
||||
}
|
||||
|
||||
async function onLogoSelect(item: any) {
|
||||
await UserSettingApi.deleteLogoById(item.id);
|
||||
await loadLogoList();
|
||||
}
|
||||
function getIsWallpaper(url: string) {
|
||||
return url === store.getCurWallPaperUrl;
|
||||
}
|
||||
async function toggleWidgetShow() {
|
||||
const id = store.curUserSettingInfo?.id;
|
||||
if (!!store.curUserSettingInfo) store.curUserSettingInfo.isWidgetShow = !store.getWidgetDragObj.isWidgetShow;
|
||||
await UserSettingApi.toggleWidgetShow(id);
|
||||
}
|
||||
|
||||
function linkSysSetting() {
|
||||
isUserInfoShow.value = false;
|
||||
router.push("/syssetting");
|
||||
}
|
||||
|
||||
async function linkLogoSetting() {
|
||||
await loadLogoList();
|
||||
isUserInfoShow.value = false;
|
||||
isLogoShow.value = true;
|
||||
}
|
||||
|
||||
function showSeachBox() { }
|
||||
|
||||
function showUserInfo() {
|
||||
isUserInfoShow.value = false;
|
||||
mittBus.emit("showUserInfoModal");
|
||||
}
|
||||
|
||||
function logout() {
|
||||
let url = window.location.href;
|
||||
url = "/cas/logout?service=" + url;
|
||||
url = $conf.ssoHost + url;
|
||||
if (!!window.top) window.top.location.href = url;
|
||||
else window.location.href = url;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("@/assets/styles/header.scss");
|
||||
|
||||
::v-deep .ivu-table td {
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.reset-panel .ivu-poptip-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.reset-panel .ivu-poptip-inner {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.wallpaper-modal {
|
||||
z-index: 11 !important;
|
||||
}
|
||||
|
||||
.wallpaper-modal .ivu-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logo-modal {
|
||||
z-index: 11 !important;
|
||||
}
|
||||
|
||||
.logo-modal .ivu-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ivu-table-cell .operating-link {
|
||||
display: inline-block;
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
padding-right: 10px;
|
||||
|
||||
&.disabled {
|
||||
color: #aaa !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ivu-table:before {
|
||||
content: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div :class="menuClass">
|
||||
<ul class="menu-ul" v-if="menuVisible">
|
||||
<li class="operate-item" @click="menuVisible = false" title="收起菜单">
|
||||
<SvgIcon name="menu-operate" color="#ececec" />
|
||||
</li>
|
||||
<template v-for="(item, index) in menuList" :key="index">
|
||||
<li
|
||||
v-wave="myWave"
|
||||
@click="linkPage(item, index)"
|
||||
:class="{ active: index == activeIndex, 'first-bottom': index == menuFirstBottomIndex }"
|
||||
>
|
||||
<a :title="item.name">
|
||||
<FetchSvg
|
||||
size="24"
|
||||
:src="$conf.serviceHost + item.icon"
|
||||
:color="index == activeIndex ? '#fff' : '#ececec'"
|
||||
/>
|
||||
<span>{{ item.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- <li class="fengche-item" v-wave="myWave">
|
||||
<a href="javascript:void(0)">
|
||||
<SvgIcon name="fengche" color="#ffffff" />
|
||||
<span>随机壁纸</span>
|
||||
</a>
|
||||
</li> -->
|
||||
<!-- <li class="fengche-item" v-wave="myWave">
|
||||
<RouterLink :to="{ name: 'AppDetails', params: { type: 1, id: 500 } }">
|
||||
<SvgIcon name="fengche" color="#ffffff" />
|
||||
<span>随机壁纸</span>
|
||||
</RouterLink>
|
||||
</li> -->
|
||||
</ul>
|
||||
<div class="menu-arrow_box" title="展开菜单" v-else @click="menuVisible = true">
|
||||
<SvgIcon name="menu-hide-arrow" color="white" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watchEffect, inject, nextTick } from "vue";
|
||||
// import { BaseDict } from "@/types/Basics.ts";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { LinkOpenTypeEnum, StateEnum } from "@/types";
|
||||
import FetchSvg from "@/components/FetchSvg.vue";
|
||||
import mittBus from "@/utils/mitt";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import { bschLinkPage, isTickeRoterByName } from "@/utils/bschLinkPage";
|
||||
|
||||
const { $conf } = useCurrentInstance();
|
||||
const myWave = ref({
|
||||
color: "#fff",
|
||||
initialOpacity: 0.5,
|
||||
duration: 0.45,
|
||||
easing: "ease-in",
|
||||
});
|
||||
const route = useRoute(),
|
||||
router = useRouter();
|
||||
|
||||
const store = useDefaultStore();
|
||||
const { menuVisible } = storeToRefs(store);
|
||||
const menuList = computed<any[]>(() => {
|
||||
let topArr = store.curUserMenuList.filter(x => x.isBottom !== true).sort((a, b) => a.orderNum - b.orderNum);
|
||||
let bottomArr = store.curUserMenuList.filter(x => x.isBottom === true).sort((a, b) => a.orderNum - b.orderNum);
|
||||
return [...topArr, ...bottomArr];
|
||||
});
|
||||
|
||||
//第一个悬浮底部的index
|
||||
const menuFirstBottomIndex = computed<number>(() => menuList.value.findIndex(x => x.isBottom === true));
|
||||
const activeIndex = ref(-1);
|
||||
|
||||
watchEffect(() => {
|
||||
let findIndex = menuList.value.findIndex(x => x.url === route.path);
|
||||
if (findIndex > -1) {
|
||||
activeIndex.value = findIndex;
|
||||
return;
|
||||
}
|
||||
if (isTickeRoterByName(route.name as string)) {
|
||||
let menuId = route.params.menuId as string;
|
||||
activeIndex.value = menuList.value.findIndex(x => x.id == menuId);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
$(".app-desktop").addClass("app-desktop__animation");
|
||||
}, 0);
|
||||
});
|
||||
|
||||
const menuClass = computed(() => {
|
||||
return ["menu-wrap", menuVisible.value ? "menu-wrap__show" : "menu-wrap__hide"];
|
||||
});
|
||||
|
||||
const isAdmin = computed(() => store.getIsAdmin);
|
||||
async function linkPage(item: any, index: number) {
|
||||
// if (!item.isNative && !item.isHeader) {
|
||||
// bschLinkPage({ url: item.url, type: item.openType });
|
||||
// return;
|
||||
// }
|
||||
if (!item.isNative && item.openType === LinkOpenTypeEnum.Open) {
|
||||
window.open(item.url);
|
||||
return;
|
||||
}
|
||||
|
||||
mittBus.emit("updateState", StateEnum.Desktop);
|
||||
mittBus.emit("reload");
|
||||
if (!!item.isNative) {
|
||||
router.push(item.url);
|
||||
} else {
|
||||
router.push({ name: "AppDetails", params: { menuId: item.id } });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-wrap {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 70px;
|
||||
z-index: 10;
|
||||
// background: rgba(0, 0, 0, 0.4);
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(15px);
|
||||
padding: 5px;
|
||||
transition: left 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
&__show {
|
||||
left: 0;
|
||||
}
|
||||
&__hide {
|
||||
left: -70px;
|
||||
}
|
||||
}
|
||||
.menu-ul {
|
||||
--textColor: #ececec;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
-webkit-transform: translateZ(0);
|
||||
background-color: none;
|
||||
transform: translateZ(0);
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow: hidden;
|
||||
-webkit-transition-duration: 0.5s;
|
||||
transition-duration: 0.5s;
|
||||
-webkit-transition-property: color, background-color;
|
||||
transition-property: color, background-color;
|
||||
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 8px 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
a {
|
||||
color: var(--textColor);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
line-height: normal;
|
||||
}
|
||||
svg {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
li.operate-item {
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
li.fengche-item {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 5px;
|
||||
}
|
||||
li:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
li.active {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
--textColor: #fff;
|
||||
// color: #fff;
|
||||
}
|
||||
li.first-bottom {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
.menu-arrow_box {
|
||||
position: relative;
|
||||
left: 62px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
height: 42px;
|
||||
border-radius: 0px 8px 8px 0px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(15px);
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-model="isLightAppModalShow"
|
||||
footer-hide
|
||||
class-name="bsch-oepn-modal"
|
||||
scrollable
|
||||
:width="modalWidth"
|
||||
:fullscreen="isFullscreen"
|
||||
:title="modalTitle"
|
||||
:transfer="false"
|
||||
@on-cancel="handleModalCancel"
|
||||
>
|
||||
<template #header>
|
||||
<div class="fix custom-title">
|
||||
<div class="fl">{{ modalTitle }}</div>
|
||||
<a class="fr" @click="isFullState = !isFullState">
|
||||
<Icon v-if="!isFullState" :size="18" type="ios-expand" />
|
||||
<Icon v-if="isFullState" :size="18" type="ios-contract" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<div class="tc" :style="bschOpenIframeStyle" v-if="!!isLoading">
|
||||
<img class="align-middle" style="width: 100px" :src="requireImg('page-loading.svg')" alt="" />
|
||||
</div>
|
||||
<iframe
|
||||
v-if="isIframeShow"
|
||||
v-show="!isLoading"
|
||||
@load="onIframeLoad"
|
||||
:style="bschOpenIframeStyle"
|
||||
:src="iframeUrl"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import mittBus from "@/utils/mitt";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
import { LightAppModal } from "@/types";
|
||||
|
||||
const isLightAppModalShow = ref(false),
|
||||
isIframeShow = ref(false),
|
||||
isLoading = ref(true),
|
||||
modalWidth = ref(80),
|
||||
modalHeight = ref(70),
|
||||
modalTitle = ref(),
|
||||
iframeUrl = ref();
|
||||
|
||||
const isFullState = ref(false);
|
||||
const bschOpenIframeStyle = computed(() => {
|
||||
if (isFullscreen.value)
|
||||
return {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "none",
|
||||
};
|
||||
let height = ((document.documentElement.clientHeight - 52) * modalHeight.value) / 100;
|
||||
return {
|
||||
width: "100%",
|
||||
height: height + "px",
|
||||
border: "none",
|
||||
};
|
||||
});
|
||||
|
||||
const isFullscreen = computed(() => (modalWidth.value == 100 && modalHeight.value == 100) || !!isFullState.value);
|
||||
|
||||
onMounted(function () {
|
||||
mittBus.on("showLightAppModal", (params: LightAppModal) => {
|
||||
const { title, url, width, height } = params;
|
||||
isIframeShow.value = true;
|
||||
isLoading.value = true;
|
||||
modalTitle.value = title;
|
||||
iframeUrl.value = url;
|
||||
modalWidth.value = width;
|
||||
modalHeight.value = height;
|
||||
console.log(title, url, width, height);
|
||||
isLightAppModalShow.value = true;
|
||||
setTimeout(() => {
|
||||
isLoading.value = false;
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
onUnmounted(function () {
|
||||
mittBus.off("showLightAppModal");
|
||||
});
|
||||
|
||||
function onIframeLoad() {
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
function handleModalCancel() {
|
||||
setTimeout(() => {
|
||||
isFullState.value = false;
|
||||
isIframeShow.value = false;
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.bsch-oepn-modal {
|
||||
iframe {
|
||||
width: 100%;
|
||||
}
|
||||
.ivu-modal-body {
|
||||
padding: 0;
|
||||
max-height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-title {
|
||||
width: calc(100% - 30px);
|
||||
line-height: 20px;
|
||||
color: #17233d;
|
||||
position: relative;
|
||||
a {
|
||||
color: #999 !important;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
&:hover {
|
||||
color: #17233d !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-model="isTableModalShow"
|
||||
class-name="menuwapper-modal"
|
||||
width="1160"
|
||||
:mask="true"
|
||||
title="边栏设置"
|
||||
:transfer="false"
|
||||
>
|
||||
<div class="fix mb-10">
|
||||
<Button type="primary" size="small" class="fl ml-10 width-10" ghost @click="onEditFormShow()">
|
||||
新建边栏
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
:columns="menuBarColumns"
|
||||
:height="360"
|
||||
:loading="tableLoading"
|
||||
:data="tableList"
|
||||
class="link-table"
|
||||
no-data-text="暂无数据"
|
||||
>
|
||||
<template #icon="{ row }">
|
||||
<img class="icon-box" v-if="!!row.icon" :src="$conf.serviceHost + row.icon" alt="" />
|
||||
</template>
|
||||
<template #isShow="{ row }">
|
||||
<Switch :model-value="row.isShow" true-color="#5979ea" @on-change="onMenuBarShowChange(row.id)" />
|
||||
</template>
|
||||
|
||||
<template #roleIds="{ row }">
|
||||
<span>{{ row.roleIds.map((item: number) => RoleTypeDict.find(x => x.k == item)?.v).join(",") }}</span>
|
||||
</template>
|
||||
<!-- <template #orderNum="{ row }">
|
||||
<InputNumber :model-value="row.orderNum" :min="1" />
|
||||
</template> -->
|
||||
</Table>
|
||||
<template #footer>
|
||||
<Button type="primary" ghost @click="isTableModalShow = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal
|
||||
v-model="isEditModalShow"
|
||||
scrollable
|
||||
width="550"
|
||||
:title="(!formData.id ? '添加' : '编辑') + '边栏'"
|
||||
:transfer="true"
|
||||
>
|
||||
<Form ref="refEditForm" class="mr-30" :model="formData" :rules="ruleValidate" :label-width="110">
|
||||
<Form-Item label="权限角色:" class="fix" prop="roleIds">
|
||||
<Checkbox
|
||||
class="fl"
|
||||
:indeterminate="indeterminate"
|
||||
:model-value="compCheckAll"
|
||||
@click.prevent="handleCheckAll"
|
||||
>
|
||||
全部
|
||||
</Checkbox>
|
||||
<CheckboxGroup v-model="formData.roleIds">
|
||||
<Checkbox v-for="item in RoleTypeDict" :label="item.k">{{ item.v }}</Checkbox>
|
||||
</CheckboxGroup>
|
||||
</Form-Item>
|
||||
<Form-Item label="名称:" prop="name">
|
||||
<Input max="4" v-model="formData.name" maxlength="30" placeholder="请输入名称" />
|
||||
</Form-Item>
|
||||
<Form-Item label="网址:" prop="url">
|
||||
<Input v-model="formData.url" placeholder="请输入链接" />
|
||||
</Form-Item>
|
||||
<Form-Item label="图标:" prop="icon">
|
||||
<Upload
|
||||
:on-success="uploadIconSuccess"
|
||||
:show-upload-list="false"
|
||||
:action="$conf.serviceHost + '/api/iconUpload'"
|
||||
accept=".svg"
|
||||
:with-credentials="true"
|
||||
:headers="uploadHeaders"
|
||||
>
|
||||
<div v-if="!formData.icon" class="icon-upload-wrap">
|
||||
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
|
||||
</div>
|
||||
<div class="icon-preview-wrap" v-else>
|
||||
<SvgIcon
|
||||
@click.stop="formData.icon = null"
|
||||
class="cloase-btn"
|
||||
name="widget-close"
|
||||
size="18"
|
||||
color="#fff"
|
||||
/>
|
||||
<img :src="$conf.serviceHost + formData.icon" alt="" />
|
||||
</div>
|
||||
</Upload>
|
||||
</Form-Item>
|
||||
|
||||
<Form-Item label="排序号:" prop="orderNum">
|
||||
<InputNumber style="width: 120px" v-model="formData.orderNum" :min="1" placeholder="请输入排序号" />
|
||||
</Form-Item>
|
||||
<Form-Item label="是否悬浮底部:" prop="isNative">
|
||||
<Switch v-model="formData.isBottom" true-color="#5979ea" />
|
||||
</Form-Item>
|
||||
<Form-Item label="是否系统页面:" prop="isNative">
|
||||
<Switch v-model="formData.isNative" true-color="#5979ea" />
|
||||
</Form-Item>
|
||||
|
||||
<Transition name="fade" :duration="300">
|
||||
<div class="ovh" style="height: 34px">
|
||||
<Form-Item v-if="!formData.isNative" label="打开方式:" prop="linkOpenType">
|
||||
<RadioGroup v-model="formData.linkOpenType" type="button" button-style="solid">
|
||||
<Radio :label="item.k" v-for="item in linkOpenTypeDict">{{ item.v }}</Radio>
|
||||
</RadioGroup>
|
||||
</Form-Item>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="fade" :duration="300">
|
||||
<div class="ovh mt-24" style="height: 35px">
|
||||
<Form-Item
|
||||
v-if="!formData.isNative && formData.linkOpenType === LinkOpenTypeEnum.IFrame"
|
||||
label="是否显示头部:"
|
||||
prop="isHeader"
|
||||
>
|
||||
<Switch v-model="formData.isHeader" true-color="#5979ea" />
|
||||
</Form-Item>
|
||||
</div>
|
||||
</Transition>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button type="primary" :loading="editLoading" @click="onEditSave">保存</Button>
|
||||
<Button @click="isEditModalShow = false">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MenuBarApi from "@/api/MenuBarApi";
|
||||
import { LinkOpenTypeDict, LinkOpenTypeEnum, RoleTypeEnum, RoleTypeDict } from "@/types";
|
||||
import Utils from "@/utils";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { BaseDict } from "@/types/Basics";
|
||||
import { find } from "lodash";
|
||||
|
||||
interface FormData {
|
||||
id: string | null;
|
||||
roleIds: number[];
|
||||
name: string | null;
|
||||
url: string | null;
|
||||
linkOpenType: number;
|
||||
icon: string | null;
|
||||
orderNum: number | null;
|
||||
isBottom: boolean | null;
|
||||
isNative: boolean;
|
||||
isShow: boolean;
|
||||
isEdit: boolean;
|
||||
isHeader: boolean | null;
|
||||
}
|
||||
|
||||
// 初始化 formData 的默认值
|
||||
const defaultFormData: FormData = {
|
||||
id: null,
|
||||
roleIds: [], // 添加 roleIds 属性
|
||||
name: "",
|
||||
url: "",
|
||||
linkOpenType: 0,
|
||||
icon: "",
|
||||
orderNum: null,
|
||||
isBottom: false,
|
||||
isNative: false,
|
||||
isHeader: false,
|
||||
isShow: true,
|
||||
isEdit: true,
|
||||
};
|
||||
|
||||
const { $conf } = useCurrentInstance();
|
||||
|
||||
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
|
||||
|
||||
const refEditForm = ref<any>(null);
|
||||
|
||||
const isTableModalShow = ref(false),
|
||||
isEditModalShow = ref(false),
|
||||
tableLoading = ref(false),
|
||||
editLoading = ref(false),
|
||||
tableList = ref<any[]>([]),
|
||||
formData = reactive(defaultFormData);
|
||||
|
||||
const ruleValidate = ref({
|
||||
roleIds: { required: true, type: "array", message: "请选择权限角色", trigger: "change,submit" },
|
||||
name: { required: true, message: "请输入名称", trigger: "blur" },
|
||||
url: { required: true, message: "请输入网址", trigger: "blur" },
|
||||
icon: { required: true, message: "请上传图标", trigger: "change,submit" },
|
||||
});
|
||||
|
||||
const linkOpenTypeDict = computed(() => LinkOpenTypeDict.filter(x => x.k != LinkOpenTypeEnum.Modal));
|
||||
const compCheckAll = computed(() => formData.roleIds.length == RoleTypeDict.length && RoleTypeDict.length > 0);
|
||||
const indeterminate = computed(() => formData.roleIds.length > 0 && formData.roleIds.length < RoleTypeDict.length);
|
||||
|
||||
const menuBarColumns = ref([
|
||||
{
|
||||
title: "名称",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "图标",
|
||||
slot: "icon",
|
||||
},
|
||||
{
|
||||
title: "角色",
|
||||
width: 150,
|
||||
slot: "roleIds",
|
||||
},
|
||||
{
|
||||
title: "地址",
|
||||
key: "url",
|
||||
width: 150,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: "打开方式",
|
||||
key: "linkType",
|
||||
},
|
||||
{
|
||||
title: "系统页面",
|
||||
key: "isNative",
|
||||
width: 110,
|
||||
align: "center",
|
||||
render(h: any, params: any) {
|
||||
return h("span", params.row.isNative ? "是" : "否");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "是否显示",
|
||||
width: 100,
|
||||
align: "center",
|
||||
slot: "isShow",
|
||||
},
|
||||
{
|
||||
title: "排序号",
|
||||
key: "orderNum",
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 120,
|
||||
align: "center",
|
||||
render: (h: any, params: any) => {
|
||||
const disabled = !params.row.isEdit,
|
||||
id = params.row.id;
|
||||
|
||||
return [
|
||||
Utils.operatingTextHandle(h, "编辑", "mr-2", () => onEditFormShow(id), disabled),
|
||||
Utils.operatingTextHandle(h, "删除", "text-error", () => onDeleteMenu(id), disabled),
|
||||
];
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
async function show() {
|
||||
isTableModalShow.value = true;
|
||||
await getMenuBarList();
|
||||
console.log("show");
|
||||
}
|
||||
|
||||
async function getMenuBarList() {
|
||||
tableLoading.value = true;
|
||||
|
||||
const list = await MenuBarApi.getMenuBarSchList();
|
||||
list.forEach(element => {
|
||||
element.linkType = find(linkOpenTypeDict.value, { k: element.linkOpenType })?.v;
|
||||
});
|
||||
tableList.value = list;
|
||||
tableLoading.value = false;
|
||||
}
|
||||
|
||||
function handleCheckAll() {
|
||||
formData.roleIds = !compCheckAll.value ? RoleTypeDict.map(item => item.k) : [];
|
||||
}
|
||||
function uploadIconSuccess(response: any, file: any, fileListt: any) {
|
||||
if (response.code == 0) formData.icon = response.data.url;
|
||||
else Message.error("上传错误!");
|
||||
// console.log(response, file, fileListt);
|
||||
}
|
||||
|
||||
async function onEditFormShow(id?: number) {
|
||||
await refEditForm.value?.resetFields();
|
||||
isEditModalShow.value = false;
|
||||
if (!id) {
|
||||
formData.id = null;
|
||||
formData.linkOpenType = LinkOpenTypeEnum.Open;
|
||||
} else {
|
||||
let find = tableList.value.find(x => x.id == id);
|
||||
Object.assign(formData, find);
|
||||
}
|
||||
isEditModalShow.value = true;
|
||||
}
|
||||
async function onDeleteMenu(id: number) {
|
||||
await MyConfirm({
|
||||
title: "删除",
|
||||
msg: "确定删除该边栏吗?",
|
||||
info: "删除后无法恢复!",
|
||||
async onOk() {
|
||||
await MenuBarApi.deleteMenuBar(id);
|
||||
},
|
||||
});
|
||||
await getMenuBarList();
|
||||
}
|
||||
|
||||
async function onEditSave() {
|
||||
let validate: boolean = await refEditForm.value?.validate();
|
||||
if (!validate) return false;
|
||||
editLoading.value = true;
|
||||
try {
|
||||
await MenuBarApi.saveMenuBar(formData);
|
||||
Message.success("操作成功!");
|
||||
await getMenuBarList();
|
||||
isEditModalShow.value = false;
|
||||
} catch (error) {
|
||||
Message.success("操作失败!");
|
||||
} finally {
|
||||
editLoading.value = false;
|
||||
}
|
||||
}
|
||||
function onMenuBarShowChange(id: number) {
|
||||
MenuBarApi.toggleMenuBarShow(id);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-box {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,178 +0,0 @@
|
||||
<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>
|
||||
@@ -1,221 +0,0 @@
|
||||
<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);
|
||||
// 第4参 callbackFlag=true:点击文字切换勾选时同步触发 onCheck,与点击勾选框行为一致
|
||||
treeObj.checkNode(treeNode, !treeNode.checked, true, 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;
|
||||
},
|
||||
getCheckedNodes() {
|
||||
const _domId = this.domId;
|
||||
var treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
if (!treeObj) return [];
|
||||
return treeObj.getCheckedNodes(true);
|
||||
},
|
||||
// 仅更新节点勾选状态,不级联、不触发 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>
|
||||
@@ -1,234 +0,0 @@
|
||||
<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>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div :style="searchStyle" @click="toggleSearch()">
|
||||
<div :class="searchClass">
|
||||
<div class="app-search-input" @click="e => e.stopPropagation()">
|
||||
<Input
|
||||
ref="refInput"
|
||||
v-model="searchName"
|
||||
prefix="ios-search"
|
||||
placeholder="输入应用名称搜索"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<vuescroll>
|
||||
<div class="app-search-list">
|
||||
<div v-for="(item, index) in appList" class="app-container">
|
||||
<img :src="$utils.appIconPath(item.icon)" />
|
||||
<div class="app-name">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</vuescroll>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { ref, inject, computed, onMounted, watch } from "vue";
|
||||
import vuescroll from "vuescroll";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
|
||||
const { $utils } = useCurrentInstance();
|
||||
|
||||
const refInput = ref();
|
||||
|
||||
const searchName = ref();
|
||||
|
||||
const store = useDefaultStore();
|
||||
const searchClass = ref<Array<string>>(["app-search-warp"]);
|
||||
const isSearchShow: any = inject("isSearchShow");
|
||||
const toggleSearch: any = inject("toggleSearch");
|
||||
|
||||
const searchStyle = computed(() => `background-image: url(${store.getCurWallPaperUrl})`);
|
||||
const appList = computed(() => {
|
||||
const allAppList = store.getFlattenAppList;
|
||||
if (!searchName.value) return allAppList;
|
||||
return allAppList.filter(x => x.name.indexOf(searchName.value) !== -1);
|
||||
});
|
||||
|
||||
onMounted(async () => {});
|
||||
|
||||
watch(isSearchShow, async val => {
|
||||
if (val) {
|
||||
setTimeout(() => {
|
||||
searchClass.value.push("app-search-warp__animate");
|
||||
refInput.value.focus();
|
||||
}, 50);
|
||||
} else {
|
||||
searchClass.value = ["app-search-warp"];
|
||||
searchName.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-search-warp {
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
inset: 0;
|
||||
outline: none;
|
||||
transition: transform 0.2s ease-in;
|
||||
transform: scale(1.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 50px;
|
||||
z-index: 2000;
|
||||
&__animate {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.app-search-input {
|
||||
width: 300px;
|
||||
margin: 50px auto 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
::v-deep .app-search-input .ivu-input {
|
||||
background-color: #28323c;
|
||||
border-color: #58626c;
|
||||
height: 36px;
|
||||
color: #c5c8ce;
|
||||
&::placeholder {
|
||||
color: #727c86;
|
||||
}
|
||||
&:hover {
|
||||
border-color: #8a949e;
|
||||
}
|
||||
&:focus {
|
||||
border-color: #2396fa;
|
||||
}
|
||||
}
|
||||
.app-search-list {
|
||||
width: 860px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
.app-container {
|
||||
margin: 20px 25px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-model="isTableModalShow"
|
||||
class-name="menuwapper-modal"
|
||||
width="1160"
|
||||
:mask="true"
|
||||
title="统计模块设置"
|
||||
:transfer="false"
|
||||
>
|
||||
<div class="fix mb-10">
|
||||
<Button type="primary" size="small" class="fl ml-10 width-10" ghost @click="onEditFormShow()">
|
||||
新建统计模块
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
:columns="menuBarColumns"
|
||||
:height="360"
|
||||
:loading="tableLoading"
|
||||
:data="tableList"
|
||||
class="link-table"
|
||||
no-data-text="暂无数据"
|
||||
>
|
||||
<template #icon="{ row }">
|
||||
<img class="icon-box" v-if="!!row.icon" :src="$conf.serviceHost + row.icon" alt="" />
|
||||
</template>
|
||||
<template #isShow="{ row }">
|
||||
<Switch :model-value="row.isShow" true-color="#5979ea" @on-change="onMenuBarShowChange(row.id)" />
|
||||
</template>
|
||||
|
||||
<template #roleIds="{ row }">
|
||||
<span>{{ row.roleIds.map((item: number) => RoleTypeDict.find(x => x.k == item)?.v).join(",") }}</span>
|
||||
</template>
|
||||
<!-- <template #orderNum="{ row }">
|
||||
<InputNumber :model-value="row.orderNum" :min="1" />
|
||||
</template> -->
|
||||
</Table>
|
||||
<template #footer>
|
||||
<Button type="primary" ghost @click="isTableModalShow = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal
|
||||
v-model="isEditModalShow"
|
||||
scrollable
|
||||
width="550"
|
||||
:title="(!formData.id ? '添加' : '编辑') + '统计模块'"
|
||||
:transfer="true"
|
||||
>
|
||||
<Form ref="refEditForm" class="mr-30" :model="formData" :rules="ruleValidate" :label-width="110">
|
||||
<Form-Item label="权限角色:" class="fix" prop="roleIds">
|
||||
<Checkbox
|
||||
class="fl"
|
||||
:indeterminate="indeterminate"
|
||||
:model-value="compCheckAll"
|
||||
@click.prevent="handleCheckAll"
|
||||
>
|
||||
全部
|
||||
</Checkbox>
|
||||
<CheckboxGroup v-model="formData.roleIds">
|
||||
<Checkbox v-for="item in RoleTypeDict" :label="item.k">{{ item.v }}</Checkbox>
|
||||
</CheckboxGroup>
|
||||
</Form-Item>
|
||||
<Form-Item label="名称:" prop="name">
|
||||
<Input max="4" v-model="formData.name" maxlength="30" placeholder="请输入名称" />
|
||||
</Form-Item>
|
||||
<Form-Item label="网址:" prop="url">
|
||||
<Input v-model="formData.url" placeholder="请输入链接" />
|
||||
</Form-Item>
|
||||
<Form-Item label="图标:" prop="icon">
|
||||
<Upload
|
||||
:on-success="uploadIconSuccess"
|
||||
:show-upload-list="false"
|
||||
:action="$conf.serviceHost + '/api/iconUpload'"
|
||||
:format="['jpg', 'jpeg', 'png', 'gif']"
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png"
|
||||
:with-credentials="true"
|
||||
:headers="uploadHeaders"
|
||||
>
|
||||
<div v-if="!formData.icon" class="icon-upload-wrap">
|
||||
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
|
||||
</div>
|
||||
<div class="icon-preview-wrap" v-else>
|
||||
<SvgIcon
|
||||
@click.stop="formData.icon = null"
|
||||
class="cloase-btn"
|
||||
name="widget-close"
|
||||
size="18"
|
||||
color="#fff"
|
||||
/>
|
||||
<img :src="$conf.serviceHost + formData.icon" alt="" />
|
||||
</div>
|
||||
</Upload>
|
||||
</Form-Item>
|
||||
|
||||
<Form-Item label="排序号:" prop="orderNum">
|
||||
<InputNumber style="width: 120px" v-model="formData.orderNum" :min="1" placeholder="请输入排序号" />
|
||||
</Form-Item>
|
||||
<Form-Item label="打开方式:" prop="linkOpenType">
|
||||
<RadioGroup v-model="formData.linkOpenType" type="button" button-style="solid">
|
||||
<Radio :label="item.k" v-for="item in linkOpenTypeDict">{{ item.v }}</Radio>
|
||||
</RadioGroup>
|
||||
</Form-Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button type="primary" :loading="editLoading" @click="onEditSave">保存</Button>
|
||||
<Button @click="isEditModalShow = false">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import StatsBarApi from "@/api/StatsBarApi";
|
||||
import { LinkOpenTypeDict, LinkOpenTypeEnum, RoleTypeEnum, RoleTypeDict } from "@/types";
|
||||
import Utils from "@/utils";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { BaseDict } from "@/types/Basics";
|
||||
import { find } from "lodash";
|
||||
|
||||
interface FormData {
|
||||
id: string | null;
|
||||
roleIds: number[];
|
||||
name: string | null;
|
||||
url: string | null;
|
||||
linkOpenType: number;
|
||||
icon: string | null;
|
||||
orderNum: number | null;
|
||||
isShow: boolean;
|
||||
isEdit: boolean;
|
||||
}
|
||||
|
||||
// 初始化 formData 的默认值
|
||||
const defaultFormData: FormData = {
|
||||
id: null,
|
||||
roleIds: [], // 添加 roleIds 属性
|
||||
name: "",
|
||||
url: "",
|
||||
linkOpenType: 0,
|
||||
icon: "",
|
||||
orderNum: null,
|
||||
isShow: true,
|
||||
isEdit: true,
|
||||
};
|
||||
|
||||
const { $conf } = useCurrentInstance();
|
||||
|
||||
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
|
||||
|
||||
const refEditForm = ref<any>(null);
|
||||
|
||||
const isTableModalShow = ref(false),
|
||||
isEditModalShow = ref(false),
|
||||
tableLoading = ref(false),
|
||||
editLoading = ref(false),
|
||||
tableList = ref<any[]>([]),
|
||||
formData = reactive(defaultFormData);
|
||||
|
||||
const ruleValidate = ref({
|
||||
roleIds: { required: true, type: "array", message: "请选择权限角色", trigger: "change,submit" },
|
||||
name: { required: true, message: "请输入名称", trigger: "blur" },
|
||||
url: { required: true, message: "请输入网址", trigger: "blur" },
|
||||
icon: { required: true, message: "请上传图标", trigger: "change,submit" },
|
||||
});
|
||||
|
||||
const linkOpenTypeDict = computed(() => LinkOpenTypeDict.filter(x => x.k != LinkOpenTypeEnum.Modal));
|
||||
const compCheckAll = computed(() => formData.roleIds.length == RoleTypeDict.length && RoleTypeDict.length > 0);
|
||||
const indeterminate = computed(() => formData.roleIds.length > 0 && formData.roleIds.length < RoleTypeDict.length);
|
||||
|
||||
const menuBarColumns = ref([
|
||||
{
|
||||
title: "名称",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "图标",
|
||||
slot: "icon",
|
||||
},
|
||||
{
|
||||
title: "角色",
|
||||
width: 150,
|
||||
slot: "roleIds",
|
||||
},
|
||||
{
|
||||
title: "地址",
|
||||
key: "url",
|
||||
width: 150,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: "打开方式",
|
||||
key: "linkType",
|
||||
},
|
||||
{
|
||||
title: "是否显示",
|
||||
width: 100,
|
||||
align: "center",
|
||||
slot: "isShow",
|
||||
},
|
||||
{
|
||||
title: "排序号",
|
||||
key: "orderNum",
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 120,
|
||||
align: "center",
|
||||
render: (h: any, params: any) => {
|
||||
const disabled = !params.row.isEdit,
|
||||
id = params.row.id;
|
||||
|
||||
return [
|
||||
Utils.operatingTextHandle(h, "编辑", "mr-2", () => onEditFormShow(id), disabled),
|
||||
Utils.operatingTextHandle(h, "删除", "text-error", () => onDeleteMenu(id), disabled),
|
||||
];
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
async function show() {
|
||||
isTableModalShow.value = true;
|
||||
await getMenuBarList();
|
||||
console.log("show");
|
||||
}
|
||||
|
||||
async function getMenuBarList() {
|
||||
tableLoading.value = true;
|
||||
|
||||
const list = await StatsBarApi.getStatsBarSchList();
|
||||
list.forEach(element => {
|
||||
element.linkType = find(linkOpenTypeDict.value, { k: element.linkOpenType })?.v;
|
||||
});
|
||||
tableList.value = list;
|
||||
tableLoading.value = false;
|
||||
}
|
||||
|
||||
function handleCheckAll() {
|
||||
formData.roleIds = !compCheckAll.value ? RoleTypeDict.map(item => item.k) : [];
|
||||
}
|
||||
function uploadIconSuccess(response: any, file: any, fileListt: any) {
|
||||
if (response.code == 0) formData.icon = response.data.url;
|
||||
else Message.error("上传错误!");
|
||||
// console.log(response, file, fileListt);
|
||||
}
|
||||
|
||||
async function onEditFormShow(id?: number) {
|
||||
await refEditForm.value?.resetFields();
|
||||
isEditModalShow.value = false;
|
||||
if (!id) {
|
||||
formData.id = null;
|
||||
formData.linkOpenType = LinkOpenTypeEnum.Open;
|
||||
} else {
|
||||
let find = tableList.value.find(x => x.id == id);
|
||||
Object.assign(formData, find);
|
||||
}
|
||||
isEditModalShow.value = true;
|
||||
}
|
||||
async function onDeleteMenu(id: number) {
|
||||
await MyConfirm({
|
||||
title: "删除",
|
||||
msg: "确定删除该统计模块吗?",
|
||||
info: "删除后无法恢复!",
|
||||
async onOk() {
|
||||
await StatsBarApi.deleteStatsBar(id);
|
||||
},
|
||||
});
|
||||
await getMenuBarList();
|
||||
}
|
||||
|
||||
async function onEditSave() {
|
||||
let validate: boolean = await refEditForm.value?.validate();
|
||||
if (!validate) return false;
|
||||
editLoading.value = true;
|
||||
try {
|
||||
await StatsBarApi.saveStatsBar(formData);
|
||||
Message.success("操作成功!");
|
||||
await getMenuBarList();
|
||||
isEditModalShow.value = false;
|
||||
} catch (error) {
|
||||
Message.success("操作失败!");
|
||||
} finally {
|
||||
editLoading.value = false;
|
||||
}
|
||||
}
|
||||
function onMenuBarShowChange(id: number) {
|
||||
StatsBarApi.toggleStatsBarShow(id);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-box {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<RadioGroup v-model="compModel" type="button" @on-change="onGroupChange">
|
||||
<radio :label="1">今日</radio>
|
||||
<radio :label="2">本周</radio>
|
||||
<radio :label="3">本月</radio>
|
||||
<radio :label="4">本学期</radio>
|
||||
<radio :label="5">累计</radio>
|
||||
<radio :label="6">自定义</radio>
|
||||
</RadioGroup>
|
||||
<Date-Picker
|
||||
class="ml-10"
|
||||
style="width: 220px"
|
||||
v-if="isDatePickerShow"
|
||||
type="daterange"
|
||||
separator=" 至 "
|
||||
:options="options"
|
||||
:editable="false"
|
||||
v-model="cusDate"
|
||||
@on-change="onDataChange"
|
||||
placeholder="选择日期范围"
|
||||
></Date-Picker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Utils from "@/utils";
|
||||
import { ref, reactive, computed } from "vue";
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: "update:modelValue", dataType: string): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "3",
|
||||
},
|
||||
});
|
||||
const cusDate = ref([]);
|
||||
const options = reactive({
|
||||
shortcuts: [
|
||||
{
|
||||
text: "前一周",
|
||||
value: function () {
|
||||
var end = new Date();
|
||||
var start = new Date();
|
||||
start.setDate(new Date().getDate() - 7);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "前一月",
|
||||
value: function () {
|
||||
var end = new Date();
|
||||
var start = new Date();
|
||||
start.setMonth(new Date().getMonth() - 1);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "前三月",
|
||||
value: function () {
|
||||
var end = new Date();
|
||||
var start = new Date();
|
||||
start.setMonth(new Date().getMonth() - 3);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "前半年",
|
||||
value: function () {
|
||||
var end = new Date();
|
||||
var start = new Date();
|
||||
start.setMonth(new Date().getMonth() - 6);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "前一年",
|
||||
value: function () {
|
||||
var end = new Date();
|
||||
var start = new Date();
|
||||
start.setMonth(new Date().getMonth() - 12);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const isDatePickerShow = ref(false);
|
||||
|
||||
const compModel = computed({
|
||||
get() {
|
||||
if (!isNaN(Number(props.modelValue))) return Number(props.modelValue);
|
||||
else return 6;
|
||||
},
|
||||
set(val) {
|
||||
isDatePickerShow.value = val == 6;
|
||||
if (val == 6) return;
|
||||
if (!isNaN(Number(val))) emits("update:modelValue", val.toString());
|
||||
},
|
||||
});
|
||||
|
||||
function onGroupChange(type: number) {
|
||||
cusDate.value = [];
|
||||
if (type == 6) return;
|
||||
emits("update:modelValue", type + "");
|
||||
}
|
||||
|
||||
function onDataChange() {
|
||||
if (cusDate.value.length == 0) return;
|
||||
if (!cusDate.value[0] || !cusDate.value[1]) return;
|
||||
const timeStr = `${Utils.dateFormat(cusDate.value[0], "yyyy/MM/dd")}-${Utils.dateFormat(
|
||||
cusDate.value[1],
|
||||
"yyyy/MM/dd"
|
||||
)}`;
|
||||
emits("update:modelValue", timeStr);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="chart-wrap">
|
||||
<Spin size="large" fix :show="loading">
|
||||
<slot>
|
||||
<SvgIcon name="loading" size="50" color="6280eb" />
|
||||
<div class="table-spin-load">加载中</div>
|
||||
</slot>
|
||||
</Spin>
|
||||
<div class="chart-box" v-show="!loading">
|
||||
<Tabs type="card" v-model="tabActive" :animated="false" v-if="chartList.length > 0">
|
||||
<TabPane v-for="item in chartList" :label="item.title">
|
||||
<div class="chart-item" :id="item.id">123</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, watch, nextTick } from "vue";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
|
||||
interface Props {
|
||||
chartList: Array<any>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const { $echarts: echarts } = useCurrentInstance();
|
||||
const tabActive = ref(0);
|
||||
const pieChartColors = ref([
|
||||
"#b2aefa",
|
||||
"#21b664",
|
||||
"#eeb329",
|
||||
"#eae213",
|
||||
"#20b6ab",
|
||||
"#f79854",
|
||||
"#ea5e45",
|
||||
"#0978c9",
|
||||
"#118eea",
|
||||
"#68b9f1",
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(val: boolean) => {
|
||||
if (!val) initChart(props.chartList[tabActive.value]);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => tabActive.value,
|
||||
(val: number) => {
|
||||
initChart(props.chartList[val]);
|
||||
}
|
||||
);
|
||||
|
||||
async function initChart(data: any) {
|
||||
await nextTick();
|
||||
console.log(data);
|
||||
var chartDom = document.getElementById(data.id);
|
||||
var myChart = echarts.init(chartDom);
|
||||
|
||||
var titles = data.list.map((x: any) => x.k);
|
||||
|
||||
var data = data.list.map((x: any) => {
|
||||
return {
|
||||
value: x.v,
|
||||
name: x.k,
|
||||
};
|
||||
});
|
||||
|
||||
var option = {
|
||||
title: {
|
||||
text: data.title,
|
||||
x: "center",
|
||||
y: "center",
|
||||
itemGap: 50,
|
||||
textStyle: {
|
||||
color: "#666",
|
||||
fontSize: 12,
|
||||
fontWeight: "normal",
|
||||
},
|
||||
subtextStyle: {
|
||||
color: "#de4751",
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "数据来源",
|
||||
type: "pie",
|
||||
radius: ["50%", "70%"],
|
||||
hoverAnimation: false,
|
||||
data: data,
|
||||
minAngle: 15,
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
show: true,
|
||||
formatter: "{b} \t{c}",
|
||||
//formatter: '{b}'
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
color: pieChartColors.value,
|
||||
};
|
||||
// 使用刚指定的配置项和数据显示图表。
|
||||
myChart.setOption(option);
|
||||
|
||||
// option && myChart.setOption(option);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-wrap {
|
||||
// border: solid 1px green;
|
||||
position: relative;
|
||||
.chart-box {
|
||||
height: 100%;
|
||||
}
|
||||
.chart-item {
|
||||
margin-top: 40px;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
::v-deep .table-spin-load {
|
||||
color: #577afb;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@ interface Props {
|
||||
name?: string;
|
||||
color?: string;
|
||||
hoverColor?: string;
|
||||
size?: string | number;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<script lang="ts" setup>
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { PageData, PageParams } from "@/types/Basics";
|
||||
import Utils from "@/utils";
|
||||
import { Message } from "view-ui-plus";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import { ref, onBeforeMount, onMounted, nextTick } from "vue";
|
||||
interface ReplaceParams {
|
||||
page: string;
|
||||
count: string;
|
||||
}
|
||||
interface ReplaceResult {
|
||||
list: string;
|
||||
total: string;
|
||||
}
|
||||
/* 表格插件参数类型 */
|
||||
interface TablePageProps {
|
||||
dataApi: (params: PageParams) => Promise<any>;
|
||||
columns: object[];
|
||||
tableHeight?: number;
|
||||
pageParams: PageParams | undefined;
|
||||
dataFilter?: ((resp: object) => object[]) | undefined;
|
||||
autoLoad?: boolean | undefined;
|
||||
pageType?: "small" | "default";
|
||||
replaceParams?: ReplaceParams; //替换传入参数
|
||||
replaceResult?: ReplaceResult; //替换返回结果
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TablePageProps>(), {
|
||||
autoLoad: true,
|
||||
pageType: "default",
|
||||
});
|
||||
const tableRef = ref();
|
||||
const list = ref<any[]>([]),
|
||||
selectList = ref<number[]>([]),
|
||||
total = ref(0),
|
||||
tableH = ref(0),
|
||||
tableLoading = ref(false);
|
||||
const params = ref<PageParams>({ page: 1, count: 10 });
|
||||
onBeforeMount(async () => {
|
||||
if (props.tableHeight) tableH.value = props.tableHeight;
|
||||
else tableH.value = Utils.useTableHeight();
|
||||
if (!!props.pageParams) params.value = props.pageParams;
|
||||
if (props.autoLoad) await getData();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
window.onresize = _.debounce(() => {
|
||||
tableH.value = Utils.useTableHeight();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
const getData = async () => {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
let tempParams: any = _.cloneDeep(params.value);
|
||||
if (!!props.replaceParams) {
|
||||
const { page, count } = props.replaceParams;
|
||||
tempParams = Utils.replaceField(tempParams, [
|
||||
{ n: page, o: "page" },
|
||||
{ n: count, o: "count" },
|
||||
]);
|
||||
}
|
||||
var data = await props.dataApi(tempParams);
|
||||
|
||||
if (!!props.replaceResult) {
|
||||
const { total, list } = props.replaceResult;
|
||||
data = Utils.replaceField(data, [
|
||||
{ n: "total", o: total },
|
||||
{ n: "list", o: list },
|
||||
]);
|
||||
}
|
||||
// console.log(data);
|
||||
|
||||
total.value = data.total;
|
||||
if (typeof props.dataFilter != "undefined") list.value = props.dataFilter(data);
|
||||
else list.value = data.list;
|
||||
|
||||
await nextTick();
|
||||
// setTimeout(() => {
|
||||
// console.log(tableRef.value.target);
|
||||
// }, 1000);
|
||||
//ivu-table-tbody
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
Message.error(error);
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
function selectChange(selection: any) {
|
||||
selectList.value = selection;
|
||||
}
|
||||
async function pageUpdate(page: number) {
|
||||
params.value.page = page;
|
||||
await getData();
|
||||
}
|
||||
async function pageSizeUpdate(size: number) {
|
||||
params.value.page = 1;
|
||||
params.value.count = size;
|
||||
await getData();
|
||||
}
|
||||
|
||||
/** 刷新数据-更新删除用 */
|
||||
// "add" | "del" | "edit" | "sel"
|
||||
async function refreshData() {
|
||||
params.value.page = 1;
|
||||
await getData();
|
||||
}
|
||||
|
||||
/** 重置数据 */
|
||||
// "add" | "del" | "edit" | "sel"
|
||||
async function reloadData(type: OperatorEnum = OperatorEnum.sel) {
|
||||
let { page, count } = params.value;
|
||||
if (type == OperatorEnum.add) {
|
||||
if (page * count == total.value) page += 1;
|
||||
} else if (type == OperatorEnum.del) {
|
||||
if ((page - 1) * count + 1 == total.value) page -= 1;
|
||||
} else if (type == OperatorEnum.edit) {
|
||||
//page=page
|
||||
} else if (type == OperatorEnum.sel) page = 1;
|
||||
params.value.page = page;
|
||||
await getData();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
list,
|
||||
refreshData,
|
||||
reloadData,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:height="tableHeight"
|
||||
:data="list"
|
||||
ref="tableRef"
|
||||
@on-selection-change="selectChange"
|
||||
class="link-table"
|
||||
:loading="tableLoading"
|
||||
no-data-text="暂无数据"
|
||||
>
|
||||
<template #loading>
|
||||
<SvgIcon name="loading" size="50" color="6280eb" />
|
||||
<div class="table-spin-load">加载中</div>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="fix mt-10">
|
||||
<Page
|
||||
:size="pageType"
|
||||
class="fr"
|
||||
:total="total"
|
||||
:model-value="params.page"
|
||||
:page-size="params.count"
|
||||
:page-size-opts="[10, 20, 30, 40]"
|
||||
@on-page-size-change="pageSizeUpdate"
|
||||
@on-change="pageUpdate"
|
||||
show-total
|
||||
:show-sizer="pageType == 'default'"
|
||||
></Page>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .ivu-table::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
::v-deep .ivu-table-header thead tr th {
|
||||
padding: 5px 0;
|
||||
}
|
||||
::v-deep .table-spin-load {
|
||||
color: #577afb;
|
||||
}
|
||||
</style>
|
||||
@@ -1,119 +0,0 @@
|
||||
<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>
|
||||
@@ -1,105 +0,0 @@
|
||||
<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 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>
|
||||
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-model="isTableModalShow"
|
||||
class-name="menuwapper-modal"
|
||||
width="1160"
|
||||
:mask="true"
|
||||
title="教师助手设置"
|
||||
:transfer="false"
|
||||
>
|
||||
<div class="fix mb-10">
|
||||
<Button type="primary" size="small" class="fl ml-10 width-10" ghost @click="onEditFormShow()">
|
||||
新建页面
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
:columns="menuBarColumns"
|
||||
:height="360"
|
||||
:loading="tableLoading"
|
||||
:data="tableList"
|
||||
class="link-table"
|
||||
no-data-text="暂无数据"
|
||||
>
|
||||
<template #icon="{ row }">
|
||||
<img class="icon-box" v-if="!!row.icon" :src="$conf.serviceHost + row.icon" alt="" />
|
||||
</template>
|
||||
<template #isShow="{ row }">
|
||||
<Switch :model-value="row.isShow" true-color="#5979ea" @on-change="onMenuBarShowChange(row.id)" />
|
||||
</template>
|
||||
|
||||
<template #type="{ row }">
|
||||
{{ teacherCopilotTypeDict.find(x => x.k == row.type)?.v }}
|
||||
</template>
|
||||
|
||||
<template #roleIds="{ row }">
|
||||
<span>{{ row.roleIds.map((item: number) => RoleTypeDict.find(x => x.k == item)?.v).join(",") }}</span>
|
||||
</template>
|
||||
<!-- <template #orderNum="{ row }">
|
||||
<InputNumber :model-value="row.orderNum" :min="1" />
|
||||
</template> -->
|
||||
</Table>
|
||||
<template #footer>
|
||||
<Button type="primary" ghost @click="isTableModalShow = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal
|
||||
v-model="isEditModalShow"
|
||||
scrollable
|
||||
width="550"
|
||||
:title="(!formData.id ? '添加' : '编辑') + '页面'"
|
||||
:transfer="true"
|
||||
>
|
||||
<Form ref="refEditForm" class="mr-30" :model="formData" :rules="ruleValidate" :label-width="110">
|
||||
<Form-Item label="权限角色:" class="fix" prop="roleIds">
|
||||
<Checkbox
|
||||
class="fl"
|
||||
:indeterminate="indeterminate"
|
||||
:model-value="compCheckAll"
|
||||
@click.prevent="handleCheckAll"
|
||||
>
|
||||
全部
|
||||
</Checkbox>
|
||||
<CheckboxGroup v-model="formData.roleIds">
|
||||
<Checkbox v-for="item in RoleTypeDict" :label="item.k">{{ item.v }}</Checkbox>
|
||||
</CheckboxGroup>
|
||||
</Form-Item>
|
||||
<Form-Item label="名称:" prop="name">
|
||||
<Input max="4" v-model="formData.name" maxlength="30" placeholder="请输入名称" />
|
||||
</Form-Item>
|
||||
<Form-Item label="网址:" prop="url">
|
||||
<Input v-model="formData.url" placeholder="请输入链接" />
|
||||
</Form-Item>
|
||||
<Form-Item label="图标:" prop="icon">
|
||||
<Upload
|
||||
:on-success="uploadIconSuccess"
|
||||
:show-upload-list="false"
|
||||
:action="$conf.serviceHost + '/api/iconUpload'"
|
||||
:format="['jpg', 'jpeg', 'png', 'gif']"
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png"
|
||||
:with-credentials="true"
|
||||
:headers="uploadHeaders"
|
||||
>
|
||||
<div v-if="!formData.icon" class="icon-upload-wrap">
|
||||
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
|
||||
</div>
|
||||
<div class="icon-preview-wrap" v-else>
|
||||
<SvgIcon
|
||||
@click.stop="formData.icon = null"
|
||||
class="cloase-btn"
|
||||
name="widget-close"
|
||||
size="18"
|
||||
color="#fff"
|
||||
/>
|
||||
<img :src="$conf.serviceHost + formData.icon" alt="" />
|
||||
</div>
|
||||
</Upload>
|
||||
</Form-Item>
|
||||
<Form-Item label="排序号:" prop="orderNum">
|
||||
<InputNumber style="width: 120px" v-model="formData.orderNum" :min="1" placeholder="请输入排序号" />
|
||||
</Form-Item>
|
||||
<Form-Item label="打开方式:" prop="linkOpenType">
|
||||
<RadioGroup v-model="formData.linkOpenType" type="button" button-style="solid">
|
||||
<Radio :label="item.k" v-for="item in linkOpenTypeDict">{{ item.v }}</Radio>
|
||||
</RadioGroup>
|
||||
</Form-Item>
|
||||
<Form-Item label="所属类别:" prop="type">
|
||||
<RadioGroup v-model="formData.type" type="button" button-style="solid">
|
||||
<Radio :label="item.k" v-for="item in teacherCopilotTypeDict">{{ item.v }}</Radio>
|
||||
</RadioGroup>
|
||||
</Form-Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button type="primary" :loading="editLoading" @click="onEditSave">保存</Button>
|
||||
<Button @click="isEditModalShow = false">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TeacherCopilotApi from "@/api/TeacherCopilotApi";
|
||||
import { LinkOpenTypeDict, LinkOpenTypeEnum, TeacherCopilotTypeDict, RoleTypeEnum, RoleTypeDict } from "@/types";
|
||||
import Utils from "@/utils";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { find } from "lodash";
|
||||
|
||||
interface FormData {
|
||||
id: string | null;
|
||||
roleIds: number[];
|
||||
name: string | null;
|
||||
url: string | null;
|
||||
linkOpenType: number;
|
||||
type: number;
|
||||
icon: string | null;
|
||||
orderNum: number | null;
|
||||
isShow: boolean;
|
||||
isEdit: boolean;
|
||||
}
|
||||
|
||||
// 初始化 formData 的默认值
|
||||
const defaultFormData: FormData = {
|
||||
id: null,
|
||||
roleIds: [], // 添加 roleIds 属性
|
||||
name: "",
|
||||
url: "",
|
||||
linkOpenType: 0,
|
||||
type: 0,
|
||||
icon: "",
|
||||
orderNum: null,
|
||||
isShow: true,
|
||||
isEdit: true,
|
||||
};
|
||||
|
||||
const { $conf } = useCurrentInstance();
|
||||
|
||||
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
|
||||
|
||||
const refEditForm = ref<any>(null);
|
||||
|
||||
const isTableModalShow = ref(false),
|
||||
isEditModalShow = ref(false),
|
||||
tableLoading = ref(false),
|
||||
editLoading = ref(false),
|
||||
tableList = ref<any[]>([]),
|
||||
formData = reactive(defaultFormData);
|
||||
|
||||
const ruleValidate = ref({
|
||||
roleIds: { required: true, type: "array", message: "请选择权限角色", trigger: "change,submit" },
|
||||
name: { required: true, message: "请输入名称", trigger: "blur" },
|
||||
url: { required: true, message: "请输入网址", trigger: "blur" },
|
||||
icon: { required: true, message: "请上传图标", trigger: "change,submit" },
|
||||
type: { required: true, message: "请选择类别", type: "number", trigger: "change,submit" },
|
||||
});
|
||||
|
||||
const linkOpenTypeDict = computed(() => LinkOpenTypeDict.filter(x => x.k != LinkOpenTypeEnum.Modal));
|
||||
const teacherCopilotTypeDict = computed(() => TeacherCopilotTypeDict);
|
||||
|
||||
const compCheckAll = computed(() => formData.roleIds.length == RoleTypeDict.length && RoleTypeDict.length > 0);
|
||||
const indeterminate = computed(() => formData.roleIds.length > 0 && formData.roleIds.length < RoleTypeDict.length);
|
||||
|
||||
const menuBarColumns = ref([
|
||||
{
|
||||
title: "名称",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "图标",
|
||||
slot: "icon",
|
||||
},
|
||||
{
|
||||
title: "角色",
|
||||
width: 150,
|
||||
slot: "roleIds",
|
||||
},
|
||||
{
|
||||
title: "地址",
|
||||
key: "url",
|
||||
width: 150,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: "打开方式",
|
||||
key: "linkType",
|
||||
},
|
||||
{
|
||||
title: "类别",
|
||||
slot: "type",
|
||||
},
|
||||
{
|
||||
title: "是否显示",
|
||||
width: 100,
|
||||
align: "center",
|
||||
slot: "isShow",
|
||||
},
|
||||
{
|
||||
title: "排序号",
|
||||
key: "orderNum",
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 120,
|
||||
align: "center",
|
||||
render: (h: any, params: any) => {
|
||||
const disabled = !params.row.isEdit,
|
||||
id = params.row.id;
|
||||
|
||||
return [
|
||||
Utils.operatingTextHandle(h, "编辑", "mr-2", () => onEditFormShow(id), disabled),
|
||||
Utils.operatingTextHandle(h, "删除", "text-error", () => onDeleteMenu(id), disabled),
|
||||
];
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
async function show() {
|
||||
isTableModalShow.value = true;
|
||||
await getMenuBarList();
|
||||
console.log("show");
|
||||
}
|
||||
|
||||
async function getMenuBarList() {
|
||||
tableLoading.value = true;
|
||||
|
||||
const list = await TeacherCopilotApi.getSchList();
|
||||
list.forEach(element => {
|
||||
element.linkType = find(linkOpenTypeDict.value, { k: element.linkOpenType })?.v;
|
||||
});
|
||||
tableList.value = list;
|
||||
tableLoading.value = false;
|
||||
}
|
||||
|
||||
function handleCheckAll() {
|
||||
formData.roleIds = !compCheckAll.value ? RoleTypeDict.map(item => item.k) : [];
|
||||
}
|
||||
function uploadIconSuccess(response: any, file: any, fileListt: any) {
|
||||
if (response.code == 0) formData.icon = response.data.url;
|
||||
else Message.error("上传错误!");
|
||||
// console.log(response, file, fileListt);
|
||||
}
|
||||
|
||||
async function onEditFormShow(id?: number) {
|
||||
await refEditForm.value?.resetFields();
|
||||
isEditModalShow.value = false;
|
||||
if (!id) {
|
||||
formData.id = null;
|
||||
formData.linkOpenType = LinkOpenTypeEnum.Open;
|
||||
} else {
|
||||
let find = tableList.value.find(x => x.id == id);
|
||||
Object.assign(formData, find);
|
||||
}
|
||||
isEditModalShow.value = true;
|
||||
}
|
||||
async function onDeleteMenu(id: number) {
|
||||
await MyConfirm({
|
||||
title: "删除",
|
||||
msg: "确定删除该页面吗?",
|
||||
info: "删除后无法恢复!",
|
||||
async onOk() {
|
||||
await TeacherCopilotApi.delete(id);
|
||||
},
|
||||
});
|
||||
await getMenuBarList();
|
||||
}
|
||||
|
||||
async function onEditSave() {
|
||||
let validate: boolean = await refEditForm.value?.validate();
|
||||
if (!validate) return false;
|
||||
editLoading.value = true;
|
||||
try {
|
||||
await TeacherCopilotApi.save(formData);
|
||||
Message.success("操作成功!");
|
||||
await getMenuBarList();
|
||||
isEditModalShow.value = false;
|
||||
} catch (error) {
|
||||
Message.success("操作失败!");
|
||||
} finally {
|
||||
editLoading.value = false;
|
||||
}
|
||||
}
|
||||
function onMenuBarShowChange(id: number) {
|
||||
TeacherCopilotApi.toggleShow(id);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-box {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,169 +0,0 @@
|
||||
<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>
|
||||
@@ -1,106 +0,0 @@
|
||||
<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>
|
||||
@@ -35,7 +35,6 @@
|
||||
:on-format-error="handleFormatError"
|
||||
:on-exceeded-size="handleMaxSize"
|
||||
:default-file-list="defaultList"
|
||||
:headers="uploadHeader"
|
||||
type="drag"
|
||||
:action="uploadUrl"
|
||||
:data="data"
|
||||
@@ -78,17 +77,6 @@ export default {
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<Modal v-model="isUserInfoDetailsShow" width="500" title="个人信息" :transfer="false" class-name="userinfo-modal">
|
||||
<div class="userinfo-panel fix" v-if="!!store.userInfo">
|
||||
<Upload
|
||||
class="avatar-upload"
|
||||
:on-success="uploadAvatarSuccess"
|
||||
:on-exceeded-size="onAvatarSizeExceeded"
|
||||
:on-format-error="onAvatarFormatError"
|
||||
:show-upload-list="false"
|
||||
:action="$conf.serviceHost + '/api/avatarUpload'"
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp"
|
||||
:format="['jpg', 'jpeg', 'png', 'gif', 'webp']"
|
||||
:max-size="20480"
|
||||
:with-credentials="true"
|
||||
:headers="uploadHeaders"
|
||||
>
|
||||
<div class="avatar-wrap" title="点击更换头像">
|
||||
<img :src="requireImg('user-avatar-default.png')" v-real-img="store.userInfo.profilePicture" alt="" />
|
||||
<div class="avatar-mask">更换头像</div>
|
||||
</div>
|
||||
</Upload>
|
||||
<ul>
|
||||
<li>
|
||||
<span>用户名:</span>
|
||||
<span>{{ store.userInfo.account }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>真实姓名 :</span>
|
||||
<span>{{ store.userInfo.name }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>手机号码 :</span>
|
||||
<span>{{ store.userInfo.phone || "-" }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>身份证号 :</span>
|
||||
<span>{{ store.userInfo.idcard || "-" }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button type="primary" class="fl" @click="onUpdatePasswordDetailsShow">修改密码</Button>
|
||||
<Button type="primary" ghost @click="isUserInfoDetailsShow = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal v-model="isUpdatePasswordDetailsShow" width="450" title="修改密码" :transfer="false" class-name="userinfo-modal">
|
||||
<div class="fix">
|
||||
<Form ref="refEditForm" class="mr-20 mb-20" :model="formData" :rules="ruleValidate" :label-width="120">
|
||||
<Form-Item label="旧密码:" prop="oldPassword">
|
||||
<Input type="password" v-model="formData.oldPassword" maxlength="30" placeholder="请输入旧密码" />
|
||||
</Form-Item>
|
||||
<Form-Item label="新密码:" prop="password">
|
||||
<Input type="password" v-model="formData.password" maxlength="30"
|
||||
placeholder="8位以上密码,需包含字母、数字、符号" />
|
||||
</Form-Item>
|
||||
<Form-Item label="确认新密码:" prop="passwdCheck">
|
||||
<Input type="password" v-model="formData.passwdCheck" maxlength="30" placeholder="请确认密码" />
|
||||
</Form-Item>
|
||||
</Form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button @click="isUpdatePasswordDetailsShow = false">取消</Button>
|
||||
<Button type="primary" @click="onUpdatePasswordSave">确定</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, reactive } from "vue";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import mittBus from "@/utils/mitt";
|
||||
import BaseApi from "@/api/BaseApi";
|
||||
import Utils from "@/utils";
|
||||
import { Message } from "view-ui-plus";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
interface FormData {
|
||||
oldPassword: string;
|
||||
password: string;
|
||||
passwdCheck: string;
|
||||
}
|
||||
|
||||
const { $conf } = useCurrentInstance();
|
||||
const store = useDefaultStore();
|
||||
const isUserInfoDetailsShow = ref(false);
|
||||
const isUpdatePasswordDetailsShow = ref(false);
|
||||
const formData = reactive<FormData>({
|
||||
oldPassword: "",
|
||||
password: "",
|
||||
passwdCheck: "",
|
||||
});
|
||||
|
||||
const refEditForm = ref<any>(null);
|
||||
|
||||
const validatePass = (rule: any, value: any, callback: Function) => {
|
||||
if (value === "") {
|
||||
callback(new Error("请输入新密码!"));
|
||||
} else if (value === formData.oldPassword) {
|
||||
callback(new Error("新密码不能与旧密码一致!"));
|
||||
} else if (value.length < 8) {
|
||||
callback(new Error("密码长度不能小于8位!"));
|
||||
} else if (!/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}:";'<>?,./]).{8,}$/.test(value)) {
|
||||
callback(new Error("密码需包含字母、数字、符号!"));
|
||||
} else {
|
||||
if (formData.passwdCheck !== "") {
|
||||
// 对第二个密码框单独验证
|
||||
refEditForm.value.validateField("passwdCheck");
|
||||
}
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const validatePassCheck = (rule: any, value: any, callback: Function) => {
|
||||
if (value === "") {
|
||||
callback(new Error("请重新输入密码!"));
|
||||
} else if (value !== formData.password) {
|
||||
callback(new Error("两次密码输入不一致!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const ruleValidate = ref({
|
||||
oldPassword: { required: true, message: "请输入旧密码", trigger: "change,submit" },
|
||||
password: [{ required: true, validator: validatePass, trigger: "blur" }],
|
||||
passwdCheck: [{ required: true, validator: validatePassCheck, trigger: "blur" }],
|
||||
});
|
||||
|
||||
onMounted(function () {
|
||||
mittBus.on("showUserInfoModal", (state?: boolean) => {
|
||||
if (typeof state === "undefined") isUserInfoDetailsShow.value = true;
|
||||
else isUserInfoDetailsShow.value = state;
|
||||
});
|
||||
});
|
||||
|
||||
function onUpdatePasswordDetailsShow() {
|
||||
refEditForm.value.resetFields();
|
||||
isUpdatePasswordDetailsShow.value = true;
|
||||
}
|
||||
|
||||
// ==================== 头像上传 ====================
|
||||
|
||||
// Upload 组件直传不经 axios 封装,需手动补网关校验头(对齐 MenuBarModal 写法)
|
||||
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
|
||||
|
||||
function uploadAvatarSuccess(response: any) {
|
||||
if (response.code == 0 && response.data && response.data.url) {
|
||||
store.userInfo.profilePicture = response.data.url;
|
||||
Message.success("头像更新成功!");
|
||||
} else {
|
||||
Message.error("头像更新失败:" + (response.msg || "未知错误"));
|
||||
}
|
||||
}
|
||||
|
||||
function onAvatarSizeExceeded() {
|
||||
Message.error("图片大小不能超过20MB!");
|
||||
}
|
||||
|
||||
function onAvatarFormatError() {
|
||||
Message.error("仅支持 jpg/jpeg/png/gif/webp 格式图片!");
|
||||
}
|
||||
|
||||
async function onUpdatePasswordSave() {
|
||||
const valid = refEditForm.value.validate();
|
||||
if (!valid) return false;
|
||||
try {
|
||||
await BaseApi.updatePassword(formData.oldPassword, formData.password);
|
||||
Message.success("操作成功!");
|
||||
setTimeout(() => {
|
||||
logout();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
Message.error("操作失败:" + error);
|
||||
}
|
||||
|
||||
// let res = await store.updatePassword(formData);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
let url = window.location.href;
|
||||
url = "/cas/logout?service=" + url;
|
||||
url = $conf.ssoHost + url;
|
||||
if (!!window.top) window.top.location.href = url;
|
||||
else window.location.href = url;
|
||||
}
|
||||
onUnmounted(function () {
|
||||
mittBus.off("showUserInfoModal");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.userinfo-panel {
|
||||
.avatar-upload {
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .avatar-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
ul {
|
||||
float: left;
|
||||
margin-left: 16px;
|
||||
height: 120px;
|
||||
|
||||
li {
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,259 +0,0 @@
|
||||
<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 = "/cas/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>
|
||||
Reference in New Issue
Block a user