@truenewx/tnxvue3 3.4.1 → 3.4.3

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.
@@ -1,12 +1,40 @@
1
1
  /**
2
2
  * 基于Vue的路由器构建函数
3
3
  */
4
- import {FunctionUtil, NetUtil} from '@truenewx/tnxcore/src/tnxcore-util';
4
+ import {
5
+ createRouter,
6
+ createWebHashHistory,
7
+ Router,
8
+ RouteRecordRaw,
9
+ RouterHistory,
10
+ RouteLocationNormalized,
11
+ RouteLocationNormalizedLoaded,
12
+ RouteLocationNormalizedLoadedGeneric,
13
+ NavigationGuardNext,
14
+ } from 'vue-router';
15
+ import NetUtil from '../../tnxcore/src/util/net';
16
+
17
+ export type RouteItem = RouteRecordRaw & {
18
+ path: string;
19
+ icon?: string;
20
+ meta?: {
21
+ superiorPath?: string;
22
+ page?: string;
23
+ cache: Record<string, any>;
24
+ historyFrom?: any;
25
+ isHistory: () => boolean;
26
+ };
27
+ page?: string;
28
+ component?: () => Promise<any>;
29
+ redirect?: never;
30
+ alias?: string;
31
+ subs?: RouteItem[];
32
+ }
5
33
 
