@peng_kai/kit 0.3.0-beta.4 → 0.3.0-beta.41
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 +55 -8
- package/admin/components/filter/src/more/TableSetting.vue +95 -0
- package/admin/components/filter/src/useFilterParams.ts +9 -7
- 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/index.ts +1 -1
- package/admin/components/settings/src/SchemaForm.vue +40 -6
- 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/IP.vue +18 -4
- package/admin/components/text/src/Num.vue +192 -0
- package/admin/components/upload/src/PictureCardUpload.vue +56 -20
- 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 +168 -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 -12
- 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 +12 -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">
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { TypographyLink as ATypographyLink } from 'ant-design-vue';
|
|
3
|
+
import { computed } from 'vue';
|
|
3
4
|
|
|
4
|
-
withDefaults(defineProps<{
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
5
6
|
ip?: string
|
|
6
7
|
empty?: string
|
|
8
|
+
fullV6?: boolean
|
|
7
9
|
}>(), {
|
|
8
10
|
empty: '-',
|
|
11
|
+
fullV6: false,
|
|
9
12
|
});
|
|
10
|
-
</script>
|
|
11
13
|
|
|
14
|
+
const ip = computed(() => {
|
|
15
|
+
const ip = props.ip;
|
|
16
|
+
if (!ip)
|
|
17
|
+
return;
|
|
18
|
+
const isV6 = ip.includes(':');
|
|
19
|
+
if (isV6 && !props.fullV6) {
|
|
20
|
+
const parts = ip.split(':');
|
|
21
|
+
return `${parts[0]}:...:${parts[parts.length - 1]}`
|
|
22
|
+
}
|
|
23
|
+
return ip;
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
12
26
|
<template>
|
|
13
27
|
<ATypographyLink
|
|
14
|
-
v-if="
|
|
28
|
+
v-if="ip"
|
|
15
29
|
:href="`https://www.ip138.com/iplookup.php?ip=${$props.ip}&action=2`"
|
|
16
30
|
:copyable="{ text: $props.ip, tooltip: false }" target="_blank"
|
|
17
31
|
>
|
|
18
|
-
<span v-bind="$attrs" class="font-mono">{{
|
|
32
|
+
<span v-bind="$attrs" class="font-mono">{{ ip }}</span>
|
|
19
33
|
</ATypographyLink>
|
|
20
34
|
<span v-else v-bind="$attrs" class="font-mono">{{ $props.empty }}</span>
|
|
21
35
|
</template>
|
|
@@ -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>
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { Image as AImage, ImagePreviewGroup as AImagePreviewGroup, Upload as AUpload, message } from 'ant-design-vue';
|
|
3
3
|
import type { UploadProps } from 'ant-design-vue';
|
|
4
4
|
import type { PreviewGroupPreview } from 'ant-design-vue/es/vc-image/src/PreviewGroup';
|
|
5
|
-
import {
|
|
6
|
-
import { ref } from 'vue';
|
|
5
|
+
import { ref, customRef, nextTick, triggerRef } from 'vue';
|
|
7
6
|
import type { AwsS3 } from '../../../../utils/upload';
|
|
8
7
|
import { createAwsS3Request } from './customRequests';
|
|
8
|
+
import { urlToUploadFile } from './helpers';
|
|
9
9
|
|
|
10
10
|
type UploadFile = Parameters<NonNullable<UploadProps['onPreview']>>['0'];
|
|
11
11
|
</script>
|
|
@@ -13,8 +13,12 @@ type UploadFile = Parameters<NonNullable<UploadProps['onPreview']>>['0'];
|
|
|
13
13
|
<script lang="ts" setup>
|
|
14
14
|
const props = withDefaults(
|
|
15
15
|
defineProps<{
|
|
16
|
-
/**
|
|
16
|
+
/** 上传图片的数组 */
|
|
17
17
|
modelValue?: UploadFile[]
|
|
18
|
+
/** 上传图片的 url 数组 */
|
|
19
|
+
urls?: string[]
|
|
20
|
+
/** 是否使用 url 数组 */
|
|
21
|
+
useUrl?: boolean
|
|
18
22
|
/** awsS3 实例对象 */
|
|
19
23
|
awsS3: AwsS3
|
|
20
24
|
/** 根目录 */
|
|
@@ -27,6 +31,8 @@ const props = withDefaults(
|
|
|
27
31
|
cardSize?: string
|
|
28
32
|
/** 允许的图片后缀 */
|
|
29
33
|
allowExts?: string[]
|
|
34
|
+
/** 允许的图片大小,单位为字节 */
|
|
35
|
+
maxSize?: number
|
|
30
36
|
/** 禁用 */
|
|
31
37
|
disabled?: boolean
|
|
32
38
|
}>(),
|
|
@@ -36,14 +42,47 @@ const props = withDefaults(
|
|
|
36
42
|
rowCount: 'auto-fill',
|
|
37
43
|
cardSize: '90px',
|
|
38
44
|
allowExts: () => ['jpg', 'jpeg', 'png', 'gif', 'apng', 'webp', 'svg'],
|
|
45
|
+
maxSize: 1024 * 1024 * 2,
|
|
39
46
|
disabled: false,
|
|
47
|
+
useUrl: false,
|
|
40
48
|
},
|
|
41
49
|
);
|
|
42
50
|
const emits = defineEmits<{
|
|
43
51
|
(e: 'update:modelValue', value: UploadFile[]): void
|
|
52
|
+
(e: 'update:urls', value: string[]): void
|
|
44
53
|
}>();
|
|
45
54
|
|
|
46
|
-
const fileList =
|
|
55
|
+
const fileList = customRef<any>((track, trigger) => {
|
|
56
|
+
let tempValue: any[] = [];
|
|
57
|
+
let isAllDone = true;
|
|
58
|
+
return {
|
|
59
|
+
get() {
|
|
60
|
+
track();
|
|
61
|
+
if (props.useUrl) {
|
|
62
|
+
return isAllDone ? urlToUploadFile(props.urls || []) : tempValue;
|
|
63
|
+
}
|
|
64
|
+
return props.modelValue || [];
|
|
65
|
+
},
|
|
66
|
+
set(v: any) {
|
|
67
|
+
if (props.useUrl) {
|
|
68
|
+
isAllDone = (v || []).every((x: any) => typeof x === 'string' || x.status === 'done');
|
|
69
|
+
if (isAllDone) {
|
|
70
|
+
const urls = (v || []).map((x: any) => typeof x === 'string' ? x : x.response?.url);
|
|
71
|
+
emits('update:urls', urls);
|
|
72
|
+
tempValue = [];
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
tempValue = v;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
emits('update:modelValue', v);
|
|
80
|
+
}
|
|
81
|
+
trigger();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
47
86
|
const preview = ref<PreviewGroupPreview>({
|
|
48
87
|
visible: false,
|
|
49
88
|
onVisibleChange(value) {
|
|
@@ -54,36 +93,33 @@ const preview = ref<PreviewGroupPreview>({
|
|
|
54
93
|
function handlePreview(file: UploadFile) {
|
|
55
94
|
preview.value.src = file.url || file.thumbUrl;
|
|
56
95
|
preview.value.visible = true;
|
|
57
|
-
preview.value.current = fileList.value?.findIndex(
|
|
96
|
+
preview.value.current = fileList.value?.findIndex((x: any) => preview.value.src === (x.url || x.thumbUrl)) ?? 0;
|
|
58
97
|
}
|
|
59
98
|
|
|
60
99
|
function beforeUpload(file: UploadFile) {
|
|
61
100
|
const isImg = props.allowExts.some(item => String(file.type).toLowerCase().includes(item));
|
|
62
|
-
|
|
101
|
+
const isSizeValid = !!(props.maxSize && file.size && file.size <= props.maxSize);
|
|
63
102
|
if (!isImg)
|
|
64
103
|
message.error(`${file.name} 不是图片 (${props.allowExts.join(',')})`);
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
if (!isSizeValid)
|
|
105
|
+
message.error(`${file.name} 大小不能超过 ${props.maxSize / 1024 / 1024}MB`);
|
|
106
|
+
|
|
107
|
+
return isImg && isSizeValid;
|
|
67
108
|
}
|
|
68
109
|
</script>
|
|
69
110
|
|
|
70
111
|
<template>
|
|
71
112
|
<div :class="{ 'w-fit': typeof props.rowCount === 'number' }">
|
|
72
|
-
<AUpload
|
|
73
|
-
|
|
74
|
-
:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<i v-if="fileList.length < props.maxCount" class="i-ant-design:plus-outlined text-26px text-$antd-colorTextTertiary" />
|
|
113
|
+
<AUpload v-model:fileList="fileList" class="pic-card-upload" listType="picture-card" multiple
|
|
114
|
+
:maxCount="$props.maxCount" :disabled="props.disabled" :beforeUpload="beforeUpload"
|
|
115
|
+
:customRequest="createAwsS3Request($props.awsS3, $props.rootDir)" @preview="handlePreview">
|
|
116
|
+
<i v-if="fileList.length < props.maxCount"
|
|
117
|
+
class="i-ant-design:plus-outlined text-[26px] text-$antd-colorTextTertiary" />
|
|
78
118
|
</AUpload>
|
|
79
119
|
|
|
80
120
|
<div style="display: none">
|
|
81
121
|
<AImagePreviewGroup :preview="preview">
|
|
82
|
-
<AImage
|
|
83
|
-
v-for="item of fileList"
|
|
84
|
-
:key="item.uid"
|
|
85
|
-
:src="item.url || item.thumbUrl"
|
|
86
|
-
/>
|
|
122
|
+
<AImage v-for="item of fileList" :key="item.uid" :src="item.url || item.thumbUrl" />
|
|
87
123
|
</AImagePreviewGroup>
|
|
88
124
|
</div>
|
|
89
125
|
</div>
|
|
@@ -117,7 +153,7 @@ function beforeUpload(file: UploadFile) {
|
|
|
117
153
|
margin: 0;
|
|
118
154
|
}
|
|
119
155
|
|
|
120
|
-
:deep(.ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item::before
|
|
156
|
+
:deep(.ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item::before) {
|
|
121
157
|
width: 100%;
|
|
122
158
|
height: 100%;
|
|
123
159
|
}
|
|
@@ -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>
|