修改前端包结构.

This commit is contained in:
lijiahang
2023-10-18 17:11:27 +08:00
parent 284501b3fb
commit 17d11cef21
39 changed files with 107 additions and 36 deletions

View File

@@ -0,0 +1,148 @@
<template>
<div>
<!-- 按钮组 -->
<a-button-group class="mb4">
<!-- 全选 -->
<a-button type="text" size="mini" @click="toggleChecked">
{{ checkedKeys?.length === allCheckedKeys?.length ? '反选' : '全选' }}
</a-button>
<!-- 展开 -->
<a-button type="text" size="mini" @click="toggleExpanded">
{{ expandedKeys?.length ? '折叠' : '展开' }}
</a-button>
</a-button-group>
<!-- 菜单树 -->
<a-tree
checked-strategy="child"
:checkable="true"
:animation="false"
:only-check-leaf="true"
v-model:checked-keys="checkedKeys"
v-model:expanded-keys="expandedKeys"
:data="treeData" />
</div>
</template>
<script lang="ts">
export default {
name: 'menu-selector-tree'
};
</script>
<script lang="ts" setup>
import { ref } from 'vue';
import { TreeNodeData } from '@arco-design/web-vue';
import { useCacheStore } from '@/store';
const treeData = ref<Array<TreeNodeData>>([]);
const allCheckedKeys = ref<Array<number>>([]);
const allExpandedKeys = ref<Array<number>>([]);
const checkedKeys = ref<Array<number>>([]);
const expandedKeys = ref<Array<number>>([]);
// 修改选中状态
const toggleChecked = () => {
checkedKeys.value = checkedKeys.value.length === allCheckedKeys.value.length ? [] : allCheckedKeys.value;
};
// 修改折叠状态
const toggleExpanded = () => {
expandedKeys.value = expandedKeys?.value.length ? [] : allExpandedKeys.value;
};
// 循环选中的 key
const eachAllCheckKeys = (arr: Array<any>) => {
arr.forEach((item) => {
allCheckedKeys.value.push(item.key);
if (item.children && item.children.length) {
eachAllCheckKeys(item.children);
}
});
};
// 循环展开的 key
const eachAllExpandKeys = (arr: Array<any>) => {
arr.forEach((item) => {
if (item.children && item.children.length) {
allExpandedKeys.value.push(item.key);
eachAllExpandKeys(item.children);
}
});
};
// 渲染数据
const init = (keys: Array<number>) => {
// 初始化数据
allCheckedKeys.value = [];
allExpandedKeys.value = [];
checkedKeys.value = keys;
expandedKeys.value = [];
// 渲染菜单
const cacheStore = useCacheStore();
let render = (arr: any[]): TreeNodeData[] => {
return arr.map((s) => {
// 当前节点
const node = {
key: s.id,
title: s.name,
children: undefined as unknown
} as TreeNodeData;
// 子节点
if (s.children && s.children.length) {
node.children = render(s.children);
}
return node;
}).filter(Boolean);
};
// 加载菜单
treeData.value = render([...cacheStore.menus]);
// 加载所有选中的key
eachAllCheckKeys(treeData.value);
// 加载所有展开的key
eachAllExpandKeys(treeData.value);
};
init([]);
// 获取值
const getValue = () => {
if (!checkedKeys.value.length) {
return [];
}
// 查询子节点上级父节点
const mixed: number[] = [];
const findParent = (arr: Array<TreeNodeData>, key: number) => {
for (let node of arr) {
// 是子节点 并且相同
if (node.key === key) {
mixed.push(key);
return true;
}
if (node.children?.length) {
const isFind = findParent(node.children, key);
if (isFind) {
mixed.push(node.key as number);
return true;
}
}
}
return false;
};
// 设置所有节点
for (let key of checkedKeys.value) {
findParent(treeData.value, key);
}
return new Set(mixed);
};
defineExpose({ init, getValue });
</script>
<style scoped>
</style>;

View File

@@ -0,0 +1,194 @@
<script lang="tsx">
import { compile, computed, defineComponent, h, ref } from 'vue';
import type { RouteMeta } from 'vue-router';
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
import { useAppStore } from '@/store';
import { listenerRouteChange } from '@/utils/route-listener';
import { openWindow, regexUrl } from '@/utils';
import useMenuTree from './use-menu-tree';
export default defineComponent({
emit: ['collapse'],
setup() {
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();
const { menuTree } = useMenuTree();
const collapsed = computed({
get() {
if (appStore.device === 'desktop') return appStore.menuCollapse;
return false;
},
set(value: boolean) {
appStore.updateSettings({ menuCollapse: value });
},
});
const topMenu = computed(() => appStore.topMenu);
const openKeys = ref<string[]>([]);
const selectedKey = ref<string[]>([]);
/**
* 跳转
*/
const goto = (e: any, item: RouteRecordRaw) => {
// 打开外链
if (regexUrl.test(item.path)) {
openWindow(item.path);
selectedKey.value = [item.name as string];
return;
}
// 新页面打开
if (e.ctrlKey) {
const { href } = router.resolve({
name: item.name,
});
openWindow(href);
selectedKey.value = [item.name as string];
return;
}
// 设置 selectedKey
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
if (route.name === item.name && !hideInMenu && !activeMenu) {
selectedKey.value = [item.name as string];
return;
}
// 触发跳转
router.push({
name: item.name,
});
};
const findMenuOpenKeys = (target: string) => {
const result: string[] = [];
let isFind = false;
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
if (item.name === target) {
isFind = true;
result.push(...keys);
return;
}
if (item.children?.length) {
item.children.forEach((el) => {
backtrack(el, [...keys, el.name as string]);
});
}
};
menuTree.value.forEach((el: RouteRecordRaw) => {
if (isFind) return;
backtrack(el, [el.name as string]);
});
return result;
};
/**
* 监听路由 设置打开的 key
*/
listenerRouteChange((newRoute) => {
const { activeMenu, hideInMenu } = newRoute.meta;
if (!hideInMenu || activeMenu) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];
selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);
// 展开菜单
const setCollapse = (val: boolean) => {
if (appStore.device === 'desktop')
appStore.updateSettings({ menuCollapse: val });
};
// 渲染菜单
const renderSubMenu = () => {
function travel(_route: RouteRecordRaw[], nodes = []) {
if (_route) {
_route.forEach((element) => {
// This is demo, modify nodes as needed
const icon = element?.meta?.icon
? () => h(compile(`<${element?.meta?.icon}/>`))
: null;
const node =
element?.children && element?.children.length !== 0 ? (
<a-sub-menu
key={element?.name}
v-slots={{
icon,
// 去除国际化 title: () => h(compile(t(element?.meta?.locale || ''))),
title: () => h(compile(element?.meta?.locale || '')),
}}
>
{travel(element?.children)}
</a-sub-menu>
) : (
<a-menu-item
key={element?.name}
v-slots={{ icon }}
onClick={($event: any) => goto($event, element)}
>
{element?.meta?.locale || ''}
</a-menu-item>
);
nodes.push(node as never);
});
}
return nodes;
}
return travel(menuTree.value);
};
return () => (
<a-menu
mode={topMenu.value ? 'horizontal' : 'vertical'}
v-model:collapsed={collapsed.value}
v-model:open-keys={openKeys.value}
show-collapse-button={appStore.device !== 'mobile'}
auto-open={false}
selected-keys={selectedKey.value}
auto-open-selected={true}
level-indent={34}
style="height: 100%; width:100%;"
onCollapse={setCollapse}
>
{renderSubMenu()}
</a-menu>
);
},
});
</script>
<style lang="less" scoped>
:deep(.arco-menu-inner) {
.arco-menu-inline-header {
display: flex;
align-items: center;
}
.arco-icon {
&:not(.arco-icon-down) {
font-size: 18px;
}
}
.arco-menu-icon {
margin-right: 10px !important;
}
.arco-menu-indent-list {
width: 28px;
display: inline-block;
}
.arco-menu-title {
user-select: none;
}
}
</style>

View File

@@ -0,0 +1,66 @@
import { computed } from 'vue';
import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
import { useMenuStore } from '@/store';
import { cloneDeep } from 'lodash';
export default function useMenuTree() {
const menuStore = useMenuStore();
const appRoute = computed(() => {
return menuStore.appMenus;
});
const menuTree = computed(() => {
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
return (a.meta.order || 0) - (b.meta.order || 0);
});
function travel(_routes: RouteRecordRaw[], layer: number) {
if (!_routes) return null;
const collector: any = _routes.map((element) => {
// 隐藏子目录
if (element.meta?.hideChildrenInMenu || !element.children) {
element.children = [];
if (element.meta?.hideInMenu) {
// 如果隐藏菜单 则不显示
return null;
} else {
return element;
}
}
// 过滤不显示的菜单
element.children = element.children.filter(
(x) => x.meta?.hideInMenu !== true
);
// 关联子节点
const subItem = travel(element.children, layer + 1);
if (subItem.length) {
element.children = subItem;
return element;
}
// 第二层 (子目录)
if (layer > 1) {
element.children = subItem;
return element;
}
// 是否隐藏目录
if (element.meta?.hideInMenu === false) {
return element;
}
return null;
});
return collector.filter(Boolean);
}
return travel(copyRouter, 0);
});
return {
menuTree,
};
}

View File

