@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import dayjs, { type Dayjs } from 'dayjs/esm';
|
|
2
|
+
import { tryOnScopeDispose } from '@vueuse/core';
|
|
3
|
+
import { computed, reactive, watch, ref, type Ref } from 'vue';
|
|
4
|
+
import type { Simplify } from 'type-fest';
|
|
5
|
+
|
|
6
|
+
export function getTtaTimeZone() {
|
|
7
|
+
const win = window as any;
|
|
8
|
+
const key = '__APP_TZ__';
|
|
9
|
+
|
|
10
|
+
return (win[key] || (win[key] = ref(dayjs.tz.guess()))) as Ref<string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const onTtaTimeZone = (() => {
|
|
14
|
+
const ttaTz = getTtaTimeZone();
|
|
15
|
+
const cbList: any[] = [];
|
|
16
|
+
|
|
17
|
+
watch(ttaTz, (newTz, oldTz) => {
|
|
18
|
+
newTz !== oldTz && cbList.forEach((cb) => cb(newTz));
|
|
19
|
+
}, { flush: 'post' });
|
|
20
|
+
|
|
21
|
+
return (cb: (tz: string) => void) => {
|
|
22
|
+
cbList.push(cb);
|
|
23
|
+
|
|
24
|
+
const unbind = () => {
|
|
25
|
+
const index = cbList.indexOf(cb);
|
|
26
|
+
if (index !== -1) {
|
|
27
|
+
cbList.splice(index, 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
tryOnScopeDispose(unbind);
|
|
32
|
+
|
|
33
|
+
return unbind;
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
type OfDayType = Dayjs | undefined | null;
|
|
38
|
+
type UnitType = dayjs.QUnitType | dayjs.OpUnitType;
|
|
39
|
+
export const dayOf = {
|
|
40
|
+
rangeTs<KS extends readonly [string, string] = ['start_time', 'end_time']>(
|
|
41
|
+
unit: UnitType,
|
|
42
|
+
day: Array<OfDayType> | undefined | null,
|
|
43
|
+
keys?: KS
|
|
44
|
+
): KS extends readonly [string, string]
|
|
45
|
+
? Simplify<{ [K in KS[0]]: number } & { [K in KS[1]]: number }>
|
|
46
|
+
: { start_time: number; end_time: number } {
|
|
47
|
+
if (!day || !day[0] || !day[1])
|
|
48
|
+
return undefined as any;
|
|
49
|
+
|
|
50
|
+
const tz = getTtaTimeZone().value;
|
|
51
|
+
const _keys = (Array.isArray(keys) && keys?.length > 1) ? keys : ['start_time', 'end_time'];
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
[_keys[0]]: dayjs(day[0].valueOf()).tz(tz).startOf(unit).valueOf(),
|
|
55
|
+
[_keys[1]]: dayjs(day[1].valueOf()).tz(tz).endOf(unit).valueOf(),
|
|
56
|
+
} as any;
|
|
57
|
+
},
|
|
58
|
+
start(unit: UnitType, day: OfDayType) {
|
|
59
|
+
if (!day)
|
|
60
|
+
return undefined;
|
|
61
|
+
|
|
62
|
+
const tz = getTtaTimeZone().value;
|
|
63
|
+
return dayjs(day.valueOf()).tz(tz).startOf(unit);
|
|
64
|
+
},
|
|
65
|
+
startTs(unit: UnitType, day: OfDayType) {
|
|
66
|
+
this.start(unit, day);
|
|
67
|
+
},
|
|
68
|
+
end(unit: UnitType, day: OfDayType) {
|
|
69
|
+
if (!day)
|
|
70
|
+
return undefined;
|
|
71
|
+
|
|
72
|
+
const tz = getTtaTimeZone().value;
|
|
73
|
+
return dayjs(day.valueOf()).tz(tz).endOf(unit);
|
|
74
|
+
},
|
|
75
|
+
endTs(unit: UnitType, day: OfDayType) {
|
|
76
|
+
this.end(unit, day);
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 为 DatePicker 组件提供响应式时区支持,当时区更新时,自动调整时间戳 */
|
|
81
|
+
export function useDatePickerPropsForTz(
|
|
82
|
+
value: Ref<Dayjs | undefined>,
|
|
83
|
+
props: {
|
|
84
|
+
createPresets?: (day: Dayjs) => { label: string; value: Dayjs }[];
|
|
85
|
+
} = {},
|
|
86
|
+
tz?: Ref<string>,
|
|
87
|
+
) {
|
|
88
|
+
// 默认值
|
|
89
|
+
props.createPresets ??= () => {
|
|
90
|
+
return [
|
|
91
|
+
// { label: '昨天', value: day.subtract(1, 'day').startOf('day') },
|
|
92
|
+
// { label: '上周', value: day.subtract(1, 'week').startOf('week') },
|
|
93
|
+
// { label: '上个月', value: day.subtract(1, 'month').startOf('month') },
|
|
94
|
+
];
|
|
95
|
+
};
|
|
96
|
+
const _tz = tz || getTtaTimeZone();
|
|
97
|
+
const nowDay = computed(() => dayjs().tz(_tz.value));
|
|
98
|
+
const _value = computed(() => value.value ? dayjs(value.value.valueOf()).tz(_tz.value) : undefined);
|
|
99
|
+
|
|
100
|
+
watch(_tz, (newV, oldV) => {
|
|
101
|
+
if (!value.value) return;
|
|
102
|
+
|
|
103
|
+
const newTzOffset = dayjs().tz(newV).utcOffset();
|
|
104
|
+
const oldTzOffset = dayjs().tz(oldV).utcOffset();
|
|
105
|
+
value.value = dayjs(value.value.valueOf() + (oldTzOffset - newTzOffset) * 60 * 1000);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return reactive({
|
|
109
|
+
'value': _value,
|
|
110
|
+
'onUpdate:value': (value: any) => value.value = dayjs(value).valueOf(),
|
|
111
|
+
'presets': computed(() => props.createPresets!(nowDay.value)),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function useRangePickerPropsForTz(
|
|
116
|
+
value: Ref<[string, string] | [Dayjs, Dayjs] | undefined>,
|
|
117
|
+
props: {
|
|
118
|
+
createPresets?: (day: Dayjs) => { label: string; value: Dayjs[] }[];
|
|
119
|
+
} = {},
|
|
120
|
+
tz?: Ref<string>,
|
|
121
|
+
) {
|
|
122
|
+
const _tz = tz || getTtaTimeZone();
|
|
123
|
+
|
|
124
|
+
// 默认值
|
|
125
|
+
props.createPresets ??= (day: Dayjs) => {
|
|
126
|
+
return [
|
|
127
|
+
{ label: '今天', value: [day.startOf('day'), day.endOf('day')] },
|
|
128
|
+
{ label: '昨天', value: [day.subtract(1, 'day').startOf('day'), day.subtract(1, 'day').endOf('day')] },
|
|
129
|
+
{ label: '前一天', value: [day.subtract(1, 'day').startOf('day'), day.subtract(1, 'day').endOf('day')] },
|
|
130
|
+
{ label: '后一天', value: [day.add(1, 'day').startOf('day'), day.add(1, 'day').endOf('day')] },
|
|
131
|
+
{ label: '本周', value: [day.startOf('week'), day.endOf('week')] },
|
|
132
|
+
{ label: '上周', value: [day.subtract(1, 'week').startOf('week'), day.subtract(1, 'week').endOf('week')] },
|
|
133
|
+
{ label: '本月', value: [day.startOf('month'), day.endOf('month')] },
|
|
134
|
+
{ label: '上个月', value: [day.subtract(1, 'month').startOf('month'), day.subtract(1, 'month').endOf('month')] },
|
|
135
|
+
{ label: '上上月', value: [day.subtract(2, 'month').startOf('month'), day.subtract(2, 'month').endOf('month')] },
|
|
136
|
+
{ label: '近7天', value: [day.subtract(6, 'day').startOf('day'), day.endOf('day')] },
|
|
137
|
+
{ label: '近30天', value: [day.subtract(29, 'day').startOf('day'), day.endOf('day')] },
|
|
138
|
+
];
|
|
139
|
+
};
|
|
140
|
+
const nowDay = computed(() => dayjs().tz(_tz.value));
|
|
141
|
+
const _value = computed(() => {
|
|
142
|
+
const v = value.value;
|
|
143
|
+
if (v && v[0] && v[1]) {
|
|
144
|
+
return [
|
|
145
|
+
dayjs(v[0].valueOf()).tz(_tz.value),
|
|
146
|
+
dayjs(v[1].valueOf()).tz(_tz.value),
|
|
147
|
+
] as [Dayjs, Dayjs];
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
watch(_tz, (newV, oldV) => {
|
|
153
|
+
const v = value.value;
|
|
154
|
+
if (!v || !v[0] || !v[1]) return;
|
|
155
|
+
|
|
156
|
+
const newTzOffset = dayjs().tz(newV).utcOffset();
|
|
157
|
+
const oldTzOffset = dayjs().tz(oldV).utcOffset();
|
|
158
|
+
value.value = [
|
|
159
|
+
dayjs(v[0]).add((oldTzOffset - newTzOffset) * 60 * 1000),
|
|
160
|
+
dayjs(v[1]).add((oldTzOffset - newTzOffset) * 60 * 1000),
|
|
161
|
+
]
|
|
162
|
+
console.log('🤡 / value.value:', value.value[0].valueOf())
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return reactive({
|
|
166
|
+
'value': _value,
|
|
167
|
+
'onUpdate:value': (newVal: any) => value.value = newVal,
|
|
168
|
+
'presets': computed(() => props.createPresets!(nowDay.value)),
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/** 为 RangePicker 组件提供响应式时区支持,当时区更新时,自动调整时间戳 */
|
|
173
|
+
export function useRangePickerPropsForTz_old(
|
|
174
|
+
ts: [Ref<number | undefined>, Ref<number | undefined>],
|
|
175
|
+
props: {
|
|
176
|
+
createPresets?: (day: Dayjs) => { label: string; value: Dayjs[] }[];
|
|
177
|
+
} = {},
|
|
178
|
+
tz?: Ref<string>,
|
|
179
|
+
) {
|
|
180
|
+
// 默认值
|
|
181
|
+
props.createPresets ??= (day: Dayjs) => {
|
|
182
|
+
return [
|
|
183
|
+
{ label: '今天', value: [day.startOf('day'), day.endOf('day')] },
|
|
184
|
+
{ label: '昨天', value: [day.subtract(1, 'day').startOf('day'), day.subtract(1, 'day').endOf('day')] },
|
|
185
|
+
{ label: '前一天', value: [day.subtract(1, 'day').startOf('day'), day.subtract(1, 'day').endOf('day')] },
|
|
186
|
+
{ label: '后一天', value: [day.add(1, 'day').startOf('day'), day.add(1, 'day').endOf('day')] },
|
|
187
|
+
{ label: '本周', value: [day.startOf('week'), day.endOf('week')] },
|
|
188
|
+
{ label: '上周', value: [day.subtract(1, 'week').startOf('week'), day.subtract(1, 'week').endOf('week')] },
|
|
189
|
+
{ label: '本月', value: [day.startOf('month'), day.endOf('month')] },
|
|
190
|
+
{ label: '上个月', value: [day.subtract(1, 'month').startOf('month'), day.subtract(1, 'month').endOf('month')] },
|
|
191
|
+
{ label: '上上月', value: [day.subtract(2, 'month').startOf('month'), day.subtract(2, 'month').endOf('month')] },
|
|
192
|
+
{ label: '近7天', value: [day.subtract(6, 'day').startOf('day'), day.endOf('day')] },
|
|
193
|
+
{ label: '近30天', value: [day.subtract(29, 'day').startOf('day'), day.endOf('day')] },
|
|
194
|
+
];
|
|
195
|
+
};
|
|
196
|
+
const _tz = tz || getTtaTimeZone();
|
|
197
|
+
const nowDay = computed(() => dayjs().tz(_tz.value));
|
|
198
|
+
const inputDay = computed(() => {
|
|
199
|
+
console.log('🤡 / ts:', ts);
|
|
200
|
+
if (ts[0].value && ts[1].value)
|
|
201
|
+
return [dayjs(ts[0].value).tz(_tz.value), dayjs(ts[1].value).tz(_tz.value)] as [Dayjs, Dayjs];
|
|
202
|
+
return undefined;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
watch(_tz, (newV, oldV) => {
|
|
206
|
+
if (!ts[0].value || !ts[1].value) return;
|
|
207
|
+
|
|
208
|
+
const newTzOffset = dayjs().tz(newV).utcOffset();
|
|
209
|
+
const oldTzOffset = dayjs().tz(oldV).utcOffset();
|
|
210
|
+
ts[0].value = ts[0].value + (oldTzOffset - newTzOffset) * 60 * 1000;
|
|
211
|
+
ts[1].value = ts[1].value + (oldTzOffset - newTzOffset) * 60 * 1000;
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
return reactive({
|
|
215
|
+
'value': inputDay,
|
|
216
|
+
'onUpdate:value': (value: any) => {
|
|
217
|
+
console.log('🤡 / value:', value);
|
|
218
|
+
ts[0].value = dayjs(value[0]).valueOf();
|
|
219
|
+
ts[1].value = dayjs(value[1]).valueOf();
|
|
220
|
+
},
|
|
221
|
+
'presets': computed(() => props.createPresets!(nowDay.value)),
|
|
222
|
+
});
|
|
223
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as TimeFieldSelectForLabel } from './TimeFieldSelectForLabel.vue';
|
|
2
|
+
export { default as TtaTimeZone } from './TtaTimeZone.vue';
|
|
3
|
+
export { default as PeriodPicker } from './PeriodPicker.vue';
|
|
4
|
+
export { useDatePickerPropsForTz, useRangePickerPropsForTz, getTtaTimeZone, onTtaTimeZone, dayOf } from './helpers';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { Button
|
|
2
|
+
import { Button, Menu, MenuItem, Dropdown } from 'ant-design-vue';
|
|
3
3
|
import { computed } from 'vue';
|
|
4
|
+
import { onTtaTimeZone } from '../../date';
|
|
4
5
|
|
|
5
6
|
const props = withDefaults(defineProps<{
|
|
6
7
|
loading?: boolean
|
|
@@ -38,17 +39,33 @@ function reset() {
|
|
|
38
39
|
props.filterParams?.update?.(true);
|
|
39
40
|
emits('reset');
|
|
40
41
|
}
|
|
42
|
+
|
|
43
|
+
onTtaTimeZone(() => {
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
props.filterParams?.update?.(true);
|
|
46
|
+
}, 100)
|
|
47
|
+
});
|
|
41
48
|
</script>
|
|
42
49
|
|
|
43
50
|
<template>
|
|
44
51
|
<div class="btns flex-none flex w-min ml-auto gap-2">
|
|
45
52
|
<slot :loading="loading" :filter="filter" :reset="reset">
|
|
46
|
-
<
|
|
53
|
+
<Button class="filter-btn" type="primary" htmlType="submit" :loading="loading" @click="filter()">
|
|
47
54
|
查询
|
|
48
|
-
</
|
|
49
|
-
<
|
|
55
|
+
</Button>
|
|
56
|
+
<Button :disabled="loading" @click="reset()">
|
|
50
57
|
重置
|
|
51
|
-
</
|
|
58
|
+
</Button>
|
|
59
|
+
<Dropdown trigger="click" placement="bottomRight">
|
|
60
|
+
<Button class="px-1 w-8">
|
|
61
|
+
<i class="i-si:more-square-horiz-fill scale-130 transform-origin-bottom-center" />
|
|
62
|
+
</Button>
|
|
63
|
+
<template #overlay>
|
|
64
|
+
<Menu>
|
|
65
|
+
<MenuItem>设置表格</MenuItem>
|
|
66
|
+
</Menu>
|
|
67
|
+
</template>
|
|
68
|
+
</Dropdown>
|
|
52
69
|
</slot>
|
|
53
70
|
</div>
|
|
54
71
|
</template>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Router } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
export const admin = {};
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
router: Router
|
|
10
|
+
}>();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div>
|
|
15
|
+
<slot />
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { reactive, readonly, ref, watch } from 'vue';
|
|
2
|
+
import type { AsyncReturnType } from 'type-fest';
|
|
3
|
+
|
|
4
|
+
type TRole = () => Promise<Record<string, boolean> | null | undefined>;
|
|
5
|
+
interface TRoles {
|
|
6
|
+
main: TRole
|
|
7
|
+
[key: string]: TRole
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createAdminPermission(roles: TRoles) {
|
|
11
|
+
const role = ref('main');
|
|
12
|
+
const permissionCodes = ref<AsyncReturnType<TRole>>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 刷新权限
|
|
16
|
+
*/
|
|
17
|
+
const refreshPermission = async () => {
|
|
18
|
+
permissionCodes.value = await roles?.[role.value]?.();
|
|
19
|
+
|
|
20
|
+
return permissionCodes.value;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 是否满足权限要求
|
|
25
|
+
* @param codes 权限 code
|
|
26
|
+
*/
|
|
27
|
+
const hasPermission = (codes: string | string[]) => {
|
|
28
|
+
const _codes = Array.isArray(codes) ? codes : [codes];
|
|
29
|
+
const _permissionCodes = permissionCodes.value;
|
|
30
|
+
|
|
31
|
+
if (_permissionCodes === null)
|
|
32
|
+
return false;
|
|
33
|
+
else if (_permissionCodes === undefined)
|
|
34
|
+
// undefined 则表示没有权限要求,返回 true
|
|
35
|
+
return true;
|
|
36
|
+
else
|
|
37
|
+
return _codes.every(code => !!_permissionCodes[code]);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
watch(role, refreshPermission, { immediate: true });
|
|
41
|
+
|
|
42
|
+
return reactive({
|
|
43
|
+
codes: readonly(permissionCodes),
|
|
44
|
+
role,
|
|
45
|
+
has: hasPermission,
|
|
46
|
+
refresh: refreshPermission,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { computed, defineComponent, h, reactive, ref, shallowRef, toRef, watch } from 'vue';
|
|
2
|
+
import type { MaybeRef, PropType, Ref, ShallowRef, UnwrapNestedRefs, VNode } from 'vue';
|
|
3
|
+
import type { RouteLocationNormalizedGeneric, RouteLocationNormalizedLoaded, Router } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
interface TOptions {
|
|
6
|
+
excludeTabs?: Array<string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createAdminRouter(options: TOptions) {
|
|
10
|
+
const router = shallowRef<Router | null>(null);
|
|
11
|
+
const { menus, breadcrumbs, menuPath, addMenu, generateMenus } = useMenu(router);
|
|
12
|
+
const { tabs, currentTab: tab, tabsCached, addTab, delTab, getTab } = useTab(router, options);
|
|
13
|
+
|
|
14
|
+
function setRouter(newRouter: Router) {
|
|
15
|
+
router.value = newRouter;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return reactive({
|
|
19
|
+
menus,
|
|
20
|
+
menuPath,
|
|
21
|
+
breadcrumbs,
|
|
22
|
+
tabs,
|
|
23
|
+
tabsCached,
|
|
24
|
+
tab,
|
|
25
|
+
setRouter,
|
|
26
|
+
addMenu,
|
|
27
|
+
addTab,
|
|
28
|
+
delTab,
|
|
29
|
+
getTab,
|
|
30
|
+
generateMenus,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const PageRefresh = defineComponent({
|
|
35
|
+
props: {
|
|
36
|
+
router: { type: Object as PropType<Router>, required: true },
|
|
37
|
+
originalMainComp: { type: Object as PropType<any>, required: true },
|
|
38
|
+
},
|
|
39
|
+
mounted() {
|
|
40
|
+
const { router } = this.$props;
|
|
41
|
+
const fullPath = router.currentRoute.value.fullPath;
|
|
42
|
+
router.replace({ ...router.resolve(fullPath), force: true });
|
|
43
|
+
},
|
|
44
|
+
setup() {
|
|
45
|
+
return () => null;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function useTab(router: ShallowRef<Router | null>, options: TOptions = {}) {
|
|
50
|
+
const tabs = ref<TTab[]>([]);
|
|
51
|
+
const currentTab = computed(() => [...tabs.value].sort((a, b) => b.lastTime - a.lastTime)[0]);
|
|
52
|
+
const tabsCached = ref<string[]>([]);
|
|
53
|
+
|
|
54
|
+
function addTab(tab: TTabConfig) {
|
|
55
|
+
tabs.value.push(reactive(tab));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function delTab(path: string) {
|
|
59
|
+
if (path === currentTab.value?.path) {
|
|
60
|
+
const index = tabs.value.findIndex(tab => tab.path === path);
|
|
61
|
+
const nextTab = tabs.value[index + 1] || tabs.value[index - 1];
|
|
62
|
+
|
|
63
|
+
if (nextTab) {
|
|
64
|
+
tabs.value.splice(index, 1);
|
|
65
|
+
router.value?.replace(nextTab.path);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const index = tabs.value.findIndex(tab => tab.path === path);
|
|
70
|
+
if (index >= 0)
|
|
71
|
+
tabs.value.splice(index, 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getTab(path: string) {
|
|
76
|
+
return tabs.value.find(tab => tab.path === path);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function switchTab(to: RouteLocationNormalizedGeneric, from: RouteLocationNormalizedGeneric) {
|
|
80
|
+
const toTab = getTab(to.fullPath);
|
|
81
|
+
const fromTab = getTab(from.fullPath);
|
|
82
|
+
const lastTime = Date.now();
|
|
83
|
+
const isReplaced = history.state?.replaced;
|
|
84
|
+
const tabType = Number(history.state?.tabType ?? 1); // 0:不新增 tab, 1:新增 tab, 2: 固定 tab(不可关闭)
|
|
85
|
+
|
|
86
|
+
// 0. 刷新页面
|
|
87
|
+
if (toTab && toTab?.path === fromTab?.path) {
|
|
88
|
+
const components = to.matched[1].components as Record<string, any>;
|
|
89
|
+
|
|
90
|
+
if (!components?.default || !router.value)
|
|
91
|
+
return;
|
|
92
|
+
|
|
93
|
+
if (components.default.originalMainComp) {
|
|
94
|
+
components.default = components.default.originalMainComp;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const pageRefreshComp = h(PageRefresh, { router: router.value, originalMainComp:components.default }) as any;
|
|
98
|
+
pageRefreshComp.originalMainComp = components.default;
|
|
99
|
+
components.default = pageRefreshComp;
|
|
100
|
+
toTab.lastTime = 0;
|
|
101
|
+
setTimeout(() => toTab.lastTime = lastTime, 10);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 1. 在已有的 tab 中互相切换
|
|
105
|
+
else if (toTab && fromTab && !isReplaced) {
|
|
106
|
+
toTab.lastTime = lastTime;
|
|
107
|
+
fromTab.lastTime = lastTime - 1;
|
|
108
|
+
}
|
|
109
|
+
// 2. 仅改变当前 tab 的参数
|
|
110
|
+
else if (!toTab && fromTab && isReplaced) {
|
|
111
|
+
fromTab.lastTime = lastTime;
|
|
112
|
+
fromTab.path = to.fullPath;
|
|
113
|
+
fromTab.routeName = String(to.name);
|
|
114
|
+
}
|
|
115
|
+
// 3. 删除当前 tab
|
|
116
|
+
else if (toTab && !fromTab && isReplaced) {
|
|
117
|
+
toTab.lastTime = lastTime;
|
|
118
|
+
}
|
|
119
|
+
// 4. 新增 tab
|
|
120
|
+
else {
|
|
121
|
+
// 4.1 排除不需要新增 tab 的页面
|
|
122
|
+
const isExcluded = tabType === 0 || options.excludeTabs?.includes(String(to.name)) || getTab(to.fullPath);
|
|
123
|
+
if (isExcluded)
|
|
124
|
+
return;
|
|
125
|
+
|
|
126
|
+
const title = getTitle(to) || String(to.name || to.fullPath);
|
|
127
|
+
addTab({
|
|
128
|
+
lastTime,
|
|
129
|
+
title,
|
|
130
|
+
type: tabType,
|
|
131
|
+
path: to.fullPath,
|
|
132
|
+
routeName: String(to.name),
|
|
133
|
+
routeTitle: title,
|
|
134
|
+
});
|
|
135
|
+
fromTab?.lastTime && (fromTab.lastTime = lastTime - 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
watch(tabs, (tabs) => {
|
|
140
|
+
let tabCached = tabs.filter(tab => tab.lastTime > 0);
|
|
141
|
+
|
|
142
|
+
if (tabCached.length > 10)
|
|
143
|
+
tabCached = tabCached.sort((a, b) => a.lastTime - b.lastTime).slice(0, 10);
|
|
144
|
+
|
|
145
|
+
tabsCached.value = tabCached.map(tab => tab.path);
|
|
146
|
+
}, { immediate: true, deep: 2 });
|
|
147
|
+
|
|
148
|
+
watch(router, (router) => {
|
|
149
|
+
router?.afterEach((to, from) => {
|
|
150
|
+
switchTab(to, from);
|
|
151
|
+
|
|
152
|
+
// 使用 to.fullPath 重命名 main 组件
|
|
153
|
+
if (to.matched.length > 1) {
|
|
154
|
+
const mainComp: any = to.matched[1].components?.default;
|
|
155
|
+
const compName = currentTab.value?.path;
|
|
156
|
+
|
|
157
|
+
if (mainComp && compName)
|
|
158
|
+
mainComp.__name = compName;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return { tabs, currentTab, tabsCached, addTab, delTab, getTab, switchTab };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function useMenu(router: ShallowRef<Router | null>) {
|
|
167
|
+
const currentRouteName = computed(() => String(router.value?.currentRoute.value.name || ''));
|
|
168
|
+
const menus = ref<TMenu[]>([]);
|
|
169
|
+
const menuPath = computed(() => getMenuPath(menus.value, currentRouteName.value));
|
|
170
|
+
const breadcrumbs = computed(() => getMenuPath(menus.value, currentRouteName.value).map(menuToBreadcrumb));
|
|
171
|
+
|
|
172
|
+
function addMenu(menuConfig: TMenuConfig, parentKey?: string) {
|
|
173
|
+
const labelGetter = typeof menuConfig.label === 'function' ? menuConfig.label : (() => menuConfig.label) as (() => string);
|
|
174
|
+
const iconGetter = typeof menuConfig.icon === 'function' ? menuConfig.icon : () => null;
|
|
175
|
+
|
|
176
|
+
const _menu = reactive<IMenuReactive>({
|
|
177
|
+
key: menuConfig.key,
|
|
178
|
+
label: toRef(labelGetter),
|
|
179
|
+
icon: toRef(iconGetter),
|
|
180
|
+
trigger: menuConfig.trigger,
|
|
181
|
+
order: menuConfig.order,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (parentKey) {
|
|
185
|
+
const parentMenu = findMenu(menus.value, parentKey);
|
|
186
|
+
|
|
187
|
+
if (!parentMenu)
|
|
188
|
+
return;
|
|
189
|
+
|
|
190
|
+
const children = reactive(parentMenu.children ?? []);
|
|
191
|
+
children.push(_menu);
|
|
192
|
+
children.sort((a, b) => b.order - a.order);
|
|
193
|
+
parentMenu.children = children;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
menus.value.push(_menu);
|
|
197
|
+
menus.value.sort((a, b) => b.order - a.order);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function generateMenus(
|
|
202
|
+
filter?: (menu: ReturnType<typeof getMenusByRouter>[number]) => boolean,
|
|
203
|
+
otherRouter?: Router,
|
|
204
|
+
) {
|
|
205
|
+
if (!router.value)
|
|
206
|
+
return;
|
|
207
|
+
|
|
208
|
+
menus.value.length = 0;
|
|
209
|
+
|
|
210
|
+
const finalFilter = filter ?? (() => true);
|
|
211
|
+
const finalRouter = otherRouter ?? router.value;
|
|
212
|
+
|
|
213
|
+
getMenusByRouter(finalRouter).filter(finalFilter).forEach(({ menu, parentKey }) => addMenu(menu, parentKey));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
watch(router, (router) => {
|
|
217
|
+
generateMenus(undefined, router || undefined);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return { menus, menuPath, breadcrumbs, addMenu, generateMenus };
|
|
221
|
+
}
|
|
222
|
+
interface TMenuConfig {
|
|
223
|
+
key: string
|
|
224
|
+
label: string | (() => string)
|
|
225
|
+
icon?: () => VNode
|
|
226
|
+
order: number
|
|
227
|
+
trigger: () => void
|
|
228
|
+
children?: TMenuConfig[]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface IMenuReactive {
|
|
232
|
+
key: string
|
|
233
|
+
label: Ref<string>
|
|
234
|
+
icon: Ref<VNode | null>
|
|
235
|
+
order: number
|
|
236
|
+
trigger: () => void
|
|
237
|
+
children?: IMenuReactive[]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type TMenu = UnwrapNestedRefs<IMenuReactive>;
|
|
241
|
+
|
|
242
|
+
export interface TBreadcrumb {
|
|
243
|
+
title: string
|
|
244
|
+
icon?: VNode | null
|
|
245
|
+
trigger?: () => void
|
|
246
|
+
children?: TBreadcrumb[]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface TTabConfig {
|
|
250
|
+
title: MaybeRef<string>
|
|
251
|
+
routeTitle: MaybeRef<string>
|
|
252
|
+
path: string
|
|
253
|
+
routeName: string
|
|
254
|
+
lastTime: number
|
|
255
|
+
type: number
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export type TTab = UnwrapNestedRefs<TTabConfig>;
|
|
259
|
+
|
|
260
|
+
export function getTitle(route?: Pick<RouteLocationNormalizedLoaded, 'meta'>) {
|
|
261
|
+
const mTitle = route?.meta?.title;
|
|
262
|
+
|
|
263
|
+
return typeof mTitle === 'function' ? mTitle() : mTitle;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function getMenusByRouter(router: Router) {
|
|
267
|
+
const menuOrderRE = /^(?<key>\w*)@(?<order>\d+)$/;
|
|
268
|
+
const routes = router.getRoutes();
|
|
269
|
+
const menus: Array<{ parentKey?: string, menu: TMenuConfig }> = routes
|
|
270
|
+
.filter(route => menuOrderRE.test((route.meta.menuOrder ?? '')))
|
|
271
|
+
.map((route) => {
|
|
272
|
+
const res = route.meta!.menuOrder!.match(menuOrderRE);
|
|
273
|
+
const parentKey = res?.groups?.key;
|
|
274
|
+
const order = Number(res?.groups?.order);
|
|
275
|
+
const name = route.name as string;
|
|
276
|
+
const menu = {
|
|
277
|
+
key: name,
|
|
278
|
+
label: route.meta.title ?? name,
|
|
279
|
+
icon: route.meta.icon,
|
|
280
|
+
trigger: () => router.push({ name }),
|
|
281
|
+
order,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return { parentKey, menu };
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// 将含有子菜单的菜单的 trigger 设置为空函数
|
|
288
|
+
const hasSubMenus = menus.filter(menu => menus.some(m => m.parentKey === menu.menu.key));
|
|
289
|
+
hasSubMenus.forEach((menu) => {
|
|
290
|
+
menu.menu.trigger = (() => {}) as any;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return menus;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function printRounesNameInterface(routes: { name: PropertyKey }[]) {
|
|
297
|
+
console.groupCollapsed('路由命名');
|
|
298
|
+
const routesName = new Set();
|
|
299
|
+
routes.forEach((route) => {
|
|
300
|
+
if (typeof route.name === 'string')
|
|
301
|
+
routesName.add(route.name);
|
|
302
|
+
});
|
|
303
|
+
console.log([...routesName.values()].sort().filter(name => !!name).map(name => `${name}: true`).join('\n'));
|
|
304
|
+
console.groupEnd();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function findMenu(menus: TMenu[], key: string) {
|
|
308
|
+
const queue = [...menus];
|
|
309
|
+
|
|
310
|
+
while (queue.length > 0) {
|
|
311
|
+
const menu = queue.shift()!;
|
|
312
|
+
|
|
313
|
+
if (menu.key === key)
|
|
314
|
+
return menu;
|
|
315
|
+
|
|
316
|
+
if (menu.children)
|
|
317
|
+
queue.push(...menu.children);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return undefined;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
function menuToBreadcrumb(menu: TMenu) {
|
|
324
|
+
const ret: TBreadcrumb = reactive({
|
|
325
|
+
title: menu.label,
|
|
326
|
+
icon: menu.icon,
|
|
327
|
+
trigger: menu.trigger,
|
|
328
|
+
children: menu.children?.map(menuToBreadcrumb),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return ret;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function getMenuPath(menus: TMenu[], key: string) {
|
|
335
|
+
const path: TMenu[] = [];
|
|
336
|
+
|
|
337
|
+
const _getMenuPath = (menus: TMenu[], key: string) => {
|
|
338
|
+
for (const menu of menus) {
|
|
339
|
+
if (menu.key === key) {
|
|
340
|
+
path.push(menu);
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (menu.children) {
|
|
345
|
+
path.push(menu);
|
|
346
|
+
if (_getMenuPath(menu.children, key))
|
|
347
|
+
return true;
|
|
348
|
+
path.pop();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return false;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
_getMenuPath(menus, key);
|
|
356
|
+
|
|
357
|
+
return path as TMenu[];
|
|
358
|
+
};
|