@peng_kai/kit 0.3.0-beta.1 → 0.3.0-beta.10
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.
- package/.vscode/settings.json +2 -2
- package/admin/components/currency/src/CurrencyIcon.vue +35 -33
- package/admin/components/date/PeriodPicker.vue +122 -0
- package/admin/components/date/TimeFieldSelectForLabel.vue +24 -0
- package/admin/components/date/TtaTimeZone.vue +516 -0
- package/admin/components/date/helpers.ts +223 -0
- package/admin/components/date/index.ts +4 -0
- package/admin/components/filter/src/FilterReset.vue +22 -5
- package/admin/components/provider/Admin.vue +17 -0
- package/admin/components/provider/admin-permission.ts +48 -0
- package/admin/components/provider/admin-router.ts +358 -0
- package/admin/components/provider/index.ts +3 -0
- package/admin/components/text/index.ts +2 -0
- package/admin/components/text/src/Amount.v2.vue +127 -0
- package/admin/components/text/src/Datetime.vue +15 -11
- package/admin/components/text/src/Num.vue +192 -0
- package/admin/components/upload/src/customRequests.ts +1 -1
- package/admin/components/upload/src/helpers.ts +1 -0
- package/admin/layout/large/Breadcrumb.vue +10 -23
- package/admin/layout/large/Content.vue +9 -6
- package/admin/layout/large/Layout.vue +129 -0
- package/admin/layout/large/Menu.vue +24 -17
- package/admin/layout/large/Notice.vue +140 -0
- package/admin/layout/large/Tabs.vue +177 -0
- package/admin/layout/large/index.ts +3 -1
- package/admin/layout/large/y682.mp3 +0 -0
- package/admin/permission/routerGuard.ts +15 -8
- package/admin/permission/vuePlugin.ts +5 -10
- package/admin/route-guards/index.ts +0 -1
- package/admin/stores/index.ts +1 -0
- package/admin/styles/classCover.scss +1 -1
- package/admin/styles/globalCover.scss +4 -0
- package/admin/styles/index.scss +2 -2
- package/antd/hooks/useAntdForm.helpers.ts +10 -1
- package/antd/hooks/useAntdModal.ts +20 -8
- package/antd/hooks/useAntdTable.ts +2 -0
- package/antd/hooks/useAntdTheme.ts +7 -0
- package/antd/index.ts +1 -1
- package/libs/bignumber.ts +1 -1
- package/libs/dayjs.ts +11 -1
- package/libs/fingerprintjs.ts +1 -0
- package/package.json +27 -28
- package/request/interceptors/getDeviceInfo.ts +9 -0
- package/utils/LocaleManager.ts +1 -1
- package/utils/locale/LocaleManager.ts +2 -1
- package/utils/number.ts +1 -2
- package/utils/storage.ts +31 -0
- package/utils/string.ts +14 -0
- package/utils/upload/AwsS3.ts +1 -1
- package/admin/layout/large/PageTab.vue +0 -70
- package/admin/route-guards/collapseMenu.ts +0 -11
- package/libs/a-calc.ts +0 -1
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { computed, ref, watch } from 'vue';
|
|
3
3
|
import { Menu as AMenu } from 'ant-design-vue';
|
|
4
4
|
import type { ItemType } from 'ant-design-vue';
|
|
5
|
-
import {
|
|
6
|
-
import type { TMenu } from '../../stores/createUseMenuStore';
|
|
5
|
+
import type { TMenu } from '../../components/provider/admin-router';
|
|
7
6
|
|
|
8
7
|
function formatMenu(menu: TMenu): ItemType {
|
|
9
8
|
return {
|
|
@@ -21,24 +20,19 @@ function formatMenu(menu: TMenu): ItemType {
|
|
|
21
20
|
</script>
|
|
22
21
|
|
|
23
22
|
<script setup lang="ts">
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const props = defineProps<{
|
|
24
|
+
path: TMenu[]
|
|
25
|
+
menus: TMenu[]
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const items = computed(() => props.menus.map(formatMenu));
|
|
27
29
|
const openKeys = ref<string[]>([]);
|
|
28
30
|
const selectedKeys = ref<string[]>([]);
|
|
29
31
|
|
|
30
|
-
watch(
|
|
31
|
-
(
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
-
return;
|
|
35
|
-
|
|
36
|
-
const menuPath = menuStore.getMenuPath(route.name as string);
|
|
37
|
-
openKeys.value = menuPath.map(menu => menu.key);
|
|
38
|
-
selectedKeys.value = [openKeys.value.pop()!];
|
|
39
|
-
},
|
|
40
|
-
{ immediate: true },
|
|
41
|
-
);
|
|
32
|
+
watch(() => props.path, (menuPath) => {
|
|
33
|
+
openKeys.value = menuPath.map(menu => menu.key);
|
|
34
|
+
selectedKeys.value = [openKeys.value.pop()!];
|
|
35
|
+
}, { immediate: true });
|
|
42
36
|
</script>
|
|
43
37
|
|
|
44
38
|
<template>
|
|
@@ -64,5 +58,18 @@ watch(
|
|
|
64
58
|
.ant-menu {
|
|
65
59
|
background-color: transparent;
|
|
66
60
|
}
|
|
61
|
+
|
|
62
|
+
:deep(.ant-menu-submenu-arrow){
|
|
63
|
+
top: 52%;
|
|
64
|
+
inset-inline-end: 8px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:deep(.ant-menu-item-only-child) {
|
|
68
|
+
padding-left: 43px !important;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:deep(.ant-menu-sub.ant-menu-inline) {
|
|
72
|
+
background-color: transparent;
|
|
73
|
+
}
|
|
67
74
|
}
|
|
68
75
|
</style>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useIntervalFn } from '@vueuse/core';
|
|
3
|
+
import { Empty, Dropdown, Badge, Menu, MenuItem } from 'ant-design-vue';
|
|
4
|
+
import { type UseQueryReturnType } from '@tanstack/vue-query'
|
|
5
|
+
import { watch, ref, computed } from 'vue'
|
|
6
|
+
import dayjs from '../../../libs/dayjs';
|
|
7
|
+
import { useRouter } from 'vue-router';
|
|
8
|
+
import audioURL from './y682.mp3';
|
|
9
|
+
|
|
10
|
+
const noticeAudio = new Audio(audioURL);
|
|
11
|
+
noticeAudio.src = audioURL;
|
|
12
|
+
noticeAudio.muted = true;
|
|
13
|
+
document.body.append(noticeAudio);
|
|
14
|
+
|
|
15
|
+
const logoFaviconEle = document.querySelector('link[rel="icon"]');
|
|
16
|
+
const numFaviconEle = document.createElement('link');
|
|
17
|
+
numFaviconEle.rel = 'icon';
|
|
18
|
+
const numCanvas = document.createElement('canvas') as (HTMLCanvasElement & { num: number });
|
|
19
|
+
numCanvas.width = 32;
|
|
20
|
+
numCanvas.height = 32;
|
|
21
|
+
|
|
22
|
+
interface TNotice {
|
|
23
|
+
title: string;
|
|
24
|
+
type: number;
|
|
25
|
+
url: string;
|
|
26
|
+
time: number;
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script lang="ts" setup>
|
|
31
|
+
const props = defineProps<{
|
|
32
|
+
noticesQry: UseQueryReturnType<TNotice[], Error>
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
const visible = ref(false);
|
|
37
|
+
const noticeNum = computed(() => props.noticesQry.data.value?.length ?? 0);
|
|
38
|
+
|
|
39
|
+
function operate(notice: TNotice) {
|
|
40
|
+
if (router.currentRoute.value.path !== notice.url) {
|
|
41
|
+
router.push(notice.url);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
watch(noticeNum, (num = 0, oldNum = 0) => {
|
|
46
|
+
num > oldNum && noticeAudio.play();
|
|
47
|
+
}, { immediate: true });
|
|
48
|
+
|
|
49
|
+
useIntervalFn(() => {
|
|
50
|
+
const num = noticeNum.value;
|
|
51
|
+
|
|
52
|
+
if (!num || document.visibilityState === 'visible') {
|
|
53
|
+
if (numFaviconEle.parentNode) {
|
|
54
|
+
numFaviconEle.remove();
|
|
55
|
+
logoFaviconEle && document.head.appendChild(logoFaviconEle);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (logoFaviconEle?.parentNode) {
|
|
61
|
+
if (numCanvas.num !== num) {
|
|
62
|
+
numCanvas.num = num;
|
|
63
|
+
const ctx = numCanvas.getContext('2d')!;
|
|
64
|
+
ctx.fillStyle = '#f00';
|
|
65
|
+
ctx.beginPath();
|
|
66
|
+
ctx.arc(16, 16, 16, 0, Math.PI * 2, true);
|
|
67
|
+
ctx.fill();
|
|
68
|
+
ctx.fillStyle = '#fff';
|
|
69
|
+
ctx.font = 'bold 22px sans-serif';
|
|
70
|
+
ctx.textAlign = 'center';
|
|
71
|
+
ctx.textBaseline = 'middle';
|
|
72
|
+
ctx.fillText(num.toString(), 16, 16);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
numFaviconEle.href = numCanvas.toDataURL();
|
|
76
|
+
logoFaviconEle.parentNode.replaceChild(numFaviconEle, logoFaviconEle);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
numFaviconEle.parentNode?.replaceChild(logoFaviconEle!, numFaviconEle);
|
|
80
|
+
}
|
|
81
|
+
}, 500);
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<Badge :count="noticeNum" size="small">
|
|
86
|
+
<Dropdown v-model:open="visible" placement="bottomRight" trigger="click" arrow>
|
|
87
|
+
<div class="h-7 flex cursor-pointer items-center justify-center rounded-1 px-1">
|
|
88
|
+
<i class="i-ri:notification-4-line block text-5" :class="{ shake: !!noticeNum }" />
|
|
89
|
+
</div>
|
|
90
|
+
<template #overlay>
|
|
91
|
+
<Menu v-if="noticeNum" @click="visible = false">
|
|
92
|
+
<MenuItem v-for="(item, i) of noticesQry.data.value" :key="i" @click="operate(item)">
|
|
93
|
+
<div class="min-w-50">
|
|
94
|
+
<div>{{ item.title }}</div>
|
|
95
|
+
<div class="mt-1 text-xs text-$antd-colorTextSecondary">
|
|
96
|
+
{{ dayjs().format('MM-DD HH:mm:ss') }}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</MenuItem>
|
|
100
|
+
</Menu>
|
|
101
|
+
<div v-else class="h-40 min-w-50 flex items-center justify-center rounded-2 bg-$antd-colorBgElevated shadow"
|
|
102
|
+
@click="visible = false">
|
|
103
|
+
<Empty description="暂无通知" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
|
104
|
+
</div>
|
|
105
|
+
</template>
|
|
106
|
+
</Dropdown>
|
|
107
|
+
</Badge>
|
|
108
|
+
</template>
|
|
109
|
+
|
|
110
|
+
<style lang="scss" scoped>
|
|
111
|
+
@keyframes shake {
|
|
112
|
+
0% {
|
|
113
|
+
transform: rotate(0deg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
25% {
|
|
117
|
+
transform: rotate(10deg);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
50% {
|
|
121
|
+
transform: rotate(-10deg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
75% {
|
|
125
|
+
transform: rotate(10deg);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
100% {
|
|
129
|
+
transform: rotate(-10deg);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.shake {
|
|
134
|
+
animation: shake 0.5s infinite;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.shadow {
|
|
138
|
+
box-shadow: 0 6px 16px 0 rgb(0 0 0 / 8%), 0 3px 6px -4px rgb(0 0 0 / 12%), 0 9px 28px 8px rgb(0 0 0 / 5%);
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import { autoResetRef, useEventListener, useMutationObserver, useResizeObserver } from '@vueuse/core';
|
|
4
|
+
import type { TTab } from '../../components/provider/admin-router';
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
current: TTab
|
|
8
|
+
tabs: TTab[]
|
|
9
|
+
}>();
|
|
10
|
+
const emits = defineEmits<{
|
|
11
|
+
close: [TTab]
|
|
12
|
+
open: [TTab]
|
|
13
|
+
refresh: [TTab]
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const $tabList = ref<HTMLElement>();
|
|
17
|
+
const refreshAnim = autoResetRef(false, 300);
|
|
18
|
+
const isLeftmost = ref(false);
|
|
19
|
+
const isRightmost = ref(false);
|
|
20
|
+
|
|
21
|
+
useEventListener($tabList, 'wheel', (e) => {
|
|
22
|
+
if ($tabList.value) {
|
|
23
|
+
const deltaY = e.deltaY < 0 ? -100 : 100;
|
|
24
|
+
$tabList.value.scrollLeft += deltaY;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function isScrollEnd() {
|
|
29
|
+
if ($tabList.value) {
|
|
30
|
+
const { scrollLeft, scrollWidth, clientWidth } = $tabList.value;
|
|
31
|
+
isRightmost.value = scrollLeft === 0;
|
|
32
|
+
isLeftmost.value = Math.ceil(Math.abs(scrollLeft) + clientWidth) >= scrollWidth;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function refresh(tab: TTab) {
|
|
37
|
+
if (!refreshAnim.value) {
|
|
38
|
+
refreshAnim.value = true;
|
|
39
|
+
emits('refresh', tab);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
useEventListener($tabList, 'scroll', isScrollEnd);
|
|
44
|
+
useResizeObserver($tabList, isScrollEnd);
|
|
45
|
+
useMutationObserver($tabList, isScrollEnd, { childList: true });
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="flex items-center">
|
|
50
|
+
<div ref="$tabList" class="tab-list relative ml-auto" :class="{ 'is-leftmost': isLeftmost, 'is-rightmost': isRightmost }">
|
|
51
|
+
<div
|
|
52
|
+
v-for="tab of props.tabs" :key="tab.path"
|
|
53
|
+
class="tab" :class="{ opened: tab.path === props.current?.path }"
|
|
54
|
+
@click="emits('open', tab)"
|
|
55
|
+
>
|
|
56
|
+
<span class="title">{{ tab.title }}</span>
|
|
57
|
+
<span v-if="tab.type !== 2 && props.tabs.length > 1" class="close-btn" @click.stop="emits('close', tab)">
|
|
58
|
+
<i class="i-material-symbols:close-rounded" />
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div v-memo="[refreshAnim]" class="operation-btn ml-2" @click="refresh(props.current)">
|
|
63
|
+
<i class="i-material-symbols:refresh-rounded" :class="{ 'refresh-spin': refreshAnim }" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<style lang="scss" scoped>
|
|
69
|
+
.tab-list {
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: 4px;
|
|
72
|
+
align-items: center;
|
|
73
|
+
overflow-x: auto;
|
|
74
|
+
scrollbar-width: none;
|
|
75
|
+
flex: 1;
|
|
76
|
+
mask-image: linear-gradient(to left, #0000 0%, #000 5%, #000 95%, #0000 100%);
|
|
77
|
+
direction: rtl;
|
|
78
|
+
|
|
79
|
+
&.is-leftmost:not(.is-rightmost) {
|
|
80
|
+
mask-image: linear-gradient(to right, #000 0%, #000 5%, #000 95%, #0000 100%);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&.is-rightmost:not(.is-leftmost) {
|
|
84
|
+
mask-image: linear-gradient(to left, #000 0%, #000 5%, #000 95%, #0000 100%);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&.is-leftmost.is-rightmost {
|
|
88
|
+
mask-image: none;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.tab {
|
|
93
|
+
position: relative;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
padding: 0 8px;
|
|
97
|
+
height: 28px;
|
|
98
|
+
font-size: 14px;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
transition: all 0.2s;
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
line-height: 1em;
|
|
104
|
+
color: var(--antd-colorTextTertiary);
|
|
105
|
+
|
|
106
|
+
&:not(.opened):hover {
|
|
107
|
+
color: var(--antd-colorText);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&.opened {
|
|
111
|
+
color: var(--antd-colorPrimary);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.title {
|
|
115
|
+
transition: transform 0.1s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&:has(.close-btn):hover .title {
|
|
119
|
+
transform: translateX(-4px);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.close-btn {
|
|
123
|
+
position: absolute;
|
|
124
|
+
right: -6px;
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
width: 1em;
|
|
128
|
+
height: 1em;
|
|
129
|
+
padding: 2px;
|
|
130
|
+
opacity: 0;
|
|
131
|
+
margin-bottom: -1px;
|
|
132
|
+
transition: all 0.2s;
|
|
133
|
+
transform: scale(0);
|
|
134
|
+
color: var(--antd-colorTextTertiary);
|
|
135
|
+
|
|
136
|
+
&:hover {
|
|
137
|
+
color: var(--antd-colorText);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
&:hover .close-btn {
|
|
142
|
+
margin-left: 0;
|
|
143
|
+
opacity: 1;
|
|
144
|
+
transform: scale(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.operation-btn {
|
|
149
|
+
flex: none;
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
height: 28px;
|
|
154
|
+
font-size: 18px;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
border-radius: 4px;
|
|
157
|
+
|
|
158
|
+
&:hover {
|
|
159
|
+
color: var(--antd-colorPrimaryText);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.refresh-spin {
|
|
164
|
+
& {
|
|
165
|
+
animation: spin 200ms linear;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@keyframes spin {
|
|
169
|
+
0% {
|
|
170
|
+
transform: rotate(0deg);
|
|
171
|
+
}
|
|
172
|
+
100% {
|
|
173
|
+
transform: rotate(360deg);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
</style>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as Breadcrumb } from './Breadcrumb.vue';
|
|
2
2
|
export { default as Content } from './Content.vue';
|
|
3
3
|
export { default as Menu } from './Menu.vue';
|
|
4
|
-
export { default as
|
|
4
|
+
export { default as Tabs } from './Tabs.vue';
|
|
5
|
+
export { default as Layout } from './Layout.vue';
|
|
6
|
+
export { default as Notice } from './Notice.vue';
|
|
Binary file
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import type { Router } from 'vue-router';
|
|
2
|
-
import { adminPlugin } from '../adminPlugin';
|
|
3
2
|
import { hasToken } from '../../utils';
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
interface TPermission {
|
|
5
|
+
refresh: () => Promise<any>
|
|
6
|
+
has: (code: string) => boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setupPermissionRouterGuard(
|
|
10
|
+
router: Router,
|
|
11
|
+
permission: TPermission | undefined,
|
|
12
|
+
rouneNames: { index: string, login: string, 403: string },
|
|
13
|
+
) {
|
|
6
14
|
router.beforeEach(async (to, _, next) => {
|
|
7
|
-
const permissionStore = adminPlugin.deps.usePermissionStore();
|
|
8
15
|
const isLogin = hasToken();
|
|
9
16
|
const needLogin = Boolean(to.meta?.requireAuth);
|
|
10
17
|
let hasPermission = false;
|
|
11
18
|
|
|
12
19
|
if (isLogin) {
|
|
13
|
-
await
|
|
20
|
+
await permission?.refresh();
|
|
14
21
|
|
|
15
22
|
const permissionCode = to.meta?.permissionCode;
|
|
16
|
-
hasPermission = permissionCode ?
|
|
23
|
+
hasPermission = permissionCode ? (permission?.has(permissionCode) ?? true) : true;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
// 已登录状态跳转登录页,跳转至首页
|
|
20
27
|
if (isLogin && to.name === rouneNames.login)
|
|
21
|
-
return next({ name: rouneNames.index, replace: true });
|
|
28
|
+
return next({ name: rouneNames.index, replace: true, state: { tabType: 2 } });
|
|
22
29
|
|
|
23
30
|
// 不需要登录权限的页面直接通行
|
|
24
31
|
else if (!needLogin)
|
|
@@ -26,7 +33,7 @@ export function setupPermissionRouterGuard(router: Router, rouneNames: { index:
|
|
|
26
33
|
|
|
27
34
|
// 未登录状态进入需要登录权限的页面
|
|
28
35
|
else if (!isLogin && needLogin)
|
|
29
|
-
return next({ name: rouneNames.login,
|
|
36
|
+
return next({ name: rouneNames.login, query: { redirect: to.fullPath }, state: { tabType: 0 }, replace: true });
|
|
30
37
|
|
|
31
38
|
// 登录状态进入需要登录权限的页面,有权限直接通行
|
|
32
39
|
else if (isLogin && needLogin && hasPermission)
|
|
@@ -34,7 +41,7 @@ export function setupPermissionRouterGuard(router: Router, rouneNames: { index:
|
|
|
34
41
|
|
|
35
42
|
// 登录状态进入需要登录权限的页面,无权限,重定向到无权限页面
|
|
36
43
|
else if (isLogin && needLogin && !hasPermission)
|
|
37
|
-
return next({ name: rouneNames[403], replace: true });
|
|
44
|
+
return next({ name: rouneNames[403], state: { tabType: 0 }, replace: true });
|
|
38
45
|
|
|
39
46
|
return next(false);
|
|
40
47
|
});
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import type { App } from 'vue';
|
|
2
2
|
import { watch } from 'vue';
|
|
3
|
-
import {
|
|
3
|
+
import type { createAdminPermission } from '../components/provider/admin-permission';
|
|
4
4
|
|
|
5
5
|
type TCodes = string | string[];
|
|
6
6
|
|
|
7
7
|
const PLUGIN_NAME = 'has-permission';
|
|
8
8
|
const UNWATCH_NAME = `v-${PLUGIN_NAME}@unwatch`;
|
|
9
9
|
|
|
10
|
-
export function setupPermissionPlugin(app: App) {
|
|
10
|
+
export function setupPermissionPlugin(app: App, permission: ReturnType<typeof createAdminPermission>) {
|
|
11
11
|
app.directive<HTMLElement, TCodes>(PLUGIN_NAME, {
|
|
12
12
|
mounted(el, binding) {
|
|
13
|
-
console.log('🤡 / el:', el);
|
|
14
|
-
const permissionStore = adminPlugin.deps.usePermissionStore();
|
|
15
|
-
|
|
16
13
|
function updateVisibility() {
|
|
17
14
|
const codes = binding.value;
|
|
18
|
-
el.style.display =
|
|
15
|
+
el.style.display = permission.has(codes) ? '' : 'none';
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
(el as any)[UNWATCH_NAME] = watch(
|
|
22
|
-
() =>
|
|
19
|
+
() => permission.codes,
|
|
23
20
|
updateVisibility,
|
|
24
21
|
{ immediate: true },
|
|
25
22
|
);
|
|
@@ -32,9 +29,7 @@ export function setupPermissionPlugin(app: App) {
|
|
|
32
29
|
app.use({
|
|
33
30
|
install(app) {
|
|
34
31
|
app.config.globalProperties.$hasPermission = (codes: TCodes) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return permissionStore.hasPermission(codes);
|
|
32
|
+
return permission.has(codes);
|
|
38
33
|
};
|
|
39
34
|
},
|
|
40
35
|
});
|
package/admin/stores/index.ts
CHANGED
package/admin/styles/index.scss
CHANGED
|
@@ -74,9 +74,10 @@ function groupingForm(groupName: string, items: Record<string, FormItemProps>) {
|
|
|
74
74
|
* 格式化表单项组
|
|
75
75
|
* @param name - 组名
|
|
76
76
|
* @param items - 包含表单数据
|
|
77
|
+
* @param removeTempKey - 是否移除临时字段(建议删除前克隆 items)
|
|
77
78
|
* @returns 返回一个数组,其元素为分组后的表单项对象,如果没有找到匹配的组名,则返回 undefined
|
|
78
79
|
*/
|
|
79
|
-
function formatGroup(name: string, items: Record<string, any
|
|
80
|
+
function formatGroup(name: string, items: Record<string, any>, removeTempKey = false) {
|
|
80
81
|
const group: any[] = [];
|
|
81
82
|
|
|
82
83
|
for (const k in items) {
|
|
@@ -89,6 +90,14 @@ function formatGroup(name: string, items: Record<string, any>) {
|
|
|
89
90
|
group[params.index][params.fieldName] = items[k];
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
if (removeTempKey) {
|
|
94
|
+
const keyRE = new RegExp(`^${name}${GROUP_SEP}\\d+${GROUP_SEP}`);
|
|
95
|
+
Object.keys(items).forEach((key) => {
|
|
96
|
+
if (keyRE.test(key))
|
|
97
|
+
delete items[key];
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
return group?.length ? group.filter(g => !!g) : undefined;
|
|
93
102
|
}
|
|
94
103
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Modal as AntModal } from 'ant-design-vue';
|
|
2
|
-
import {
|
|
3
|
-
import { createVNode, defineComponent, isProxy, onMounted, reactive, toRef, toRefs } from 'vue';
|
|
2
|
+
import { tryOnBeforeUnmount } from '@vueuse/core';
|
|
3
|
+
import { createVNode, defineComponent, isProxy, onDeactivated, onMounted, reactive, toRef, toRefs } from 'vue';
|
|
4
4
|
import type { Component } from 'vue';
|
|
5
5
|
import type { ModalProps } from 'ant-design-vue';
|
|
6
6
|
import type { ComponentEmit, ComponentProps } from 'vue-component-type-helpers';
|
|
@@ -72,14 +72,25 @@ export function useAntdModal<Comp extends Component>(
|
|
|
72
72
|
newCompProps?: typeof compProps,
|
|
73
73
|
newAntdModalProps?: Omit<Partial<ModalProps>, 'open'>,
|
|
74
74
|
) => {
|
|
75
|
-
Object.assign(_modalProps, newAntdModalProps);
|
|
76
|
-
Object.assign(compProps, newCompProps);
|
|
77
|
-
|
|
78
75
|
if (_modalProps.open) {
|
|
79
76
|
// return Promise.reject(new Error('Modal is already open'));
|
|
80
77
|
return;
|
|
81
78
|
}
|
|
82
79
|
|
|
80
|
+
Object.assign(_modalProps, newAntdModalProps);
|
|
81
|
+
|
|
82
|
+
// eslint-disable-next-line no-lone-blocks
|
|
83
|
+
{
|
|
84
|
+
if (!(comp as any).props) {
|
|
85
|
+
Object.keys(compProps).forEach((key) => {
|
|
86
|
+
if (!['ref', 'onClose', 'onConfirm'].includes(key))
|
|
87
|
+
delete compProps[key];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Object.assign(compProps, newCompProps);
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
promiseResolvers = Promise.withResolvers();
|
|
84
95
|
_modalProps.open = true;
|
|
85
96
|
|
|
@@ -95,11 +106,11 @@ export function useAntdModal<Comp extends Component>(
|
|
|
95
106
|
};
|
|
96
107
|
|
|
97
108
|
const PresetComponent = defineComponent({
|
|
98
|
-
setup() {
|
|
109
|
+
setup(props) {
|
|
99
110
|
onMounted(() => _modalProps.opener?.(open as any));
|
|
100
111
|
|
|
101
112
|
return () => {
|
|
102
|
-
return createVNode(AntModal, _modalProps, {
|
|
113
|
+
return createVNode(AntModal, { ..._modalProps, ...props }, {
|
|
103
114
|
[modalSlotName]: () => createVNode(_comp.is, compProps as any),
|
|
104
115
|
});
|
|
105
116
|
};
|
|
@@ -114,7 +125,8 @@ export function useAntdModal<Comp extends Component>(
|
|
|
114
125
|
(compProps as any).onClose = close;
|
|
115
126
|
(compProps as any).onConfirm = confirm;
|
|
116
127
|
|
|
117
|
-
|
|
128
|
+
tryOnBeforeUnmount(_onClose);
|
|
129
|
+
onDeactivated(_onClose);
|
|
118
130
|
|
|
119
131
|
return {
|
|
120
132
|
PresetComponent,
|
|
@@ -78,8 +78,10 @@ export function useAntdTable<
|
|
|
78
78
|
showSizeChanger: true,
|
|
79
79
|
showQuickJumper: true,
|
|
80
80
|
pageSizeOptions,
|
|
81
|
+
size: 'default',
|
|
81
82
|
showTotal: total => `共 ${total} 条`,
|
|
82
83
|
},
|
|
84
|
+
size: 'small',
|
|
83
85
|
loading: isLoading.value,
|
|
84
86
|
scroll: { x: 'max-content' },
|
|
85
87
|
sticky: true,
|
|
@@ -20,6 +20,13 @@ export function useAntdTheme(mode: Ref<string>, config: Ref<Record<string, any>>
|
|
|
20
20
|
const token: ThemeConfig['token'] = {
|
|
21
21
|
...algorithm({ ...theme.defaultSeed, colorPrimary: _config.colors.primary.DEFAULT, colorInfo: '#17B2FF' }),
|
|
22
22
|
borderRadius: 4,
|
|
23
|
+
|
|
24
|
+
motionUnit: 0.05,
|
|
25
|
+
motionBase: 0,
|
|
26
|
+
motionDurationFast: "0.05s",
|
|
27
|
+
motionDurationMid: "0.1s",
|
|
28
|
+
motionDurationSlow: "0.15s",
|
|
29
|
+
|
|
23
30
|
screenXS,
|
|
24
31
|
screenXSMin: screenXS,
|
|
25
32
|
screenXSMax: screenSM - 0.1,
|
package/antd/index.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { useAntdForm } from './hooks/useAntdForm';
|
|
|
5
5
|
export { useAntdTheme } from './hooks/useAntdTheme';
|
|
6
6
|
export { createAntdModal } from './hooks/createAntdModal';
|
|
7
7
|
export { default as InputNumberRange } from './components/InputNumberRange.vue';
|
|
8
|
-
export type { SchemaConfig, ItemSchema } from './hooks/useAntdForm.ts';
|
|
8
|
+
export type { SchemaConfig, ItemSchema } from './hooks/useAntdForm.ts';
|
package/libs/bignumber.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default } from 'bignumber.js';
|
|
2
|
-
export { BigNumber } from 'bignumber.js';
|
|
2
|
+
export { type BigNumber } from 'bignumber.js';
|
package/libs/dayjs.ts
CHANGED
|
@@ -5,15 +5,25 @@ import weekday from 'dayjs/esm/plugin/weekday';
|
|
|
5
5
|
import localeData from 'dayjs/esm/plugin/localeData';
|
|
6
6
|
import utc from 'dayjs/esm/plugin/utc';
|
|
7
7
|
import tz from 'dayjs/esm/plugin/timezone';
|
|
8
|
+
import weekOfYear from 'dayjs/esm/plugin/weekOfYear';
|
|
9
|
+
import weekYear from 'dayjs/esm/plugin/weekYear';
|
|
10
|
+
import quarterOfYear from 'dayjs/esm/plugin/quarterOfYear';
|
|
11
|
+
import advancedFormat from 'dayjs/esm/plugin/advancedFormat';
|
|
12
|
+
import customParseFormat from 'dayjs/esm/plugin/customParseFormat';
|
|
8
13
|
import 'dayjs/esm/locale/zh';
|
|
9
14
|
import 'dayjs/esm/locale/en';
|
|
10
15
|
|
|
11
16
|
export type { Dayjs, PluginFunc, UnitType, UnitTypeLong, UnitTypeLongPlural, UnitTypeShort, QUnitType, ConfigType, ConfigTypeMap, OpUnitType, OptionType, ManipulateType } from 'dayjs';
|
|
12
17
|
export default dayjs;
|
|
13
18
|
|
|
14
|
-
dayjs.locale('zh');
|
|
15
19
|
dayjs.extend(relativeTime);
|
|
16
20
|
dayjs.extend(weekday);
|
|
17
21
|
dayjs.extend(localeData);
|
|
18
22
|
dayjs.extend(utc);
|
|
19
23
|
dayjs.extend(tz);
|
|
24
|
+
dayjs.extend(weekOfYear);
|
|
25
|
+
dayjs.extend(weekYear);
|
|
26
|
+
dayjs.extend(quarterOfYear);
|
|
27
|
+
dayjs.extend(advancedFormat);
|
|
28
|
+
dayjs.extend(customParseFormat);
|
|
29
|
+
dayjs.locale('zh');
|