@@ -0,0 +1,132 @@
<template>
<a-spin style="display: block" :loading="loading">
<a-tabs v-model:activeKey="messageType" type="rounded" destroy-on-hide>
<a-tab-pane v-for="item in tabList" :key="item.key">
<template #title>
<span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
</template>
<a-result v-if="!renderList.length" status="404">
<template #subtitle>暂无内容</template>
</a-result>
<List :render-list="renderList"
:unread-count="unreadCount"
@item-click="handleItemClick" />
</a-tab-pane>
<template #extra>
<a-button type="text" @click="emptyList">
清空
</a-button>
</template>
</a-tabs>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, reactive, toRefs, computed } from 'vue';
import {
queryMessageList,
setMessageStatus,
MessageRecord,
MessageListType,
} from '@/api/system/message';
import useLoading from '@/hooks/loading';
import List from './list.vue';
interface TabItem {
key: string;
title: string;
avatar?: string;
}
const { loading, setLoading } = useLoading(true);
const messageType = ref('message');
const messageData = reactive<{
renderList: MessageRecord[];
messageList: MessageRecord[];
}>({
renderList: [],
messageList: [],
});
toRefs(messageData);
const tabList: TabItem[] = [
{
key: 'message',
title: '消息',
},
{
key: 'notice',
title: '通知',
},
{
key: 'todo',
title: '待办',
},
];
async function fetchSourceData() {
setLoading(true);
try {
const { data } = await queryMessageList();
messageData.messageList = data;
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false);
}
}
async function readMessage(data: MessageListType) {
const ids = data.map((item) => item.id);
await setMessageStatus({ ids });
await fetchSourceData();
}
const renderList = computed(() => {
return messageData.messageList.filter(
(item) => messageType.value === item.type
);
});
const unreadCount = computed(() => {
return renderList.value.filter((item) => !item.status).length;
});
const getUnreadList = (type: string) => {
const list = messageData.messageList.filter(
(item) => item.type === type && !item.status
);
return list;
};
const formatUnreadLength = (type: string) => {
const list = getUnreadList(type);
return list.length ? `(${list.length})` : '';
};
const handleItemClick = (items: MessageListType) => {
if (renderList.value.length) readMessage([...items]);
};
const emptyList = () => {
messageData.messageList = [];
};
fetchSourceData();
</script>
<style scoped lang="less">
:deep(.arco-popover-popup-content) {
padding: 0;
}
:deep(.arco-list-item-meta) {
align-items: flex-start;
}
:deep(.arco-tabs-nav) {
padding: 14px 0 12px 16px;
border-bottom: 1px solid var(--color-neutral-3);
}
:deep(.arco-tabs-content) {
padding-top: 0;
.arco-result-subtitle {
color: rgb(var(--gray-6));
}
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<a-list :bordered="false">
<a-list-item
v-for="item in renderList"
:key="item.id"
action-layout="vertical"
:style="{
opacity: item.status ? 0.5 : 1,
}"
>
<template #extra>
<a-tag v-if="item.messageType === 0" color="gray">未开始</a-tag>
<a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
<a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
<a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
</template>
<div class="item-wrap" @click="onItemClick(item)">
<a-list-item-meta>
<template v-if="item.avatar" #avatar>
<a-avatar shape="circle">
<img v-if="item.avatar" :src="item.avatar" />
<icon-desktop v-else />
</a-avatar>
</template>
<template #title>
<a-space :size="4">
<span>{{ item.title }}</span>
<a-typography-text type="secondary">
{{ item.subTitle }}
</a-typography-text>
</a-space>
</template>
<template #description>
<div>
<a-typography-paragraph
:ellipsis="{
rows: 1,
}"
>{{ item.content }}
</a-typography-paragraph
>
<a-typography-text
v-if="item.type === 'message'"
class="time-text"
>
{{ item.time }}
</a-typography-text>
</div>
</template>
</a-list-item-meta>
</div>
</a-list-item>
<template #footer>
<a-space
fill
:size="0"
:class="{ 'add-border-top': renderList.length < showMax }"
>
<div class="footer-wrap">
<a-link @click="allRead">全部已读</a-link>
</div>
<div class="footer-wrap">
<a-link>查看更多</a-link>
</div>
</a-space>
</template>
<div
v-if="renderList.length && renderList.length < 3"
:style="{ height: (showMax - renderList.length) * 86 + 'px' }"
></div>
</a-list>
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
import { MessageRecord, MessageListType } from '@/api/system/message';
const props = defineProps({
renderList: {
type: Array as PropType<MessageListType>,
required: true,
},
unreadCount: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['itemClick']);
const allRead = () => {
emit('itemClick', [...props.renderList]);
};
const onItemClick = (item: MessageRecord) => {
if (!item.status) {
emit('itemClick', [item]);
}
};
const showMax = 3;
</script>
<style scoped lang="less">
:deep(.arco-list) {
.arco-list-item {
min-height: 86px;
border-bottom: 1px solid rgb(var(--gray-3));
}
.arco-list-item-extra {
position: absolute;
right: 20px;
}
.arco-list-item-meta-content {
flex: 1;
}
.item-wrap {
cursor: pointer;
}
.time-text {
font-size: 12px;
color: rgb(var(--gray-6));
}
.arco-empty {
display: none;
}
.arco-list-footer {
padding: 0;
height: 50px;
line-height: 50px;
border-top: none;
.arco-space-item {
width: 100%;
border-right: 1px solid rgb(var(--gray-3));
&:last-child {
border-right: none;
}
}
.add-border-top {
border-top: 1px solid rgb(var(--gray-3));
}
}
.footer-wrap {
text-align: center;
}
.arco-typography {
margin-bottom: 0;
}
.add-border {
border-top: 1px solid rgb(var(--gray-3));
}
}
</style>