@kine-design/crud 0.0.1-beta.5 → 0.0.1-beta.7

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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @description KCrudPage 配置驱动的 ERP 列表页组件
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ *
9
+ * 一个配置出一整个页面:标题 + 筛选 + 表格 + 分页 + 状态标签。
10
+ */
11
+ import { defineComponent, type PropType } from 'vue';
12
+ import KTableColumn from 'kine-ui/components/tableColumn/KTableColumn.tsx';
13
+ import KTag from 'kine-ui/components/tag/KTag.tsx';
14
+ import KPageHeader from '../pageHeader/KPageHeader.tsx';
15
+ import KSearchTable from '../searchTable/KSearchTable.tsx';
16
+ import { useCrudPage } from '../../composables/page/useCrudPage';
17
+ import type { CrudPageConfig, CrudColumnConfig } from '../../composables/page/types';
18
+ import './crudPage.css';
19
+
20
+ export default defineComponent({
21
+ name: 'KCrudPage',
22
+
23
+ props: {
24
+ config: {
25
+ type: Object as PropType<CrudPageConfig>,
26
+ required: true,
27
+ },
28
+ },
29
+
30
+ setup(props, { slots }) {
31
+ const {
32
+ page, pageSize, total, list, loading,
33
+ filters, onPageChange, onSearch, onReset,
34
+ } = useCrudPage(props.config);
35
+
36
+ /** 格式化日期 */
37
+ const formatDate = (val: unknown) => {
38
+ if (!val) return '';
39
+ const d = new Date(val as string);
40
+ return isNaN(d.getTime()) ? String(val) : d.toLocaleDateString('zh-CN');
41
+ };
42
+
43
+ /** 格式化日期时间 */
44
+ const formatDateTime = (val: unknown) => {
45
+ if (!val) return '';
46
+ const d = new Date(val as string);
47
+ return isNaN(d.getTime()) ? String(val) : d.toLocaleString('zh-CN');
48
+ };
49
+
50
+ /** 渲染状态标签 */
51
+ const renderStatus = (val: unknown) => {
52
+ const statusMap = props.config.statusMap;
53
+ if (!statusMap || !val) return String(val ?? '');
54
+ const mapped = statusMap[val as string];
55
+ if (!mapped) return String(val);
56
+ return <KTag type={mapped.type ?? 'default'} size="small">{mapped.label}</KTag>;
57
+ };
58
+
59
+ /** 按列类型渲染单元格 */
60
+ const renderCell = (col: CrudColumnConfig, row: Record<string, unknown>) => {
61
+ const val = row[col.param];
62
+ switch (col.type) {
63
+ case 'status': return renderStatus(val);
64
+ case 'date': return formatDate(val);
65
+ case 'datetime': return formatDateTime(val);
66
+ default: return val != null ? String(val) : '';
67
+ }
68
+ };
69
+
70
+ /** 渲染筛选区表单项 */
71
+ const renderFilters = () => {
72
+ if (!props.config.filters?.length) return null;
73
+ return props.config.filters.map(f => (
74
+ <div class="k-crud-page-filter-item" key={f.param}>
75
+ <label class="k-crud-page-filter-label">{f.label}</label>
76
+ {f.type === 'select' ? (
77
+ <select
78
+ class="k-crud-page-filter-control"
79
+ value={(filters[f.param] as string) ?? ''}
80
+ onChange={(e: Event) => {
81
+ filters[f.param] = (e.target as HTMLSelectElement).value || undefined;
82
+ }}
83
+ >
84
+ <option value="">{f.placeholder ?? `全部`}</option>
85
+ {f.options?.map(opt => (
86
+ <option key={String(opt.value)} value={opt.value}>{opt.label}</option>
87
+ ))}
88
+ </select>
89
+ ) : (
90
+ <input
91
+ class="k-crud-page-filter-control"
92
+ type="text"
93
+ placeholder={f.placeholder ?? `请输入${f.label}`}
94
+ value={(filters[f.param] as string) ?? ''}
95
+ onInput={(e: Event) => {
96
+ filters[f.param] = (e.target as HTMLInputElement).value || undefined;
97
+ }}
98
+ />
99
+ )}
100
+ </div>
101
+ ));
102
+ };
103
+
104
+ return () => (
105
+ <div class="k-crud-page">
106
+ <KPageHeader title={props.config.title}>
107
+ {{ extra: slots.headerExtra }}
108
+ </KPageHeader>
109
+
110
+ <KSearchTable
111
+ data={list.value}
112
+ loading={loading.value}
113
+ total={total.value}
114
+ page={page.value}
115
+ pageSize={pageSize.value}
116
+ searchable={!!props.config.filters?.length}
117
+ onUpdate:page={onPageChange}
118
+ onSearch={onSearch}
119
+ onReset={onReset}
120
+ >
121
+ {{
122
+ search: renderFilters,
123
+ toolbar: slots.toolbar,
124
+ default: () => props.config.columns.map(col => {
125
+ const customSlot = slots[`column-${col.param}`];
126
+ return (
127
+ <KTableColumn
128
+ key={col.param}
129
+ param={col.param}
130
+ label={col.label}
131
+ width={col.width ?? ''}
132
+ >
133
+ {{
134
+ default: customSlot
135
+ ? (scope: { row: Record<string, unknown>; index: number }) => customSlot(scope)
136
+ : col.type
137
+ ? (scope: { row: Record<string, unknown> }) => renderCell(col, scope.row)
138
+ : undefined,
139
+ }}
140
+ </KTableColumn>
141
+ );
142
+ }),
143
+ empty: slots.empty,
144
+ }}
145
+ </KSearchTable>
146
+ </div>
147
+ );
148
+ },
149
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @description KCrudPage 样式
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ */
7
+
8
+ /* ===== 根容器 ===== */
9
+ .k-crud-page {
10
+ display: flex;
11
+ flex-direction: column;
12
+ height: 100%;
13
+ gap: 12px;
14
+ padding: 16px;
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ /* ===== 筛选项 ===== */
19
+ .k-crud-page-filter-item {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 4px;
23
+ min-width: 140px;
24
+ }
25
+
26
+ .k-crud-page-filter-label {
27
+ font-size: var(--kine-font-size-sm);
28
+ color: var(--kine-color-text-secondary);
29
+ line-height: 1;
30
+ }
31
+
32
+ .k-crud-page-filter-control {
33
+ height: 32px;
34
+ padding: 0 8px;
35
+ border: 1px solid var(--kine-color-border-default);
36
+ border-radius: var(--kine-radius-sm);
37
+ font-size: var(--kine-font-size-lg);
38
+ font-family: var(--kine-font-family-system);
39
+ background: var(--kine-color-bg-primary);
40
+ color: var(--kine-color-text-primary);
41
+ box-sizing: border-box;
42
+ outline: none;
43
+ transition: border-color var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
44
+ }
45
+
46
+ .k-crud-page-filter-control:focus {
47
+ border-color: var(--kine-color-accent-default);
48
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @description KCrudPage barrel export
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ export { default as KCrudPage } from './KCrudPage';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @description CrudPage composable barrel export
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ export { useCrudPage } from './useCrudPage';
11
+ export type { CrudPageConfig, CrudColumnConfig, CrudFilterConfig, StatusMapItem } from './types';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @description CrudPage 配置驱动页面类型定义
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ /** 列配置 */
11
+ export interface CrudColumnConfig {
12
+ /** 字段名,对应数据对象的 key */
13
+ param: string;
14
+ /** 列标题 */
15
+ label: string;
16
+ /** 列宽 */
17
+ width?: string;
18
+ /** 列类型,影响渲染方式 */
19
+ type?: 'text' | 'status' | 'date' | 'datetime';
20
+ }
21
+
22
+ /** 筛选项配置 */
23
+ export interface CrudFilterConfig {
24
+ /** 字段名,作为请求参数的 key */
25
+ param: string;
26
+ /** 筛选项标签 */
27
+ label: string;
28
+ /** 筛选项类型 */
29
+ type: 'input' | 'select';
30
+ /** select 类型的选项列表 */
31
+ options?: { label: string; value: string | number }[];
32
+ /** 占位文本 */
33
+ placeholder?: string;
34
+ }
35
+
36
+ /** 状态映射:字段值 → KTag 的 label + type */
37
+ export type StatusMapItem = {
38
+ label: string;
39
+ type?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
40
+ };
41
+
42
+ /** CrudPage 完整配置 */
43
+ export interface CrudPageConfig {
44
+ /** 页面标题 */
45
+ title: string;
46
+ /** API 路径(基于 createRequest 的 baseURL) */
47
+ api: string;
48
+ /** 列定义 */
49
+ columns: CrudColumnConfig[];
50
+ /** 筛选项定义,为空则不显示搜索区 */
51
+ filters?: CrudFilterConfig[];
52
+ /** 状态字段的值 → 显示文本 + 标签颜色 映射 */
53
+ statusMap?: Record<string, StatusMapItem>;
54
+ /** 每页条数,默认 20 */
55
+ pageSize?: number;
56
+ /** 行主键字段,默认 'id' */
57
+ rowKey?: string;
58
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @description CrudPage composable — 配置驱动的列表页数据层
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import { ref, reactive, computed, onMounted } from 'vue';
11
+ import { useRequestClient } from '../../setup';
12
+ import type { CrudPageConfig } from './types';
13
+
14
+ export function useCrudPage(config: CrudPageConfig) {
15
+ const client = useRequestClient();
16
+
17
+ const page = ref(1);
18
+ const pageSize = ref(config.pageSize ?? 20);
19
+ const total = ref(0);
20
+ const list = ref<Record<string, unknown>[]>([]);
21
+ const loading = ref(false);
22
+
23
+ // 筛选表单状态
24
+ const filters = reactive<Record<string, unknown>>({});
25
+ if (config.filters) {
26
+ for (const f of config.filters) {
27
+ filters[f.param] = undefined;
28
+ }
29
+ }
30
+
31
+ const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
32
+
33
+ async function fetchData() {
34
+ loading.value = true;
35
+ try {
36
+ const params: Record<string, unknown> = {
37
+ page: page.value,
38
+ pageSize: pageSize.value,
39
+ };
40
+ for (const [k, v] of Object.entries(filters)) {
41
+ if (v !== undefined && v !== '' && v !== null) {
42
+ params[k] = v;
43
+ }
44
+ }
45
+ const res = await client.get<any>(config.api, params);
46
+ // 兼容包装格式 { success, data: { total, list } } 和裸格式 { total, list }
47
+ const payload = (res && typeof res === 'object' && 'success' in res) ? res.data : res;
48
+ list.value = payload?.items ?? payload?.list ?? [];
49
+ total.value = payload?.total ?? 0;
50
+ } finally {
51
+ loading.value = false;
52
+ }
53
+ }
54
+
55
+ function onPageChange(p: number) {
56
+ page.value = p;
57
+ fetchData();
58
+ }
59
+
60
+ function onSearch() {
61
+ page.value = 1;
62
+ fetchData();
63
+ }
64
+
65
+ function onReset() {
66
+ for (const key of Object.keys(filters)) {
67
+ filters[key] = undefined;
68
+ }
69
+ page.value = 1;
70
+ fetchData();
71
+ }
72
+
73
+ onMounted(fetchData);
74
+
75
+ return {
76
+ page,
77
+ pageSize,
78
+ total,
79
+ totalPages,
80
+ list,
81
+ loading,
82
+ filters,
83
+ fetchData,
84
+ onPageChange,
85
+ onSearch,
86
+ onReset,
87
+ };
88
+ }
@@ -0,0 +1,14 @@
1
+ import { PropType } from 'vue';
2
+ import { CrudPageConfig } from '../../composables/page/types';
3
+ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
4
+ config: {
5
+ type: PropType<CrudPageConfig>;
6
+ required: true;
7
+ };
8
+ }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
9
+ config: {
10
+ type: PropType<CrudPageConfig>;
11
+ required: true;
12
+ };
13
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
14
+ export default _default;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @description KCrudPage barrel export
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ export { default as KCrudPage } from './KCrudPage';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @description CrudPage composable barrel export
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ export { useCrudPage } from './useCrudPage';
10
+ export type { CrudPageConfig, CrudColumnConfig, CrudFilterConfig, StatusMapItem } from './types';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @description CrudPage 配置驱动页面类型定义
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ /** 列配置 */
10
+ export interface CrudColumnConfig {
11
+ /** 字段名,对应数据对象的 key */
12
+ param: string;
13
+ /** 列标题 */
14
+ label: string;
15
+ /** 列宽 */
16
+ width?: string;
17
+ /** 列类型,影响渲染方式 */
18
+ type?: 'text' | 'status' | 'date' | 'datetime';
19
+ }
20
+ /** 筛选项配置 */
21
+ export interface CrudFilterConfig {
22
+ /** 字段名,作为请求参数的 key */
23
+ param: string;
24
+ /** 筛选项标签 */
25
+ label: string;
26
+ /** 筛选项类型 */
27
+ type: 'input' | 'select';
28
+ /** select 类型的选项列表 */
29
+ options?: {
30
+ label: string;
31
+ value: string | number;
32
+ }[];
33
+ /** 占位文本 */
34
+ placeholder?: string;
35
+ }
36
+ /** 状态映射:字段值 → KTag 的 label + type */
37
+ export type StatusMapItem = {
38
+ label: string;
39
+ type?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
40
+ };
41
+ /** CrudPage 完整配置 */
42
+ export interface CrudPageConfig {
43
+ /** 页面标题 */
44
+ title: string;
45
+ /** API 路径(基于 createRequest 的 baseURL) */
46
+ api: string;
47
+ /** 列定义 */
48
+ columns: CrudColumnConfig[];
49
+ /** 筛选项定义,为空则不显示搜索区 */
50
+ filters?: CrudFilterConfig[];
51
+ /** 状态字段的值 → 显示文本 + 标签颜色 映射 */
52
+ statusMap?: Record<string, StatusMapItem>;
53
+ /** 每页条数,默认 20 */
54
+ pageSize?: number;
55
+ /** 行主键字段,默认 'id' */
56
+ rowKey?: string;
57
+ }
@@ -0,0 +1,14 @@
1
+ import { CrudPageConfig } from './types';
2
+ export declare function useCrudPage(config: CrudPageConfig): {
3
+ page: import('vue').Ref<number, number>;
4
+ pageSize: import('vue').Ref<number, number>;
5
+ total: import('vue').Ref<number, number>;
6
+ totalPages: import('vue').ComputedRef<number>;
7
+ list: import('vue').Ref<Record<string, unknown>[], Record<string, unknown>[]>;
8
+ loading: import('vue').Ref<boolean, boolean>;
9
+ filters: Record<string, unknown>;
10
+ fetchData: () => Promise<void>;
11
+ onPageChange: (p: number) => void;
12
+ onSearch: () => void;
13
+ onReset: () => void;
14
+ };