@kine-design/crud 0.0.1-beta.2 → 0.0.1-beta.21

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.
Files changed (118) hide show
  1. package/.vlaude/last-session-id +1 -0
  2. package/components/crudPage/KCrudPage.tsx +178 -0
  3. package/components/crudPage/crudPage.css +64 -0
  4. package/components/crudPage/index.ts +10 -0
  5. package/components/editableTable/KEditableTable.tsx +281 -0
  6. package/components/editableTable/editableTable.css +268 -0
  7. package/components/editableTable/index.ts +10 -0
  8. package/components/formPage/KApprovalDialog.tsx +142 -0
  9. package/components/formPage/KFormCard.tsx +65 -0
  10. package/components/formPage/KFormPage.tsx +128 -0
  11. package/components/formPage/KMasterDetailPage.tsx +205 -0
  12. package/components/formPage/KStickyActionBar.tsx +33 -0
  13. package/components/formPage/formPage.css +629 -0
  14. package/components/formPage/index.ts +14 -0
  15. package/components/layout/KContent.tsx +20 -0
  16. package/components/layout/KHeader.tsx +37 -0
  17. package/components/layout/KLayout.tsx +82 -0
  18. package/components/layout/KSider.tsx +80 -0
  19. package/components/layout/index.ts +18 -0
  20. package/components/layout/layout.css +262 -0
  21. package/components/login/KLoginPage.tsx +129 -0
  22. package/components/login/index.ts +10 -0
  23. package/components/login/login.css +118 -0
  24. package/components/navMenu/KNavMenu.tsx +175 -0
  25. package/components/navMenu/index.ts +2 -0
  26. package/components/navMenu/navMenu.css +197 -0
  27. package/components/pageHeader/KPageHeader.tsx +85 -0
  28. package/components/pageHeader/index.ts +9 -0
  29. package/components/pageHeader/pageHeader.css +93 -0
  30. package/components/searchTable/KSearchTable.tsx +138 -0
  31. package/components/searchTable/index.ts +10 -0
  32. package/components/searchTable/searchTable.css +121 -0
  33. package/components/upload/KFileList.tsx +95 -0
  34. package/components/upload/KImageUpload.tsx +286 -0
  35. package/components/upload/KUpload.tsx +206 -0
  36. package/components/upload/index.ts +13 -0
  37. package/components/upload/types.ts +26 -0
  38. package/components/upload/upload.css +345 -0
  39. package/composables/auth/authGuard.ts +128 -0
  40. package/composables/auth/index.ts +23 -0
  41. package/composables/auth/types.ts +109 -0
  42. package/composables/auth/useAuth.ts +278 -0
  43. package/composables/auth/vCan.ts +95 -0
  44. package/composables/defineRepository.ts +224 -0
  45. package/composables/error/createErrorHandler.ts +46 -0
  46. package/composables/error/defaultFeedbackHandler.ts +76 -0
  47. package/composables/error/dispatchError.ts +70 -0
  48. package/composables/error/index.ts +32 -0
  49. package/composables/error/types.ts +57 -0
  50. package/composables/error/useErrorHandler.ts +41 -0
  51. package/composables/form/index.ts +18 -0
  52. package/composables/form/renderFormField.tsx +119 -0
  53. package/composables/form/types.ts +129 -0
  54. package/composables/form/useFormPage.ts +183 -0
  55. package/composables/index.ts +62 -0
  56. package/composables/page/index.ts +11 -0
  57. package/composables/page/types.ts +62 -0
  58. package/composables/page/useCrudPage.ts +88 -0
  59. package/composables/request/composables.ts +206 -0
  60. package/composables/request/controlGate.ts +143 -0
  61. package/composables/request/createRequest.ts +173 -0
  62. package/composables/request/index.ts +71 -0
  63. package/composables/request/orchestrator.ts +145 -0
  64. package/composables/request/requestBuilder.ts +418 -0
  65. package/composables/request/transport/fetchTransport.ts +79 -0
  66. package/composables/request/transport/xhrTransport.ts +100 -0
  67. package/composables/request/types.ts +226 -0
  68. package/composables/request/upload.ts +146 -0
  69. package/composables/router/createRouterGuard.ts +134 -0
  70. package/composables/router/defineCrudRoutes.ts +116 -0
  71. package/composables/router/index.ts +22 -0
  72. package/composables/router/types.ts +128 -0
  73. package/composables/router/useMenuFromRoutes.ts +109 -0
  74. package/composables/router/useTabStore.ts +183 -0
  75. package/composables/search/index.ts +11 -0
  76. package/composables/search/useAutoCompleteSearch.ts +161 -0
  77. package/composables/setupCrud.ts +43 -0
  78. package/composables/storage/createStorageAdapter.ts +72 -0
  79. package/composables/storage/index.ts +13 -0
  80. package/composables/storage/types.ts +30 -0
  81. package/composables/storage/useStorage.ts +108 -0
  82. package/composables/store/defineUserStore.ts +122 -0
  83. package/composables/store/index.ts +11 -0
  84. package/composables/types.ts +118 -0
  85. package/dist/components/crudPage/KCrudPage.d.ts +14 -0
  86. package/dist/components/crudPage/index.d.ts +9 -0
  87. package/dist/components/editableTable/KEditableTable.d.ts +146 -0
  88. package/dist/components/editableTable/index.d.ts +10 -0
  89. package/dist/components/formPage/KApprovalDialog.d.ts +99 -0
  90. package/dist/components/formPage/KFormCard.d.ts +49 -0
  91. package/dist/components/formPage/KFormPage.d.ts +14 -0
  92. package/dist/components/formPage/KMasterDetailPage.d.ts +14 -0
  93. package/dist/components/formPage/KStickyActionBar.d.ts +16 -0
  94. package/dist/components/formPage/index.d.ts +14 -0
  95. package/dist/components/layout/KLayout.d.ts +7 -4
  96. package/dist/composables/auth/useAuth.d.ts +5 -5
  97. package/dist/composables/error/types.d.ts +2 -1
  98. package/dist/composables/form/index.d.ts +12 -0
  99. package/dist/composables/form/renderFormField.d.ts +11 -0
  100. package/dist/composables/form/types.d.ts +104 -0
  101. package/dist/composables/form/useFormPage.d.ts +38 -0
  102. package/dist/composables/index.d.ts +2 -0
  103. package/dist/composables/page/index.d.ts +10 -0
  104. package/dist/composables/page/types.d.ts +61 -0
  105. package/dist/composables/page/useCrudPage.d.ts +14 -0
  106. package/dist/composables/request/createRequest.d.ts +2 -0
  107. package/dist/composables/request/requestBuilder.d.ts +2 -0
  108. package/dist/composables/search/index.d.ts +10 -0
  109. package/dist/composables/search/useAutoCompleteSearch.d.ts +50 -0
  110. package/dist/crud.css +2499 -663
  111. package/dist/crud.js +11512 -2910
  112. package/dist/index.d.ts +11 -0
  113. package/dist/setup.d.ts +2 -2
  114. package/index.ts +144 -0
  115. package/package.json +20 -19
  116. package/setup.ts +288 -0
  117. package/tsconfig.json +12 -0
  118. package/vite.config.build.ts +52 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * @description 请求模块类型定义
3
+ * @author 阿怪
4
+ * @date 2026/3/15
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ // ────────────────────────────────────────────────────────────────────────────
11
+ // 请求方法
12
+ // ────────────────────────────────────────────────────────────────────────────
13
+
14
+ export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
15
+
16
+ // ────────────────────────────────────────────────────────────────────────────
17
+ // 控制策略
18
+ // ────────────────────────────────────────────────────────────────────────────
19
+
20
+ export interface ControlPolicy {
21
+ /** 防抖延迟(毫秒) */
22
+ debounce?: number;
23
+ /** 节流间隔(毫秒) */
24
+ throttle?: number;
25
+ /** 是否去重(相同请求复用同一个 Promise) */
26
+ deduplicate?: boolean;
27
+ /** 优先级,数值越大越优先 */
28
+ priority?: number;
29
+ }
30
+
31
+ // ────────────────────────────────────────────────────────────────────────────
32
+ // 缓存策略 — discriminated union
33
+ // ────────────────────────────────────────────────────────────────────────────
34
+
35
+ export type CachePolicy =
36
+ | { type: 'none' }
37
+ | { type: 'cacheFirst'; maxAge: number }
38
+ | { type: 'staleWhileRevalidate'; maxAge: number };
39
+
40
+ // ────────────────────────────────────────────────────────────────────────────
41
+ // 重试策略 — discriminated union
42
+ // ────────────────────────────────────────────────────────────────────────────
43
+
44
+ export type RetryPolicy =
45
+ | { type: 'none' }
46
+ | { type: 'fixed'; maxAttempts: number; delay: number }
47
+ | { type: 'exponential'; maxAttempts: number; initialDelay: number; multiplier: number; maxDelay: number };
48
+
49
+ // ────────────────────────────────────────────────────────────────────────────
50
+ // 生命周期
51
+ // ────────────────────────────────────────────────────────────────────────────
52
+
53
+ /** vue: 跟随组件生命周期自动取消;persistent: 不自动取消;manual: 手动控制 */
54
+ export type Lifecycle = 'vue' | 'persistent' | 'manual';
55
+
56
+ // ────────────────────────────────────────────────────────────────────────────
57
+ // 请求配置
58
+ // ────────────────────────────────────────────────────────────────────────────
59
+
60
+ export interface RequestConfig {
61
+ lifecycle?: Lifecycle;
62
+ control?: ControlPolicy;
63
+ cache?: CachePolicy;
64
+ retry?: RetryPolicy;
65
+ /** 单次请求超时(毫秒) */
66
+ timeout?: number;
67
+ /** 包含重试在内的总超时(毫秒) */
68
+ totalTimeout?: number;
69
+ }
70
+
71
+ // ────────────────────────────────────────────────────────────────────────────
72
+ // 响应包装
73
+ // ────────────────────────────────────────────────────────────────────────────
74
+
75
+ export interface WrappedResponse<T> {
76
+ success: boolean;
77
+ data: T;
78
+ message: string;
79
+ }
80
+
81
+ // ────────────────────────────────────────────────────────────────────────────
82
+ // 错误类型
83
+ // ────────────────────────────────────────────────────────────────────────────
84
+
85
+ /** 业务错误:后端返回 success=false */
86
+ export class BusinessError extends Error {
87
+ override readonly name = 'BusinessError';
88
+ constructor(message: string) {
89
+ super(message);
90
+ }
91
+ }
92
+
93
+ /** 网络错误 — discriminated union */
94
+ export type NetworkError =
95
+ | { type: 'invalidURL'; url: string }
96
+ | { type: 'timeout' }
97
+ | { type: 'httpError'; status: number; statusText: string }
98
+ | { type: 'decodingFailed'; reason: string }
99
+ | { type: 'aborted' }
100
+ | { type: 'unknown'; error: unknown };
101
+
102
+ /** 将 NetworkError 包装为可 throw 的 Error */
103
+ export class NetworkRequestError extends Error {
104
+ override readonly name = 'NetworkRequestError';
105
+ readonly detail: NetworkError;
106
+
107
+ constructor(detail: NetworkError) {
108
+ super(networkErrorMessage(detail));
109
+ this.detail = detail;
110
+ }
111
+ }
112
+
113
+ function networkErrorMessage(e: NetworkError): string {
114
+ switch (e.type) {
115
+ case 'invalidURL': return `无效的 URL: ${e.url}`;
116
+ case 'timeout': return '请求超时';
117
+ case 'httpError': return `HTTP ${e.status} ${e.statusText}`;
118
+ case 'decodingFailed': return `解码失败: ${e.reason}`;
119
+ case 'aborted': return '请求已取消';
120
+ case 'unknown': return `未知错误: ${String(e.error)}`;
121
+ }
122
+ }
123
+
124
+ // ────────────────────────────────────────────────────────────────────────────
125
+ // 用户反馈接口
126
+ // ────────────────────────────────────────────────────────────────────────────
127
+
128
+ export interface UserFeedbackHandler {
129
+ showSuccess(message: string): void;
130
+ showError(message: string): void;
131
+ showWarning(message: string): void;
132
+ handleAuthenticationFailure(): void;
133
+ }
134
+
135
+ // ────────────────────────────────────────────────────────────────────────────
136
+ // Transport 传输层接口
137
+ // ────────────────────────────────────────────────────────────────────────────
138
+
139
+ export interface TransportRequest {
140
+ url: string;
141
+ method: RequestMethod;
142
+ headers: Record<string, string>;
143
+ body?: BodyInit | null;
144
+ signal?: AbortSignal;
145
+ timeout?: number;
146
+ /** 上传进度回调(仅 XHR 支持) */
147
+ onUploadProgress?: (event: { loaded: number; total: number }) => void;
148
+ }
149
+
150
+ export interface TransportResponse {
151
+ status: number;
152
+ statusText: string;
153
+ headers: Record<string, string>;
154
+ body: ReadableStream<Uint8Array> | null;
155
+ /** 读取响应文本 */
156
+ text(): Promise<string>;
157
+ /** 读取响应 JSON */
158
+ json<T = unknown>(): Promise<T>;
159
+ /** 读取响应 Blob */
160
+ blob(): Promise<Blob>;
161
+ }
162
+
163
+ export interface Transport {
164
+ send(request: TransportRequest): Promise<TransportResponse>;
165
+ }
166
+
167
+ // ────────────────────────────────────────────────────────────────────────────
168
+ // 请求客户端配置
169
+ // ────────────────────────────────────────────────────────────────────────────
170
+
171
+ export interface RequestOptions {
172
+ /** 基础 URL,所有请求自动拼接 */
173
+ baseURL?: string;
174
+ /** 默认请求头 */
175
+ headers?: Record<string, string>;
176
+ /** 获取 token 的函数 */
177
+ getToken?: () => string | null | undefined;
178
+ /** 401 时的回调 */
179
+ onUnauthorized?: () => void;
180
+ /** 默认请求配置 */
181
+ defaultConfig?: RequestConfig;
182
+ /** 传输层实现,默认使用 fetchTransport */
183
+ transport?: Transport;
184
+ /** 用户反馈处理器 */
185
+ feedback?: UserFeedbackHandler;
186
+ /** 响应拦截器 */
187
+ responseInterceptor?: <T>(data: T) => T;
188
+ }
189
+
190
+ // ────────────────────────────────────────────────────────────────────────────
191
+ // 编排器
192
+ // ────────────────────────────────────────────────────────────────────────────
193
+
194
+ export interface OrchestratorNode<T = unknown> {
195
+ id: string;
196
+ execute: () => Promise<T>;
197
+ dependencies: string[];
198
+ }
199
+
200
+ export type FailureStrategy = 'failFast' | 'continueOnError';
201
+
202
+ // ────────────────────────────────────────────────────────────────────────────
203
+ // 上传
204
+ // ────────────────────────────────────────────────────────────────────────────
205
+
206
+ export interface UploadOptions {
207
+ /** 上传接口 URL */
208
+ url: string;
209
+ /** 自定义请求头 */
210
+ headers?: Record<string, string>;
211
+ /** 文件字段名,默认 'file' */
212
+ fieldName?: string;
213
+ /** 附加表单数据 */
214
+ data?: Record<string, string>;
215
+ /** 进度回调 */
216
+ onProgress?: (percent: number) => void;
217
+ }
218
+
219
+ export interface UploaderOptions {
220
+ /** 基础 URL */
221
+ baseURL?: string;
222
+ /** 获取 token */
223
+ getToken?: () => string | null | undefined;
224
+ /** 默认请求头 */
225
+ headers?: Record<string, string>;
226
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @description 上传模块 — 基于 XHR 的文件上传
3
+ * @author 阿怪
4
+ * @date 2026/3/15
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import { ref, type Ref } from 'vue';
11
+ import type { UploaderOptions, UploadOptions, WrappedResponse } from './types';
12
+ import { BusinessError, NetworkRequestError } from './types';
13
+ import { XhrTransport } from './transport/xhrTransport';
14
+
15
+ // ────────────────────────────────────────────────────────────────────────────
16
+ // 上传结果
17
+ // ────────────────────────────────────────────────────────────────────────────
18
+
19
+ export interface UploadHandle<T = unknown> {
20
+ /** 上传进度(0-100) */
21
+ progress: Ref<number>;
22
+ /** 取消上传 */
23
+ abort(): void;
24
+ /** 上传结果 Promise */
25
+ promise: Promise<T>;
26
+ }
27
+
28
+ // ────────────────────────────────────────────────────────────────────────────
29
+ // createUploader
30
+ // ────────────────────────────────────────────────────────────────────────────
31
+
32
+ export interface Uploader {
33
+ /** 上传单个文件 */
34
+ upload<T = unknown>(file: File, options: UploadOptions): UploadHandle<T>;
35
+ /** 上传多个文件 */
36
+ uploadMultiple<T = unknown>(files: File[], options: UploadOptions): UploadHandle<T>;
37
+ }
38
+
39
+ export function createUploader(uploaderOptions: UploaderOptions = {}): Uploader {
40
+ const transport = new XhrTransport();
41
+
42
+ function buildURL(url: string): string {
43
+ if (url.startsWith('http')) return url;
44
+ return `${uploaderOptions.baseURL ?? ''}${url}`;
45
+ }
46
+
47
+ function buildHeaders(options: UploadOptions): Record<string, string> {
48
+ const headers: Record<string, string> = { ...uploaderOptions.headers, ...options.headers };
49
+ const token = uploaderOptions.getToken?.();
50
+ if (token) {
51
+ headers['Authorization'] = `Bearer ${token}`;
52
+ }
53
+ // FormData 不设置 Content-Type,浏览器自动添加 boundary
54
+ return headers;
55
+ }
56
+
57
+ function doUpload<T>(formData: FormData, options: UploadOptions): UploadHandle<T> {
58
+ const progress = ref(0);
59
+ const abortController = new AbortController();
60
+
61
+ const promise = transport.send({
62
+ url: buildURL(options.url),
63
+ method: 'POST',
64
+ headers: buildHeaders(options),
65
+ body: formData,
66
+ signal: abortController.signal,
67
+ onUploadProgress: (event) => {
68
+ const percent = event.total > 0 ? Math.round((event.loaded / event.total) * 100) : 0;
69
+ progress.value = percent;
70
+ options.onProgress?.(percent);
71
+ },
72
+ }).then(async (response) => {
73
+ // HTTP 错误
74
+ if (response.status < 200 || response.status >= 300) {
75
+ throw new NetworkRequestError({
76
+ type: 'httpError',
77
+ status: response.status,
78
+ statusText: response.statusText,
79
+ });
80
+ }
81
+
82
+ const contentType = response.headers['content-type'] ?? '';
83
+ if (contentType.includes('application/json')) {
84
+ const json = await response.json<WrappedResponse<T> | T>();
85
+ if (isWrappedResponse<T>(json)) {
86
+ if (!json.success) throw new BusinessError(json.message || '上传失败');
87
+ return json.data;
88
+ }
89
+ return json as T;
90
+ }
91
+
92
+ return await response.text() as unknown as T;
93
+ });
94
+
95
+ return {
96
+ progress,
97
+ abort: () => abortController.abort('上传已取消'),
98
+ promise,
99
+ };
100
+ }
101
+
102
+ return {
103
+ upload<T>(file: File, options: UploadOptions): UploadHandle<T> {
104
+ const fieldName = options.fieldName ?? 'file';
105
+ const formData = new FormData();
106
+ formData.append(fieldName, file);
107
+
108
+ // 附加数据
109
+ if (options.data) {
110
+ for (const [key, value] of Object.entries(options.data)) {
111
+ formData.append(key, value);
112
+ }
113
+ }
114
+
115
+ return doUpload<T>(formData, options);
116
+ },
117
+
118
+ uploadMultiple<T>(files: File[], options: UploadOptions): UploadHandle<T> {
119
+ const fieldName = options.fieldName ?? 'files';
120
+ const formData = new FormData();
121
+ files.forEach(file => formData.append(fieldName, file));
122
+
123
+ if (options.data) {
124
+ for (const [key, value] of Object.entries(options.data)) {
125
+ formData.append(key, value);
126
+ }
127
+ }
128
+
129
+ return doUpload<T>(formData, options);
130
+ },
131
+ };
132
+ }
133
+
134
+ // ────────────────────────────────────────────────────────────────────────────
135
+ // 内部辅助
136
+ // ────────────────────────────────────────────────────────────────────────────
137
+
138
+ function isWrappedResponse<T>(value: unknown): value is WrappedResponse<T> {
139
+ return (
140
+ typeof value === 'object' &&
141
+ value !== null &&
142
+ 'success' in value &&
143
+ 'data' in value &&
144
+ typeof (value as Record<string, unknown>).success === 'boolean'
145
+ );
146
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @description 整合路由守卫 — 认证 + 权限 + tab 联动
3
+ * @author 阿怪
4
+ * @date 2026/3/15
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import type { AuthReturn } from '../auth/types';
11
+ import type { TabStore, CrudRouteMeta } from './types';
12
+
13
+ // ────────────────────────────────────────────────────────────────────────────
14
+ // 局部 vue-router 类型声明(不引入 vue-router 依赖)
15
+ // ────────────────────────────────────────────────────────────────────────────
16
+
17
+ /** 最小化 RouteLocationNormalized */
18
+ interface RouteLocation {
19
+ path: string;
20
+ name?: string | symbol;
21
+ meta: CrudRouteMeta & Record<string, unknown>;
22
+ }
23
+
24
+ /** 导航结果:undefined 表示放行,string 表示跳转目标路径 */
25
+ type NavigationResult = string | undefined;
26
+
27
+ /** beforeEach 守卫函数签名 */
28
+ export type RouterGuard = (
29
+ to: RouteLocation,
30
+ from: RouteLocation,
31
+ ) => NavigationResult | Promise<NavigationResult>;
32
+
33
+ // ────────────────────────────────────────────────────────────────────────────
34
+ // 选项
35
+ // ────────────────────────────────────────────────────────────────────────────
36
+
37
+ /** createRouterGuard 配置选项 */
38
+ export interface RouterGuardOptions {
39
+ /** useAuth() 返回值,用于认证和权限检查 */
40
+ auth: AuthReturn;
41
+ /** tab store 实例,传入后自动在导航时 addTab */
42
+ tabStore?: TabStore;
43
+ /** 登录页路径,默认 '/login' */
44
+ loginPath?: string;
45
+ /** 权限不足跳转路径,默认 '/403' */
46
+ forbiddenPath?: string;
47
+ }
48
+
49
+ // ────────────────────────────────────────────────────────────────────────────
50
+ // createRouterGuard
51
+ // ────────────────────────────────────────────────────────────────────────────
52
+
53
+ /**
54
+ * 创建整合路由守卫,统一处理认证、权限、tab 联动。
55
+ *
56
+ * 守卫逻辑顺序:
57
+ * 1. 已登录用户访问 loginPath 时重定向到首页
58
+ * 2. 权限检查(meta.can)
59
+ * 3. 登录检查(meta.requiresAuth !== false)
60
+ * 4. 自动 addTab(如果传入 tabStore)
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { useAuth, createRouterGuard } from '@kine-design/crud';
65
+ * import { createTabStore } from '@kine-design/crud';
66
+ *
67
+ * const auth = useAuth();
68
+ * const tabStore = createTabStore();
69
+ * const guard = createRouterGuard({ auth, tabStore, loginPath: '/login' });
70
+ * router.beforeEach(guard);
71
+ * ```
72
+ */
73
+ export function createRouterGuard(options: RouterGuardOptions): RouterGuard {
74
+ const {
75
+ auth,
76
+ tabStore,
77
+ loginPath = '/login',
78
+ forbiddenPath = '/403',
79
+ } = options;
80
+
81
+ return (to: RouteLocation): NavigationResult => {
82
+ const meta = to.meta;
83
+
84
+ // ── 1. 已登录用户访问登录页,重定向到首页 ────────────────────────────
85
+ if (to.path === loginPath && auth.isAuthenticated.value) {
86
+ return '/';
87
+ }
88
+
89
+ // ── 2. 路由级权限检查 ────────────────────────────────────────────────
90
+ if (meta.can) {
91
+ const { resource, action } = meta.can;
92
+ if (!auth.can(resource, action)) {
93
+ if (to.path === forbiddenPath) return undefined;
94
+ return forbiddenPath;
95
+ }
96
+ }
97
+
98
+ // ── 3. 登录检查(requiresAuth 默认为 true)──────────────────────────
99
+ const requiresAuth = meta.requiresAuth !== false;
100
+ if (requiresAuth && !auth.isAuthenticated.value) {
101
+ if (to.path === loginPath) return undefined;
102
+ return loginPath;
103
+ }
104
+
105
+ // ── 4. 自动添加 tab ─────────────────────────────────────────────────
106
+ if (tabStore) {
107
+ addTabFromRoute(tabStore, to);
108
+ }
109
+
110
+ // 放行
111
+ return undefined;
112
+ };
113
+ }
114
+
115
+ // ────────────────────────────────────────────────────────────────────────────
116
+ // addTabFromRoute
117
+ // ────────────────────────────────────────────────────────────────────────────
118
+
119
+ /**
120
+ * 从路由对象自动提取信息创建 tab。
121
+ * 也可独立使用,不依赖 createRouterGuard。
122
+ */
123
+ export function addTabFromRoute(tabStore: TabStore, route: RouteLocation): void {
124
+ const meta = route.meta;
125
+ const title = meta.title ?? (typeof route.name === 'string' ? route.name : route.path);
126
+ const name = typeof route.name === 'string' ? route.name : route.path;
127
+
128
+ tabStore.addTab({
129
+ path: route.path,
130
+ title,
131
+ name,
132
+ closable: true,
133
+ });
134
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @description CRUD 路由便利函数 — 根据实体配置批量生成路由记录
3
+ * @author 阿怪
4
+ * @date 2026/2/26
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import type { CrudRouteConfig, CrudRouteRecord } from './types';
11
+
12
+ // ────────────────────────────────────────────────────────────────────────────
13
+ // 内部辅助
14
+ // ────────────────────────────────────────────────────────────────────────────
15
+
16
+ /** 路由操作配置项 */
17
+ interface OpSpec {
18
+ /** 操作类型 */
19
+ op: 'list' | 'new' | 'modify' | 'detail';
20
+ /** 相对于 basePath 的后缀,空字符串表示直接用 basePath */
21
+ suffix: string;
22
+ /** 权限 action */
23
+ action: 'read' | 'create' | 'update';
24
+ /** 组件动态导入函数 */
25
+ component: (() => Promise<unknown>) | undefined;
26
+ /** 操作类型中文后缀,用于自动拼接 title */
27
+ titleSuffix: string;
28
+ /** 列表页默认缓存,其他默认不缓存 */
29
+ keepAlive: boolean;
30
+ /** 是否隐藏菜单(新建/编辑/详情不显示在导航中) */
31
+ hidden: boolean;
32
+ }
33
+
34
+ // ────────────────────────────────────────────────────────────────────────────
35
+ // defineCrudRoutes
36
+ // ────────────────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * 根据实体配置生成标准 CRUD 路由记录数组。
40
+ *
41
+ * 生成规则:
42
+ * - list → `path` (action: 'read')
43
+ * - new → `path/new` (action: 'create')
44
+ * - modify → `path/:id/edit` (action: 'update')
45
+ * - detail → `path/:id` (action: 'read')
46
+ *
47
+ * 未传入 component 的操作不会生成对应路由。
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const orderRoutes = defineCrudRoutes({
52
+ * name: 'Order',
53
+ * path: '/order',
54
+ * list: () => import('./views/Order.vue'),
55
+ * new: () => import('./views/NewOrder.vue'),
56
+ * modify: () => import('./views/ModifyOrder.vue'),
57
+ * detail: () => import('./views/DetailOrder.vue'),
58
+ * permission: 'order',
59
+ * });
60
+ * ```
61
+ */
62
+ export function defineCrudRoutes(config: CrudRouteConfig): CrudRouteRecord[] {
63
+ const { name, path, permission, title, icon } = config;
64
+ const resource = permission ?? name.toLowerCase();
65
+
66
+ // 规范化 basePath,确保以 / 开头、不以 / 结尾
67
+ const basePath = path.replace(/\/+$/, '');
68
+
69
+ const opSpecs: OpSpec[] = [
70
+ { op: 'list', suffix: '', action: 'read', component: config.list, titleSuffix: '', keepAlive: true, hidden: false },
71
+ { op: 'new', suffix: '/new', action: 'create', component: config.new, titleSuffix: ' - 新建', keepAlive: false, hidden: true },
72
+ { op: 'modify', suffix: '/:id/edit', action: 'update', component: config.modify, titleSuffix: ' - 编辑', keepAlive: false, hidden: true },
73
+ { op: 'detail', suffix: '/:id', action: 'read', component: config.detail, titleSuffix: ' - 详情', keepAlive: false, hidden: true },
74
+ ];
75
+
76
+ const routes: CrudRouteRecord[] = [];
77
+
78
+ for (const spec of opSpecs) {
79
+ // 未提供组件则跳过
80
+ if (!spec.component) continue;
81
+
82
+ routes.push({
83
+ path: basePath + spec.suffix,
84
+ // 路由名称格式:EntityOp,如 OrderList、OrderNew
85
+ name: `${name}${capitalize(spec.op)}`,
86
+ component: spec.component,
87
+ meta: {
88
+ crud: {
89
+ entity: name,
90
+ op: spec.op,
91
+ },
92
+ // 仅在显式传入 permission 时生成权限检查
93
+ ...(permission ? { can: { resource, action: spec.action } } : undefined),
94
+ requiresAuth: true,
95
+ // 自动填充 title:基于 config.title + 操作类型后缀
96
+ ...(title ? { title: `${title}${spec.titleSuffix}` } : undefined),
97
+ // 继承 config.icon
98
+ ...(icon ? { icon } : undefined),
99
+ keepAlive: spec.keepAlive,
100
+ hidden: spec.hidden,
101
+ },
102
+ });
103
+ }
104
+
105
+ return routes;
106
+ }
107
+
108
+ // ────────────────────────────────────────────────────────────────────────────
109
+ // 内部工具
110
+ // ────────────────────────────────────────────────────────────────────────────
111
+
112
+ /** 首字母大写 */
113
+ function capitalize(str: string): string {
114
+ if (!str) return str;
115
+ return str.charAt(0).toUpperCase() + str.slice(1);
116
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @description 路由管理层 barrel export
3
+ * @author 阿怪
4
+ * @date 2026/2/26
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ export { defineCrudRoutes } from './defineCrudRoutes';
11
+ export { createTabStore, useTabStore } from './useTabStore';
12
+ export { useMenuFromRoutes } from './useMenuFromRoutes';
13
+ export { createRouterGuard, addTabFromRoute } from './createRouterGuard';
14
+ export type { RouterGuard, RouterGuardOptions } from './createRouterGuard';
15
+ export type {
16
+ CrudRouteMeta,
17
+ CrudRouteConfig,
18
+ CrudRouteRecord,
19
+ TabItem,
20
+ TabStore,
21
+ RouteLocationLike,
22
+ } from './types';