@peng_kai/kit 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export { default as ScrollNav } from './src/ScrollNav.vue'
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { useElementSize } from '@vueuse/core'
4
+
5
+ const props = defineProps<{
6
+ selector: string
7
+ }>()
8
+
9
+ const $content = document.querySelector(props.selector) as HTMLElement
10
+ const $contentParent = $content.parentElement
11
+ const { height: contentParentH } = useElementSize($contentParent)
12
+ const { height: contentH } = useElementSize($content)
13
+ const visible = computed(() => contentParentH.value * 2 < contentH.value)
14
+
15
+ function scrollTo(top: number) {
16
+ $contentParent?.scrollTo({ top })
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <div v-if="visible" class="wrapper">
22
+ <div class="btn" @click="scrollTo(0)">
23
+ <i class="i-fluent:arrow-previous-24-filled rotate-90" />
24
+ </div>
25
+ <div class="btn" @click="scrollTo($contentParent?.scrollHeight ?? Infinity)">
26
+ <i class="i-fluent:arrow-previous-24-filled rotate-270" />
27
+ </div>
28
+ <!-- <div v-if="showReturn" class="btn" @click="toLastY()">
29
+ <i class="i-fluent:arrow-hook-down-left-24-filled rotate-270" />
30
+ </div> -->
31
+ </div>
32
+ </template>
33
+
34
+ <style lang="scss" scoped>
35
+ .wrapper {
36
+ font-size: 18px;
37
+ }
38
+
39
+ .btn {
40
+ display: flex;
41
+ width: 2em;
42
+ height: 2em;
43
+ align-items: center;
44
+ justify-content: center;
45
+ border-radius: 2px;
46
+ background: #bcbcbc;
47
+ color: #000;
48
+ cursor: pointer;
49
+ opacity: 0.7;
50
+
51
+ &:active {
52
+ transform: scale(0.9);
53
+ }
54
+
55
+ & + & {
56
+ margin-top: 1px;
57
+ }
58
+ }
59
+ </style>
@@ -0,0 +1,30 @@
1
+ import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
2
+
3
+ export function getTitle(route?: Pick<RouteLocationNormalizedLoaded, 'meta'>) {
4
+ const mTitle = route?.meta?.title
5
+
6
+ return typeof mTitle === 'function' ? mTitle() : mTitle
7
+ }
8
+
9
+ export function getMenusByRouter(router: Router) {
10
+ const menuOrderRE = /^(?<key>\w*)@(?<order>\d+)$/
11
+ const routes = router.getRoutes()
12
+
13
+ return routes
14
+ .filter(route => menuOrderRE.test((route.meta.menuOrder ?? '')))
15
+ .map((route) => {
16
+ const res = route.meta!.menuOrder!.match(menuOrderRE)
17
+ const parentKey = res?.groups?.key
18
+ const order = Number(res?.groups?.order)
19
+ const name = route.name as string
20
+ const menu = {
21
+ key: name,
22
+ label: route.meta.title ?? name,
23
+ icon: route.meta.icon,
24
+ trigger: parentKey ? () => router.push({ name }) : () => {},
25
+ order,
26
+ }
27
+
28
+ return { parentKey, menu }
29
+ })
30
+ }
@@ -1,4 +1,5 @@
1
1
  export { definePage } from './definePage'
2
2
  export { defineRoute, getRoutes } from './defineRoute'
3
+ export { getMenusByRouter, getTitle as getRouteTitle } from './defineRoute.helpers'
3
4
  export { defineRouteGuard, getRouteGuards } from './defineRouteGuard'
4
5
  export { defineStartup, getStartups } from './defineStartup'
@@ -0,0 +1,6 @@
1
+ export { usePage } from './usePage'
2
+ export type { TPageState, IBreadcrumb } from './usePage'
3
+ export { useMenu } from './useMenu'
4
+ export type { TMenu } from './useMenu'
5
+ export { usePageTab } from './usePageTab'
6
+ export { usePermission } from './usePermission'
@@ -0,0 +1,132 @@
1
+ import { createGlobalState } from '@vueuse/core'
2
+ import { reactive, ref, computed, watch } from 'vue'
3
+ import type { UnwrapNestedRefs, VNode, Ref } from 'vue'
4
+
5
+ export { useMenu }
6
+ export type { TMenu }
7
+
8
+ interface IMenuConfig {
9
+ key: string
10
+ label: string | (() => string)
11
+ icon?: () => VNode
12
+ order: number
13
+ trigger: () => void
14
+ children?: IMenuConfig[]
15
+ }
16
+
17
+ interface IMenuReactive {
18
+ key: string
19
+ label: Ref<string>
20
+ icon: Ref<VNode | null>
21
+ order: number
22
+ trigger: () => void
23
+ children?: IMenuReactive[]
24
+ }
25
+
26
+ type TMenu = UnwrapNestedRefs<IMenuReactive>
27
+
28
+ const useMenu = createGlobalState(() => {
29
+ const menus = reactive<TMenu[]>([])
30
+ const collapsed = ref(false)
31
+
32
+ watch(menus, (menus) => {
33
+ console.log('menus', menus);
34
+ })
35
+
36
+ const findMenu = (menus: TMenu[], key: string) => {
37
+ const queue = [...menus]
38
+
39
+ while (queue.length > 0) {
40
+ const menu = queue.shift()!
41
+
42
+ if (menu.key === key)
43
+ return menu
44
+
45
+ if (menu.children)
46
+ queue.push(...menu.children)
47
+ }
48
+
49
+ return undefined
50
+ }
51
+
52
+ /**
53
+ * 获取目标路由的路径
54
+ * @param key 目标路由
55
+ */
56
+ const getMenuPath = (key: string) => {
57
+ const path: TMenu[] = []
58
+
59
+ const _getMenuPath = (menus: TMenu[], key: string) => {
60
+ for (const menu of menus) {
61
+ if (menu.key === key) {
62
+ path.push(menu)
63
+ return true
64
+ }
65
+
66
+ if (menu.children) {
67
+ path.push(menu)
68
+ if (_getMenuPath(menu.children, key))
69
+ return true
70
+ path.pop()
71
+ }
72
+ }
73
+
74
+ return false
75
+ }
76
+
77
+ _getMenuPath(menus, key)
78
+
79
+ return path
80
+ }
81
+
82
+ const addMenu = (menuConfig: IMenuConfig, parentKey?: string) => {
83
+ const labelGetter = typeof menuConfig.label === 'function' ? menuConfig.label : (() => menuConfig.label) as (() => string)
84
+ const iconGetter = typeof menuConfig.icon === 'function' ? menuConfig.icon : () => null
85
+
86
+ const _menu = reactive<IMenuReactive>({
87
+ key: menuConfig.key,
88
+ label: computed(labelGetter),
89
+ icon: computed(iconGetter),
90
+ trigger: menuConfig.trigger,
91
+ order: menuConfig.order,
92
+ })
93
+
94
+ if (parentKey) {
95
+ const parentMenu = findMenu(menus, parentKey)
96
+
97
+ if (!parentMenu)
98
+ return
99
+
100
+ const children = reactive(parentMenu.children ?? [])
101
+ children.push(_menu)
102
+ children.sort((a, b) => a.order - b.order)
103
+ parentMenu.children = children
104
+ }
105
+ else {
106
+ menus.push(_menu)
107
+ menus.sort((a, b) => a.order - b.order)
108
+ }
109
+ }
110
+
111
+ const removeMenu = (key: string) => {
112
+ const _remove = (menus: TMenu[], key: string) => {
113
+ for (let i = 0; i < menus.length; i++) {
114
+ const menu = menus[i]
115
+ if (menu.key === key) {
116
+ menus.splice(i, 1)
117
+ return
118
+ }
119
+ if (menu.children)
120
+ _remove(menu.children, key)
121
+ }
122
+ }
123
+
124
+ return _remove(menus, key)
125
+ }
126
+
127
+ const getMenu = (key: string) => {
128
+ return findMenu(menus, key)
129
+ }
130
+
131
+ return { menus, collapsed, getMenuPath, addMenu, removeMenu, getMenu }
132
+ })
@@ -0,0 +1,138 @@
1
+ import { createGlobalState, usePrevious } from '@vueuse/core'
2
+ import type { RouteLocationNormalizedLoaded } from 'vue-router'
3
+ import { shallowRef, ref, reactive, computed, watch, readonly } from 'vue'
4
+ import type { VNode } from 'vue'
5
+ import { useMenu } from './useMenu'
6
+ import type { TMenu } from './useMenu'
7
+ import { getTitle } from '../defines/defineRoute.helpers'
8
+ import { kitDependencies } from "../../kitDependencies";
9
+
10
+ export { usePage }
11
+ export type { TPageState, IBreadcrumb }
12
+
13
+ type TPageState = ReturnType<typeof getPageState>
14
+
15
+ interface IBreadcrumb {
16
+ title: string
17
+ icon?: VNode | null
18
+ trigger?: () => void
19
+ children?: IBreadcrumb[]
20
+ }
21
+
22
+ const usePage = createGlobalState(() => {
23
+ const router = kitDependencies.useRouter()
24
+ const currentPageNode = shallowRef<VNode>()
25
+ const currentPageKey = ref('')
26
+ const pageCacheList = reactive<string[]>([])
27
+ const pageStateMap = new Map<string, TPageState>()
28
+ const currentPageState = computed(() => pageStateMap.get(currentPageKey.value))
29
+ const previousPageState = usePrevious(currentPageState)
30
+
31
+ const openPage = (key: string) => {
32
+ const route = pageStateMap.get(key)?.route
33
+
34
+ if (!route)
35
+ return
36
+
37
+ router.push(route.fullPath)
38
+ }
39
+
40
+ const closePage = (key: string) => {
41
+ if (!pageStateMap.has(key))
42
+ return
43
+
44
+ const cacheIndex = pageCacheList.indexOf(key)
45
+ cacheIndex >= 0 && pageCacheList.splice(cacheIndex, 1)
46
+ // 关闭当前页面则返回上一个路由
47
+ currentPageKey.value === key && router.go(-1)
48
+ pageStateMap.delete(key)
49
+ }
50
+
51
+ const setPage = (pageNode: VNode, route: RouteLocationNormalizedLoaded) => {
52
+ const pageKey = route?.meta?.pageKeyFn?.(route) ?? route.fullPath
53
+ const pageInfo = getPageState(route)
54
+ const canKeepAlive = route.meta.keepAlive && !pageCacheList.includes(pageKey)
55
+
56
+ canKeepAlive && pageCacheList.push(pageKey)
57
+ pageStateMap.has(pageKey) || pageStateMap.set(pageKey, pageInfo)
58
+
59
+ ;(pageNode as any).type.name = pageKey
60
+ currentPageKey.value = pageKey
61
+ currentPageNode.value = pageNode
62
+
63
+ return pageNode
64
+ }
65
+
66
+ const getCurrentPageState = () => {
67
+ return pageStateMap.get(currentPageKey.value)
68
+ }
69
+
70
+ // 监听 title 变化
71
+ watch(() => currentPageState.value?.title, (title) => {
72
+ let _title = title
73
+
74
+ if (!_title) {
75
+ const pageState = currentPageState.value
76
+
77
+ // 当前 title 为空时,使用路由 title
78
+ if (pageState) {
79
+ _title = getTitle(pageState?.route)
80
+ pageState.title = _title!
81
+ }
82
+ }
83
+
84
+ document.title = _title!
85
+ })
86
+
87
+ return {
88
+ currentPageNode,
89
+ pageCacheList,
90
+ pageStateMap,
91
+ currentPageKey,
92
+ currentPageState,
93
+ getCurrentPageState,
94
+ previousPageState,
95
+ setPage,
96
+ openPage,
97
+ closePage,
98
+ }
99
+ })
100
+
101
+ function getPageState(route: RouteLocationNormalizedLoaded) {
102
+ const { getMenuPath } = useMenu()
103
+ const menuPath = getMenuPath(route.name as string)
104
+ const currentMenu = menuPath.pop()
105
+ const { icon: mIcon } = route.meta
106
+ const title = currentMenu?.label ?? getTitle(route) ?? import.meta.env.VITE_APP_TITLE
107
+ const icon = currentMenu?.icon ?? mIcon?.()
108
+ const state = reactive({
109
+ title,
110
+ icon,
111
+ breadcrumbs: menuPath.map(menuToBreadcrumb),
112
+ route: readonly(route),
113
+ refresh() {
114
+ const menuKey = this.route.name as string
115
+ const { getMenu, getMenuPath } = useMenu()
116
+ const menu = getMenu(menuKey)
117
+ const menuPath = getMenuPath(menuKey)
118
+
119
+ menuPath.pop()
120
+
121
+ this.title = menu?.label ?? getTitle(this.route) ?? import.meta.env.VITE_APP_TITLE
122
+ this.breadcrumbs = menuPath.map(menuToBreadcrumb)
123
+ },
124
+ })
125
+
126
+ return state
127
+ }
128
+
129
+ function menuToBreadcrumb(menu: TMenu) {
130
+ const ret: IBreadcrumb = reactive({
131
+ title: menu.label,
132
+ icon: menu.icon,
133
+ trigger: menu.trigger,
134
+ children: menu.children?.map(menuToBreadcrumb),
135
+ })
136
+
137
+ return ret
138
+ }
@@ -0,0 +1,35 @@
1
+ import { createGlobalState } from '@vueuse/core'
2
+ import { ref, computed, watch } from 'vue'
3
+ import { usePage } from './usePage'
4
+
5
+ export { usePageTab }
6
+
7
+ const usePageTab = createGlobalState(() => {
8
+ const { currentPageKey, pageStateMap, openPage, closePage } = usePage()
9
+ const tabKeyList = ref<string[]>([])
10
+ const tabList = computed(() => tabKeyList.value.map((key) => {
11
+ const state = pageStateMap.get(key)
12
+
13
+ return state ? { key, title: state.title, icon: state.icon } : undefined!
14
+ }).filter(tab => !!tab))
15
+
16
+ const closeTab = (key: string) => {
17
+ const i = tabKeyList.value.indexOf(key)
18
+
19
+ if (i > -1) {
20
+ closePage(key)
21
+ setTimeout(() => tabKeyList.value.splice(i, 1))
22
+ }
23
+ }
24
+
25
+ watch(
26
+ currentPageKey,
27
+ (key) => {
28
+ if (!tabKeyList.value.includes(key))
29
+ tabKeyList.value.push(key)
30
+ },
31
+ { immediate: true },
32
+ )
33
+
34
+ return { activeTab: currentPageKey, tabList, closeTab, openTab: openPage }
35
+ })
@@ -0,0 +1,5 @@
1
+ import { createGlobalState } from '@vueuse/core'
2
+
3
+ export { usePermission }
4
+
5
+ const usePermission = createGlobalState(() => {})
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ import { computed } from "vue";
3
+ import type { VNode } from 'vue'
4
+ import { Breadcrumb as ABreadcrumb } from "ant-design-vue";
5
+ import type { Route as AntdBreadcrumbRoute } from 'ant-design-vue/es/breadcrumb/Breadcrumb'
6
+ import { usePage } from '../../hooks'
7
+ import type { IBreadcrumb } from '../../hooks'
8
+
9
+ interface IBreadcrumbRoute extends AntdBreadcrumbRoute {
10
+ icon?: VNode | null
11
+ trigger?: () => void
12
+ }
13
+
14
+ function _buildRoute(breadcrumb: IBreadcrumb): IBreadcrumbRoute {
15
+ return {
16
+ breadcrumbName: breadcrumb.title,
17
+ path: breadcrumb.title,
18
+ icon: breadcrumb.icon,
19
+ children: breadcrumb.children?.map(child => _buildRoute(child)),
20
+ trigger: breadcrumb.trigger,
21
+ }
22
+ }
23
+ </script>
24
+
25
+ <script setup lang="ts">
26
+ const { currentPageState } = usePage()
27
+ const routes = computed(() => {
28
+ let breadcrumbs: IBreadcrumbRoute[] = []
29
+
30
+ if (!currentPageState.value)
31
+ return breadcrumbs
32
+
33
+ breadcrumbs = currentPageState.value.breadcrumbs.map(breadcrumb => _buildRoute(breadcrumb))
34
+
35
+ breadcrumbs.push({
36
+ path: currentPageState.value.title,
37
+ breadcrumbName: currentPageState.value.title,
38
+ icon: currentPageState.value.icon,
39
+ })
40
+
41
+ return breadcrumbs
42
+ })
43
+ </script>
44
+
45
+ <template>
46
+ <ABreadcrumb class="breadcrumb" :routes="routes">
47
+ <template #itemRender="{ route }: {route: IBreadcrumbRoute}">
48
+ <div @click="route.trigger?.()">
49
+ <component :is="route.icon" v-if="route.icon" class="mb-0.2em mr-0.2em" />
50
+ <span>{{ route.breadcrumbName }}</span>
51
+ </div>
52
+ </template>
53
+ </ABreadcrumb>
54
+ </template>
55
+
56
+ <style lang="scss" scoped>
57
+ .breadcrumb {
58
+ font-size: 1rem;
59
+
60
+ :deep(.ant-dropdown-trigger) {
61
+ height: 1.6em;
62
+ }
63
+
64
+ :deep(.anticon-down) {
65
+ display: none;
66
+ }
67
+ }
68
+ </style>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import { usePage } from '../../hooks'
3
+
4
+ const { pageCacheList, setPage } = usePage()
5
+ </script>
6
+
7
+ <template>
8
+ <RouterView v-slot="{ Component, route }">
9
+ <KeepAlive :include="pageCacheList">
10
+ <Suspense>
11
+ <component :is="setPage(Component, route)" :key="(Component.type as any).name" />
12
+ <template #fallback>
13
+ <div class="flex justify-center items-center h-70">
14
+ <div class="flex items-center">
15
+ <i class="i-svg-spinners:180-ring-with-bg block color-primary scale-125 mr-2" />
16
+ <span class="text-gray">正在加载...</span>
17
+ </div>
18
+ </div>
19
+ </template>
20
+ </Suspense>
21
+ </KeepAlive>
22
+ </RouterView>
23
+ </template>
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ import { computed, ref, watch } from "vue";
3
+ import { Menu as AMenu } from "ant-design-vue";
4
+ import type { ItemType } from 'ant-design-vue'
5
+ import { useMenu } from '../../hooks'
6
+ import type { TMenu } from '../../hooks'
7
+ import { kitDependencies } from "../../../kitDependencies";
8
+
9
+ function formatMenu(menu: TMenu): ItemType {
10
+ return {
11
+ key: menu.key,
12
+ title: menu.label,
13
+ label: menu.label,
14
+ icon: menu.icon ?? undefined,
15
+ onClick: (e) => {
16
+ (e as any).stopPropagation()
17
+ menu.trigger()
18
+ },
19
+ children: menu.children?.map(formatMenu),
20
+ }
21
+ }
22
+ </script>
23
+
24
+ <script setup lang="ts">
25
+ const router = kitDependencies.useRouter()
26
+ const { menus, getMenuPath } = useMenu()
27
+ const items = computed(() => menus.map(formatMenu))
28
+ const openKeys = ref<string[]>([])
29
+ const selectedKeys = ref<string[]>([])
30
+ console.log('获取 kitDependencies');
31
+
32
+ setTimeout(() => {
33
+ }, 3000)
34
+ console.log('菜单对象对比', kitDependencies, kitDependencies.menus === menus);
35
+
36
+ watch(
37
+ () => router?.currentRoute.value,
38
+ (route) => {
39
+ if (!route) return;
40
+
41
+ const menuPath = getMenuPath(route.name as string)
42
+ openKeys.value = menuPath.map(menu => menu.key)
43
+ selectedKeys.value = [openKeys.value.pop()!]
44
+ },
45
+ { immediate: true },
46
+ )
47
+ </script>
48
+
49
+ <template>
50
+ <AMenu
51
+ class="menu"
52
+ :items="items"
53
+ :openKeys="openKeys"
54
+ :selectedKeys="selectedKeys"
55
+ mode="inline"
56
+ :inlineIndent="14"
57
+ />
58
+ </template>
59
+
60
+ <style lang="scss" scoped>
61
+ .menu {
62
+ border-inline-end: none !important;
63
+
64
+ &.ant-menu-inline-collapsed {
65
+ width: var(--app-siderbar-width);
66
+ }
67
+
68
+ &,
69
+ .ant-menu {
70
+ background-color: transparent;
71
+ }
72
+ }
73
+ </style>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import { Tabs as ATabs, TabPane as ATabPane } from "ant-design-vue";
3
+ import { usePageTab } from '../../hooks'
4
+
5
+ const { activeTab, tabList, openTab, closeTab } = usePageTab()
6
+ </script>
7
+
8
+ <template>
9
+ <ATabs
10
+ class="app-page-tabs"
11
+ :activeKey="activeTab"
12
+ type="editable-card"
13
+ size="small"
14
+ :animated="false"
15
+ :hideAdd="true"
16
+ :tabBarGutter="4"
17
+ @tabClick="(openTab as any)"
18
+ @edit="(closeTab as any)"
19
+ >
20
+ <ATabPane v-for="tab of tabList" :key="tab.key" :tab="tab.title">
21
+ <template #closeIcon>
22
+ <i class="i-icon-park-outline:close" />
23
+ </template>
24
+ </ATabPane>
25
+ </ATabs>
26
+ </template>
27
+
28
+ <style scoped lang="scss">
29
+ .app-page-tabs {
30
+ :deep(.ant-tabs-nav) {
31
+ margin: 0;
32
+ }
33
+
34
+ :deep(.ant-tabs-nav::before) {
35
+ display: none;
36
+ }
37
+
38
+ :deep(.ant-tabs-tab) {
39
+ --uno: 'border-rd-3px! text-14px p-[3px_1px_3px_8px]! select-none';
40
+ }
41
+
42
+ :deep(.ant-tabs-tab .ant-tabs-tab-btn) {
43
+ transform: translateX(8px);
44
+ }
45
+
46
+ :deep(.ant-tabs-tab-active) {
47
+ --uno: 'border-[currentColor]!';
48
+ }
49
+
50
+ :deep(.ant-tabs-tab-remove) {
51
+ display: flex;
52
+ height: 100%;
53
+ align-items: center;
54
+ margin: 0;
55
+ opacity: 0;
56
+ transition: all 150ms;
57
+ }
58
+
59
+ :deep(.ant-tabs-tab:hover .ant-tabs-tab-btn),
60
+ :deep(.ant-tabs-tab-active .ant-tabs-tab-btn) {
61
+ transform: translateX(0);
62
+ }
63
+
64
+ :deep(.ant-tabs-tab:hover .ant-tabs-tab-remove),
65
+ :deep(.ant-tabs-tab-active .ant-tabs-tab-remove) {
66
+ margin-left: 0;
67
+ opacity: 1;
68
+ }
69
+ }
70
+ </style>
@@ -0,0 +1,4 @@
1
+ export { default as Breadcrumb } from './Breadcrumb.vue'
2
+ export { default as Content } from './Content.vue'
3
+ export { default as Menu } from './Menu.vue'
4
+ export { default as PageTab } from './PageTab.vue'
@@ -1,5 +1,9 @@
1
1
  import dayjs from "dayjs";
2
+ import type { useRoute, useRouter } from 'vue-router'
2
3
 
3
4
  export const kitDependencies = {
4
- dayjs: dayjs
5
+ dayjs: dayjs,
6
+ useRoute: (() => {}) as typeof useRoute,
7
+ useRouter: (() => {}) as typeof useRouter,
8
+ menus: undefined as any,
5
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peng_kai/kit",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {