@peng_kai/kit 0.3.0-beta.3 → 0.3.0-beta.30
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 +37 -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/TtaTimeZoneSimple.vue +104 -0
- package/admin/components/date/helpers.ts +250 -0
- package/admin/components/date/index.ts +6 -0
- package/admin/components/date/presetProps.ts +19 -0
- package/admin/components/filter/src/FilterReset.vue +52 -8
- package/admin/components/filter/src/more/TableSetting.vue +95 -0
- package/admin/components/provider/Admin.vue +17 -0
- package/admin/components/provider/admin-permission.ts +48 -0
- package/admin/components/provider/admin-router.ts +361 -0
- package/admin/components/provider/index.ts +3 -0
- package/admin/components/rich-text/src/RichText.new.vue +19 -6
- package/admin/components/rich-text/src/editorConfig.ts +76 -1
- package/admin/components/settings/src/SchemaForm.vue +5 -4
- package/admin/components/settings/src/Settings.vue +1 -1
- package/admin/components/text/index.ts +2 -0
- package/admin/components/text/src/Amount.v2.vue +131 -0
- package/admin/components/text/src/Datetime.vue +17 -12
- package/admin/components/text/src/Num.vue +192 -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 +152 -0
- package/admin/layout/large/Tabs.vue +183 -0
- package/admin/layout/large/index.ts +61 -1
- package/admin/layout/large/y682.mp3 +0 -0
- package/admin/permission/routerGuard.ts +24 -11
- 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/index.scss +2 -2
- package/antd/hooks/useAntdModal.ts +27 -13
- package/antd/hooks/useAntdTable.ts +10 -7
- package/antd/hooks/useAntdTheme.ts +7 -0
- package/antd/hooks/useTableColumns.ts +83 -0
- package/antd/index.ts +1 -1
- package/libs/bignumber.ts +1 -1
- package/libs/dayjs.ts +16 -2
- package/libs/fingerprintjs.ts +1 -0
- package/package.json +64 -95
- package/request/interceptors/getDeviceInfo.ts +14 -0
- package/utils/LocaleManager.ts +1 -1
- package/utils/index.ts +1 -4
- package/utils/locale/LocaleManager.ts +2 -1
- package/utils/locale/helpers.ts +9 -0
- package/utils/number.ts +8 -10
- package/utils/storage.ts +31 -0
- package/utils/string.ts +1 -2
- package/utils/upload/AwsS3.ts +11 -4
- package/admin/layout/large/PageTab.vue +0 -70
- package/admin/route-guards/collapseMenu.ts +0 -11
- package/libs/a-calc.ts +0 -1
- package/vue/components/test/KitTest.vue +0 -9
- package/vue/components/test/testStore.ts +0 -11
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<!-- eslint-disable jsdoc/no-multi-asterisks -->
|
|
2
|
+
|
|
3
|
+
<script setup lang="ts">
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import bignumber from 'bignumber.js';
|
|
6
|
+
import CurrencyIcon, {textIcons} from '../../currency/src/CurrencyIcon.vue';
|
|
7
|
+
import Num from './Num.vue';
|
|
8
|
+
|
|
9
|
+
// 由于 Vue 目前不支持 Props `interface AmountProps extends CurrencyIconProps, NumProps{}` 多继承写法,所以暂时这样写
|
|
10
|
+
interface AmountProps {
|
|
11
|
+
/** ****************** 与 Num 组件 Props 同步 ********************/
|
|
12
|
+
/**
|
|
13
|
+
* 数值
|
|
14
|
+
*/
|
|
15
|
+
value: BigNumber.Value | undefined | null
|
|
16
|
+
/**
|
|
17
|
+
* 小数位数
|
|
18
|
+
*/
|
|
19
|
+
decimals?: number | [number, number]
|
|
20
|
+
/**
|
|
21
|
+
* 舍入方式
|
|
22
|
+
*
|
|
23
|
+
* - up: 向上取整
|
|
24
|
+
* - down: 向下取整
|
|
25
|
+
* - half-up: 四舍五入
|
|
26
|
+
* - half-even: 银行家算法
|
|
27
|
+
*/
|
|
28
|
+
round?: 'up' | 'down' | 'half-up' | 'half-even'
|
|
29
|
+
/**
|
|
30
|
+
* 格式化方式
|
|
31
|
+
*
|
|
32
|
+
* - original: 原始
|
|
33
|
+
* - pad-dec: 自适应补零,即整数位多1位,小数位就少1位
|
|
34
|
+
* - fixed-dec: 固定小数位数
|
|
35
|
+
* - max-dec: 最大小数位数
|
|
36
|
+
* - min-dec: 最小小数位数
|
|
37
|
+
* - clamp-dec: 限制小数位数
|
|
38
|
+
*/
|
|
39
|
+
format?: 'original' | 'pad-dec' | 'fixed-dec' | 'max-dec' | 'min-dec' | 'clamp-dec'
|
|
40
|
+
/**
|
|
41
|
+
* 着色
|
|
42
|
+
*
|
|
43
|
+
* - inherit: 继承
|
|
44
|
+
* - neg: 负数
|
|
45
|
+
* - pos: 正数
|
|
46
|
+
* - full: 全部
|
|
47
|
+
*/
|
|
48
|
+
colored?: 'inherit' | 'neg' | 'pos' | 'full'
|
|
49
|
+
/**
|
|
50
|
+
* 是否显示正数符号
|
|
51
|
+
*/
|
|
52
|
+
showPos?: boolean
|
|
53
|
+
/**
|
|
54
|
+
* 是否使用分组分隔符(千分符)
|
|
55
|
+
*/
|
|
56
|
+
grouping?: boolean
|
|
57
|
+
/**
|
|
58
|
+
* 是否弱化无价值的零值
|
|
59
|
+
*/
|
|
60
|
+
weakPad?: boolean
|
|
61
|
+
|
|
62
|
+
/** ****************** 与 CurrencyIcon 组件 Props 同步 ********************/
|
|
63
|
+
symbol?: string
|
|
64
|
+
size?: string
|
|
65
|
+
cdn?: string
|
|
66
|
+
iconType?: 'img' | 'text'
|
|
67
|
+
|
|
68
|
+
/** ****************** 本组件的 Props ********************/
|
|
69
|
+
/** 图标显示位置 */
|
|
70
|
+
iconPos?: 'left' | 'right' | 'hidden'
|
|
71
|
+
/** 字符图标是否使用数字的颜色 */
|
|
72
|
+
useNumColor?: boolean
|
|
73
|
+
/** 值的精度(整数 value 除以 precision) */
|
|
74
|
+
precision?: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const props = withDefaults(defineProps<AmountProps>(), {
|
|
78
|
+
// Num 组件 Props
|
|
79
|
+
value: () => bignumber(0),
|
|
80
|
+
decimals: 8,
|
|
81
|
+
round: 'down',
|
|
82
|
+
weakPad: true,
|
|
83
|
+
showPos: false,
|
|
84
|
+
colored: 'inherit',
|
|
85
|
+
format: 'original',
|
|
86
|
+
grouping: true,
|
|
87
|
+
|
|
88
|
+
// CurrencyIcon 组件 Props
|
|
89
|
+
size: '1.1em',
|
|
90
|
+
// symbol: '',
|
|
91
|
+
iconType: 'img',
|
|
92
|
+
|
|
93
|
+
// 本组件 Props
|
|
94
|
+
iconPos: 'left',
|
|
95
|
+
useNumColor: false,
|
|
96
|
+
precision: 0,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const isTextIcon = computed(() => {
|
|
100
|
+
return props.iconType === 'text' || textIcons.value.includes(String(props.symbol));
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<span class="amount flex-inline" :class="[isTextIcon ? 'items-baseline' : 'items-center']">
|
|
106
|
+
<CurrencyIcon v-if="props.iconPos === 'left' && props.symbol && !props.useNumColor" v-bind="props" class="currency-icon" />
|
|
107
|
+
<Num v-bind="props" :value="bignumber(props.value ?? 0).dividedBy(10 ** props.precision)">
|
|
108
|
+
<template #signLeft>
|
|
109
|
+
<CurrencyIcon v-if="props.iconPos === 'left' && props.symbol && props.useNumColor" v-bind="props" class="currency-icon" />
|
|
110
|
+
</template>
|
|
111
|
+
</Num>
|
|
112
|
+
<CurrencyIcon v-if="props.iconPos === 'right'" v-bind="props" class="currency-icon" />
|
|
113
|
+
</span>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<style lang="scss" scoped>
|
|
117
|
+
.amount {
|
|
118
|
+
line-height: 1em;
|
|
119
|
+
}
|
|
120
|
+
.symbol-icon {
|
|
121
|
+
line-height: inherit;
|
|
122
|
+
font-size: inherit;
|
|
123
|
+
}
|
|
124
|
+
.amount .currency-icon:first-child {
|
|
125
|
+
margin-right: 0.15em;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.amount .currency-icon:last-child {
|
|
129
|
+
margin-left: 0.15em;
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { Tooltip
|
|
2
|
+
import { Tooltip } from 'ant-design-vue';
|
|
3
3
|
import { computed } from 'vue';
|
|
4
4
|
import dayjs from '../../../../libs/dayjs';
|
|
5
|
+
import { getTtaTimeZone } from '../../date'
|
|
5
6
|
|
|
6
7
|
defineOptions({
|
|
7
8
|
inheritAttrs: false,
|
|
@@ -10,16 +11,18 @@ defineOptions({
|
|
|
10
11
|
const props = withDefaults(
|
|
11
12
|
defineProps<{
|
|
12
13
|
timestamp?: number | string
|
|
14
|
+
ts?: number | string
|
|
13
15
|
template?: string
|
|
14
|
-
utc?: boolean
|
|
15
16
|
}>(),
|
|
16
17
|
{
|
|
17
|
-
template: 'MM-DD HH:mm:ss',
|
|
18
|
-
utc: false,
|
|
18
|
+
template: 'YY-MM-DD HH:mm:ss',
|
|
19
19
|
},
|
|
20
20
|
);
|
|
21
|
+
|
|
22
|
+
const tz = getTtaTimeZone();
|
|
23
|
+
const localTz = dayjs.tz.guess();
|
|
21
24
|
const timestamp = computed(() => {
|
|
22
|
-
let tsStr = String(props.timestamp);
|
|
25
|
+
let tsStr = String(props.ts || props.timestamp);
|
|
23
26
|
|
|
24
27
|
if (tsStr.length === 10)
|
|
25
28
|
tsStr += '000';
|
|
@@ -33,19 +36,21 @@ const timestamp = computed(() => {
|
|
|
33
36
|
const text = computed(() => {
|
|
34
37
|
if (!timestamp.value)
|
|
35
38
|
return '-';
|
|
36
|
-
|
|
37
39
|
const ts = dayjs(timestamp.value);
|
|
38
|
-
|
|
39
|
-
return props.utc ? ts.utc().format(props.template) : ts.format(props.template);
|
|
40
|
+
return ts.tz(tz.value).format(props.template);
|
|
40
41
|
});
|
|
41
42
|
</script>
|
|
42
43
|
|
|
43
44
|
<template>
|
|
44
|
-
<
|
|
45
|
+
<Tooltip destroyTooltipOnHide :align="{ offset: [0, 8] }">
|
|
45
46
|
<template v-if="timestamp" #title>
|
|
46
|
-
<div>{{ dayjs(timestamp).fromNow?.() }}</div>
|
|
47
|
-
<div>
|
|
47
|
+
<div>{{ dayjs(timestamp).tz(tz).fromNow?.() }}</div>
|
|
48
|
+
<div v-if="dayjs.tz.guess() !== tz && tz !== 'UTC' ">
|
|
48
49
|
{{ dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss') }}
|
|
50
|
+
<span class="op-50">指定</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div>
|
|
53
|
+
{{ dayjs(timestamp).tz(localTz).format('YYYY-MM-DD HH:mm:ss') }}
|
|
49
54
|
<span class="op-50">本地</span>
|
|
50
55
|
</div>
|
|
51
56
|
<div v-if="dayjs().utc">
|
|
@@ -54,7 +59,7 @@ const text = computed(() => {
|
|
|
54
59
|
</div>
|
|
55
60
|
</template>
|
|
56
61
|
<span v-bind="$attrs" class="text">{{ text }}</span>
|
|
57
|
-
</
|
|
62
|
+
</Tooltip>
|
|
58
63
|
</template>
|
|
59
64
|
|
|
60
65
|
<style scoped lang="scss">
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import bignumber, { type BigNumber } from 'bignumber.js';
|
|
3
|
+
import { computed } from 'vue';
|
|
4
|
+
|
|
5
|
+
export interface NumProps {
|
|
6
|
+
/**
|
|
7
|
+
* 数值
|
|
8
|
+
*/
|
|
9
|
+
value: BigNumber.Value
|
|
10
|
+
/**
|
|
11
|
+
* 小数位数
|
|
12
|
+
*/
|
|
13
|
+
decimals?: number | [number, number]
|
|
14
|
+
/**
|
|
15
|
+
* 舍入方式
|
|
16
|
+
*
|
|
17
|
+
* - up: 向上取整
|
|
18
|
+
* - down: 向下取整
|
|
19
|
+
* - half-up: 四舍五入
|
|
20
|
+
* - half-even: 银行家算法
|
|
21
|
+
*/
|
|
22
|
+
round?: 'up' | 'down' | 'half-up' | 'half-even'
|
|
23
|
+
/**
|
|
24
|
+
* 格式化方式
|
|
25
|
+
*
|
|
26
|
+
* - original: 原始
|
|
27
|
+
* - pad-dec: 自适应补零,即整数位多1位,小数位就少1位
|
|
28
|
+
* - fixed-dec: 固定小数位数
|
|
29
|
+
* - max-dec: 最大小数位数
|
|
30
|
+
* - min-dec: 最小小数位数
|
|
31
|
+
* - clamp-dec: 限制小数位数
|
|
32
|
+
*/
|
|
33
|
+
format?: 'original' | 'pad-dec' | 'fixed-dec' | 'max-dec' | 'min-dec' | 'clamp-dec'
|
|
34
|
+
/**
|
|
35
|
+
* 着色
|
|
36
|
+
*
|
|
37
|
+
* - inherit: 继承
|
|
38
|
+
* - neg: 负数
|
|
39
|
+
* - pos: 正数
|
|
40
|
+
* - full: 全部
|
|
41
|
+
*/
|
|
42
|
+
colored?: 'inherit' | 'neg' | 'pos' | 'full'
|
|
43
|
+
/**
|
|
44
|
+
* 是否显示正数符号
|
|
45
|
+
*/
|
|
46
|
+
showPos?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* 是否使用分组分隔符(千分符)
|
|
49
|
+
*/
|
|
50
|
+
grouping?: boolean
|
|
51
|
+
/**
|
|
52
|
+
* 是否弱化无价值填充
|
|
53
|
+
*/
|
|
54
|
+
weakPad?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 匹配数字中无价值的填充(仅支持有小数的), 如 1.2000 => 1.2, 1.000 => 1
|
|
58
|
+
const insignRE = /\.?0+$/;
|
|
59
|
+
|
|
60
|
+
function formatPadDecimals(num: BigNumber.Instance, decimals: number, round: BigNumber.RoundingMode, grouping: boolean) {
|
|
61
|
+
const integerLen = num.integerValue().toString().length;
|
|
62
|
+
const showDecimalLen = Math.max(decimals - integerLen + 1, 1);
|
|
63
|
+
return grouping ? num.toFormat(showDecimalLen, round) : num.toFixed(showDecimalLen, round);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatFixedDecimals(num: BigNumber.Instance, decimals: number, round: BigNumber.RoundingMode, grouping: boolean) {
|
|
67
|
+
return grouping ? num.toFormat(decimals, round) : num.toFixed(decimals, round);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatMaxDecimals(num: BigNumber.Instance, decimals: number, round: BigNumber.RoundingMode, grouping: boolean) {
|
|
71
|
+
const showDecimalLen = Math.min(num.decimalPlaces() ?? 0, decimals);
|
|
72
|
+
return grouping ? num.toFormat(showDecimalLen, round) : num.toFixed(showDecimalLen, round);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatMinDecimals(num: BigNumber.Instance, decimals: number, round: BigNumber.RoundingMode, grouping: boolean) {
|
|
76
|
+
const showDecimalLen = Math.max(num.decimalPlaces() ?? 0, decimals);
|
|
77
|
+
return grouping ? num.toFormat(showDecimalLen, round) : num.toFixed(showDecimalLen, round);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatClampDecimals(num: BigNumber.Instance, decimals: [number, number], round: BigNumber.RoundingMode, grouping: boolean) {
|
|
81
|
+
const decimalLen = num.decimalPlaces() ?? 0;
|
|
82
|
+
const showDecimalLen = Math.min(Math.max(decimalLen, decimals[0]), decimals[1]);
|
|
83
|
+
return grouping ? num.toFormat(showDecimalLen, round) : num.toFixed(showDecimalLen, round);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatOriginal(num: BigNumber.Instance, decimals: number, round: BigNumber.RoundingMode, grouping: boolean) {
|
|
87
|
+
const decimalLen = num.decimalPlaces() ?? decimals;
|
|
88
|
+
return grouping ? num.toFormat(decimalLen, round) : num.toString();
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<script setup lang="ts">
|
|
93
|
+
const props = withDefaults(defineProps<NumProps>(), {
|
|
94
|
+
value: () => bignumber(0),
|
|
95
|
+
showPos: false,
|
|
96
|
+
decimals: 2,
|
|
97
|
+
colored: 'inherit',
|
|
98
|
+
round: 'half-up',
|
|
99
|
+
format: 'original',
|
|
100
|
+
grouping: true,
|
|
101
|
+
weakPad: false,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const numParts = computed(() => {
|
|
105
|
+
const num = bignumber(props.value).abs();
|
|
106
|
+
const roundMode = ({
|
|
107
|
+
'up': bignumber.ROUND_UP,
|
|
108
|
+
'down': bignumber.ROUND_DOWN,
|
|
109
|
+
'half-up': bignumber.ROUND_HALF_UP,
|
|
110
|
+
'half-even': bignumber.ROUND_HALF_EVEN,
|
|
111
|
+
})[props.round];
|
|
112
|
+
const isRangeDecimals = ['clamp-dec'].includes(props.format);
|
|
113
|
+
const parts = { full: '', sign: '', value: '', pad: '' };
|
|
114
|
+
let text = '';
|
|
115
|
+
|
|
116
|
+
if (isRangeDecimals) {
|
|
117
|
+
const decLen = Array.isArray(props.decimals)
|
|
118
|
+
? props.decimals
|
|
119
|
+
: [props.decimals, props.decimals] as [number, number];
|
|
120
|
+
const format = ({
|
|
121
|
+
'clamp-dec': formatClampDecimals,
|
|
122
|
+
} as Record<string, typeof formatClampDecimals>)[props.format];
|
|
123
|
+
|
|
124
|
+
text = format(num, decLen, roundMode, props.grouping);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const decLen = Array.isArray(props.decimals) ? props.decimals[0] : props.decimals;
|
|
128
|
+
const format = ({
|
|
129
|
+
'pad-dec': formatPadDecimals,
|
|
130
|
+
'fixed-dec': formatFixedDecimals,
|
|
131
|
+
'max-dec': formatMaxDecimals,
|
|
132
|
+
'min-dec': formatMinDecimals,
|
|
133
|
+
} as Record<string, typeof formatPadDecimals>)[props.format] ?? formatOriginal;
|
|
134
|
+
|
|
135
|
+
text = format(num, decLen, roundMode, props.grouping);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (text) {
|
|
139
|
+
const originalNum = bignumber(props.value);
|
|
140
|
+
|
|
141
|
+
parts.sign = originalNum.lt(0) ? '-' : (originalNum.gt(0) && props.showPos) ? '+' : '';
|
|
142
|
+
parts.full = parts.sign + text;
|
|
143
|
+
parts.pad = text.match(insignRE)?.[0] ?? '';
|
|
144
|
+
parts.pad = originalNum.decimalPlaces() || parts.pad[0] === '.' ? parts.pad : '';
|
|
145
|
+
parts.value = text.slice(0, text.length - parts.pad.length);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parts;
|
|
149
|
+
});
|
|
150
|
+
const classList = computed(() => {
|
|
151
|
+
const num = bignumber(props.value);
|
|
152
|
+
const classes = [`colored-${props.colored}`];
|
|
153
|
+
|
|
154
|
+
if (num.lt(0))
|
|
155
|
+
classes.push('sign-neg');
|
|
156
|
+
else if (num.gt(0))
|
|
157
|
+
classes.push('sign-pos');
|
|
158
|
+
else
|
|
159
|
+
classes.push('sign-zero');
|
|
160
|
+
|
|
161
|
+
if (props.weakPad)
|
|
162
|
+
classes.push('weak-pad');
|
|
163
|
+
|
|
164
|
+
return classes;
|
|
165
|
+
});
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<template>
|
|
169
|
+
<span class="num" :class="classList">
|
|
170
|
+
<slot name="signLeft" />{{ numParts.sign }}<slot name="signRight" /><slot name="numLeft" />{{ numParts.value }}<span class="pad">{{ numParts.pad }}</span><slot name="numRight" />
|
|
171
|
+
</span>
|
|
172
|
+
</template>
|
|
173
|
+
|
|
174
|
+
<style lang="scss" scoped>
|
|
175
|
+
.num {
|
|
176
|
+
font-variant-numeric: tabular-nums;
|
|
177
|
+
|
|
178
|
+
&.sign-neg.colored-neg,
|
|
179
|
+
&.sign-neg.colored-full {
|
|
180
|
+
color: var(--neg-color, #ef4444);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
&.sign-pos.colored-pos,
|
|
184
|
+
&.sign-pos.colored-full {
|
|
185
|
+
color: var(--pos-color, #22c55e);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
&.weak-pad .pad {
|
|
189
|
+
opacity: 0.5;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -3,15 +3,14 @@ import { computed } from 'vue';
|
|
|
3
3
|
import type { VNode } from 'vue';
|
|
4
4
|
import { Breadcrumb as ABreadcrumb } from 'ant-design-vue';
|
|
5
5
|
import type { Route as AntdBreadcrumbRoute } from 'ant-design-vue/es/breadcrumb/Breadcrumb';
|
|
6
|
-
import {
|
|
7
|
-
import type { IBreadcrumb } from '../../stores/createUsePageStore';
|
|
6
|
+
import type { TBreadcrumb } from '../../components/provider/admin-router';
|
|
8
7
|
|
|
9
|
-
interface
|
|
8
|
+
interface TBreadcrumbRoute extends AntdBreadcrumbRoute {
|
|
10
9
|
icon?: VNode | null
|
|
11
10
|
trigger?: () => void
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
function _buildRoute(breadcrumb:
|
|
13
|
+
function _buildRoute(breadcrumb: TBreadcrumb): TBreadcrumbRoute {
|
|
15
14
|
return {
|
|
16
15
|
breadcrumbName: breadcrumb.title,
|
|
17
16
|
path: breadcrumb.title,
|
|
@@ -23,28 +22,15 @@ function _buildRoute(breadcrumb: IBreadcrumb): IBreadcrumbRoute {
|
|
|
23
22
|
</script>
|
|
24
23
|
|
|
25
24
|
<script setup lang="ts">
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!pageStore.currentPageState)
|
|
31
|
-
return breadcrumbs;
|
|
32
|
-
|
|
33
|
-
breadcrumbs = pageStore.currentPageState.breadcrumbs.map(breadcrumb => _buildRoute(breadcrumb));
|
|
34
|
-
|
|
35
|
-
breadcrumbs.push({
|
|
36
|
-
path: pageStore.currentPageState.title,
|
|
37
|
-
breadcrumbName: pageStore.currentPageState.title,
|
|
38
|
-
icon: pageStore.currentPageState.icon,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return breadcrumbs;
|
|
42
|
-
});
|
|
25
|
+
const props = defineProps<{
|
|
26
|
+
breadcrumbs: TBreadcrumb[]
|
|
27
|
+
}>();
|
|
28
|
+
const routes = computed(() => props.breadcrumbs.map(breadcrumb => _buildRoute(breadcrumb)));
|
|
43
29
|
</script>
|
|
44
30
|
|
|
45
31
|
<template>
|
|
46
32
|
<ABreadcrumb class="breadcrumb" :routes="routes">
|
|
47
|
-
<template #itemRender="{ route }: {route:
|
|
33
|
+
<template #itemRender="{ route }: {route: TBreadcrumbRoute}">
|
|
48
34
|
<div @click="route.trigger?.()">
|
|
49
35
|
<component :is="route.icon" v-if="route.icon" class="mb-0.2em mr-0.2em" />
|
|
50
36
|
<span>{{ route.breadcrumbName }}</span>
|
|
@@ -55,10 +41,11 @@ const routes = computed(() => {
|
|
|
55
41
|
|
|
56
42
|
<style lang="scss" scoped>
|
|
57
43
|
.breadcrumb {
|
|
58
|
-
font-size:
|
|
44
|
+
font-size: 14px;
|
|
59
45
|
|
|
60
46
|
:deep(.ant-dropdown-trigger) {
|
|
61
47
|
height: 1.6em;
|
|
48
|
+
cursor: pointer;
|
|
62
49
|
}
|
|
63
50
|
|
|
64
51
|
:deep(.anticon-down) {
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { RouterView } from 'vue-router';
|
|
3
|
+
import { KeepAlive, type KeepAliveProps } from 'vue';
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
cached: KeepAliveProps['include']
|
|
7
|
+
}>();
|
|
5
8
|
</script>
|
|
6
9
|
|
|
7
10
|
<template>
|
|
8
|
-
<RouterView #default="{ Component
|
|
9
|
-
<KeepAlive :include="
|
|
11
|
+
<RouterView #default="{ Component }">
|
|
12
|
+
<KeepAlive :include="props.cached">
|
|
10
13
|
<Suspense>
|
|
11
|
-
<component :is="
|
|
14
|
+
<component :is="Component" :key="(Component as any).type?.__name" />
|
|
12
15
|
<template #fallback>
|
|
13
|
-
<div class="flex justify-center items-center h-
|
|
16
|
+
<div class="flex justify-center items-center h-full">
|
|
14
17
|
<div class="flex items-center">
|
|
15
18
|
<i class="i-svg-spinners:180-ring-with-bg block color-primary scale-125 mr-2" />
|
|
16
19
|
<span class="text-gray">正在加载...</span>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import { useScroll, useMutationObserver } from '@vueuse/core'
|
|
4
|
+
|
|
5
|
+
const hiddenSiderbar = ref(false);
|
|
6
|
+
const $content = ref<HTMLElement>();
|
|
7
|
+
const { arrivedState, measure } = useScroll($content);
|
|
8
|
+
const contentCssVars = computed(() => {
|
|
9
|
+
const {top: arrivedTop, bottom: arrivedBtm} = arrivedState;
|
|
10
|
+
return {
|
|
11
|
+
'--top-e': arrivedTop ? '0%' : '5px',
|
|
12
|
+
'--btm-s': arrivedBtm ? '100%' : '99.5%',
|
|
13
|
+
// 'margin-top': arrivedTop ? '0' : '-5px',
|
|
14
|
+
};
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
useMutationObserver($content, () => {
|
|
18
|
+
setTimeout(measure, 100)
|
|
19
|
+
}, { childList: true });
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="app-layout" :class="{ 'hidden-siderbar': hiddenSiderbar }">
|
|
24
|
+
<div class="app-logo select-none [grid-area:logo] ">
|
|
25
|
+
<slot name="logo" />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="app-header select-none [grid-area:header]">
|
|
29
|
+
<!-- <div
|
|
30
|
+
class="text-6 flex items-center h-7 -mr-5 cursor-pointer"
|
|
31
|
+
@click="hiddenSiderbar = !hiddenSiderbar"
|
|
32
|
+
>
|
|
33
|
+
<i class="i-material-symbols:menu-open-rounded" :class="{ 'rotate-180': hiddenSiderbar }" />
|
|
34
|
+
</div> -->
|
|
35
|
+
<slot name="header" />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div v-show="!hiddenSiderbar" class="app-siderbar select-none [grid-area:siderbar]">
|
|
39
|
+
<slot name="siderbar" />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div ref="$content" class="app-content [grid-area:content]" :style="contentCssVars">
|
|
43
|
+
<slot name="content" />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style lang="scss" scoped>
|
|
49
|
+
.app-layout {
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template:
|
|
53
|
+
"logo header" 56px
|
|
54
|
+
"siderbar content" 1fr
|
|
55
|
+
/ 180px 1fr;
|
|
56
|
+
column-gap: 8px;
|
|
57
|
+
width: 100vw;
|
|
58
|
+
height: 100vh;
|
|
59
|
+
padding: 0 8px 8px;
|
|
60
|
+
background-color: var(--antd-colorBgLayout);
|
|
61
|
+
|
|
62
|
+
&.hidden-siderbar {
|
|
63
|
+
grid-template:
|
|
64
|
+
"logo header" 56px
|
|
65
|
+
"content content" 1fr
|
|
66
|
+
/ 130px 1fr;
|
|
67
|
+
|
|
68
|
+
.app-logo {
|
|
69
|
+
justify-content: start;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.app-logo {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
font-size: 2rem;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
line-height: 1;
|
|
81
|
+
color: var(--antd-colorPrimary)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.app-siderbar {
|
|
85
|
+
position: relative;
|
|
86
|
+
display: flex;
|
|
87
|
+
min-height: 100%;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
margin-right: -6px;
|
|
90
|
+
overflow-y: auto;
|
|
91
|
+
scrollbar-gutter: stable;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// @property --top-e {
|
|
95
|
+
// syntax: "<percentage>";
|
|
96
|
+
// initial-value: 0%;
|
|
97
|
+
// inherits: false;
|
|
98
|
+
// }
|
|
99
|
+
// @property --btm-s {
|
|
100
|
+
// syntax: "<percentage>";
|
|
101
|
+
// initial-value: 100%;
|
|
102
|
+
// inherits: false;
|
|
103
|
+
// }
|
|
104
|
+
|
|
105
|
+
.app-content {
|
|
106
|
+
--top-s: 0%;
|
|
107
|
+
--top-e: 0%;
|
|
108
|
+
--btm-s: 99%;
|
|
109
|
+
--btm-e: 100%;
|
|
110
|
+
|
|
111
|
+
margin-right: -6px;
|
|
112
|
+
overflow-y: auto;
|
|
113
|
+
scrollbar-gutter: stable;
|
|
114
|
+
mask-image: linear-gradient(
|
|
115
|
+
to bottom,
|
|
116
|
+
#0000 var(--top-s),
|
|
117
|
+
#000 var(--top-e),
|
|
118
|
+
#000 var(--btm-s),
|
|
119
|
+
#0000 var(--btm-e)
|
|
120
|
+
);
|
|
121
|
+
transition: --top-e 0.05s, --btm-s 0.05s;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.app-header {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 36px;
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -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>
|