@peng_kai/kit 0.3.0-beta.2 → 0.3.0-beta.20
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 +250 -0
- package/admin/components/date/index.ts +5 -0
- package/admin/components/date/presetProps.ts +19 -0
- package/admin/components/filter/src/FilterReset.vue +45 -7
- package/admin/components/filter/src/more/TableSetting.vue +82 -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 +17 -4
- package/admin/components/rich-text/src/editorConfig.ts +10 -1
- package/admin/components/text/index.ts +2 -0
- package/admin/components/text/src/Amount.v2.vue +132 -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 +14 -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 +183 -0
- package/admin/layout/large/index.ts +61 -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/index.scss +2 -2
- package/antd/hooks/useAntdModal.ts +29 -15
- package/antd/hooks/useAntdTable.ts +12 -7
- package/antd/hooks/useAntdTheme.ts +7 -0
- package/antd/hooks/useTableColumns.ts +124 -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 +59 -60
- package/request/interceptors/getDeviceInfo.ts +14 -0
- package/utils/LocaleManager.ts +1 -1
- package/utils/locale/LocaleManager.ts +2 -1
- package/utils/locale/helpers.ts +9 -0
- package/utils/number.ts +0 -1
- package/utils/storage.ts +31 -0
- 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,361 @@
|
|
|
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: () => {
|
|
281
|
+
if (router.currentRoute.value.name !== name)
|
|
282
|
+
router.push({ name })
|
|
283
|
+
},
|
|
284
|
+
order,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return { parentKey, menu };
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// 将含有子菜单的菜单的 trigger 设置为空函数
|
|
291
|
+
const hasSubMenus = menus.filter(menu => menus.some(m => m.parentKey === menu.menu.key));
|
|
292
|
+
hasSubMenus.forEach((menu) => {
|
|
293
|
+
menu.menu.trigger = (() => {}) as any;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return menus;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function printRounesNameInterface(routes: { name: PropertyKey }[]) {
|
|
300
|
+
console.groupCollapsed('路由命名');
|
|
301
|
+
const routesName = new Set();
|
|
302
|
+
routes.forEach((route) => {
|
|
303
|
+
if (typeof route.name === 'string')
|
|
304
|
+
routesName.add(route.name);
|
|
305
|
+
});
|
|
306
|
+
console.log([...routesName.values()].sort().filter(name => !!name).map(name => `${name}: true`).join('\n'));
|
|
307
|
+
console.groupEnd();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function findMenu(menus: TMenu[], key: string) {
|
|
311
|
+
const queue = [...menus];
|
|
312
|
+
|
|
313
|
+
while (queue.length > 0) {
|
|
314
|
+
const menu = queue.shift()!;
|
|
315
|
+
|
|
316
|
+
if (menu.key === key)
|
|
317
|
+
return menu;
|
|
318
|
+
|
|
319
|
+
if (menu.children)
|
|
320
|
+
queue.push(...menu.children);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return undefined;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
function menuToBreadcrumb(menu: TMenu) {
|
|
327
|
+
const ret: TBreadcrumb = reactive({
|
|
328
|
+
title: menu.label,
|
|
329
|
+
icon: menu.icon,
|
|
330
|
+
trigger: menu.trigger,
|
|
331
|
+
children: menu.children?.map(menuToBreadcrumb),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return ret;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getMenuPath(menus: TMenu[], key: string) {
|
|
338
|
+
const path: TMenu[] = [];
|
|
339
|
+
|
|
340
|
+
const _getMenuPath = (menus: TMenu[], key: string) => {
|
|
341
|
+
for (const menu of menus) {
|
|
342
|
+
if (menu.key === key) {
|
|
343
|
+
path.push(menu);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (menu.children) {
|
|
348
|
+
path.push(menu);
|
|
349
|
+
if (_getMenuPath(menu.children, key))
|
|
350
|
+
return true;
|
|
351
|
+
path.pop();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return false;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
_getMenuPath(menus, key);
|
|
359
|
+
|
|
360
|
+
return path as TMenu[];
|
|
361
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useIntervalFn } from '@vueuse/core';
|
|
3
|
-
import
|
|
3
|
+
import { Ckeditor } from '@ckeditor/ckeditor5-vue';
|
|
4
4
|
import { shallowRef } from 'vue';
|
|
5
5
|
import { useInjectDisabled } from 'ant-design-vue/es/config-provider/DisabledContext';
|
|
6
6
|
import { defaultConfig } from './editorConfig';
|
|
@@ -20,7 +20,19 @@ function useClassicEditor() {
|
|
|
20
20
|
</script>
|
|
21
21
|
|
|
22
22
|
<script setup lang="ts">
|
|
23
|
-
const props =
|
|
23
|
+
const props = withDefaults(
|
|
24
|
+
defineProps<{
|
|
25
|
+
modelValue: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
plugins?: string[];
|
|
28
|
+
height: string;
|
|
29
|
+
}>(),
|
|
30
|
+
{
|
|
31
|
+
disabled: false,
|
|
32
|
+
plugins: () => [],
|
|
33
|
+
height: '500px',
|
|
34
|
+
}
|
|
35
|
+
);
|
|
24
36
|
const emits = defineEmits<{
|
|
25
37
|
(e: 'update:modelValue', value: string): void
|
|
26
38
|
}>();
|
|
@@ -33,7 +45,7 @@ editorConfig.extraPlugins = props.plugins;
|
|
|
33
45
|
|
|
34
46
|
<template>
|
|
35
47
|
<div class="editor-wrapper">
|
|
36
|
-
<
|
|
48
|
+
<Ckeditor
|
|
37
49
|
v-if="ClassicEditor"
|
|
38
50
|
:modelValue="props.modelValue"
|
|
39
51
|
:editor="ClassicEditor"
|
|
@@ -50,9 +62,10 @@ editorConfig.extraPlugins = props.plugins;
|
|
|
50
62
|
<style scoped lang="scss">
|
|
51
63
|
.editor-wrapper {
|
|
52
64
|
--loading-color: var(--antd-colorPrimary);
|
|
65
|
+
min-height: 40px;
|
|
53
66
|
|
|
54
67
|
:deep(.ck-editor__main > .ck-content) {
|
|
55
|
-
height:
|
|
68
|
+
height: v-bind('props.height');
|
|
56
69
|
overflow: auto;
|
|
57
70
|
}
|
|
58
71
|
}
|
|
@@ -95,7 +95,7 @@ export const defaultConfig: Record<string, any> = {
|
|
|
95
95
|
},
|
|
96
96
|
},
|
|
97
97
|
link: {
|
|
98
|
-
addTargetToExternalLinks:
|
|
98
|
+
addTargetToExternalLinks: false,
|
|
99
99
|
defaultProtocol: 'https://',
|
|
100
100
|
decorators: {
|
|
101
101
|
toggleDownloadable: {
|
|
@@ -105,6 +105,15 @@ export const defaultConfig: Record<string, any> = {
|
|
|
105
105
|
download: 'file',
|
|
106
106
|
},
|
|
107
107
|
},
|
|
108
|
+
openInNewTab: {
|
|
109
|
+
mode: 'manual',
|
|
110
|
+
label: '打开新标签页',
|
|
111
|
+
defaultValue: true, // This option will be selected by default.
|
|
112
|
+
attributes: {
|
|
113
|
+
target: '_blank',
|
|
114
|
+
rel: 'noopener noreferrer'
|
|
115
|
+
}
|
|
116
|
+
}
|
|
108
117
|
},
|
|
109
118
|
},
|
|
110
119
|
table: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Hash from './src/Hash.vue';
|
|
2
2
|
import Amount from './src/Amount.vue';
|
|
3
|
+
import Amount2 from './src/Amount.v2.vue';
|
|
3
4
|
import Datetime from './src/Datetime.vue';
|
|
4
5
|
import Duration from './src/Duration.vue';
|
|
5
6
|
import IP from './src/IP.vue';
|
|
@@ -10,6 +11,7 @@ export { createDateTypeSwitcher } from './src/createDateTypeSwitcher';
|
|
|
10
11
|
export const Text = {
|
|
11
12
|
Hash,
|
|
12
13
|
Amount,
|
|
14
|
+
Amount2,
|
|
13
15
|
Datetime,
|
|
14
16
|
Duration,
|
|
15
17
|
IP,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!-- eslint-disable jsdoc/no-multi-asterisks -->
|
|
2
|
+
|
|
3
|
+
<script setup lang="ts">
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import type { BigNumber } from 'bignumber.js';
|
|
6
|
+
import bignumber from 'bignumber.js';
|
|
7
|
+
import CurrencyIcon, {textIcons} from '../../currency/src/CurrencyIcon.vue';
|
|
8
|
+
import Num from './Num.vue';
|
|
9
|
+
|
|
10
|
+
// 由于 Vue 目前不支持 Props `interface AmountProps extends CurrencyIconProps, NumProps{}` 多继承写法,所以暂时这样写
|
|
11
|
+
interface AmountProps {
|
|
12
|
+
/** ****************** 与 Num 组件 Props 同步 ********************/
|
|
13
|
+
/**
|
|
14
|
+
* 数值
|
|
15
|
+
*/
|
|
16
|
+
value: BigNumber.Value | undefined | null
|
|
17
|
+
/**
|
|
18
|
+
* 小数位数
|
|
19
|
+
*/
|
|
20
|
+
decimals?: number | [number, number]
|
|
21
|
+
/**
|
|
22
|
+
* 舍入方式
|
|
23
|
+
*
|
|
24
|
+
* - up: 向上取整
|
|
25
|
+
* - down: 向下取整
|
|
26
|
+
* - half-up: 四舍五入
|
|
27
|
+
* - half-even: 银行家算法
|
|
28
|
+
*/
|
|
29
|
+
round?: 'up' | 'down' | 'half-up' | 'half-even'
|
|
30
|
+
/**
|
|
31
|
+
* 格式化方式
|
|
32
|
+
*
|
|
33
|
+
* - original: 原始
|
|
34
|
+
* - pad-dec: 自适应补零,即整数位多1位,小数位就少1位
|
|
35
|
+
* - fixed-dec: 固定小数位数
|
|
36
|
+
* - max-dec: 最大小数位数
|
|
37
|
+
* - min-dec: 最小小数位数
|
|
38
|
+
* - clamp-dec: 限制小数位数
|
|
39
|
+
*/
|
|
40
|
+
format?: 'original' | 'pad-dec' | 'fixed-dec' | 'max-dec' | 'min-dec' | 'clamp-dec'
|
|
41
|
+
/**
|
|
42
|
+
* 着色
|
|
43
|
+
*
|
|
44
|
+
* - inherit: 继承
|
|
45
|
+
* - neg: 负数
|
|
46
|
+
* - pos: 正数
|
|
47
|
+
* - full: 全部
|
|
48
|
+
*/
|
|
49
|
+
colored?: 'inherit' | 'neg' | 'pos' | 'full'
|
|
50
|
+
/**
|
|
51
|
+
* 是否显示正数符号
|
|
52
|
+
*/
|
|
53
|
+
showPos?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* 是否使用分组分隔符(千分符)
|
|
56
|
+
*/
|
|
57
|
+
grouping?: boolean
|
|
58
|
+
/**
|
|
59
|
+
* 是否弱化无价值的零值
|
|
60
|
+
*/
|
|
61
|
+
weakPad?: boolean
|
|
62
|
+
|
|
63
|
+
/** ****************** 与 CurrencyIcon 组件 Props 同步 ********************/
|
|
64
|
+
symbol?: string
|
|
65
|
+
size?: string
|
|
66
|
+
cdn?: string
|
|
67
|
+
iconType?: 'img' | 'text'
|
|
68
|
+
|
|
69
|
+
/** ****************** 本组件的 Props ********************/
|
|
70
|
+
/** 图标显示位置 */
|
|
71
|
+
iconPos?: 'left' | 'right' | 'hidden'
|
|
72
|
+
/** 字符图标是否使用数字的颜色 */
|
|
73
|
+
useNumColor?: boolean
|
|
74
|
+
/** 值的精度(整数 value 除以 precision) */
|
|
75
|
+
precision?: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const props = withDefaults(defineProps<AmountProps>(), {
|
|
79
|
+
// Num 组件 Props
|
|
80
|
+
value: () => bignumber(0),
|
|
81
|
+
decimals: 8,
|
|
82
|
+
round: 'down',
|
|
83
|
+
weakPad: true,
|
|
84
|
+
showPos: false,
|
|
85
|
+
colored: 'inherit',
|
|
86
|
+
format: 'original',
|
|
87
|
+
grouping: true,
|
|
88
|
+
|
|
89
|
+
// CurrencyIcon 组件 Props
|
|
90
|
+
size: '1.1em',
|
|
91
|
+
// symbol: '',
|
|
92
|
+
iconType: 'img',
|
|
93
|
+
|
|
94
|
+
// 本组件 Props
|
|
95
|
+
iconPos: 'left',
|
|
96
|
+
useNumColor: false,
|
|
97
|
+
precision: 0,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const isTextIcon = computed(() => {
|
|
101
|
+
return props.iconType === 'text' || textIcons.value.includes(String(props.symbol));
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<span class="amount flex-inline" :class="[isTextIcon ? 'items-baseline' : 'items-center']">
|
|
107
|
+
<CurrencyIcon v-if="props.iconPos === 'left' && props.symbol && !props.useNumColor" v-bind="props" class="currency-icon" />
|
|
108
|
+
<Num v-bind="props" :value="bignumber(props.value ?? 0).dividedBy(10 ** props.precision)">
|
|
109
|
+
<template #signLeft>
|
|
110
|
+
<CurrencyIcon v-if="props.iconPos === 'left' && props.symbol && props.useNumColor" v-bind="props" class="currency-icon" />
|
|
111
|
+
</template>
|
|
112
|
+
</Num>
|
|
113
|
+
<CurrencyIcon v-if="props.iconPos === 'right'" v-bind="props" class="currency-icon" />
|
|
114
|
+
</span>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style lang="scss" scoped>
|
|
118
|
+
.amount {
|
|
119
|
+
line-height: 1em;
|
|
120
|
+
}
|
|
121
|
+
.symbol-icon {
|
|
122
|
+
line-height: inherit;
|
|
123
|
+
font-size: inherit;
|
|
124
|
+
}
|
|
125
|
+
.amount .currency-icon:first-child {
|
|
126
|
+
margin-right: 0.15em;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.amount .currency-icon:last-child {
|
|
130
|
+
margin-left: 0.15em;
|
|
131
|
+
}
|
|
132
|
+
</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">
|