@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.
- package/.vlaude/last-session-id +1 -0
- package/components/crudPage/KCrudPage.tsx +178 -0
- package/components/crudPage/crudPage.css +64 -0
- package/components/crudPage/index.ts +10 -0
- package/components/editableTable/KEditableTable.tsx +281 -0
- package/components/editableTable/editableTable.css +268 -0
- package/components/editableTable/index.ts +10 -0
- package/components/formPage/KApprovalDialog.tsx +142 -0
- package/components/formPage/KFormCard.tsx +65 -0
- package/components/formPage/KFormPage.tsx +128 -0
- package/components/formPage/KMasterDetailPage.tsx +205 -0
- package/components/formPage/KStickyActionBar.tsx +33 -0
- package/components/formPage/formPage.css +629 -0
- package/components/formPage/index.ts +14 -0
- package/components/layout/KContent.tsx +20 -0
- package/components/layout/KHeader.tsx +37 -0
- package/components/layout/KLayout.tsx +82 -0
- package/components/layout/KSider.tsx +80 -0
- package/components/layout/index.ts +18 -0
- package/components/layout/layout.css +262 -0
- package/components/login/KLoginPage.tsx +129 -0
- package/components/login/index.ts +10 -0
- package/components/login/login.css +118 -0
- package/components/navMenu/KNavMenu.tsx +175 -0
- package/components/navMenu/index.ts +2 -0
- package/components/navMenu/navMenu.css +197 -0
- package/components/pageHeader/KPageHeader.tsx +85 -0
- package/components/pageHeader/index.ts +9 -0
- package/components/pageHeader/pageHeader.css +93 -0
- package/components/searchTable/KSearchTable.tsx +138 -0
- package/components/searchTable/index.ts +10 -0
- package/components/searchTable/searchTable.css +121 -0
- package/components/upload/KFileList.tsx +95 -0
- package/components/upload/KImageUpload.tsx +286 -0
- package/components/upload/KUpload.tsx +206 -0
- package/components/upload/index.ts +13 -0
- package/components/upload/types.ts +26 -0
- package/components/upload/upload.css +345 -0
- package/composables/auth/authGuard.ts +128 -0
- package/composables/auth/index.ts +23 -0
- package/composables/auth/types.ts +109 -0
- package/composables/auth/useAuth.ts +278 -0
- package/composables/auth/vCan.ts +95 -0
- package/composables/defineRepository.ts +224 -0
- package/composables/error/createErrorHandler.ts +46 -0
- package/composables/error/defaultFeedbackHandler.ts +76 -0
- package/composables/error/dispatchError.ts +70 -0
- package/composables/error/index.ts +32 -0
- package/composables/error/types.ts +57 -0
- package/composables/error/useErrorHandler.ts +41 -0
- package/composables/form/index.ts +18 -0
- package/composables/form/renderFormField.tsx +119 -0
- package/composables/form/types.ts +129 -0
- package/composables/form/useFormPage.ts +183 -0
- package/composables/index.ts +62 -0
- package/composables/page/index.ts +11 -0
- package/composables/page/types.ts +62 -0
- package/composables/page/useCrudPage.ts +88 -0
- package/composables/request/composables.ts +206 -0
- package/composables/request/controlGate.ts +143 -0
- package/composables/request/createRequest.ts +173 -0
- package/composables/request/index.ts +71 -0
- package/composables/request/orchestrator.ts +145 -0
- package/composables/request/requestBuilder.ts +418 -0
- package/composables/request/transport/fetchTransport.ts +79 -0
- package/composables/request/transport/xhrTransport.ts +100 -0
- package/composables/request/types.ts +226 -0
- package/composables/request/upload.ts +146 -0
- package/composables/router/createRouterGuard.ts +134 -0
- package/composables/router/defineCrudRoutes.ts +116 -0
- package/composables/router/index.ts +22 -0
- package/composables/router/types.ts +128 -0
- package/composables/router/useMenuFromRoutes.ts +109 -0
- package/composables/router/useTabStore.ts +183 -0
- package/composables/search/index.ts +11 -0
- package/composables/search/useAutoCompleteSearch.ts +161 -0
- package/composables/setupCrud.ts +43 -0
- package/composables/storage/createStorageAdapter.ts +72 -0
- package/composables/storage/index.ts +13 -0
- package/composables/storage/types.ts +30 -0
- package/composables/storage/useStorage.ts +108 -0
- package/composables/store/defineUserStore.ts +122 -0
- package/composables/store/index.ts +11 -0
- package/composables/types.ts +118 -0
- package/dist/components/crudPage/KCrudPage.d.ts +14 -0
- package/dist/components/crudPage/index.d.ts +9 -0
- package/dist/components/editableTable/KEditableTable.d.ts +146 -0
- package/dist/components/editableTable/index.d.ts +10 -0
- package/dist/components/formPage/KApprovalDialog.d.ts +99 -0
- package/dist/components/formPage/KFormCard.d.ts +49 -0
- package/dist/components/formPage/KFormPage.d.ts +14 -0
- package/dist/components/formPage/KMasterDetailPage.d.ts +14 -0
- package/dist/components/formPage/KStickyActionBar.d.ts +16 -0
- package/dist/components/formPage/index.d.ts +14 -0
- package/dist/components/layout/KLayout.d.ts +7 -4
- package/dist/composables/auth/useAuth.d.ts +5 -5
- package/dist/composables/error/types.d.ts +2 -1
- package/dist/composables/form/index.d.ts +12 -0
- package/dist/composables/form/renderFormField.d.ts +11 -0
- package/dist/composables/form/types.d.ts +104 -0
- package/dist/composables/form/useFormPage.d.ts +38 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/page/index.d.ts +10 -0
- package/dist/composables/page/types.d.ts +61 -0
- package/dist/composables/page/useCrudPage.d.ts +14 -0
- package/dist/composables/request/createRequest.d.ts +2 -0
- package/dist/composables/request/requestBuilder.d.ts +2 -0
- package/dist/composables/search/index.d.ts +10 -0
- package/dist/composables/search/useAutoCompleteSearch.d.ts +50 -0
- package/dist/crud.css +2499 -663
- package/dist/crud.js +11512 -2910
- package/dist/index.d.ts +11 -0
- package/dist/setup.d.ts +2 -2
- package/index.ts +144 -0
- package/package.json +20 -19
- package/setup.ts +288 -0
- package/tsconfig.json +12 -0
- package/vite.config.build.ts +52 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description 路由管理层类型定义
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/2/26
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// 路由 meta 约定
|
|
12
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** CRUD 路由 meta 约定,注入到 vue-router RouteRecordRaw.meta */
|
|
15
|
+
export interface CrudRouteMeta {
|
|
16
|
+
/** CRUD 操作类型声明 */
|
|
17
|
+
crud?: {
|
|
18
|
+
/** 实体名,如 'Order'、'User' */
|
|
19
|
+
entity: string;
|
|
20
|
+
/** 操作类型 */
|
|
21
|
+
op: 'list' | 'new' | 'modify' | 'detail';
|
|
22
|
+
};
|
|
23
|
+
/** 权限声明,供鉴权守卫使用 */
|
|
24
|
+
can?: {
|
|
25
|
+
/** 权限资源名 */
|
|
26
|
+
resource: string;
|
|
27
|
+
/** 操作名 */
|
|
28
|
+
action: string;
|
|
29
|
+
};
|
|
30
|
+
/** 是否需要登录,默认 true */
|
|
31
|
+
requiresAuth?: boolean;
|
|
32
|
+
/** 页面标题(面包屑、tab 标题用) */
|
|
33
|
+
title?: string;
|
|
34
|
+
/** 图标标识 */
|
|
35
|
+
icon?: string;
|
|
36
|
+
/** 是否缓存组件(keep-alive),默认 false */
|
|
37
|
+
keepAlive?: boolean;
|
|
38
|
+
/** 不显示在菜单中,默认 false */
|
|
39
|
+
hidden?: boolean;
|
|
40
|
+
/** 菜单排序权重,数值越小越靠前 */
|
|
41
|
+
sort?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// defineCrudRoutes 配置
|
|
46
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** defineCrudRoutes 输入配置 */
|
|
49
|
+
export interface CrudRouteConfig {
|
|
50
|
+
/** 实体名,如 'Order'、'User',用于 meta.crud.entity 和路由 name 前缀 */
|
|
51
|
+
name: string;
|
|
52
|
+
/** 基础路径,如 '/order' */
|
|
53
|
+
path: string;
|
|
54
|
+
/** 页面标题,用于自动生成各操作页面的 meta.title */
|
|
55
|
+
title?: string;
|
|
56
|
+
/** 图标标识,自动继承到各操作路由的 meta.icon */
|
|
57
|
+
icon?: string;
|
|
58
|
+
/** 列表页组件动态导入函数 */
|
|
59
|
+
list?: () => Promise<unknown>;
|
|
60
|
+
/** 新建页组件动态导入函数 */
|
|
61
|
+
new?: () => Promise<unknown>;
|
|
62
|
+
/** 编辑页组件动态导入函数 */
|
|
63
|
+
modify?: () => Promise<unknown>;
|
|
64
|
+
/** 详情页组件动态导入函数 */
|
|
65
|
+
detail?: () => Promise<unknown>;
|
|
66
|
+
/** 权限资源名,默认取 name.toLowerCase() */
|
|
67
|
+
permission?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 兼容 vue-router RouteRecordRaw 结构的普通对象类型。
|
|
72
|
+
* 不直接引入 vue-router,避免强绑定。
|
|
73
|
+
*/
|
|
74
|
+
export interface CrudRouteRecord {
|
|
75
|
+
/** 路由路径 */
|
|
76
|
+
path: string;
|
|
77
|
+
/** 路由名称 */
|
|
78
|
+
name: string;
|
|
79
|
+
/** 组件动态导入函数 */
|
|
80
|
+
component: () => Promise<unknown>;
|
|
81
|
+
/** 路由 meta */
|
|
82
|
+
meta: CrudRouteMeta;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
// Tab 管理
|
|
87
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/** 路由 tab 项 */
|
|
90
|
+
export interface TabItem {
|
|
91
|
+
/** 路由完整路径,作为唯一标识 */
|
|
92
|
+
path: string;
|
|
93
|
+
/** tab 显示标题 */
|
|
94
|
+
title: string;
|
|
95
|
+
/** 组件 name,用于 keep-alive include 列表 */
|
|
96
|
+
name: string;
|
|
97
|
+
/** 是否可关闭 */
|
|
98
|
+
closable: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 最小化路由位置信息,用于 addTabFromRoute */
|
|
102
|
+
export interface RouteLocationLike {
|
|
103
|
+
path: string;
|
|
104
|
+
name?: string | symbol;
|
|
105
|
+
meta: CrudRouteMeta & Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** useTabStore 返回的 store 实例 */
|
|
109
|
+
export interface TabStore {
|
|
110
|
+
/** 当前已打开的 tab 列表 */
|
|
111
|
+
tabs: import('vue').Ref<TabItem[]>;
|
|
112
|
+
/** 当前激活的 tab path */
|
|
113
|
+
activeTab: import('vue').Ref<string>;
|
|
114
|
+
/** 用于 keep-alive :include 绑定的组件名列表 */
|
|
115
|
+
cachedNames: import('vue').ComputedRef<string[]>;
|
|
116
|
+
/** 添加 tab,已存在时仅激活 */
|
|
117
|
+
addTab: (tab: TabItem) => void;
|
|
118
|
+
/** 从路由对象自动提取 title/path/name 创建 tab */
|
|
119
|
+
addTabFromRoute: (route: RouteLocationLike) => void;
|
|
120
|
+
/** 关闭指定 tab,自动激活相邻 tab,返回应导航到的路径(无需导航时返回 undefined) */
|
|
121
|
+
removeTab: (path: string) => string | undefined;
|
|
122
|
+
/** 关闭除指定路径之外的所有可关闭 tab */
|
|
123
|
+
removeOthers: (path: string) => void;
|
|
124
|
+
/** 关闭所有可关闭 tab,保留不可关闭的 */
|
|
125
|
+
removeAll: () => void;
|
|
126
|
+
/** 切换激活的 tab */
|
|
127
|
+
setActive: (path: string) => void;
|
|
128
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description 从路由配置自动生成导航菜单数据
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/15
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { computed, type ComputedRef } from 'vue';
|
|
11
|
+
import type { NavMenuData } from '../../components/navMenu';
|
|
12
|
+
import type { CrudRouteMeta } from './types';
|
|
13
|
+
|
|
14
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// 局部类型(不引入 vue-router 依赖)
|
|
16
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/** 最小化 RouteRecordRaw,只声明本文件用到的字段 */
|
|
19
|
+
interface RouteRecord {
|
|
20
|
+
path: string;
|
|
21
|
+
name?: string | symbol;
|
|
22
|
+
meta?: CrudRouteMeta & Record<string, unknown>;
|
|
23
|
+
children?: RouteRecord[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// useMenuFromRoutes
|
|
28
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 从 vue-router 路由配置自动生成 KNavMenu 所需的菜单数据。
|
|
32
|
+
*
|
|
33
|
+
* - 过滤 meta.hidden === true 的路由
|
|
34
|
+
* - 用 meta.title 作为 label,meta.icon 作为 icon
|
|
35
|
+
* - 支持嵌套路由 → 嵌套菜单
|
|
36
|
+
* - 按 meta.sort 升序排序(未设置排序权重的排在最后)
|
|
37
|
+
*
|
|
38
|
+
* @param routes 路由配置数组(RouteRecordRaw[])
|
|
39
|
+
* @returns 响应式菜单数据
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { useMenuFromRoutes } from '@kine-design/crud';
|
|
44
|
+
*
|
|
45
|
+
* const menuData = useMenuFromRoutes(router.getRoutes());
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function useMenuFromRoutes(routes: RouteRecord[]): ComputedRef<NavMenuData[]> {
|
|
49
|
+
return computed(() => transformRoutes(routes));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// 内部辅助
|
|
54
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** 递归转换路由 → 菜单数据 */
|
|
57
|
+
function transformRoutes(routes: RouteRecord[]): NavMenuData[] {
|
|
58
|
+
const result: NavMenuData[] = [];
|
|
59
|
+
|
|
60
|
+
for (const route of routes) {
|
|
61
|
+
const meta = route.meta;
|
|
62
|
+
|
|
63
|
+
// 隐藏路由不生成菜单
|
|
64
|
+
if (meta?.hidden) continue;
|
|
65
|
+
|
|
66
|
+
// 没有 title 的路由无法显示,跳过
|
|
67
|
+
const label = meta?.title;
|
|
68
|
+
if (!label) continue;
|
|
69
|
+
|
|
70
|
+
const key = typeof route.name === 'string' ? route.name : route.path;
|
|
71
|
+
|
|
72
|
+
const item: NavMenuData = {
|
|
73
|
+
key,
|
|
74
|
+
label,
|
|
75
|
+
path: route.path,
|
|
76
|
+
icon: meta?.icon,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// 递归处理子路由
|
|
80
|
+
if (route.children?.length) {
|
|
81
|
+
const children = transformRoutes(route.children);
|
|
82
|
+
if (children.length > 0) {
|
|
83
|
+
item.children = children;
|
|
84
|
+
// 有子菜单的分类节点不设置 path(避免点击跳转)
|
|
85
|
+
delete item.path;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
result.push(item);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 按 meta.sort 升序排序,未设置的排到最后
|
|
93
|
+
return result.sort((a, b) => {
|
|
94
|
+
const sortA = findSort(routes, a.key);
|
|
95
|
+
const sortB = findSort(routes, b.key);
|
|
96
|
+
return sortA - sortB;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 从路由配置中查找对应 key 的排序权重 */
|
|
101
|
+
function findSort(routes: RouteRecord[], key: string | number): number {
|
|
102
|
+
for (const route of routes) {
|
|
103
|
+
const routeKey = typeof route.name === 'string' ? route.name : route.path;
|
|
104
|
+
if (routeKey === key) {
|
|
105
|
+
return route.meta?.sort ?? Infinity;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Infinity;
|
|
109
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description 路由 tab 管理 — 基于 provide/inject,支持 keep-alive
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/2/26
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
ref,
|
|
12
|
+
computed,
|
|
13
|
+
provide,
|
|
14
|
+
inject,
|
|
15
|
+
type InjectionKey,
|
|
16
|
+
} from 'vue';
|
|
17
|
+
import type { TabItem, TabStore, RouteLocationLike } from './types';
|
|
18
|
+
|
|
19
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// Injection key
|
|
21
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const TAB_STORE_KEY: InjectionKey<TabStore> = Symbol('tab-store');
|
|
24
|
+
|
|
25
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// 内部:创建 store 实例
|
|
27
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function createTabStoreInstance(): TabStore {
|
|
30
|
+
const tabs = ref<TabItem[]>([]);
|
|
31
|
+
const activeTab = ref<string>('');
|
|
32
|
+
|
|
33
|
+
/** keep-alive include 列表,只缓存当前打开的 tab 对应的组件 */
|
|
34
|
+
const cachedNames = computed<string[]>(() =>
|
|
35
|
+
tabs.value.map(tab => tab.name),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 添加 tab。
|
|
40
|
+
* - 已存在(path 相同):仅激活,不重复添加
|
|
41
|
+
* - 不存在:追加到列表末尾并激活
|
|
42
|
+
*/
|
|
43
|
+
const addTab = (tab: TabItem): void => {
|
|
44
|
+
const exists = tabs.value.some(t => t.path === tab.path);
|
|
45
|
+
if (!exists) {
|
|
46
|
+
tabs.value.push(tab);
|
|
47
|
+
}
|
|
48
|
+
activeTab.value = tab.path;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 从路由对象自动提取信息创建 tab。
|
|
53
|
+
* 自动从 meta.title 取标题,从 route.name 取组件名。
|
|
54
|
+
*/
|
|
55
|
+
const addTabFromRoute = (route: RouteLocationLike): void => {
|
|
56
|
+
const meta = route.meta;
|
|
57
|
+
const title = meta.title ?? (typeof route.name === 'string' ? route.name : route.path);
|
|
58
|
+
const name = typeof route.name === 'string' ? route.name : route.path;
|
|
59
|
+
|
|
60
|
+
addTab({
|
|
61
|
+
path: route.path,
|
|
62
|
+
title,
|
|
63
|
+
name,
|
|
64
|
+
closable: true,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 关闭指定 tab。
|
|
70
|
+
* - 关闭后自动激活相邻 tab(优先后一个,其次前一个)
|
|
71
|
+
* - 不可关闭的 tab 忽略此操作
|
|
72
|
+
* - 返回应导航到的路径(当关闭的是当前激活 tab 时),无需导航返回 undefined
|
|
73
|
+
*/
|
|
74
|
+
const removeTab = (path: string): string | undefined => {
|
|
75
|
+
const index = tabs.value.findIndex(t => t.path === path);
|
|
76
|
+
if (index === -1) return undefined;
|
|
77
|
+
|
|
78
|
+
const target = tabs.value[index];
|
|
79
|
+
if (!target.closable) return undefined;
|
|
80
|
+
|
|
81
|
+
tabs.value.splice(index, 1);
|
|
82
|
+
|
|
83
|
+
// 激活相邻 tab,并返回导航目标
|
|
84
|
+
if (activeTab.value === path) {
|
|
85
|
+
const nextTab = tabs.value[index] ?? tabs.value[index - 1];
|
|
86
|
+
activeTab.value = nextTab ? nextTab.path : '';
|
|
87
|
+
return activeTab.value || undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return undefined;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 关闭除指定 path 之外的所有可关闭 tab。
|
|
95
|
+
* 不可关闭的 tab 始终保留。
|
|
96
|
+
*/
|
|
97
|
+
const removeOthers = (path: string): void => {
|
|
98
|
+
tabs.value = tabs.value.filter(t => !t.closable || t.path === path);
|
|
99
|
+
// 若当前激活的 tab 被关闭,则激活保留的目标 tab
|
|
100
|
+
if (!tabs.value.some(t => t.path === activeTab.value)) {
|
|
101
|
+
activeTab.value = path;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 关闭所有可关闭 tab,保留不可关闭的(如首页/固定 tab)。
|
|
107
|
+
*/
|
|
108
|
+
const removeAll = (): void => {
|
|
109
|
+
tabs.value = tabs.value.filter(t => !t.closable);
|
|
110
|
+
// 激活第一个保留的 tab,若全部清空则置空
|
|
111
|
+
const first = tabs.value[0];
|
|
112
|
+
activeTab.value = first ? first.path : '';
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** 切换激活的 tab */
|
|
116
|
+
const setActive = (path: string): void => {
|
|
117
|
+
activeTab.value = path;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
tabs,
|
|
122
|
+
activeTab,
|
|
123
|
+
cachedNames,
|
|
124
|
+
addTab,
|
|
125
|
+
addTabFromRoute,
|
|
126
|
+
removeTab,
|
|
127
|
+
removeOthers,
|
|
128
|
+
removeAll,
|
|
129
|
+
setActive,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// 公开 API
|
|
135
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 在 App 根组件(或布局组件)调用,创建并 provide 全局 tab store 实例。
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```vue
|
|
142
|
+
* <!-- App.vue -->
|
|
143
|
+
* <script setup>
|
|
144
|
+
* import { createTabStore } from '@kine-design/crud';
|
|
145
|
+
* const tabStore = createTabStore();
|
|
146
|
+
* </script>
|
|
147
|
+
*
|
|
148
|
+
* <template>
|
|
149
|
+
* <router-view v-slot="{ Component }">
|
|
150
|
+
* <keep-alive :include="tabStore.cachedNames.value">
|
|
151
|
+
* <component :is="Component" />
|
|
152
|
+
* </keep-alive>
|
|
153
|
+
* </router-view>
|
|
154
|
+
* </template>
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function createTabStore(): TabStore {
|
|
158
|
+
const store = createTabStoreInstance();
|
|
159
|
+
provide(TAB_STORE_KEY, store);
|
|
160
|
+
return store;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 在子组件中 inject 使用 tab store。
|
|
165
|
+
* 必须在 createTabStore() 调用的后代组件中使用。
|
|
166
|
+
*
|
|
167
|
+
* @throws 若在 createTabStore() 之外的组件树中调用,会抛出错误提示
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* const tabStore = useTabStore();
|
|
172
|
+
* tabStore.addTab({ path: '/order', title: '订单列表', name: 'Order', closable: true });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export function useTabStore(): TabStore {
|
|
176
|
+
const store = inject(TAB_STORE_KEY);
|
|
177
|
+
if (!store) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
'[useTabStore] 未找到 tab store 实例,请确保在父级组件中调用了 createTabStore()',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return store;
|
|
183
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description search composables barrel export
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/22
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { useAutoCompleteSearch } from './useAutoCompleteSearch';
|
|
11
|
+
export type { AutoCompleteSearchOptions, AutoCompleteSearchReturn } from './useAutoCompleteSearch';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description AutoComplete 搜索 hook — 基于 defineRepository 的 useList,为 KAutoComplete 提供搜索能力
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/22
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*
|
|
9
|
+
* 将孟加拉项目的 useAutoComplete 升级:
|
|
10
|
+
* - 不再直接传 getList 函数,而是传 requestClient + api 路径
|
|
11
|
+
* - 统一使用 crud 层的请求体系
|
|
12
|
+
* - 返回 KAutoComplete 组件可直接消费的 props
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ref, shallowRef, type Ref, type ShallowRef } from 'vue';
|
|
16
|
+
import { useRequestClient } from '../../setup';
|
|
17
|
+
|
|
18
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// 类型
|
|
20
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface AutoCompleteSearchOptions<T = any> {
|
|
23
|
+
/** API 路径,如 '/supplier' */
|
|
24
|
+
api: string;
|
|
25
|
+
/** 搜索参数名,如 'nameLike',输入值会赋给这个 key */
|
|
26
|
+
searchKey: string;
|
|
27
|
+
/** 显示字段名,如 'supplierName',用于回填输入框 */
|
|
28
|
+
labelKey: string;
|
|
29
|
+
/** 值字段名,默认 'id' */
|
|
30
|
+
valueKey?: string;
|
|
31
|
+
/** 每次搜索返回条数,默认 5 */
|
|
32
|
+
pageSize?: number;
|
|
33
|
+
/** 额外的固定查询参数 */
|
|
34
|
+
extraParams?: Record<string, unknown>;
|
|
35
|
+
/** 自定义过滤函数 */
|
|
36
|
+
filter?: (item: T) => boolean;
|
|
37
|
+
/** 自定义映射函数,将 API 返回的数据转换为 AutoComplete 需要的格式 */
|
|
38
|
+
mapFn?: (item: T) => { label: string; value: string | number; raw: T };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AutoCompleteSearchReturn<T = any> {
|
|
42
|
+
/** 输入框绑定值 */
|
|
43
|
+
inputValue: Ref<string>;
|
|
44
|
+
/** 建议列表 */
|
|
45
|
+
suggestions: ShallowRef<Array<{ label: string; value: string | number; raw: T }>>;
|
|
46
|
+
/** 加载状态 */
|
|
47
|
+
loading: Ref<boolean>;
|
|
48
|
+
/** 当前选中的原始数据 */
|
|
49
|
+
selected: ShallowRef<T | null>;
|
|
50
|
+
/** 搜索回调,绑定给 KAutoComplete 的 onSearch / fetchSuggestions */
|
|
51
|
+
onSearch: (query: string) => Promise<void>;
|
|
52
|
+
/** 选中回调 */
|
|
53
|
+
onSelect: (item: { label: string; value: string | number; raw: T }) => void;
|
|
54
|
+
/** 清空选中 */
|
|
55
|
+
onClear: () => void;
|
|
56
|
+
/** 手动设置选中值(用于表单回填) */
|
|
57
|
+
setValue: (label: string, data: T | null) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// 实现
|
|
62
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export function useAutoCompleteSearch<T = any>(
|
|
65
|
+
options: AutoCompleteSearchOptions<T>,
|
|
66
|
+
): AutoCompleteSearchReturn<T> {
|
|
67
|
+
const {
|
|
68
|
+
api,
|
|
69
|
+
searchKey,
|
|
70
|
+
labelKey,
|
|
71
|
+
valueKey = 'id',
|
|
72
|
+
pageSize = 5,
|
|
73
|
+
extraParams,
|
|
74
|
+
filter,
|
|
75
|
+
mapFn,
|
|
76
|
+
} = options;
|
|
77
|
+
|
|
78
|
+
const client = useRequestClient();
|
|
79
|
+
|
|
80
|
+
const inputValue = ref('');
|
|
81
|
+
const suggestions = shallowRef([]) as ShallowRef<Array<{ label: string; value: string | number; raw: T }>>;
|
|
82
|
+
const loading = ref(false);
|
|
83
|
+
const selected = shallowRef(null) as ShallowRef<T | null>;
|
|
84
|
+
|
|
85
|
+
/** 默认映射函数 */
|
|
86
|
+
const defaultMapFn = (item: T): { label: string; value: string | number; raw: T } => ({
|
|
87
|
+
label: String((item as any)[labelKey] ?? ''),
|
|
88
|
+
value: (item as any)[valueKey],
|
|
89
|
+
raw: item,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const mapper = mapFn ?? defaultMapFn;
|
|
93
|
+
|
|
94
|
+
/** 执行搜索 */
|
|
95
|
+
const onSearch = async (query: string) => {
|
|
96
|
+
loading.value = true;
|
|
97
|
+
try {
|
|
98
|
+
const params: Record<string, unknown> = {
|
|
99
|
+
page: 1,
|
|
100
|
+
pageSize,
|
|
101
|
+
...extraParams,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (query) {
|
|
105
|
+
params[searchKey] = query;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const res = await client.get<any>(api, { params });
|
|
109
|
+
let list: T[] = [];
|
|
110
|
+
|
|
111
|
+
// 兼容 { data: { data: [...] } } 和 { data: [...] } 两种返回格式
|
|
112
|
+
if (res?.data?.data && Array.isArray(res.data.data)) {
|
|
113
|
+
list = res.data.data;
|
|
114
|
+
} else if (res?.data && Array.isArray(res.data)) {
|
|
115
|
+
list = res.data;
|
|
116
|
+
} else if (Array.isArray(res)) {
|
|
117
|
+
list = res;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (filter) {
|
|
121
|
+
list = list.filter(filter);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
suggestions.value = list.map(mapper);
|
|
125
|
+
} catch {
|
|
126
|
+
suggestions.value = [];
|
|
127
|
+
} finally {
|
|
128
|
+
loading.value = false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** 选中项 */
|
|
133
|
+
const onSelect = (item: { label: string; value: string | number; raw: T }) => {
|
|
134
|
+
selected.value = item.raw;
|
|
135
|
+
inputValue.value = item.label;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** 清空 */
|
|
139
|
+
const onClear = () => {
|
|
140
|
+
selected.value = null;
|
|
141
|
+
inputValue.value = '';
|
|
142
|
+
suggestions.value = [];
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** 手动设置值(编辑页回填) */
|
|
146
|
+
const setValue = (label: string, data: T | null) => {
|
|
147
|
+
inputValue.value = label;
|
|
148
|
+
selected.value = data;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
inputValue,
|
|
153
|
+
suggestions,
|
|
154
|
+
loading,
|
|
155
|
+
selected,
|
|
156
|
+
onSearch,
|
|
157
|
+
onSelect,
|
|
158
|
+
onClear,
|
|
159
|
+
setValue,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description @kine-design/crud 一键安装函数,注册 VueQueryPlugin
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/2/26
|
|
5
|
+
* @version v0.0.1
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { App } from 'vue';
|
|
11
|
+
import { VueQueryPlugin, type VueQueryPluginOptions } from '@tanstack/vue-query';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 在 Vue App 实例上安装 CRUD 基础设施(TanStack Query)。
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // main.ts
|
|
19
|
+
* import { createApp } from 'vue';
|
|
20
|
+
* import { setupCrud } from '@kine-design/crud';
|
|
21
|
+
*
|
|
22
|
+
* const app = createApp(App);
|
|
23
|
+
* setupCrud(app);
|
|
24
|
+
* app.mount('#app');
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function setupCrud(app: App, options?: VueQueryPluginOptions): void {
|
|
28
|
+
app.use(VueQueryPlugin, {
|
|
29
|
+
queryClientConfig: {
|
|
30
|
+
defaultOptions: {
|
|
31
|
+
queries: {
|
|
32
|
+
// 窗口重新聚焦时不自动重新请求,业务层按需覆盖
|
|
33
|
+
refetchOnWindowFocus: false,
|
|
34
|
+
// 失败后重试 1 次
|
|
35
|
+
retry: 1,
|
|
36
|
+
// 数据保鲜时间 1 分钟
|
|
37
|
+
staleTime: 1000 * 60,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
...options,
|
|
42
|
+
});
|
|
43
|
+
}
|