@liujitcn/kratos-admin-core 0.0.47 → 0.0.48

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/README.md CHANGED
@@ -177,3 +177,9 @@ pnpm --filter @liujitcn/kratos-admin-core type:check
177
177
  pnpm --filter @liujitcn/kratos-admin-core build:package
178
178
  pnpm --filter @liujitcn/kratos-admin-core pack
179
179
  ```
180
+
181
+ 时间列的对齐方式由页面通过 `align` 显式配置,ProTable 不按时间字段名称判断;修改时间列使用 `align: "center"` 居中显示。
182
+
183
+ ProTable 的 `cellType: "actions"` 操作列按当前行的 `hidden` 过滤按钮,优先展示使用 `common.action.edit`、`common.action.delete` 文案的编辑、删除操作;可见按钮超过三项时,其余操作收入“更多”菜单。未配置或无权限的编辑、删除不会补充显示,菜单保留图标、禁用状态和点击参数;自定义 render 或插槽不参与折叠。
184
+
185
+ 操作列默认固定在右侧。配置式操作列(`cellType: "actions"`)根据当前页实际展示按钮和表头的最大宽度自适应,计入翻译文案、图标和间距,随语言、字体和行权限变化重新测量,最小宽度为 `80px`;折叠菜单内的按钮不参与测量。自定义 render 或插槽操作列仍默认 `160px`。页面可通过 `width` 或 `minWidth` 覆盖自动宽度,通过 `fixed: "left"` 调整固定位置,或通过 `fixed: false` 取消固定。
@@ -1 +1 @@
1
- export { formatJson, formatSrc, normalizeRichTextMediaPaths, normalizeStaticAssetPath } from "./utils/utils";
1
+ export { formatDateTime, formatJson, formatSrc, normalizeRichTextMediaPaths, normalizeStaticAssetPath } from "./utils/utils";
@@ -1,10 +1,9 @@
1
- import type { ColumnProps, EnumProps } from "../components/ProTable/interface";
1
+ import type { EnumProps } from "../components/ProTable/interface";
2
+ export { resolveTableColumnAlign, type TableAlign } from "./tableColumn";
2
3
  /** 字典值输出给表格枚举时的目标类型。 */
3
4
  type DictValueType = "number" | "string";
4
5
  /** 表格批量操作支持的主键类型。 */
5
6
  type SelectedId = string | number;
6
- /** 表格单元格支持的对齐方式。 */
7
- export type TableAlign = "left" | "center" | "right";
8
7
  /** 表格分页入参。 */
9
8
  type PageRequestParams = Record<string, any> & {
10
9
  /** 当前页码。 */
@@ -19,14 +18,6 @@ type NormalizedPageRequest<T extends PageRequestParams> = T & {
19
18
  /** 接口请求每页条数。 */
20
19
  page_size: number;
21
20
  };
22
- /**
23
- * 根据列语义和当前表格数据解析单元格对齐方式。
24
- *
25
- * 显式 align 始终优先;数据表映射按文本左对齐,字典、静态枚举及预置状态列居中,
26
- * 金额列和纯数字列右对齐,其余内容左对齐。空表时会先按列语义返回默认值,
27
- * 数据加载后由响应式表格重新解析。
28
- */
29
- export declare function resolveTableColumnAlign(column: ColumnProps, rows?: Record<string, any>[]): TableAlign;
30
21
  /**
31
22
  * 将字典缓存转换为 ProTable 搜索枚举数据。
32
23
  */
@@ -41,4 +32,3 @@ export declare function buildPageRequest<T extends PageRequestParams>(params: T)
41
32
  * 统一整理表格多选和单条操作产生的 ID 集合。
42
33
  */
43
34
  export declare function normalizeSelectedIds(selected?: SelectedId | SelectedId[]): SelectedId[];
44
- export {};
@@ -0,0 +1,11 @@
1
+ import type { ColumnProps } from "../components/ProTable/interface";
2
+ /** 表格单元格支持的对齐方式。 */
3
+ export type TableAlign = "left" | "center" | "right";
4
+ /**
5
+ * 根据列语义和当前表格数据解析单元格对齐方式。
6
+ *
7
+ * 显式 align 始终优先;字典、枚举及预置状态列居中,
8
+ * 金额列和纯数字列右对齐,其余内容左对齐。空表时会先按列语义返回默认值,
9
+ * 数据加载后由响应式表格重新解析。
10
+ */
11
+ export declare function resolveTableColumnAlign(column: ColumnProps, rows?: Record<string, any>[]): TableAlign;
@@ -6,6 +6,8 @@ export declare function formatJson(str: string): string;
6
6
  * 将后端分单位金额转换为元字符串。
7
7
  */
8
8
  export declare function formatPrice(price?: number): string;
9
+ /** 将时间值格式化为统一的本地日期时间文本。 */
10
+ export declare function formatDateTime(value?: string | number | Date): string;
9
11
  /**
10
12
  * 按静态资源域名补齐图片地址。
11
13
  */
@@ -3,8 +3,19 @@
3
3
  </template>
4
4
 
5
5
  <script setup lang="ts" name="TableColumn">
6
- import { h, inject, isProxy, markRaw, ref, toRaw, useSlots } from "vue";
7
- import { ElButton, ElImage, ElSwitch, ElTableColumn, ElTag, ElText } from "element-plus";
6
+ import { h, inject, isProxy, markRaw, onUnmounted, ref, toRaw, useSlots, withDirectives } from "vue";
7
+ import type { ObjectDirective } from "vue";
8
+ import {
9
+ ElButton,
10
+ ElDropdown,
11
+ ElDropdownItem,
12
+ ElDropdownMenu,
13
+ ElImage,
14
+ ElSwitch,
15
+ ElTableColumn,
16
+ ElTag,
17
+ ElText
18
+ } from "element-plus";
8
19
  import DictLabel from "../../../components/Dict/DictLabel.vue";
9
20
  import { ColumnProps, HeaderRenderScope, RenderScope, TableActionProps } from "../../../components/ProTable/interface";
10
21
  import type { TableAlign } from "../../../utils/proTable";
@@ -21,6 +32,57 @@ const slots = useSlots();
21
32
 
22
33
  const enumMap = inject("enumMap", ref(new Map()));
23
34
 
35
+ const actionWidths = ref(new Map<string, number>());
36
+ const measuredCells = new Map<HTMLElement, string>();
37
+ let resizeObserver: ResizeObserver | undefined;
38
+ let measureFrame = 0;
39
+
40
+ /** 合并当前列所有可见行和表头的实际宽度,支持语言、字体和权限变化。 */
41
+ const scheduleActionMeasurement = () => {
42
+ cancelAnimationFrame(measureFrame);
43
+ measureFrame = requestAnimationFrame(() => {
44
+ const widths = new Map<string, number>();
45
+ measuredCells.forEach((key, element) => {
46
+ if (!element.isConnected || !element.getClientRects().length) return;
47
+ const cell = element.closest(".cell");
48
+ if (!cell) return;
49
+ const style = getComputedStyle(cell);
50
+ const width = Math.ceil(
51
+ element.getBoundingClientRect().width + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + 2
52
+ );
53
+ widths.set(key, Math.max(widths.get(key) ?? 80, width));
54
+ });
55
+ if (widths.size !== actionWidths.value.size || [...widths].some(([key, width]) => actionWidths.value.get(key) !== width)) {
56
+ actionWidths.value = widths;
57
+ }
58
+ });
59
+ };
60
+
61
+ /** 观察渲染后的操作内容,避免按字符个数估算不同语言的文本宽度。 */
62
+ const measureActionContent: ObjectDirective<HTMLElement, string> = {
63
+ mounted(element, binding) {
64
+ measuredCells.set(element, binding.value);
65
+ resizeObserver ??= new ResizeObserver(scheduleActionMeasurement);
66
+ resizeObserver.observe(element);
67
+ scheduleActionMeasurement();
68
+ },
69
+ updated(element, binding) {
70
+ measuredCells.set(element, binding.value);
71
+ scheduleActionMeasurement();
72
+ },
73
+ unmounted(element) {
74
+ measuredCells.delete(element);
75
+ resizeObserver?.unobserve(element);
76
+ scheduleActionMeasurement();
77
+ }
78
+ };
79
+
80
+ onUnmounted(() => {
81
+ resizeObserver?.disconnect();
82
+ cancelAnimationFrame(measureFrame);
83
+ measuredCells.clear();
84
+ });
85
+
24
86
  /**
25
87
  * 透传给 Element Plus 前移除图标组件上的响应式代理,避免 Vue 对组件对象发出性能告警。
26
88
  */
@@ -166,18 +228,23 @@ const renderMoneyCell = (item: ColumnProps, scope: RenderScope<any>) => {
166
228
  };
167
229
 
168
230
  /**
169
- * 渲染操作按钮列,统一处理显隐、禁用与透传参数。
231
+ * 渲染操作按钮列,优先展示编辑、删除,超过三项时将其他操作折叠。
170
232
  */
171
233
  const renderActionsCell = (item: ColumnProps, scope: RenderScope<any>) => {
172
234
  if (!item.actions?.length) return "--";
173
235
  const visibleActions = item.actions.filter(action => !getBooleanValue(action.hidden, scope));
174
236
  if (!visibleActions.length) return "--";
175
- return visibleActions.map((action: TableActionProps) => {
237
+ const primaryLabels = [t("common.action.edit"), t("common.action.delete")];
238
+ const primaryActions = primaryLabels.flatMap(label => visibleActions.filter(action => action.label === label));
239
+ const otherActions = visibleActions.filter(action => !primaryLabels.includes(action.label));
240
+ const inlineActions = visibleActions.length > 3 ? primaryActions : [...primaryActions, ...otherActions];
241
+ const buttons = inlineActions.map((action: TableActionProps) => {
176
242
  const params = resolveColumnParams(action.params, scope);
177
243
  return h(
178
244
  ElButton,
179
245
  {
180
246
  key: action.label,
247
+ size: "small",
181
248
  type: action.type ?? "primary",
182
249
  link: action.link ?? true,
183
250
  icon: normalizeActionIcon(action.icon),
@@ -187,6 +254,41 @@ const renderActionsCell = (item: ColumnProps, scope: RenderScope<any>) => {
187
254
  { default: () => action.label }
188
255
  );
189
256
  });
257
+ if (visibleActions.length > 3 && otherActions.length)
258
+ buttons.push(
259
+ h(
260
+ ElDropdown,
261
+ { trigger: "click", placement: "bottom-end", size: "small" },
262
+ {
263
+ default: () =>
264
+ h(ElButton, { type: "primary", link: true, size: "small", icon: ArrowDown }, () => t("common.action.more")),
265
+ dropdown: () =>
266
+ h(ElDropdownMenu, null, () =>
267
+ otherActions.map(action => {
268
+ const disabled = getBooleanValue(action.disabled, scope);
269
+ const params = resolveColumnParams(action.params, scope);
270
+ return h(
271
+ ElDropdownItem,
272
+ {
273
+ key: action.label,
274
+ icon: normalizeActionIcon(action.icon),
275
+ disabled,
276
+ style: disabled ? undefined : { color: `var(--el-color-${action.type ?? "primary"})` },
277
+ onClick: () => {
278
+ if (!disabled) action.onClick(scope, params);
279
+ }
280
+ },
281
+ () => action.label
282
+ );
283
+ })
284
+ )
285
+ }
286
+ )
287
+ );
288
+ const content = h("span", { class: "pro-table-actions" }, buttons);
289
+ return item.width == null && item.minWidth == null
290
+ ? withDirectives(content, [[measureActionContent, item.prop ?? item.label ?? ""]])
291
+ : content;
190
292
  };
191
293
 
192
294
  /** 渲染默认表头标题,避免窄列换行并保留完整标题提示。 */
@@ -213,12 +315,23 @@ const renderPresetCell = (item: ColumnProps, scope: RenderScope<any>) => {
213
315
  }
214
316
  };
215
317
 
318
+ /** 渲染表格列,为操作列提供自适应宽度和右侧固定默认值。 */
216
319
  const RenderTableColumn = (item: ColumnProps) => {
217
320
  if (!item.isShow) return null;
321
+ const isActionColumn = item.cellType === "actions" || item.prop === "operation";
322
+ const autoActionWidth = item.cellType === "actions" && !item.render && !(item.prop && slots[handleProp(item.prop)]);
218
323
  return h(
219
324
  ElTableColumn,
220
325
  {
221
326
  ...item,
327
+ width:
328
+ item.width ??
329
+ (isActionColumn && item.minWidth == null
330
+ ? autoActionWidth
331
+ ? (actionWidths.value.get(item.prop ?? item.label ?? "") ?? 80)
332
+ : 160
333
+ : undefined),
334
+ fixed: item.fixed ?? (isActionColumn ? "right" : undefined),
222
335
  align: item.align ?? props.resolveAlign?.(item) ?? "left",
223
336
  showOverflowTooltip: item.showOverflowTooltip ?? item.prop !== "operation"
224
337
  },
@@ -235,9 +348,39 @@ const RenderTableColumn = (item: ColumnProps) => {
235
348
  header: (scope: HeaderRenderScope<any>) => {
236
349
  if (item.headerRender) return item.headerRender(scope);
237
350
  if (item.prop && slots[`${handleProp(item.prop)}Header`]) return slots[`${handleProp(item.prop)}Header`]!(scope);
351
+ if (autoActionWidth && item.width == null && item.minWidth == null) {
352
+ return withDirectives(h("span", { class: "pro-table-action-header" }, item.label), [
353
+ [measureActionContent, item.prop ?? item.label ?? ""]
354
+ ]);
355
+ }
238
356
  return renderHeaderLabel(item);
239
357
  }
240
358
  }
241
359
  );
242
360
  };
243
361
  </script>
362
+
363
+ <style lang="scss">
364
+ // 操作容器在函数式列的插槽中生成,不带本组件的 scopeId,使用专属类名限定样式。
365
+ .pro-table-actions {
366
+ display: inline-flex;
367
+ flex-wrap: nowrap;
368
+ gap: 4px;
369
+ align-items: center;
370
+ width: max-content;
371
+ vertical-align: middle;
372
+ white-space: nowrap;
373
+ .el-button {
374
+ margin-left: 0;
375
+ }
376
+ .el-dropdown {
377
+ display: inline-flex;
378
+ align-items: center;
379
+ }
380
+ }
381
+ .pro-table-action-header {
382
+ display: inline-block;
383
+ width: max-content;
384
+ white-space: nowrap;
385
+ }
386
+ </style>
@@ -1 +1 @@
1
- export { formatJson, formatSrc, normalizeRichTextMediaPaths, normalizeStaticAssetPath } from "./utils/utils";
1
+ export { formatDateTime, formatJson, formatSrc, normalizeRichTextMediaPaths, normalizeStaticAssetPath } from "./utils/utils";
@@ -1,16 +1,14 @@
1
- import type { ColumnProps, EnumProps, TypeProps } from "../components/ProTable/interface";
1
+ import type { EnumProps } from "../components/ProTable/interface";
2
2
  import type { OptionBaseDictResponse_BaseDictItem } from "../rpc/system/admin/v1/base_dict";
3
3
  import { useDictStoreHook } from "../stores/modules/dict";
4
- import { isRef } from "vue";
4
+
5
+ export { resolveTableColumnAlign, type TableAlign } from "./tableColumn";
5
6
 
6
7
  /** 字典值输出给表格枚举时的目标类型。 */
7
8
  type DictValueType = "number" | "string";
8
9
  /** 表格批量操作支持的主键类型。 */
9
10
  type SelectedId = string | number;
10
11
 
11
- /** 表格单元格支持的对齐方式。 */
12
- export type TableAlign = "left" | "center" | "right";
13
-
14
12
  /** 表格分页入参。 */
15
13
  type PageRequestParams = Record<string, any> & {
16
14
  /** 当前页码。 */
@@ -27,54 +25,6 @@ type NormalizedPageRequest<T extends PageRequestParams> = T & {
27
25
  page_size: number;
28
26
  };
29
27
 
30
- /**
31
- * 获取表格行的列值,支持点号分隔的多级字段。
32
- */
33
- function getTableCellValue(row: Record<string, any>, prop: string) {
34
- return prop.split(".").reduce((value, key) => (value == null ? undefined : value[key]), row as any);
35
- }
36
-
37
- /**
38
- * 判断值是否为可用于数值对齐的 Number 类型。
39
- */
40
- function isPureNumber(value: unknown) {
41
- return typeof value === "number" && Number.isFinite(value);
42
- }
43
-
44
- /**
45
- * 判断枚举是否来自数据表映射,远程函数和响应式选项都属于动态数据映射。
46
- */
47
- function isDataTableEnum(enumValue: ColumnProps["enum"]) {
48
- return typeof enumValue === "function" || isRef(enumValue);
49
- }
50
-
51
- /**
52
- * 根据列语义和当前表格数据解析单元格对齐方式。
53
- *
54
- * 显式 align 始终优先;数据表映射按文本左对齐,字典、静态枚举及预置状态列居中,
55
- * 金额列和纯数字列右对齐,其余内容左对齐。空表时会先按列语义返回默认值,
56
- * 数据加载后由响应式表格重新解析。
57
- */
58
- export function resolveTableColumnAlign(column: ColumnProps, rows: Record<string, any>[] = []): TableAlign {
59
- if (column.align === "left" || column.align === "center" || column.align === "right") return column.align;
60
-
61
- const centeredTypes: TypeProps[] = ["selection", "radio", "index", "expand", "sort"];
62
- if (centeredTypes.includes(column.type as TypeProps)) return "center";
63
- if (column.cellType === "actions" || column.cellType === "status" || column.cellType === "image") return "center";
64
- if (column.cellType === "money") return "right";
65
- if (column.dictCode || column.tag) return "center";
66
- if (column.enum) return isDataTableEnum(column.enum) ? "left" : "center";
67
-
68
- if (column.prop) {
69
- const values = rows
70
- .map(row => getTableCellValue(row, column.prop as string))
71
- .filter(value => value !== undefined && value !== null && value !== "");
72
- if (values.length && values.every(isPureNumber)) return "right";
73
- }
74
-
75
- return "left";
76
- }
77
-
78
28
  /**
79
29
  * 按配置将字典值转换为表格枚举可识别的类型。
80
30
  */
@@ -0,0 +1,55 @@
1
+ import type { ColumnProps, TypeProps } from "../components/ProTable/interface";
2
+
3
+ /** 表格单元格支持的对齐方式。 */
4
+ export type TableAlign = "left" | "center" | "right";
5
+
6
+ /** 获取表格行的列值,支持点号分隔的多级字段。 */
7
+ function getTableCellValue(row: Record<string, any>, prop: string) {
8
+ return prop.split(".").reduce((value, key) => (value == null ? undefined : value[key]), row as any);
9
+ }
10
+
11
+ /** 判断值是否为可用于数值对齐的 Number 类型。 */
12
+ function isPureNumber(value: unknown) {
13
+ return typeof value === "number" && Number.isFinite(value);
14
+ }
15
+
16
+ /** 判断列字段名是否表示时间值。 */
17
+ function isTimeColumn(column: ColumnProps) {
18
+ const prop = column.prop?.toLowerCase();
19
+ return !!prop && /(^|[._])(created|updated|deleted|expires|scheduled|execute|start|end|login|logout|last)[._]?at?$/.test(prop);
20
+ }
21
+
22
+ /** 判断列字段名是否表示数值。 */
23
+ function isNumericColumn(column: ColumnProps) {
24
+ const prop = column.prop?.toLowerCase();
25
+ return !!prop && /(^|[._])(id|sort|size|count|total|rows|priority|status_code|latency|duration|process_time|ttl|retention|attempt|qps|rate|number|num|amount|days|minutes|seconds|ms)$/.test(prop);
26
+ }
27
+
28
+ /**
29
+ * 根据列语义和当前表格数据解析单元格对齐方式。
30
+ *
31
+ * 显式 align 始终优先;字典、枚举及预置状态列居中,
32
+ * 金额列和纯数字列右对齐,其余内容左对齐。空表时会先按列语义返回默认值,
33
+ * 数据加载后由响应式表格重新解析。
34
+ */
35
+ export function resolveTableColumnAlign(column: ColumnProps, rows: Record<string, any>[] = []): TableAlign {
36
+ if (column.align === "left" || column.align === "center" || column.align === "right") return column.align;
37
+
38
+ const centeredTypes: TypeProps[] = ["selection", "radio", "index", "expand", "sort"];
39
+ if (centeredTypes.includes(column.type as TypeProps)) return "center";
40
+ if (column.cellType === "actions" || column.cellType === "status" || column.cellType === "image") return "center";
41
+ if (column.cellType === "money") return "right";
42
+ if (column.dictCode || column.tag) return "center";
43
+ if (column.enum) return "center";
44
+ if (isTimeColumn(column)) return "center";
45
+ if (isNumericColumn(column)) return "right";
46
+
47
+ if (column.prop) {
48
+ const values = rows
49
+ .map(row => getTableCellValue(row, column.prop as string))
50
+ .filter(value => value !== undefined && value !== null && value !== "");
51
+ if (values.length && values.every(isPureNumber)) return "right";
52
+ }
53
+
54
+ return "left";
55
+ }
@@ -17,6 +17,15 @@ export function formatPrice(price?: number) {
17
17
  return (price / 100).toFixed(2);
18
18
  }
19
19
 
20
+ /** 将时间值格式化为统一的本地日期时间文本。 */
21
+ export function formatDateTime(value?: string | number | Date) {
22
+ if (value === undefined || value === null || value === "") return "-";
23
+ const date = value instanceof Date ? value : new Date(value);
24
+ if (Number.isNaN(date.getTime())) return String(value);
25
+ const pad = (part: number) => String(part).padStart(2, "0");
26
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
27
+ }
28
+
20
29
  /**
21
30
  * 按静态资源域名补齐图片地址。
22
31
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liujitcn/kratos-admin-core",
3
3
  "private": false,
4
- "version": "0.0.47",
4
+ "version": "0.0.48",
5
5
  "type": "module",
6
6
  "description": "kratos-admin frontend runtime and base modules",
7
7
  "author": {
@@ -32,7 +32,7 @@
32
32
  "dayjs": "^1.11.21",
33
33
  "dompurify": "^3.4.5",
34
34
  "driver.js": "^1.3.1",
35
- "echarts": "^5.5.1",
35
+ "echarts": "^6.1.0",
36
36
  "echarts-liquidfill": "^3.1.0",
37
37
  "element-plus": "^2.7.6",
38
38
  "go-captcha-vue": "2.0.6",