6
- function addRoute(routes, superiorPath, item, fnImportPage) {
34
+ function addRoute(routes: RouteRecordRaw[], superiorPath: string, item: RouteItem, fnImportPage: (page: string) => Promise<any>): void {
7
35
  if (item && item.path) {
8
36
  let page = item.page || item.path.replace(/\/:[a-zA-Z0-9_]+/g, '');
9
- let route = {
37
+ let route: RouteItem = {
10
38
  path: item.path,
11
39
  meta: {
12
40
  superiorPath: superiorPath,
@@ -34,7 +62,7 @@ function addRoute(routes, superiorPath, item, fnImportPage) {
34
62
  }
35
63
  }
36
64
 
37
- function applyItemsToRoutes(superiorPath, items, routes, fnImportPage) {
65
+ function applyItemsToRoutes(superiorPath: string, items: RouteItem[], routes: RouteRecordRaw[], fnImportPage: (page: string) => Promise<any>): void {
38
66
  if (items && items.length) {
39
67
  items.forEach(item => {
40
68
  if (item) {
@@ -45,14 +73,14 @@ function applyItemsToRoutes(superiorPath, items, routes, fnImportPage) {
45
73
  }
46
74
  }
47
75
 
48
- function instantiatePath(path, params) {
49
- if (path && path.contains('/:')) {
76
+ function instantiatePath(path: string, params?: Record<string, any>): string {
77
+ if (path && path.includes('/:')) {
50
78
  if (params) {
51
79
  Object.keys(params).forEach(key => {
52
80
  path = path.replace('/:' + key + '/', '/' + params[key] + '/');
53
81
  });
54
82
  }
55
- if (path.contains('/:')) { // 参数替换完之后,还有路径参数,则为无效路径,返回首页
83
+ if (path.includes('/:')) { // 参数替换完之后,还有路径参数,则为无效路径,返回首页
56
84
  console.warn(
57
85
  '路径中的参数无法获得参数值,请确保具有参数的路径所属菜单项的下级菜单路径包含相同的参数:' + path);
58
86
  return '/';
@@ -61,33 +89,29 @@ function instantiatePath(path, params) {
61
89
  return path;
62
90
  }
63
91
 
64
- function getCurrentRoute(router) {
65
- return router.currentRoute._value;
92
+ function getCurrentRoute(router: Router): RouteLocationNormalizedLoadedGeneric {
93
+ return router.currentRoute.value;
66
94
  }
67
95
 
68
- export default function (VueRouter, menu, fnImportPage) {
69
- let items;
70
- if (Array.isArray(menu)) {
71
- items = [];
72
- menu.forEach((m) => {
73
- if (Array.isArray(m.items)) {
74
- items = items.concat(m.items);
75
- } else { // menu实际上是items
76
- items.push(m);
77
- }
78
- });
79
- } else {
80
- items = menu.items;
81
- }
96
+ export type VueRouter = Router & {
97
+ history: RouterHistory;
98
+ prev?: RouteLocationNormalizedLoaded;
99
+ $beforeLeaveHandlers: Record<string, (to: RouteLocationNormalized) => boolean>;
100
+ beforeLeave: (handler: (to: RouteLocationNormalized) => boolean) => void;
101
+ pushState: (path: string) => boolean;
102
+ replaceState: (path: string) => boolean;
103
+ backTo: (path?: string) => void;
104
+ }
82
105
 
83
- const routes = [];
84
- applyItemsToRoutes(undefined, items, routes, fnImportPage);
106
+ export default function (items: RouteItem[], fnImportPage: (page: string) => Promise<any>): VueRouter {
107
+ const routes: RouteRecordRaw[] = [];
108
+ applyItemsToRoutes('', items, routes, fnImportPage);
85
109
 
86
- const routerHistory = VueRouter.createHistory();
87
- const router = VueRouter.createRouter({
110
+ const routerHistory = createWebHashHistory();
111
+ const router: VueRouter = createRouter({
88
112
  history: routerHistory,
89
113
  routes,
90
- });
114
+ }) as VueRouter;
91
115
  router.history = routerHistory;
92
116
 
93
117
  // 浏览器的返回事件触发位于VueRouter的钩子执行和页面渲染之后,这意味着$route.meta.historyFrom必须在页面渲染完之后才具有正确的值
@@ -103,19 +127,13 @@ export default function (VueRouter, menu, fnImportPage) {
103
127
 
104
128
  // 注册离开页面前事件处理支持
105
129
  router.$beforeLeaveHandlers = {};
106
- router.beforeLeave = function (handler) {
107
- if (typeof handler === 'function') {
108
- let $route = getCurrentRoute(router);
109
- let path = $route.path;
110
- router.$beforeLeaveHandlers[path] = handler;
111
- }
130
+ router.beforeLeave = function (handler: (to: RouteLocationNormalized) => boolean): void {
131
+ let $route = getCurrentRoute(router);
132
+ let path = $route.path;
133
+ router.$beforeLeaveHandlers[path] = handler;
112
134
  };
113
135
 
114
- router.beforeEach((to, from, next) => {
115
- if (typeof window.tnx.router.beforeLeave === 'function') {
116
- window.tnx.router.beforeLeave(router, from);
117
- }
118
-
136
+ router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, next: NavigationGuardNext): void => {
119
137
  let allow = true;
120
138
  let beforeLeaveHandler = router.$beforeLeaveHandlers[from.path];
121
139
  if (beforeLeaveHandler) {
@@ -128,7 +146,7 @@ export default function (VueRouter, menu, fnImportPage) {
128
146
  }
129
147
  });
130
148
 
131
- router.afterEach((to, from) => {
149
+ router.afterEach((to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded): void => {
132
150
  router.prev = from;
133
151
  // 前后路径相同,但全路径不同(意味着参数不同),则需要刷新页面,否则页面不会刷新
134
152
  if (to.path === from.path && to.fullPath !== from.fullPath) {
@@ -136,11 +154,11 @@ export default function (VueRouter, menu, fnImportPage) {
136
154
  }
137
155
  });
138
156
 
139
- router.back = FunctionUtil.around(router.back, function (back, path) {
140
- if (!router.prev || !router.prev.href) { // 没有href,说明当前页面为刷新后进入的第一个页面,无法简单返回
157
+ router.backTo = function (path?: string): void {
158
+ if (!router.prev?.path) { // 没有path,说明当前页面为刷新后进入的第一个页面,无法简单返回
141
159
  let $route = getCurrentRoute(router);
142
160
  if (!path) { // 未指定默认返回路径,则返回上一级页面
143
- path = $route.meta.superiorPath;
161
+ path = $route.meta.superiorPath as string;
144
162
  }
145
163
  path = instantiatePath(path, $route.params);
146
164
  if (path) {
@@ -148,10 +166,10 @@ export default function (VueRouter, menu, fnImportPage) {
148
166
  return;
149
167
  }
150
168
  }
151
- back.call(router);
152
- });
169
+ router.back.call(router);
170
+ }
153
171
 
154
- router.pushState = function (path) {
172
+ router.pushState = function (path: string): boolean {
155
173
  let success = NetUtil.pushState('#' + path);
156
174
  if (!success) {
157
175
  this.push(path);
@@ -159,7 +177,7 @@ export default function (VueRouter, menu, fnImportPage) {
159
177
  return success;
160
178
  };
161
179
 
162
- router.replaceState = function (path) {
180
+ router.replaceState = function (path: string): boolean {
163
181
  let success = NetUtil.replaceState('#' + path);
164
182
  if (!success) {
165
183
  this.replace(path);
package/src/tnxvue.ts ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * 基于Vue 3的扩展支持
3
+ */
4
+ import Tnx from '../../tnxcore/src/tnxcore';
5
+ import Text from './text/Text.vue';
6
+ import Percent from './percent/Percent.vue';
7
+ import * as Vue from 'vue';
8
+ import mitt, {Emitter, EventType} from 'mitt';
9
+ import {Router} from 'vue-router';
10
+
11
+ export type EventBus = Emitter<Record<EventType, unknown>> & {
12
+ once(name: EventType, handler: (arg: unknown) => void): void;
13
+ };
14
+
15
+ export type ButtonOptions = {
16
+ text: string;
17
+ click: ((yes: boolean, close: () => void) => boolean | undefined) | ((close: () => void) => boolean | undefined);
18
+ type?: string;
19
+ }
20
+
21
+ export type DialogOptions = {
22
+ type?: OpenType;
23
+ theme?: string;
24
+ click?: boolean | ((yes: boolean, close: () => void) => boolean | undefined) | ((close: () => void) => boolean | undefined);
25
+ buttonText?: string | string[];
26
+ buttons?: ButtonOptions[];
27
+ }
28
+
29
+ export type OpenType = 'alert' | 'confirm' | 'close' | 'none';
30
+
31
+ export type DrawerOptions = DialogOptions
32
+
33
+ export type OpenOptions = DialogOptions & {
34
+ mode?: 'dialog' | 'drawer';
35
+ title?: string;
36
+ }
37
+
38
+ export default class TnxVue extends Tnx {
39
+
40
+ router: Router;
41
+ eventBus: EventBus;
42
+
43
+ static {
44
+ TnxVue.Libs.Vue = Vue;
45
+ }
46
+
47
+ components: Record<string, Vue.Component | Vue.DefineComponent> = {
48
+ Text,
49
+ Percent,
50
+ };
51
+
52
+ constructor(apiBaseUrl: string) {
53
+ super(apiBaseUrl);
54
+ }
55
+
56
+ install(app: Vue.App) {
57
+ for (let key of Object.keys(this.components)) {
58
+ const component = this.components[key];
59
+ app.component(component.name, component);
60
+ }
61
+ }
62
+
63
+ createVueApp(rootComponent: Vue.Component, router: Router, rootProps?: Record<string, any>): Vue.App {
64
+ let app = Vue.createApp(rootComponent, rootProps);
65
+ app.use(this);
66
+ if (router) {
67
+ app.use(router);
68
+ this.router = app.config.globalProperties.$router;
69
+ } else if (this.router) {
70
+ app.config.globalProperties.$router = this.router;
71
+ }
72
+
73
+ if (!this.eventBus) {
74
+ this.eventBus = mitt() as EventBus;
75
+ }
76
+ this.eventBus.once = (name: EventType, handler: (arg: unknown) => void) => {
77
+ this.eventBus.all.set(name, [handler]);
78
+ }
79
+
80
+ return app;
81
+ }
82
+
83
+ /**
84
+ * 深度监听指定对象,在created()中调用才能生效
85
+ * @param vm vue页面实例
86
+ * @param target 要监听的对象
87
+ * @param handler 处理函数
88
+ */
89
+ deepWatch(vm: Vue.ComponentPublicInstance, target: object, handler: (newValue: any, oldValue: any, path: string) => void): void {
90
+ vm.$watch(() => {
91
+ try {
92
+ return JSON.stringify(target);
93
+ } catch (e) {
94
+ console.error(e);
95
+ }
96
+ return undefined;
97
+ }, (newValue: string, oldValue: string): void => {
98
+ try {
99
+ if (newValue !== oldValue) {
100
+ const newObject = JSON.parse(newValue);
101
+ const oldObject = JSON.parse(oldValue);
102
+ this.deepCompare(newObject, oldObject, '', handler);
103
+ }
104
+ } catch (e) {
105
+ console.error(e);
106
+ }
107
+ }, {deep: true});
108
+ }
109
+
110
+ deepCompare(object1: object, object2: object, path: string, handler: (newValue: any, oldValue: any, path: string) => void): void {
111
+ if (object1) {
112
+ const keys = Object.keys(object1);
113
+ keys.forEach(key => {
114
+ const fullPath = path ? `${path}.${key}` : key;
115
+ if (object2) {
116
+ if (Array.isArray(object1[key]) && Array.isArray(object2[key])) {
117
+ object1[key].forEach((item, index) => {
118
+ this.deepCompare(item, object2[key][index], `${fullPath}[${index}]`, handler);
119
+ });
120
+ } else if (typeof object1[key] === 'object' && typeof object2[key] === 'object') {
121
+ this.deepCompare(object1[key], object2[key], fullPath, handler);
122
+ } else if (object1[key] !== object2[key]) {
123
+ handler(object1[key], object2[key], fullPath);
124
+ }
125
+ } else {
126
+ handler(object1[key], undefined, fullPath);
127
+ }
128
+ });
129
+ }
130
+ }
131
+
132
+ nextTickTimeout(vm: Vue.ComponentPublicInstance, handler: () => void, timeout?: number) {
133
+ vm.$nextTick().then(() => {
134
+ setTimeout(handler, timeout);
135
+ });
136
+ }
137
+
138
+ dialog(content: string | Vue.Component | Vue.DefineComponent,
139
+ title?: string,
140
+ options: DialogOptions = {},
141
+ contentProps: Record<string, any> = {}) {
142
+ const buttons = this.getOptionsButtons(options);
143
+ // 默认不实现,由UI框架扩展层实现
144
+ throw new Error('Unsupported function');
145
+ }
146
+
147
+ protected getOptionsButtons(options: DialogOptions): ButtonOptions[] {
148
+ const buttons = options.buttons || this.getDefaultDialogButtons(options.type, options.click, options.theme);
149
+ if (options.buttonText) {
150
+ if (!Array.isArray(options.buttonText)) {
151
+ options.buttonText = [options.buttonText];
152
+ }
153
+ for (let i = 0; i < buttons.length; i++) {
154
+ let buttonText = options.buttonText[i];
155
+ if (buttonText) {
156
+ buttons[i].text = buttonText;
157
+ }
158
+ }
159
+ }
160
+ return buttons;
161
+ }
162
+
163
+ protected getDefaultDialogButtons(
164
+ type: OpenType,
165
+ click: boolean | ((yes: boolean, close: () => void) => boolean | undefined) | ((close: () => void) => boolean | undefined),
166
+ theme?: string): ButtonOptions[] {
167
+ if (click !== false) {
168
+ if (type === 'none') {
169
+ return [];
170
+ } else if (type === 'confirm') {
171
+ return [{
172
+ text: '确定',
173
+ type: theme || 'primary',
174
+ click(close: () => void) {
175
+ if (typeof click === 'function') {
176
+ return click.call(this, true, close);
177
+ }
178
+ }
179
+ }, {
180
+ text: '取消',
181
+ click(close: () => void) {
182
+ if (typeof click === 'function') {
183
+ return click.call(this, false, close);
184
+ }
185
+ }
186
+ }];
187
+ } else if (type === 'close') {
188
+ return [{
189
+ text: '关闭',
190
+ type: theme,
191
+ click(close: () => void) {
192
+ if (typeof click === 'function') {
193
+ return click.call(this, close);
194
+ }
195
+ }
196
+ }];
197
+ } else {
198
+ return [{
199
+ text: '确定',
200
+ type: theme || 'primary',
201
+ click(close: () => void) {
202
+ if (typeof click === 'function') {
203
+ return click.call(this, close);
204
+ }
205
+ }
206
+ }];
207
+ }
208
+ }
209
+ return [];
210
+ }
211
+
212
+ drawer(content: string | Vue.Component | Vue.DefineComponent,
213
+ title?: string,
214
+ options: DrawerOptions = {},
215
+ contentProps: Record<string, any> = {}) {
216
+ const buttons = this.getOptionsButtons(options);
217
+ // 默认不实现,由UI框架扩展层实现
218
+ throw new Error('Unsupported function');
219
+ }
220
+
221
+ open(component: Vue.Component | Vue.DefineComponent, props?: Record<string, any>, options: OpenOptions = {}) {
222
+ const c = component as any;
223
+ if (typeof c.open === 'function') {
224
+ options = Object.assign({}, c.open(props), options);
225
+ } else if (c.methods && typeof c.methods.open === 'function') {
226
+ options = Object.assign({}, c.methods.open(props), options);
227
+ }
228
+
229
+ let mode = options.mode;
230
+ delete options.mode;
231
+ const title = options.title;
232
+ delete options.title;
233
+ if (mode === 'drawer') {
234
+ return this.drawer(component, title, options, props);
235
+ }
236
+ return this.dialog(component, title, options, props);
237
+ }
238
+
239
+ /**
240
+ * 判断指定对象是否组件实例
241
+ * @param obj 对象
242
+ * @returns {boolean} 是否组件实例
243
+ */
244
+ isComponent(obj: any): boolean {
245
+ return (typeof obj === 'object') && (typeof obj.render === 'function');
246
+ }
247
+
248
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { DefineComponent } from 'vue';
2
+ import '../../tnxcore/src/types.d.ts';
3
+
4
+ declare module '*.vue' {
5
+ const component: DefineComponent<{}, {}, any>;
6
+ export default component;
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": false,
7
+ "noImplicitAny": false,
8
+ "allowJs": false,
9
+ "resolveJsonModule": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "lib": [
13
+ "ES2020",
14
+ "DOM"
15
+ ],
16
+ "types": []
17
+ },
18
+ "include": [
19
+ "src/**/*"
20
+ ]
21
+ }
package/eslint.config.cjs DELETED
@@ -1,58 +0,0 @@
1
- const js = require('@eslint/js');
2
- const globals = require('globals');
3
- const vue = require('eslint-plugin-vue');
4
- const vueParser = require('vue-eslint-parser');
5
-
6
- module.exports = [
7
- js.configs.recommended,
8
- {
9
- files: ['**/*.js'],
10
- languageOptions: {
11
- ecmaVersion: 'latest',
12
- sourceType: 'module',
13
- globals: {
14
- ...globals.browser,
15
- ...globals.node
16
- }
17
- },
18
- rules: {
19
- 'eqeqeq': 'warn',
20
- 'no-unused-vars': ['warn', {
21
- vars: 'local',
22
- args: 'none'
23
- }],
24
- 'no-undef': 'warn',
25
- 'no-useless-escape': 'warn',
26
- 'no-empty': 'off',
27
- }
28
- },
29
- {
30
- files: ['**/*.vue'],
31
- languageOptions: {
32
- parser: vueParser,
33
- parserOptions: {
34
- ecmaVersion: 'latest',
35
- sourceType: 'module'
36
- },
37
- globals: {
38
- ...globals.browser,
39
- ...globals.node
40
- }
41
- },
42
- plugins: {
43
- vue
44
- },
45
- rules: {
46
- 'eqeqeq': 'warn',
47
- 'no-unused-vars': ['warn', {
48
- vars: 'local',
49
- args: 'none'
50
- }],
51
- 'no-undef': 'warn',
52
- 'no-useless-escape': 'warn',
53
- 'no-empty': 'off',
54
- 'vue/no-side-effects-in-computed-properties': 'warn',
55
- 'vue/no-mutating-props': 'warn'
56
- }
57
- }
58
- ];
package/index.html DELETED
@@ -1,12 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>tnxvue3</title>
7
- </head>
8
- <body>
9
- <div id="app"></div>
10
- <script type="module" src="./sample/main.js"></script>
11
- </body>
12
- </html>