@kine-design/crud 0.0.1-beta.10 → 0.0.1-beta.13

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.
@@ -9,9 +9,9 @@
9
9
  * 一个配置出一整个页面:标题 + 筛选 + 表格 + 分页 + 状态标签。
10
10
  */
11
11
  import { defineComponent, type PropType } from 'vue';
12
- import { useRouter } from 'vue-router';
13
12
  import KTableColumn from 'kine-ui/components/tableColumn/KTableColumn.tsx';
14
13
  import KTag from 'kine-ui/components/tag/KTag.tsx';
14
+ import KImage from 'kine-ui/components/image/KImage.tsx';
15
15
  import KInput from 'kine-ui/components/input/KInput.tsx';
16
16
  import KSelect from 'kine-ui/components/select/KSelect.tsx';
17
17
  import KPageHeader from '../pageHeader/KPageHeader.tsx';
@@ -31,7 +31,6 @@ export default defineComponent({
31
31
  },
32
32
 
33
33
  setup(props, { slots }) {
34
- const router = useRouter();
35
34
  const {
36
35
  page, pageSize, total, list, loading,
37
36
  filters, onPageChange, onSearch, onReset,
@@ -67,32 +66,26 @@ export default defineComponent({
67
66
  case 'status': return renderStatus(val);
68
67
  case 'date': return formatDate(val);
69
68
  case 'datetime': return formatDateTime(val);
69
+ case 'image': {
70
+ if (!val) return '';
71
+ const raw = String(val);
72
+ const resolver = props.config.imageResolver;
73
+ const src = resolver ? resolver(raw) : raw;
74
+ return (
75
+ <KImage
76
+ src={src}
77
+ previewSrcList={[src]}
78
+ width={40}
79
+ height={40}
80
+ fit="cover"
81
+ lazy
82
+ />
83
+ );
84
+ }
70
85
  default: return val != null ? String(val) : '';
71
86
  }
72
87
  };
73
88
 
74
- /** 行点击 → 跳转详情 */
75
- const onRowClick = (e: MouseEvent) => {
76
- const detailPath = props.config.detailPath;
77
- if (!detailPath) return;
78
-
79
- const tr = (e.target as HTMLElement).closest?.('tr');
80
- if (!tr) return;
81
- const tbody = tr.closest('tbody');
82
- if (!tbody || tbody.classList.contains('k-table-empty')) return;
83
-
84
- const rows = Array.from(tbody.querySelectorAll('tr'));
85
- const index = rows.indexOf(tr as HTMLTableRowElement);
86
- if (index < 0 || index >= list.value.length) return;
87
-
88
- const row = list.value[index];
89
- const key = props.config.rowKey ?? 'id';
90
- const id = row[key];
91
- if (id != null) {
92
- router.push(`${detailPath}/${id}`);
93
- }
94
- };
95
-
96
89
  /** 渲染筛选区表单项 */
97
90
  const renderFilters = () => {
98
91
  if (!props.config.filters?.length) return null;
@@ -126,13 +119,12 @@ export default defineComponent({
126
119
  };
127
120
 
128
121
  return () => (
129
- <div class={['k-crud-page', props.config.detailPath ? 'k-crud-page--clickable' : '']}>
122
+ <div class="k-crud-page">
130
123
  <KPageHeader title={props.config.title}>
131
124
  {{ extra: slots.headerExtra }}
132
125
  </KPageHeader>
133
126
 
134
- <div onClick={onRowClick}>
135
- <KSearchTable
127
+ <KSearchTable
136
128
  data={list.value}
137
129
  loading={loading.value}
138
130
  total={total.value}
@@ -168,7 +160,6 @@ export default defineComponent({
168
160
  empty: slots.empty,
169
161
  }}
170
162
  </KSearchTable>
171
- </div>
172
163
  </div>
173
164
  );
174
165
  },
@@ -34,7 +34,3 @@
34
34
  width: 100%;
35
35
  }
36
36
 
37
- /* ===== 行可点击 ===== */
38
- .k-crud-page--clickable .k-tbody .k-tr {
39
- cursor: pointer;
40
- }
@@ -52,50 +52,52 @@ export default defineComponent({
52
52
  const title = isEdit.value ? `编辑${props.config.title}` : `新建${props.config.title}`;
53
53
 
54
54
  return (
55
- <div class="k-form-page">
56
- {/* 页头 */}
57
- <div class="k-fp-header">
58
- <div class="k-fp-header-left">
59
- <KButton text="" onClick={goBack} />
60
- <h1 class="k-fp-title">{title}</h1>
55
+ <div class="k-form-page-wrapper">
56
+ <div class="k-form-page">
57
+ {/* 页头 */}
58
+ <div class="k-fp-header">
59
+ <div class="k-fp-header-left">
60
+ <KButton text="" onClick={goBack} />
61
+ <h1 class="k-fp-title">{title}</h1>
62
+ </div>
63
+ <div class="k-fp-header-right">
64
+ {slots.headerExtra?.()}
65
+ </div>
61
66
  </div>
62
- <div class="k-fp-header-right">
63
- {slots.headerExtra?.()}
64
- </div>
65
- </div>
66
67
 
67
- {/* 表单 */}
68
- <KFormCard>
69
- {slots.beforeFields?.({ formData })}
68
+ {/* 表单 */}
69
+ <KFormCard>
70
+ {slots.beforeFields?.({ formData })}
70
71
 
71
- <div class={`k-fp-grid k-fp-grid--${cols}`}>
72
- {props.config.fields.map(field => {
73
- const span = field.span === 'full' ? cols : (field.span ?? 1);
74
- return (
75
- <div
76
- key={field.param}
77
- class="k-fp-field"
78
- style={span > 1 ? { gridColumn: `span ${span}` } : undefined}
79
- >
80
- <label class="k-fp-label">
81
- {field.label}
82
- {field.required && <span class="k-fp-required">*</span>}
83
- </label>
84
- {renderFormField(field, formData, errors.value, validateField, slots)}
85
- {errors.value[field.param] && (
86
- <span class="k-fp-error">{errors.value[field.param]}</span>
87
- )}
88
- </div>
89
- );
90
- })}
91
- </div>
72
+ <div class={`k-fp-grid k-fp-grid--${cols}`}>
73
+ {props.config.fields.map(field => {
74
+ const span = field.span === 'full' ? cols : (field.span ?? 1);
75
+ return (
76
+ <div
77
+ key={field.param}
78
+ class="k-fp-field"
79
+ style={span > 1 ? { gridColumn: `span ${span}` } : undefined}
80
+ >
81
+ <label class="k-fp-label">
82
+ {field.label}
83
+ {field.required && <span class="k-fp-required">*</span>}
84
+ </label>
85
+ {renderFormField(field, formData, errors.value, validateField, slots)}
86
+ {errors.value[field.param] && (
87
+ <span class="k-fp-error">{errors.value[field.param]}</span>
88
+ )}
89
+ </div>
90
+ );
91
+ })}
92
+ </div>
92
93
 
93
- {slots.afterFields?.({ formData })}
94
- </KFormCard>
94
+ {slots.afterFields?.({ formData })}
95
+ </KFormCard>
95
96
 
96
- {slots.default?.({ formData })}
97
+ {slots.default?.({ formData })}
98
+ </div>
97
99
 
98
- {/* 底部操作栏 */}
100
+ {/* 底部操作栏 — 在 max-width 容器外,撑满内容区 */}
99
101
  <KStickyActionBar>
100
102
  {{
101
103
  default: () => slots.actions?.({ formData, submit, saveDraft, submitting }) ?? (
@@ -79,10 +79,10 @@
79
79
  }
80
80
 
81
81
  .k-form-card-body {
82
- padding: 0 var(--kine-spacing-10) var(--kine-spacing-10);
82
+ padding: var(--kine-spacing-10);
83
83
  }
84
84
 
85
- /* 当有 header 时,body 顶部加分割线 */
85
+ /* header 时,body 顶部不需要额外间距(header 自带 padding-bottom) */
86
86
  .k-form-card-header + .k-form-card-body {
87
87
  padding-top: 0;
88
88
  }
@@ -97,7 +97,7 @@
97
97
  z-index: 10;
98
98
  display: flex;
99
99
  align-items: center;
100
- justify-content: space-between;
100
+ justify-content: flex-end;
101
101
  padding: var(--kine-spacing-6) var(--kine-spacing-12);
102
102
  background: var(--kine-color-bg-tertiary, #fff);
103
103
  border-top: 1px solid var(--kine-color-border-default, #e5e7eb);
@@ -247,9 +247,19 @@
247
247
  KFormPage / KMasterDetailPage — 表单页布局
248
248
  ================================================================ */
249
249
 
250
+ /* wrapper: 全宽容器,承载 sticky bar 出血 */
251
+ .k-form-page-wrapper {
252
+ display: flex;
253
+ flex-direction: column;
254
+ min-height: 100%;
255
+ }
256
+
250
257
  .k-form-page {
251
258
  max-width: 960px;
252
- padding: 0 0 80px;
259
+ width: 100%;
260
+ margin: 0 auto;
261
+ padding: 0 0 var(--kine-spacing-10);
262
+ flex: 1;
253
263
  }
254
264
 
255
265
  .k-master-detail-page {
@@ -16,7 +16,7 @@ export interface CrudColumnConfig {
16
16
  /** 列宽 */
17
17
  width?: string;
18
18
  /** 列类型,影响渲染方式 */
19
- type?: 'text' | 'status' | 'date' | 'datetime';
19
+ type?: 'text' | 'status' | 'date' | 'datetime' | 'image';
20
20
  }
21
21
 
22
22
  /** 筛选项配置 */
@@ -55,6 +55,8 @@ export interface CrudPageConfig {
55
55
  pageSize?: number;
56
56
  /** 行主键字段,默认 'id' */
57
57
  rowKey?: string;
58
- /** 详情页路由前缀,设置后点击行自动跳转到 `${detailPath}/${row[rowKey]}` */
58
+ /** 详情页路由前缀,用于构建查看/编辑路由:`${detailPath}/${row[rowKey]}` */
59
59
  detailPath?: string;
60
+ /** 图片列 src 转换函数,将原始值转为可访问 URL */
61
+ imageResolver?: (raw: string) => string;
60
62
  }
@@ -17,6 +17,8 @@ import { RequestBuilder, type RequestBuilderContext } from './requestBuilder';
17
17
  // ────────────────────────────────────────────────────────────────────────────
18
18
 
19
19
  export interface RequestClient {
20
+ /** 请求基础 URL */
21
+ readonly baseURL: string;
20
22
  /** 简便方法,直接返回 Promise<T> */
21
23
  send<T>(method: RequestMethod, url: string, body?: unknown): Promise<T>;
22
24
  /** GET 请求 */
@@ -85,6 +87,8 @@ export function createRequest(options: RequestOptions = {}): RequestClient {
85
87
  }
86
88
 
87
89
  const client: RequestClient = {
90
+ baseURL: context.baseURL,
91
+
88
92
  send: <T>(method: RequestMethod, url: string, body?: unknown) =>
89
93
  createBuilder(method, url, body).execute<T>(),
90
94
 
@@ -15,7 +15,7 @@ export interface CrudColumnConfig {
15
15
  /** 列宽 */
16
16
  width?: string;
17
17
  /** 列类型,影响渲染方式 */
18
- type?: 'text' | 'status' | 'date' | 'datetime';
18
+ type?: 'text' | 'status' | 'date' | 'datetime' | 'image';
19
19
  }
20
20
  /** 筛选项配置 */
21
21
  export interface CrudFilterConfig {
@@ -54,6 +54,8 @@ export interface CrudPageConfig {
54
54
  pageSize?: number;
55
55
  /** 行主键字段,默认 'id' */
56
56
  rowKey?: string;
57
- /** 详情页路由前缀,设置后点击行自动跳转到 `${detailPath}/${row[rowKey]}` */
57
+ /** 详情页路由前缀,用于构建查看/编辑路由:`${detailPath}/${row[rowKey]}` */
58
58
  detailPath?: string;
59
+ /** 图片列 src 转换函数,将原始值转为可访问 URL */
60
+ imageResolver?: (raw: string) => string;
59
61
  }
@@ -1,6 +1,8 @@
1
1
  import { RequestMethod, RequestOptions } from './types';
2
2
  import { RequestBuilder } from './requestBuilder';
3
3
  export interface RequestClient {
4
+ /** 请求基础 URL */
5
+ readonly baseURL: string;
4
6
  /** 简便方法,直接返回 Promise<T> */
5
7
  send<T>(method: RequestMethod, url: string, body?: unknown): Promise<T>;
6
8
  /** GET 请求 */
package/dist/crud.css CHANGED
@@ -737,6 +737,213 @@
737
737
  pointer-events: none;
738
738
  cursor: not-allowed;
739
739
  }
740
+ /**
741
+ * @description kine-ui image 样式 — Phosphor 主题
742
+ * @author 阿怪
743
+ * @date 2026/2/26
744
+ * @version v1.0.0
745
+ *
746
+ * 江湖的业务千篇一律,复杂的代码好几百行。
747
+ */
748
+
749
+ /* === 图片容器 === */
750
+ .k-image {
751
+ position: relative;
752
+ display: inline-block;
753
+ overflow: hidden;
754
+ width: 100%;
755
+ height: 100%;
756
+ background: var(--kine-color-bg-secondary);
757
+ border-radius: var(--kine-radius-xs);
758
+ }
759
+
760
+ /* 有预览时鼠标变指针 */
761
+ .k-image-previewable {
762
+ cursor: zoom-in;
763
+ }
764
+
765
+ /* === 实际图片元素 === */
766
+ .k-image-inner {
767
+ display: block;
768
+ width: 100%;
769
+ height: 100%;
770
+ transition: opacity var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
771
+ }
772
+
773
+ .k-image-inner-hidden {
774
+ opacity: 0;
775
+ position: absolute;
776
+ top: 0;
777
+ left: 0;
778
+ pointer-events: none;
779
+ }
780
+
781
+ /* === 占位容器(loading / error 共用) === */
782
+ .k-image-placeholder {
783
+ display: flex;
784
+ flex-direction: column;
785
+ align-items: center;
786
+ justify-content: center;
787
+ gap: var(--kine-spacing-4);
788
+ min-width: 100px;
789
+ min-height: 100px;
790
+ width: 100%;
791
+ height: 100%;
792
+ font-family: var(--kine-font-family-mono);
793
+ font-size: var(--kine-font-size-sm);
794
+ }
795
+
796
+ .k-image-placeholder-icon {
797
+ width: 40px;
798
+ height: 40px;
799
+ }
800
+
801
+ /* loading 状态:accent 微光 + 动画 */
802
+ .k-image-loading {
803
+ color: var(--kine-color-text-muted);
804
+ }
805
+
806
+ .k-image-loading .k-image-placeholder-icon {
807
+ animation: k-image-pulse 1.6s ease-in-out infinite;
808
+ color: var(--kine-color-accent-default);
809
+ }
810
+
811
+ @keyframes k-image-pulse {
812
+ 0%, 100% { opacity: 0.3; }
813
+ 50% { opacity: 1; }
814
+ }
815
+
816
+ /* error 状态 */
817
+ .k-image-error {
818
+ color: var(--kine-color-semantic-error);
819
+ }
820
+
821
+ .k-image-error-text {
822
+ color: var(--kine-color-text-muted);
823
+ }
824
+
825
+ /* === 全屏预览遮罩层 === */
826
+ .k-image-preview-mask {
827
+ position: fixed;
828
+ inset: 0;
829
+ background: color-mix(in srgb, var(--kine-color-bg-primary) 90%, transparent);
830
+ display: flex;
831
+ align-items: center;
832
+ justify-content: center;
833
+ }
834
+
835
+ /* === 预览图片本体 === */
836
+ .k-image-preview-img {
837
+ max-width: 90vw;
838
+ max-height: 85vh;
839
+ object-fit: contain;
840
+ border-radius: var(--kine-radius-xs);
841
+ box-shadow: 0 0 40px color-mix(in srgb, var(--kine-color-accent-default) 20%, transparent);
842
+ transition: transform var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
843
+ user-select: none;
844
+ }
845
+
846
+ /* === 工具栏 === */
847
+ .k-image-preview-toolbar {
848
+ position: absolute;
849
+ top: var(--kine-spacing-8);
850
+ left: 50%;
851
+ transform: translateX(-50%);
852
+ display: flex;
853
+ align-items: center;
854
+ gap: var(--kine-spacing-2);
855
+ background: var(--kine-color-bg-secondary);
856
+ border: 1px solid var(--kine-color-border-default);
857
+ border-radius: var(--kine-radius-xs);
858
+ padding: var(--kine-spacing-2) var(--kine-spacing-4);
859
+ font-family: var(--kine-font-family-mono);
860
+ }
861
+
862
+ /* === 工具按钮 === */
863
+ .k-image-preview-btn {
864
+ display: inline-flex;
865
+ align-items: center;
866
+ justify-content: center;
867
+ width: 28px;
868
+ height: 28px;
869
+ border: none;
870
+ border-radius: var(--kine-radius-xs);
871
+ background: transparent;
872
+ color: var(--kine-color-text-secondary);
873
+ font-size: 16px;
874
+ cursor: pointer;
875
+ transition:
876
+ color var(--kine-motion-duration-fast) var(--kine-motion-easing-default),
877
+ background var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
878
+ }
879
+
880
+ .k-image-preview-btn:hover {
881
+ color: var(--kine-color-text-primary);
882
+ background: color-mix(in srgb, var(--kine-color-accent-default) 15%, transparent);
883
+ }
884
+
885
+ .k-image-preview-close:hover {
886
+ color: var(--kine-color-semantic-error);
887
+ background: color-mix(in srgb, var(--kine-color-semantic-error) 15%, transparent);
888
+ }
889
+
890
+ /* 当前缩放比例显示 */
891
+ .k-image-preview-scale {
892
+ font-size: var(--kine-font-size-sm);
893
+ color: var(--kine-color-text-muted);
894
+ min-width: 40px;
895
+ text-align: center;
896
+ }
897
+
898
+ /* === 预览切换箭头 === */
899
+ .k-image-preview-arrow {
900
+ position: absolute;
901
+ top: 50%;
902
+ transform: translateY(-50%);
903
+ display: flex;
904
+ align-items: center;
905
+ justify-content: center;
906
+ width: 44px;
907
+ height: 44px;
908
+ border: 1px solid var(--kine-color-border-default);
909
+ border-radius: 50%;
910
+ background: color-mix(in srgb, var(--kine-color-bg-secondary) 80%, transparent);
911
+ color: var(--kine-color-text-primary);
912
+ font-size: 26px;
913
+ cursor: pointer;
914
+ transition:
915
+ background var(--kine-motion-duration-fast) var(--kine-motion-easing-default),
916
+ border-color var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
917
+ }
918
+
919
+ .k-image-preview-arrow:hover {
920
+ background: var(--kine-color-bg-secondary);
921
+ border-color: var(--kine-color-accent-default);
922
+ color: var(--kine-color-accent-default);
923
+ }
924
+
925
+ .k-image-preview-arrow-prev {
926
+ left: 20px;
927
+ }
928
+
929
+ .k-image-preview-arrow-next {
930
+ right: 20px;
931
+ }
932
+
933
+ /* === 图片计数 === */
934
+ .k-image-preview-counter {
935
+ position: absolute;
936
+ bottom: var(--kine-spacing-8);
937
+ left: 50%;
938
+ transform: translateX(-50%);
939
+ font-family: var(--kine-font-family-mono);
940
+ font-size: var(--kine-font-size-sm);
941
+ color: var(--kine-color-text-muted);
942
+ background: var(--kine-color-bg-secondary);
943
+ border: 1px solid var(--kine-color-border-default);
944
+ border-radius: var(--kine-radius-xs);
945
+ padding: var(--kine-spacing-1) var(--kine-spacing-5);
946
+ }
740
947
  /**
741
948
  * @description kine-ui input 样式
742
949
  * @author 阿怪
@@ -1101,10 +1308,6 @@ textarea.k-input {
1101
1308
  width: 100%;
1102
1309
  }
1103
1310
 
1104
- /* ===== 行可点击 ===== */
1105
- .k-crud-page--clickable .k-tbody .k-tr {
1106
- cursor: pointer;
1107
- }
1108
1311
  /**
1109
1312
  * @description KLoginPage 登录页样式 — Phosphor 主题
1110
1313
  * @author 阿怪
@@ -1846,10 +2049,10 @@ textarea.k-input {
1846
2049
  }
1847
2050
 
1848
2051
  .k-form-card-body {
1849
- padding: 0 var(--kine-spacing-10) var(--kine-spacing-10);
2052
+ padding: var(--kine-spacing-10);
1850
2053
  }
1851
2054
 
1852
- /* 当有 header 时,body 顶部加分割线 */
2055
+ /* header 时,body 顶部不需要额外间距(header 自带 padding-bottom) */
1853
2056
  .k-form-card-header + .k-form-card-body {
1854
2057
  padding-top: 0;
1855
2058
  }
@@ -1864,7 +2067,7 @@ textarea.k-input {
1864
2067
  z-index: 10;
1865
2068
  display: flex;
1866
2069
  align-items: center;
1867
- justify-content: space-between;
2070
+ justify-content: flex-end;
1868
2071
  padding: var(--kine-spacing-6) var(--kine-spacing-12);
1869
2072
  background: var(--kine-color-bg-tertiary, #fff);
1870
2073
  border-top: 1px solid var(--kine-color-border-default, #e5e7eb);
@@ -2014,9 +2217,19 @@ textarea.k-input {
2014
2217
  KFormPage / KMasterDetailPage — 表单页布局
2015
2218
  ================================================================ */
2016
2219
 
2220
+ /* wrapper: 全宽容器,承载 sticky bar 出血 */
2221
+ .k-form-page-wrapper {
2222
+ display: flex;
2223
+ flex-direction: column;
2224
+ min-height: 100%;
2225
+ }
2226
+
2017
2227
  .k-form-page {
2018
2228
  max-width: 960px;
2019
- padding: 0 0 80px;
2229
+ width: 100%;
2230
+ margin: 0 auto;
2231
+ padding: 0 0 var(--kine-spacing-10);
2232
+ flex: 1;
2020
2233
  }
2021
2234
 
2022
2235
  .k-master-detail-page {
package/dist/crud.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Comment, Fragment, Teleport, computed, createApp, createTextVNode, createVNode, defineComponent, h, inject, isRef, mergeProps, nextTick, onBeforeUnmount, onMounted, onUnmounted, provide, reactive, ref, resolveComponent, shallowRef, toRef, triggerRef, watch } from "vue";
2
- import { useRoute, useRouter } from "vue-router";
3
2
  import { QueryClient, VueQueryPlugin, useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
4
3
  import { createPinia, defineStore } from "pinia";
4
+ import { useRoute, useRouter } from "vue-router";
5
5
  //#region \0rolldown/runtime.js
6
6
  var __create = Object.create;
7
7
  var __defProp = Object.defineProperty;
@@ -153,7 +153,7 @@ var KContent_default = /* @__PURE__ */ defineComponent({
153
153
  });
154
154
  //#endregion
155
155
  //#region ../core/components/template/table/api.ts
156
- var props$8 = {
156
+ var props$10 = {
157
157
  data: {
158
158
  type: Array,
159
159
  default: () => []
@@ -268,8 +268,8 @@ function useTable() {
268
268
  *
269
269
  * 江湖的业务千篇一律,复杂的代码好几百行。
270
270
  */
271
- var { props: props$7 } = {
272
- props: props$8,
271
+ var { props: props$9 } = {
272
+ props: props$10,
273
273
  useTable
274
274
  };
275
275
  var KTable_default = /* @__PURE__ */ defineComponent((_props, { slots }) => {
@@ -336,11 +336,11 @@ var KTable_default = /* @__PURE__ */ defineComponent((_props, { slots }) => {
336
336
  };
337
337
  }, {
338
338
  name: "KTable",
339
- props: props$7
339
+ props: props$9
340
340
  });
341
341
  //#endregion
342
342
  //#region ../core/components/base/input/api.ts
343
- var props$6 = {
343
+ var props$8 = {
344
344
  type: {
345
345
  type: String,
346
346
  default: "text"
@@ -412,12 +412,12 @@ function useInput(props, ctx) {
412
412
  * 江湖的业务千篇一律,复杂的代码好几百行。
413
413
  */
414
414
  var InputCore = {
415
- props: props$6,
415
+ props: props$8,
416
416
  useInput
417
417
  };
418
418
  //#endregion
419
419
  //#region ../core/components/base/button/api.ts
420
- var props$5 = {
420
+ var props$7 = {
421
421
  text: {
422
422
  type: String,
423
423
  default: ""
@@ -494,12 +494,12 @@ function useButton(props, { slots }) {
494
494
  * 江湖的业务千篇一律,复杂的代码好几百行。
495
495
  */
496
496
  var ButtonCore = {
497
- props: props$5,
497
+ props: props$7,
498
498
  useButton
499
499
  };
500
500
  //#endregion
501
501
  //#region ../core/components/base/select/api.ts
502
- var props$4 = {
502
+ var props$6 = {
503
503
  modelValue: {
504
504
  type: void 0,
505
505
  default: ""
@@ -809,7 +809,7 @@ function useSelect$1(props, ctx) {
809
809
  * 江湖的业务千篇一律,复杂的代码好几百行。
810
810
  */
811
811
  var SelectCore = {
812
- props: props$4,
812
+ props: props$6,
813
813
  useSelect: useSelect$1
814
814
  };
815
815
  //#endregion
@@ -6159,7 +6159,7 @@ var computePosition = (reference, floating, options) => {
6159
6159
  };
6160
6160
  //#endregion
6161
6161
  //#region ../core/components/template/pagination/api.ts
6162
- var props$2 = {
6162
+ var props$4 = {
6163
6163
  total: {
6164
6164
  type: Number,
6165
6165
  default: 0
@@ -6318,7 +6318,7 @@ function usePagination(props, currentValue) {
6318
6318
  * 江湖的业务千篇一律,复杂的代码好几百行。
6319
6319
  */
6320
6320
  var PaginationCore = {
6321
- props: props$2,
6321
+ props: props$4,
6322
6322
  usePagination
6323
6323
  };
6324
6324
  //#endregion
@@ -6761,6 +6761,154 @@ var TableColumnCore = { props: {
6761
6761
  }
6762
6762
  } };
6763
6763
  //#endregion
6764
+ //#region ../core/components/base/image/api.ts
6765
+ var props$2 = {
6766
+ src: {
6767
+ type: String,
6768
+ required: true
6769
+ },
6770
+ alt: {
6771
+ type: String,
6772
+ default: ""
6773
+ },
6774
+ fit: {
6775
+ type: String,
6776
+ default: "cover",
6777
+ enum: [
6778
+ "contain",
6779
+ "cover",
6780
+ "fill",
6781
+ "none",
6782
+ "scale-down"
6783
+ ]
6784
+ },
6785
+ width: {
6786
+ type: [String, Number],
6787
+ default: void 0
6788
+ },
6789
+ height: {
6790
+ type: [String, Number],
6791
+ default: void 0
6792
+ },
6793
+ lazy: {
6794
+ type: Boolean,
6795
+ default: false
6796
+ },
6797
+ previewSrcList: {
6798
+ type: Array,
6799
+ default: () => []
6800
+ },
6801
+ zIndex: {
6802
+ type: Number,
6803
+ default: 2e3
6804
+ }
6805
+ };
6806
+ //#endregion
6807
+ //#region ../core/components/base/image/useImage.ts
6808
+ /**
6809
+ * @description image hook
6810
+ * @author 阿怪
6811
+ * @date 2026/2/26
6812
+ * @version v1.0.0
6813
+ *
6814
+ * 江湖的业务千篇一律,复杂的代码好几百行。
6815
+ */
6816
+ function useImage(props, emit) {
6817
+ const status = ref("loading");
6818
+ const previewVisible = ref(false);
6819
+ const previewIndex = ref(0);
6820
+ const previewScale = ref(1);
6821
+ const previewRotate = ref(0);
6822
+ /** 重置加载状态(src 变化时调用) */
6823
+ const reset = () => {
6824
+ status.value = "loading";
6825
+ };
6826
+ const handleLoad = (e) => {
6827
+ status.value = "loaded";
6828
+ emit("load", e);
6829
+ };
6830
+ const handleError = (e) => {
6831
+ status.value = "error";
6832
+ emit("error", e);
6833
+ };
6834
+ /** 打开全屏预览,定位到 src 在 previewSrcList 中的位置 */
6835
+ const openPreview = () => {
6836
+ if (!props.previewSrcList || props.previewSrcList.length === 0) return;
6837
+ const idx = props.previewSrcList.indexOf(props.src);
6838
+ previewIndex.value = idx >= 0 ? idx : 0;
6839
+ previewScale.value = 1;
6840
+ previewRotate.value = 0;
6841
+ previewVisible.value = true;
6842
+ };
6843
+ const closePreview = () => {
6844
+ previewVisible.value = false;
6845
+ };
6846
+ /** 预览切换到上一张 */
6847
+ const previewPrev = () => {
6848
+ const total = props.previewSrcList.length;
6849
+ if (total === 0) return;
6850
+ previewIndex.value = (previewIndex.value - 1 + total) % total;
6851
+ previewScale.value = 1;
6852
+ previewRotate.value = 0;
6853
+ emit("switch", previewIndex.value);
6854
+ };
6855
+ /** 预览切换到下一张 */
6856
+ const previewNext = () => {
6857
+ const total = props.previewSrcList.length;
6858
+ if (total === 0) return;
6859
+ previewIndex.value = (previewIndex.value + 1) % total;
6860
+ previewScale.value = 1;
6861
+ previewRotate.value = 0;
6862
+ emit("switch", previewIndex.value);
6863
+ };
6864
+ /** 预览放大 */
6865
+ const zoomIn = () => {
6866
+ previewScale.value = Math.min(previewScale.value + .25, 5);
6867
+ };
6868
+ /** 预览缩小 */
6869
+ const zoomOut = () => {
6870
+ previewScale.value = Math.max(previewScale.value - .25, .25);
6871
+ };
6872
+ /** 顺时针旋转 90° */
6873
+ const rotate = () => {
6874
+ previewRotate.value = (previewRotate.value + 90) % 360;
6875
+ };
6876
+ watch(() => props.src, reset);
6877
+ onMounted(() => {
6878
+ if (props.lazy) status.value = "loading";
6879
+ });
6880
+ return {
6881
+ status,
6882
+ previewVisible,
6883
+ previewIndex,
6884
+ previewScale,
6885
+ previewRotate,
6886
+ handleLoad,
6887
+ handleError,
6888
+ openPreview,
6889
+ closePreview,
6890
+ previewPrev,
6891
+ previewNext,
6892
+ zoomIn,
6893
+ zoomOut,
6894
+ rotate
6895
+ };
6896
+ }
6897
+ //#endregion
6898
+ //#region ../core/components/base/image/index.ts
6899
+ /**
6900
+ * @description image core 导出
6901
+ * @author 阿怪
6902
+ * @date 2026/2/26
6903
+ * @version v1.0.0
6904
+ *
6905
+ * 江湖的业务千篇一律,复杂的代码好几百行。
6906
+ */
6907
+ var ImageCore = {
6908
+ props: props$2,
6909
+ useImage
6910
+ };
6911
+ //#endregion
6764
6912
  //#region ../ui/components/pagination/KPagination.tsx
6765
6913
  /**
6766
6914
  * @description kine-ui pagination 组件
@@ -6986,7 +7134,7 @@ var KTableColumn_default = /* @__PURE__ */ defineComponent({
6986
7134
  *
6987
7135
  * 江湖的业务千篇一律,复杂的代码好几百行。
6988
7136
  */
6989
- var { props } = TagCore;
7137
+ var { props: props$1 } = TagCore;
6990
7138
  var KTag_default = /* @__PURE__ */ defineComponent((_props, { slots, emit }) => {
6991
7139
  const props = _props;
6992
7140
  return () => {
@@ -7020,10 +7168,153 @@ var KTag_default = /* @__PURE__ */ defineComponent((_props, { slots, emit }) =>
7020
7168
  };
7021
7169
  }, {
7022
7170
  name: "KTag",
7023
- props,
7171
+ props: props$1,
7024
7172
  emits: ["close", "click"]
7025
7173
  });
7026
7174
  //#endregion
7175
+ //#region ../ui/components/image/KImage.tsx
7176
+ /**
7177
+ * @description kine-ui image 图片组件
7178
+ * @author 阿怪
7179
+ * @date 2026/2/26
7180
+ * @version v1.0.0
7181
+ *
7182
+ * 江湖的业务千篇一律,复杂的代码好几百行。
7183
+ */
7184
+ var { props } = ImageCore;
7185
+ /** 加载中占位图标(sci-fi 风格) */
7186
+ var LoadingPlaceholder = () => createVNode("div", { "class": "k-image-placeholder k-image-loading" }, [createVNode("svg", {
7187
+ "viewBox": "0 0 24 24",
7188
+ "fill": "none",
7189
+ "class": "k-image-placeholder-icon"
7190
+ }, [
7191
+ createVNode("rect", {
7192
+ "x": "3",
7193
+ "y": "3",
7194
+ "width": "18",
7195
+ "height": "18",
7196
+ "rx": "2",
7197
+ "stroke": "currentColor",
7198
+ "stroke-width": "1.5"
7199
+ }, null),
7200
+ createVNode("circle", {
7201
+ "cx": "8.5",
7202
+ "cy": "8.5",
7203
+ "r": "1.5",
7204
+ "fill": "currentColor",
7205
+ "opacity": "0.5"
7206
+ }, null),
7207
+ createVNode("path", {
7208
+ "d": "M3 15l5-5 4 4 3-3 6 6",
7209
+ "stroke": "currentColor",
7210
+ "stroke-width": "1.5",
7211
+ "stroke-linecap": "round"
7212
+ }, null)
7213
+ ])]);
7214
+ /** 加载失败占位图标 */
7215
+ var ErrorPlaceholder = () => createVNode("div", { "class": "k-image-placeholder k-image-error" }, [createVNode("svg", {
7216
+ "viewBox": "0 0 24 24",
7217
+ "fill": "none",
7218
+ "class": "k-image-placeholder-icon"
7219
+ }, [createVNode("rect", {
7220
+ "x": "3",
7221
+ "y": "3",
7222
+ "width": "18",
7223
+ "height": "18",
7224
+ "rx": "2",
7225
+ "stroke": "currentColor",
7226
+ "stroke-width": "1.5"
7227
+ }, null), createVNode("path", {
7228
+ "d": "M9 9l6 6M15 9l-6 6",
7229
+ "stroke": "currentColor",
7230
+ "stroke-width": "1.5",
7231
+ "stroke-linecap": "round"
7232
+ }, null)]), createVNode("span", { "class": "k-image-error-text" }, [createTextVNode("加载失败")])]);
7233
+ var KImage_default = /* @__PURE__ */ defineComponent((_props, { slots, emit }) => {
7234
+ const cProps = _props;
7235
+ const { status, previewVisible, previewIndex, previewScale, previewRotate, handleLoad, handleError, openPreview, closePreview, previewPrev, previewNext, zoomIn, zoomOut, rotate } = useImage(cProps, (event, payload) => emit(event, payload));
7236
+ const hasPreview = computed(() => cProps.previewSrcList && cProps.previewSrcList.length > 0);
7237
+ return () => {
7238
+ const currentPreviewSrc = hasPreview.value ? cProps.previewSrcList[previewIndex.value] : cProps.src;
7239
+ return createVNode(Fragment, null, [createVNode("div", {
7240
+ "class": ["k-image", status.value === "loaded" && hasPreview.value ? "k-image-previewable" : ""],
7241
+ "style": {
7242
+ width: cProps.width != null ? typeof cProps.width === "number" ? `${cProps.width}px` : cProps.width : void 0,
7243
+ height: cProps.height != null ? typeof cProps.height === "number" ? `${cProps.height}px` : cProps.height : void 0
7244
+ },
7245
+ "onClick": hasPreview.value ? openPreview : void 0
7246
+ }, [
7247
+ status.value === "loading" && (slots.placeholder ? slots.placeholder() : createVNode(LoadingPlaceholder, null, null)),
7248
+ status.value === "error" && (slots.error ? slots.error() : createVNode(ErrorPlaceholder, null, null)),
7249
+ createVNode("img", {
7250
+ "class": ["k-image-inner", status.value !== "loaded" ? "k-image-inner-hidden" : ""],
7251
+ "src": cProps.src,
7252
+ "alt": cProps.alt,
7253
+ "loading": cProps.lazy ? "lazy" : "eager",
7254
+ "style": { objectFit: cProps.fit },
7255
+ "onLoad": handleLoad,
7256
+ "onError": handleError
7257
+ }, null)
7258
+ ]), hasPreview.value && previewVisible.value && createVNode(Teleport, { "to": "body" }, { default: () => [createVNode("div", {
7259
+ "class": "k-image-preview-mask",
7260
+ "style": { zIndex: cProps.zIndex },
7261
+ "onClick": (e) => {
7262
+ if (e.target === e.currentTarget) closePreview();
7263
+ }
7264
+ }, [
7265
+ createVNode("div", { "class": "k-image-preview-toolbar" }, [
7266
+ createVNode("button", {
7267
+ "class": "k-image-preview-btn",
7268
+ "onClick": zoomOut,
7269
+ "title": "缩小"
7270
+ }, [createTextVNode("-")]),
7271
+ createVNode("span", { "class": "k-image-preview-scale" }, [Math.round(previewScale.value * 100), createTextVNode("%")]),
7272
+ createVNode("button", {
7273
+ "class": "k-image-preview-btn",
7274
+ "onClick": zoomIn,
7275
+ "title": "放大"
7276
+ }, [createTextVNode("+")]),
7277
+ createVNode("button", {
7278
+ "class": "k-image-preview-btn",
7279
+ "onClick": rotate,
7280
+ "title": "旋转"
7281
+ }, [createTextVNode("↻")]),
7282
+ createVNode("button", {
7283
+ "class": "k-image-preview-btn k-image-preview-close",
7284
+ "onClick": closePreview,
7285
+ "title": "关闭"
7286
+ }, [createTextVNode("✕")])
7287
+ ]),
7288
+ createVNode("img", {
7289
+ "class": "k-image-preview-img",
7290
+ "src": currentPreviewSrc,
7291
+ "style": { transform: `scale(${previewScale.value}) rotate(${previewRotate.value}deg)` },
7292
+ "alt": ""
7293
+ }, null),
7294
+ cProps.previewSrcList.length > 1 && createVNode(Fragment, null, [createVNode("button", {
7295
+ "class": "k-image-preview-arrow k-image-preview-arrow-prev",
7296
+ "onClick": previewPrev
7297
+ }, [createTextVNode("‹")]), createVNode("button", {
7298
+ "class": "k-image-preview-arrow k-image-preview-arrow-next",
7299
+ "onClick": previewNext
7300
+ }, [createTextVNode("›")])]),
7301
+ cProps.previewSrcList.length > 1 && createVNode("div", { "class": "k-image-preview-counter" }, [
7302
+ previewIndex.value + 1,
7303
+ createTextVNode(" / "),
7304
+ cProps.previewSrcList.length
7305
+ ])
7306
+ ])] })]);
7307
+ };
7308
+ }, {
7309
+ name: "KImage",
7310
+ props,
7311
+ emits: [
7312
+ "load",
7313
+ "error",
7314
+ "switch"
7315
+ ]
7316
+ });
7317
+ //#endregion
7027
7318
  //#region ../ui/components/input/KInput.tsx
7028
7319
  /**
7029
7320
  * @description kine-ui input 组件
@@ -8002,6 +8293,7 @@ function createRequest(options = {}) {
8002
8293
  return builder;
8003
8294
  }
8004
8295
  const client = {
8296
+ baseURL: context.baseURL,
8005
8297
  send: (method, url, body) => createBuilder(method, url, body).execute(),
8006
8298
  get: (url, params) => createBuilder("GET", url, params).execute(),
8007
8299
  post: (url, body) => createBuilder("POST", url, body).execute(),
@@ -8387,7 +8679,6 @@ var KCrudPage_default = /* @__PURE__ */ defineComponent({
8387
8679
  required: true
8388
8680
  } },
8389
8681
  setup(props, { slots }) {
8390
- const router = useRouter();
8391
8682
  const { page, pageSize, total, list, loading, filters, onPageChange, onSearch, onReset } = useCrudPage(props.config);
8392
8683
  /** 格式化日期 */
8393
8684
  const formatDate = (val) => {
@@ -8419,22 +8710,23 @@ var KCrudPage_default = /* @__PURE__ */ defineComponent({
8419
8710
  case "status": return renderStatus(val);
8420
8711
  case "date": return formatDate(val);
8421
8712
  case "datetime": return formatDateTime(val);
8713
+ case "image": {
8714
+ if (!val) return "";
8715
+ const raw = String(val);
8716
+ const resolver = props.config.imageResolver;
8717
+ const src = resolver ? resolver(raw) : raw;
8718
+ return createVNode(KImage_default, {
8719
+ "src": src,
8720
+ "previewSrcList": [src],
8721
+ "width": 40,
8722
+ "height": 40,
8723
+ "fit": "cover",
8724
+ "lazy": true
8725
+ }, null);
8726
+ }
8422
8727
  default: return val != null ? String(val) : "";
8423
8728
  }
8424
8729
  };
8425
- /** 行点击 → 跳转详情 */
8426
- const onRowClick = (e) => {
8427
- const detailPath = props.config.detailPath;
8428
- if (!detailPath) return;
8429
- const tr = e.target.closest?.("tr");
8430
- if (!tr) return;
8431
- const tbody = tr.closest("tbody");
8432
- if (!tbody || tbody.classList.contains("k-table-empty")) return;
8433
- const index = Array.from(tbody.querySelectorAll("tr")).indexOf(tr);
8434
- if (index < 0 || index >= list.value.length) return;
8435
- const id = list.value[index][props.config.rowKey ?? "id"];
8436
- if (id != null) router.push(`${detailPath}/${id}`);
8437
- };
8438
8730
  /** 渲染筛选区表单项 */
8439
8731
  const renderFilters = () => {
8440
8732
  if (!props.config.filters?.length) return null;
@@ -8460,7 +8752,7 @@ var KCrudPage_default = /* @__PURE__ */ defineComponent({
8460
8752
  }
8461
8753
  }, null)]));
8462
8754
  };
8463
- return () => createVNode("div", { "class": ["k-crud-page", props.config.detailPath ? "k-crud-page--clickable" : ""] }, [createVNode(KPageHeader_default, { "title": props.config.title }, { extra: slots.headerExtra }), createVNode("div", { "onClick": onRowClick }, [createVNode(KSearchTable_default, {
8755
+ return () => createVNode("div", { "class": "k-crud-page" }, [createVNode(KPageHeader_default, { "title": props.config.title }, { extra: slots.headerExtra }), createVNode(KSearchTable_default, {
8464
8756
  "data": list.value,
8465
8757
  "loading": loading.value,
8466
8758
  "total": total.value,
@@ -8483,7 +8775,7 @@ var KCrudPage_default = /* @__PURE__ */ defineComponent({
8483
8775
  }, { default: customSlot ? (scope) => customSlot(scope) : col.type ? (scope) => renderCell(col, scope.row) : void 0 });
8484
8776
  }),
8485
8777
  empty: slots.empty
8486
- })])]);
8778
+ })]);
8487
8779
  }
8488
8780
  });
8489
8781
  //#endregion
@@ -10688,7 +10980,7 @@ var KFormPage_default = /* @__PURE__ */ defineComponent({
10688
10980
  return () => {
10689
10981
  if (loading.value) return createVNode("div", { "class": "k-fp-loading" }, [createTextVNode("加载中...")]);
10690
10982
  const title = isEdit.value ? `编辑${props.config.title}` : `新建${props.config.title}`;
10691
- return createVNode("div", { "class": "k-form-page" }, [
10983
+ return createVNode("div", { "class": "k-form-page-wrapper" }, [createVNode("div", { "class": "k-form-page" }, [
10692
10984
  createVNode("div", { "class": "k-fp-header" }, [createVNode("div", { "class": "k-fp-header-left" }, [createVNode(KButton_default, {
10693
10985
  "text": "←",
10694
10986
  "onClick": goBack
@@ -10709,31 +11001,30 @@ var KFormPage_default = /* @__PURE__ */ defineComponent({
10709
11001
  })]),
10710
11002
  slots.afterFields?.({ formData })
10711
11003
  ] }),
10712
- slots.default?.({ formData }),
10713
- createVNode(KStickyActionBar_default, null, { default: () => slots.actions?.({
10714
- formData,
10715
- submit,
10716
- saveDraft,
10717
- submitting
10718
- }) ?? createVNode(Fragment, null, [
10719
- createVNode(KButton_default, {
10720
- "text": "取消",
10721
- "onClick": goBack
10722
- }, null),
10723
- props.config.showDraft && createVNode(KButton_default, {
10724
- "text": "保存草稿",
10725
- "disabled": submitting.value,
10726
- "onClick": () => saveDraft()
10727
- }, null),
10728
- createVNode(KButton_default, {
10729
- "type": "primary",
10730
- "text": submitting.value ? "保存中..." : props.config.submitText ?? "保存",
10731
- "disabled": submitting.value,
10732
- "loading": submitting.value,
10733
- "onClick": () => submit()
10734
- }, null)
10735
- ]) })
10736
- ]);
11004
+ slots.default?.({ formData })
11005
+ ]), createVNode(KStickyActionBar_default, null, { default: () => slots.actions?.({
11006
+ formData,
11007
+ submit,
11008
+ saveDraft,
11009
+ submitting
11010
+ }) ?? createVNode(Fragment, null, [
11011
+ createVNode(KButton_default, {
11012
+ "text": "取消",
11013
+ "onClick": goBack
11014
+ }, null),
11015
+ props.config.showDraft && createVNode(KButton_default, {
11016
+ "text": "保存草稿",
11017
+ "disabled": submitting.value,
11018
+ "onClick": () => saveDraft()
11019
+ }, null),
11020
+ createVNode(KButton_default, {
11021
+ "type": "primary",
11022
+ "text": submitting.value ? "保存中..." : props.config.submitText ?? "保存",
11023
+ "disabled": submitting.value,
11024
+ "loading": submitting.value,
11025
+ "onClick": () => submit()
11026
+ }, null)
11027
+ ]) })]);
10737
11028
  };
10738
11029
  }
10739
11030
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kine-design/crud",
3
- "version": "0.0.1-beta.10",
3
+ "version": "0.0.1-beta.13",
4
4
  "type": "module",
5
5
  "main": "./dist/crud.js",
6
6
  "types": "./dist/index.d.ts",
@@ -9,8 +9,8 @@
9
9
  "pinia": "^3.0.3",
10
10
  "vue": "^3.5.30",
11
11
  "vue-router": "^5.0.3",
12
- "@kine-design/core": "0.0.1-beta.3",
13
- "kine-ui": "0.0.1-beta.4"
12
+ "kine-ui": "0.0.1-beta.6",
13
+ "@kine-design/core": "0.0.1-beta.3"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public",