@peng_kai/kit 0.2.5 → 0.2.6-base.0
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/admin/components/2fa/index.ts +31 -0
- package/admin/components/2fa/src/Setting2FA.vue +139 -0
- package/admin/components/2fa/src/Verify2FA.vue +89 -0
- package/admin/stores/createUsePageStore.ts +11 -5
- package/admin/stores/createUsePageTabStore.ts +18 -12
- package/admin/stores/index.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineAsyncComponent } from 'vue';
|
|
2
|
+
import { useAntdModal } from '../../../antd/hooks/useAntdModal';
|
|
3
|
+
|
|
4
|
+
export { default as Setting2FA, setDeps as setSetting2FADeps } from './src/Setting2FA.vue';
|
|
5
|
+
export { default as Verify2FA, setDeps as setVerify2FADeps } from './src/Verify2FA.vue';
|
|
6
|
+
|
|
7
|
+
export function createSetting2FAModal() {
|
|
8
|
+
return useAntdModal(
|
|
9
|
+
{ type: 'modal', is: defineAsyncComponent(() => import('./src/Setting2FA.vue')) },
|
|
10
|
+
{ width: 550, footer: null },
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createVerify2FAModal() {
|
|
15
|
+
const verify2FAModal = useAntdModal(
|
|
16
|
+
defineAsyncComponent(() => import('./src/Verify2FA.vue')),
|
|
17
|
+
{ title: '双重身份验证', width: 350, footer: null },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
PresetComponent: verify2FAModal.PresetComponent,
|
|
22
|
+
open: (requirePassword = false) => {
|
|
23
|
+
return new Promise<{ password: string, security_code: string }>((resolve, reject) => {
|
|
24
|
+
verify2FAModal.open(
|
|
25
|
+
{ onConfirm: resolve, requirePassword },
|
|
26
|
+
{ onCancel: reject },
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
|
|
3
|
+
import { computed, nextTick } from 'vue';
|
|
4
|
+
import { useAntdForm } from '../../../../antd/hooks/useAntdForm';
|
|
5
|
+
|
|
6
|
+
export { setDeps };
|
|
7
|
+
|
|
8
|
+
interface IDeps {
|
|
9
|
+
adminInfoApi: Api.Request
|
|
10
|
+
qrCodeApi: Api.Request
|
|
11
|
+
authSwitchApi: Api.Request
|
|
12
|
+
encryptPassword?: (pwd: string) => string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let _deps = {} as Required<IDeps>;
|
|
16
|
+
|
|
17
|
+
function setDeps(deps: IDeps) {
|
|
18
|
+
_deps = {
|
|
19
|
+
encryptPassword: pwd => pwd,
|
|
20
|
+
...deps,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
const emits = defineEmits<{
|
|
27
|
+
close: []
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const queryClient = useQueryClient();
|
|
31
|
+
const userInfoQry = useQuery({
|
|
32
|
+
queryKey: [_deps.adminInfoApi.id],
|
|
33
|
+
queryFn: () => _deps.adminInfoApi(undefined),
|
|
34
|
+
});
|
|
35
|
+
const enableAuth = computed(() => userInfoQry.data.value?.enable_authenticator);
|
|
36
|
+
const authQRcodeQry = useQuery({
|
|
37
|
+
enabled: computed(() => enableAuth.value === 0),
|
|
38
|
+
queryKey: [_deps.qrCodeApi.id],
|
|
39
|
+
queryFn: () => _deps.qrCodeApi(undefined),
|
|
40
|
+
gcTime: 0,
|
|
41
|
+
});
|
|
42
|
+
const switch2FAMut = useMutation({
|
|
43
|
+
mutationKey: [_deps.authSwitchApi.id],
|
|
44
|
+
mutationFn: _deps.authSwitchApi.setDefaultConfig({ successMessage: true }),
|
|
45
|
+
onSuccess() {
|
|
46
|
+
queryClient.invalidateQueries({
|
|
47
|
+
queryKey: [_deps.adminInfoApi.id],
|
|
48
|
+
exact: false,
|
|
49
|
+
});
|
|
50
|
+
form.$form.resetFields?.();
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const title = computed(() => {
|
|
54
|
+
const _enableAuth = enableAuth.value;
|
|
55
|
+
const title = '双重认证';
|
|
56
|
+
|
|
57
|
+
return _enableAuth === 1 ? `解绑${title}` : _enableAuth === 0 ? `绑定${title}` : title;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const form = useAntdForm({
|
|
61
|
+
security_code: {
|
|
62
|
+
value: '',
|
|
63
|
+
rules: [
|
|
64
|
+
{ required: true, type: 'string', len: 6, message: '请输入6位数字的安全码' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
password: {
|
|
68
|
+
value: '',
|
|
69
|
+
rules: [
|
|
70
|
+
{ required: true },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
async function onSubmit() {
|
|
76
|
+
await nextTick();
|
|
77
|
+
const state = await form.$form.validate?.()?.catch(() => {});
|
|
78
|
+
|
|
79
|
+
if (!state)
|
|
80
|
+
return;
|
|
81
|
+
|
|
82
|
+
const _enableAuth = enableAuth.value === 1 ? 0 : 1;
|
|
83
|
+
await switch2FAMut.mutateAsync({ requestBody: {
|
|
84
|
+
password: _deps.encryptPassword(state.password),
|
|
85
|
+
security_code: state.security_code,
|
|
86
|
+
enable_authenticator: _enableAuth,
|
|
87
|
+
} });
|
|
88
|
+
|
|
89
|
+
if (_enableAuth)
|
|
90
|
+
emits('close');
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<ACard class="antd-cover__card-in-modal bg-$antd-colorBgElevated" :loading="userInfoQry.isLoading.value" :title="title">
|
|
96
|
+
<template #extra>
|
|
97
|
+
<div class="close-btn" @click="emits('close')" />
|
|
98
|
+
</template>
|
|
99
|
+
<div class="flex">
|
|
100
|
+
<AQrcode
|
|
101
|
+
v-if="enableAuth === 0"
|
|
102
|
+
:status="authQRcodeQry.data.value ? 'active' : 'loading'"
|
|
103
|
+
:size="207"
|
|
104
|
+
:value="authQRcodeQry.data.value?.qr_code ?? ''"
|
|
105
|
+
style="flex: none"
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
<AForm v-bind="form.props" class="ml4 flex-1" layout="vertical" @submit="onSubmit()">
|
|
109
|
+
<AFormItem v-bind="form.itemProps.password" label="账户密码">
|
|
110
|
+
<AInputPassword v-model:value="form.state.password" size="large">
|
|
111
|
+
<template #prefix>
|
|
112
|
+
<i class="i-ant-design:lock-outlined" />
|
|
113
|
+
</template>
|
|
114
|
+
</AInputPassword>
|
|
115
|
+
</AFormItem>
|
|
116
|
+
<AFormItem v-bind="form.itemProps.security_code" label="安全码">
|
|
117
|
+
<AInput v-model:value="form.state.security_code" size="large">
|
|
118
|
+
<template #prefix>
|
|
119
|
+
<i class="i-ant-design:safety-outlined" />
|
|
120
|
+
</template>
|
|
121
|
+
</AInput>
|
|
122
|
+
<template #extra>
|
|
123
|
+
<span v-if="enableAuth">请使用身份验证器APP,获取6位数字安全码</span>
|
|
124
|
+
<span v-else>请使用身份验证器APP扫描左侧二维码,获取6位数字安全码</span>
|
|
125
|
+
</template>
|
|
126
|
+
</AFormItem>
|
|
127
|
+
<button class="hidden" type="submit">
|
|
128
|
+
提交
|
|
129
|
+
</button>
|
|
130
|
+
</AForm>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<template v-if="userInfoQry.isSuccess.value" #actions>
|
|
134
|
+
<AButton type="primary" @click="onSubmit">
|
|
135
|
+
{{ enableAuth ? '解绑' : '绑定' }}
|
|
136
|
+
</AButton>
|
|
137
|
+
</template>
|
|
138
|
+
</ACard>
|
|
139
|
+
</template>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { nextTick } from 'vue';
|
|
3
|
+
import { hasToken } from '../../../../utils';
|
|
4
|
+
import { useAntdForm } from '../../../../antd/hooks/useAntdForm';
|
|
5
|
+
|
|
6
|
+
export { setDeps };
|
|
7
|
+
|
|
8
|
+
interface IDeps {
|
|
9
|
+
setting2FA: any
|
|
10
|
+
encryptPassword?: (pwd: string) => string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let _deps = {} as Required<IDeps>;
|
|
14
|
+
|
|
15
|
+
function setDeps(deps: IDeps) {
|
|
16
|
+
_deps = {
|
|
17
|
+
encryptPassword: pwd => pwd,
|
|
18
|
+
...deps,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
const props = withDefaults(defineProps<{
|
|
25
|
+
requirePassword?: boolean
|
|
26
|
+
}>(), {
|
|
27
|
+
requirePassword: false,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const emits = defineEmits<{
|
|
31
|
+
confirm: [{ password: string, security_code: string }]
|
|
32
|
+
close: []
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
const form = useAntdForm({
|
|
36
|
+
password: {
|
|
37
|
+
value: '',
|
|
38
|
+
rules: [
|
|
39
|
+
{ required: true },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
security_code: {
|
|
43
|
+
value: '',
|
|
44
|
+
rules: [
|
|
45
|
+
{ required: true, type: 'string', len: 6, message: '请输入6位数字的安全码' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
async function onSubmit() {
|
|
51
|
+
await nextTick();
|
|
52
|
+
const state = await form.$form.validate?.().catch(err => console.log(err));
|
|
53
|
+
|
|
54
|
+
if (state) {
|
|
55
|
+
emits('confirm', { ...form.state, password: _deps.encryptPassword(state.password) });
|
|
56
|
+
emits('close');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<div>
|
|
63
|
+
<AForm v-bind="form.props" @submit="onSubmit()">
|
|
64
|
+
<AFormItem v-if="props.requirePassword" v-bind="form.itemProps.password">
|
|
65
|
+
<AInputPassword v-model:value="form.state.password" size="large" placeholder="请输入账户密码">
|
|
66
|
+
<template #prefix>
|
|
67
|
+
<i class="i-ant-design:lock-outlined" />
|
|
68
|
+
</template>
|
|
69
|
+
</AInputPassword>
|
|
70
|
+
</AFormItem>
|
|
71
|
+
<AFormItem v-bind="form.itemProps.security_code">
|
|
72
|
+
<AInput v-model:value="form.state.security_code" size="large" placeholder="请输入安全码">
|
|
73
|
+
<template #prefix>
|
|
74
|
+
<i class="i-ant-design:safety-outlined" />
|
|
75
|
+
</template>
|
|
76
|
+
</AInput>
|
|
77
|
+
<template #extra>
|
|
78
|
+
<span>请输入身份验证器上显示的6位数字安全码</span>
|
|
79
|
+
</template>
|
|
80
|
+
</AFormItem>
|
|
81
|
+
<AButton type="primary" size="large" block htmlType="submit">
|
|
82
|
+
验证
|
|
83
|
+
</AButton>
|
|
84
|
+
<AButton v-if="hasToken()" class="mt2 -mb2" type="link" block @click="_deps.setting2FA?.open?.()">
|
|
85
|
+
没有安全码?去绑定
|
|
86
|
+
</AButton>
|
|
87
|
+
</AForm>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
@@ -2,15 +2,14 @@ import { usePrevious } from '@vueuse/core';
|
|
|
2
2
|
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
|
3
3
|
import { defineStore } from 'pinia';
|
|
4
4
|
import { computed, reactive, readonly, ref, shallowRef, watch } from 'vue';
|
|
5
|
-
import type { VNode } from 'vue';
|
|
5
|
+
import type { DeepReadonly, VNode } from 'vue';
|
|
6
6
|
import { getTitle } from '../defines/route';
|
|
7
7
|
import { adminPlugin } from '../adminPlugin';
|
|
8
8
|
import type { TMenu } from './createUseMenuStore';
|
|
9
9
|
|
|
10
10
|
export { createUsePageStore };
|
|
11
|
-
export type {
|
|
11
|
+
export type { TUsePageStore, IBreadcrumb, IPageState };
|
|
12
12
|
|
|
13
|
-
type TPageState = ReturnType<typeof getPageState>;
|
|
14
13
|
type TUsePageStore = ReturnType<typeof createUsePageStore>;
|
|
15
14
|
|
|
16
15
|
interface IBreadcrumb {
|
|
@@ -29,7 +28,7 @@ function storeSetup() {
|
|
|
29
28
|
const currentPageNode = shallowRef<VNode>();
|
|
30
29
|
const currentPageKey = ref('');
|
|
31
30
|
const pageCacheList = reactive<string[]>([]);
|
|
32
|
-
const pageStateMap = new Map<string,
|
|
31
|
+
const pageStateMap = new Map<string, IPageState>();
|
|
33
32
|
const currentPageState = computed(() => pageStateMap.get(currentPageKey.value));
|
|
34
33
|
const previousPageState = usePrevious(currentPageState);
|
|
35
34
|
|
|
@@ -103,7 +102,14 @@ function storeSetup() {
|
|
|
103
102
|
};
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
|
|
105
|
+
interface IPageState {
|
|
106
|
+
title: string
|
|
107
|
+
icon?: VNode
|
|
108
|
+
breadcrumbs: IBreadcrumb[]
|
|
109
|
+
readonly route: DeepReadonly<RouteLocationNormalizedLoaded>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getPageState(route: RouteLocationNormalizedLoaded): IPageState {
|
|
107
113
|
const menuStore = adminPlugin.deps.useMenuStore()!;
|
|
108
114
|
const appName = adminPlugin.meta.appName;
|
|
109
115
|
const menuPath = menuStore.getMenuPath(route.name as string);
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { defineStore, storeToRefs } from 'pinia';
|
|
2
|
-
import { computed, ref, watch } from 'vue';
|
|
2
|
+
import { type ComputedRef, type Ref, computed, ref, watch } from 'vue';
|
|
3
|
+
import type { Simplify } from 'type-fest';
|
|
3
4
|
import { adminPlugin } from '../adminPlugin';
|
|
5
|
+
import type { IPageState } from './createUsePageStore';
|
|
4
6
|
|
|
5
7
|
export { createUsePageTabStore };
|
|
6
8
|
export type { TUsePageTabStore };
|
|
7
9
|
|
|
8
10
|
type TUsePageTabStore = ReturnType<typeof createUsePageTabStore>;
|
|
11
|
+
type TTab = Simplify<Pick<IPageState, 'title' | 'icon'> & { key: string }>;
|
|
9
12
|
|
|
10
13
|
function createUsePageTabStore() {
|
|
11
14
|
return defineStore('appPageTab', () => storeSetup());
|
|
@@ -13,14 +16,16 @@ function createUsePageTabStore() {
|
|
|
13
16
|
|
|
14
17
|
function storeSetup() {
|
|
15
18
|
const pageStore = adminPlugin.deps.usePageStore!();
|
|
16
|
-
const
|
|
19
|
+
const pageRefs = storeToRefs(pageStore);
|
|
20
|
+
const activeTab: Ref<string> = pageRefs.currentPageKey;
|
|
17
21
|
const tabKeyList = ref<string[]>([]);
|
|
18
|
-
const tabList = computed(() => tabKeyList.value.map((key) => {
|
|
22
|
+
const tabList: ComputedRef<TTab[]> = computed(() => tabKeyList.value.map((key) => {
|
|
19
23
|
const state = pageStore.pageStateMap.get(key);
|
|
20
24
|
|
|
21
25
|
return state ? { key, title: state.title, icon: state.icon } : undefined!;
|
|
22
26
|
}).filter(tab => !!tab));
|
|
23
27
|
|
|
28
|
+
const openTab: (key: string) => void = pageStore.openPage;
|
|
24
29
|
const closeTab = (key: string) => {
|
|
25
30
|
const i = tabKeyList.value.indexOf(key);
|
|
26
31
|
|
|
@@ -30,14 +35,15 @@ function storeSetup() {
|
|
|
30
35
|
}
|
|
31
36
|
};
|
|
32
37
|
|
|
33
|
-
watch(
|
|
34
|
-
()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
tabKeyList.value.push(key);
|
|
38
|
-
},
|
|
39
|
-
{ immediate: true },
|
|
40
|
-
);
|
|
38
|
+
watch(activeTab, (key) => {
|
|
39
|
+
if (!tabKeyList.value.includes(key))
|
|
40
|
+
tabKeyList.value.push(key);
|
|
41
|
+
}, { immediate: true });
|
|
41
42
|
|
|
42
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
activeTab,
|
|
45
|
+
tabList,
|
|
46
|
+
closeTab,
|
|
47
|
+
openTab,
|
|
48
|
+
};
|
|
43
49
|
}
|
package/admin/stores/index.ts
CHANGED
|
@@ -3,6 +3,6 @@ export { createUseMenuStore } from './createUseMenuStore';
|
|
|
3
3
|
export { createUsePageTabStore } from './createUsePageTabStore';
|
|
4
4
|
export { createUsePermissionStore } from './createUsePermissionStore';
|
|
5
5
|
|
|
6
|
-
export type {
|
|
6
|
+
export type { IPageState, TUsePageStore, IBreadcrumb } from './createUsePageStore';
|
|
7
7
|
export type { TUseMenuStore } from './createUseMenuStore';
|
|
8
8
|
export type { TUsePageTabStore } from './createUsePageTabStore';
|