@liujitcn/kratos-admin-core 0.0.46 → 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,5 +1,5 @@
1
1
  import type { FormRules } from "element-plus";
2
- import type { ProFormField } from "../../components/ProForm/interface";
2
+ import type { ProFormField, ProFormLabelPosition } from "../../components/ProForm/interface";
3
3
  /** 表单弹窗组件属性。 */
4
4
  interface FormDialogProps {
5
5
  modelValue: boolean;
@@ -10,6 +10,7 @@ interface FormDialogProps {
10
10
  fields: ProFormField[];
11
11
  rules?: FormRules;
12
12
  labelWidth?: string;
13
+ labelPosition?: ProFormLabelPosition;
13
14
  gutter?: number;
14
15
  colSpan?: number;
15
16
  confirmText?: string;
@@ -60,6 +61,7 @@ declare const __VLS_base: import("vue").DefineComponent<FormDialogProps, {
60
61
  confirmLoading: boolean;
61
62
  rules: FormRules;
62
63
  labelWidth: string;
64
+ labelPosition: ProFormLabelPosition;
63
65
  gutter: number;
64
66
  colSpan: number;
65
67
  formProps: Record<string, any>;
@@ -1,3 +1,4 @@
1
+ import type { ProFormLabelPosition } from "../../components/ProForm/interface";
1
2
  import type { PasswordCryptoScene } from "../../utils/passwordCrypto";
2
3
  import type { PasswordCrypto } from "../../rpc/common/v1/types";
3
4
  /** 通用密码验证弹窗属性。 */
@@ -32,6 +33,8 @@ interface PasswordVerifyDialogProps {
32
33
  closeOnPressEscape?: boolean;
33
34
  /** 表单标签宽度。 */
34
35
  labelWidth?: string;
36
+ /** 表单标签布局方向。 */
37
+ labelPosition?: ProFormLabelPosition;
35
38
  }
36
39
  declare const __VLS_export: import("vue").DefineComponent<PasswordVerifyDialogProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
37
40
  confirm: (password: PasswordCrypto) => any;
@@ -57,6 +60,7 @@ declare const __VLS_export: import("vue").DefineComponent<PasswordVerifyDialogPr
57
60
  cancelText: string;
58
61
  confirmLoading: boolean;
59
62
  labelWidth: string;
63
+ labelPosition: ProFormLabelPosition;
60
64
  passwordLabel: string;
61
65
  passwordPlaceholder: string;
62
66
  scene: PasswordCryptoScene;
@@ -1 +1 @@
1
- export { formatJson, formatSrc, normalizeRichTextMediaPaths, normalizeStaticAssetPath } from "./utils/utils";
1
+ export { formatDateTime, formatJson, formatSrc, normalizeRichTextMediaPaths, normalizeStaticAssetPath } from "./utils/utils";
@@ -12,6 +12,33 @@ export declare const LOCALE_MESSAGES: {
12
12
  "common.action.delete": string;
13
13
  "common.action.download": string;
14
14
  "common.action.edit": string;
15
+ "common.action.next": string;
16
+ "common.action.previous": string;
17
+ "common.action.refresh": string;
18
+ "common.action.enable": string;
19
+ "common.action.disable": string;
20
+ "common.action.export": string;
21
+ "common.action.import": string;
22
+ "common.action.copy": string;
23
+ "common.action.detail": string;
24
+ "common.action.submit": string;
25
+ "common.action.clear": string;
26
+ "common.action.filter": string;
27
+ "common.action.more": string;
28
+ "common.action.publish": string;
29
+ "common.action.revoke": string;
30
+ "common.action.retry": string;
31
+ "common.action.start": string;
32
+ "common.action.stop": string;
33
+ "common.action.pause": string;
34
+ "common.action.resume": string;
35
+ "common.action.upload": string;
36
+ "common.action.preview": string;
37
+ "common.action.configure": string;
38
+ "common.action.continue": string;
39
+ "common.action.skip": string;
40
+ "common.action.select_all": string;
41
+ "common.action.deselect_all": string;
15
42
  "common.action.expand": string;
16
43
  "common.action.collapse": string;
17
44
  "common.action.login": string;
@@ -350,6 +377,33 @@ export declare const LOCALE_MESSAGES: {
350
377
  "common.action.delete": string;
351
378
  "common.action.download": string;
352
379
  "common.action.edit": string;
380
+ "common.action.next": string;
381
+ "common.action.previous": string;
382
+ "common.action.refresh": string;
383
+ "common.action.enable": string;
384
+ "common.action.disable": string;
385
+ "common.action.export": string;
386
+ "common.action.import": string;
387
+ "common.action.copy": string;
388
+ "common.action.detail": string;
389
+ "common.action.submit": string;
390
+ "common.action.clear": string;
391
+ "common.action.filter": string;
392
+ "common.action.more": string;
393
+ "common.action.publish": string;
394
+ "common.action.revoke": string;
395
+ "common.action.retry": string;
396
+ "common.action.start": string;
397
+ "common.action.stop": string;
398
+ "common.action.pause": string;
399
+ "common.action.resume": string;
400
+ "common.action.upload": string;
401
+ "common.action.preview": string;
402
+ "common.action.configure": string;
403
+ "common.action.continue": string;
404
+ "common.action.skip": string;
405
+ "common.action.select_all": string;
406
+ "common.action.deselect_all": string;
353
407
  "common.action.expand": string;
354
408
  "common.action.collapse": string;
355
409
  "common.action.login": string;
@@ -688,6 +742,33 @@ export declare const LOCALE_MESSAGES: {
688
742
  "common.action.delete": string;
689
743
  "common.action.download": string;
690
744
  "common.action.edit": string;
745
+ "common.action.next": string;
746
+ "common.action.previous": string;
747
+ "common.action.refresh": string;
748
+ "common.action.enable": string;
749
+ "common.action.disable": string;
750
+ "common.action.export": string;
751
+ "common.action.import": string;
752
+ "common.action.copy": string;
753
+ "common.action.detail": string;
754
+ "common.action.submit": string;
755
+ "common.action.clear": string;
756
+ "common.action.filter": string;
757
+ "common.action.more": string;
758
+ "common.action.publish": string;
759
+ "common.action.revoke": string;
760
+ "common.action.retry": string;
761
+ "common.action.start": string;
762
+ "common.action.stop": string;
763
+ "common.action.pause": string;
764
+ "common.action.resume": string;
765
+ "common.action.upload": string;
766
+ "common.action.preview": string;
767
+ "common.action.configure": string;
768
+ "common.action.continue": string;
769
+ "common.action.skip": string;
770
+ "common.action.select_all": string;
771
+ "common.action.deselect_all": string;
691
772
  "common.action.expand": string;
692
773
  "common.action.collapse": string;
693
774
  "common.action.login": string;
@@ -1026,6 +1107,33 @@ export declare const LOCALE_MESSAGES: {
1026
1107
  "common.action.delete": string;
1027
1108
  "common.action.download": string;
1028
1109
  "common.action.edit": string;
1110
+ "common.action.next": string;
1111
+ "common.action.previous": string;
1112
+ "common.action.refresh": string;
1113
+ "common.action.enable": string;
1114
+ "common.action.disable": string;
1115
+ "common.action.export": string;
1116
+ "common.action.import": string;
1117
+ "common.action.copy": string;
1118
+ "common.action.detail": string;
1119
+ "common.action.submit": string;
1120
+ "common.action.clear": string;
1121
+ "common.action.filter": string;
1122
+ "common.action.more": string;
1123
+ "common.action.publish": string;
1124
+ "common.action.revoke": string;
1125
+ "common.action.retry": string;
1126
+ "common.action.start": string;
1127
+ "common.action.stop": string;
1128
+ "common.action.pause": string;
1129
+ "common.action.resume": string;
1130
+ "common.action.upload": string;
1131
+ "common.action.preview": string;
1132
+ "common.action.configure": string;
1133
+ "common.action.continue": string;
1134
+ "common.action.skip": string;
1135
+ "common.action.select_all": string;
1136
+ "common.action.deselect_all": string;
1029
1137
  "common.action.expand": string;
1030
1138
  "common.action.collapse": string;
1031
1139
  "common.action.login": string;
@@ -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
  */
@@ -23,6 +23,7 @@
23
23
  :fields="fields"
24
24
  :rules="rules"
25
25
  :label-width="labelWidth"
26
+ :label-position="labelPosition"
26
27
  :gutter="gutter"
27
28
  :col-span="colSpan"
28
29
  v-bind="formProps"
@@ -50,7 +51,7 @@ import { computed, nextTick, ref, useSlots, watch } from "vue";
50
51
  import type { FormRules } from "element-plus";
51
52
  import ProDialog from "../../components/Dialog/ProDialog.vue";
52
53
  import ProForm from "../../components/ProForm/index.vue";
53
- import type { ProFormField, ProFormInstance } from "../../components/ProForm/interface";
54
+ import type { ProFormField, ProFormInstance, ProFormLabelPosition } from "../../components/ProForm/interface";
54
55
  import { useLocaleStore } from "../../locales";
55
56
 
56
57
  const { t } = useLocaleStore();
@@ -65,6 +66,7 @@ interface FormDialogProps {
65
66
  fields: ProFormField[];
66
67
  rules?: FormRules;
67
68
  labelWidth?: string;
69
+ labelPosition?: ProFormLabelPosition;
68
70
  gutter?: number;
69
71
  colSpan?: number;
70
72
  confirmText?: string;
@@ -81,7 +83,8 @@ const props = withDefaults(defineProps<FormDialogProps>(), {
81
83
  width: "500px",
82
84
  top: "8vh",
83
85
  rules: () => ({}),
84
- labelWidth: "180px",
86
+ labelWidth: "6em",
87
+ labelPosition: "right",
85
88
  gutter: 20,
86
89
  colSpan: 24,
87
90
  confirmText: "",
@@ -23,6 +23,7 @@
23
23
  :fields="fields"
24
24
  :rules="rules"
25
25
  :label-width="labelWidth"
26
+ :label-position="labelPosition"
26
27
  @keyup.enter.prevent="handleConfirm"
27
28
  />
28
29
  </ProDialog>
@@ -33,7 +34,7 @@ import { computed, reactive, ref } from "vue";
33
34
  import type { FormRules } from "element-plus";
34
35
  import ProDialog from "../../components/Dialog/ProDialog.vue";
35
36
  import ProForm from "../../components/ProForm/index.vue";
36
- import type { ProFormField, ProFormInstance } from "../../components/ProForm/interface";
37
+ import type { ProFormField, ProFormInstance, ProFormLabelPosition } from "../../components/ProForm/interface";
37
38
  import { useLocaleStore } from "../../locales";
38
39
  import { PASSWORD_CRYPTO_SCENE, encryptPassword } from "../../utils/passwordCrypto";
39
40
  import type { PasswordCryptoScene } from "../../utils/passwordCrypto";
@@ -73,6 +74,8 @@ interface PasswordVerifyDialogProps {
73
74
  closeOnPressEscape?: boolean;
74
75
  /** 表单标签宽度。 */
75
76
  labelWidth?: string;
77
+ /** 表单标签布局方向。 */
78
+ labelPosition?: ProFormLabelPosition;
76
79
  }
77
80
 
78
81
  const props = withDefaults(defineProps<PasswordVerifyDialogProps>(), {
@@ -89,7 +92,8 @@ const props = withDefaults(defineProps<PasswordVerifyDialogProps>(), {
89
92
  destroyOnClose: false,
90
93
  closeOnClickModal: false,
91
94
  closeOnPressEscape: true,
92
- labelWidth: "auto"
95
+ labelWidth: "6em",
96
+ labelPosition: "right"
93
97
  });
94
98
 
95
99
  const emit = defineEmits<{
@@ -66,8 +66,8 @@ interface ProFormProps {
66
66
 
67
67
  const props = withDefaults(defineProps<ProFormProps>(), {
68
68
  rules: () => ({}),
69
- labelWidth: "180px",
70
- labelPosition: "top",
69
+ labelWidth: "6em",
70
+ labelPosition: "right",
71
71
  gutter: 20,
72
72
  colSpan: 24
73
73
  });
@@ -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";
@@ -7,6 +7,33 @@
7
7
  "common.action.delete": "Delete",
8
8
  "common.action.download": "Download",
9
9
  "common.action.edit": "Edit",
10
+ "common.action.next": "Next",
11
+ "common.action.previous": "Previous",
12
+ "common.action.refresh": "Refresh",
13
+ "common.action.enable": "Enable",
14
+ "common.action.disable": "Disable",
15
+ "common.action.export": "Export",
16
+ "common.action.import": "Import",
17
+ "common.action.copy": "Copy",
18
+ "common.action.detail": "Details",
19
+ "common.action.submit": "Submit",
20
+ "common.action.clear": "Clear",
21
+ "common.action.filter": "Filter",
22
+ "common.action.more": "More",
23
+ "common.action.publish": "Publish",
24
+ "common.action.revoke": "Revoke",
25
+ "common.action.retry": "Retry",
26
+ "common.action.start": "Start",
27
+ "common.action.stop": "Stop",
28
+ "common.action.pause": "Pause",
29
+ "common.action.resume": "Resume",
30
+ "common.action.upload": "Upload",
31
+ "common.action.preview": "Preview",
32
+ "common.action.configure": "Configure",
33
+ "common.action.continue": "Continue",
34
+ "common.action.skip": "Skip",
35
+ "common.action.select_all": "Select all",
36
+ "common.action.deselect_all": "Deselect all",
10
37
  "common.action.expand": "Expand",
11
38
  "common.action.collapse": "Collapse",
12
39
  "common.action.login": "Log in",
@@ -72,23 +72,30 @@ export function registerLocaleMessages(modules: AdminModule[]): void {
72
72
 
73
73
  modules.forEach(module => {
74
74
  if (!module.messages) return;
75
- const expectedKeys = requiredLocaleKeys(module.messages[DEFAULT_LOCALE] ?? {});
75
+ const defaultMessages = module.messages[DEFAULT_LOCALE] ?? {};
76
+ const expectedKeys = requiredLocaleKeys(defaultMessages);
76
77
  SUPPORTED_LOCALES.forEach(locale => {
77
78
  const messages = module.messages?.[locale];
78
- if (!messages) throw new Error(`${module.name} 缺少 ${locale} 语言包`);
79
- const keys = requiredLocaleKeys(messages);
79
+ if (!messages) {
80
+ warnLocaleIssue(`${module.name} 缺少 ${locale} 语言包,已回退到 ${DEFAULT_LOCALE}`);
81
+ }
82
+ const targetMessages = messages ?? {};
83
+ const keys = requiredLocaleKeys(targetMessages);
80
84
  if (keys.join("\u0000") !== expectedKeys.join("\u0000")) {
81
- throw new Error(`${module.name} 的 ${locale} 语言包键集合不一致`);
85
+ warnLocaleIssue(`${module.name} 的 ${locale} 语言包键集合不一致,缺失文案已回退到 ${DEFAULT_LOCALE}`);
82
86
  }
83
87
 
84
88
  const target = merged.get(locale) as LocaleMessages;
85
- Object.keys(messages).forEach(key => {
89
+ expectedKeys.forEach(key => {
90
+ let message = targetMessages[key] ?? defaultMessages[key];
86
91
  assertLocaleKeyNamespace(module.name, key);
87
92
  if (Object.prototype.hasOwnProperty.call(target, key)) {
88
93
  throw new Error(`${locale} 语言键重复: ${key}`);
89
94
  }
90
- assertLocalePlaceholders(module.name, key, messages[key], module.messages?.[DEFAULT_LOCALE]?.[key] ?? "");
91
- target[key] = messages[key];
95
+ if (!hasMatchingLocalePlaceholders(module.name, key, message, defaultMessages[key] ?? "")) {
96
+ message = defaultMessages[key] ?? "";
97
+ }
98
+ target[key] = message;
92
99
  });
93
100
  });
94
101
  });
@@ -215,9 +222,16 @@ function requiredLocaleKeys(messages: LocaleMessages): string[] {
215
222
  .sort();
216
223
  }
217
224
 
218
- function assertLocalePlaceholders(moduleName: string, key: string, message: string, sourceMessage: string): void {
225
+ function hasMatchingLocalePlaceholders(moduleName: string, key: string, message: string, sourceMessage: string): boolean {
219
226
  const placeholders = (value: string) => [...value.matchAll(/\{([A-Za-z0-9_]+)\}/g)].map(match => match[1]).sort();
220
227
  if (placeholders(message).join("\u0000") !== placeholders(sourceMessage).join("\u0000")) {
221
- throw new Error(`${moduleName} 的 ${key} 占位符集合不一致`);
228
+ warnLocaleIssue(`${moduleName} 的 ${key} 占位符集合不一致,已回退到 ${DEFAULT_LOCALE}`);
229
+ return false;
222
230
  }
231
+ return true;
232
+ }
233
+
234
+ /** 记录语言包问题但不阻断应用启动。 */
235
+ function warnLocaleIssue(message: string): void {
236
+ console.warn(`[i18n] ${message}`);
223
237
  }
@@ -7,6 +7,33 @@
7
7
  "common.action.delete": "削除",
8
8
  "common.action.download": "ダウンロード",
9
9
  "common.action.edit": "編集者",
10
+ "common.action.next": "次へ",
11
+ "common.action.previous": "前へ",
12
+ "common.action.refresh": "更新",
13
+ "common.action.enable": "有効化",
14
+ "common.action.disable": "無効化",
15
+ "common.action.export": "エクスポート",
16
+ "common.action.import": "インポート",
17
+ "common.action.copy": "コピー",
18
+ "common.action.detail": "詳細",
19
+ "common.action.submit": "送信",
20
+ "common.action.clear": "クリア",
21
+ "common.action.filter": "フィルター",
22
+ "common.action.more": "その他",
23
+ "common.action.publish": "公開",
24
+ "common.action.revoke": "取り消し",
25
+ "common.action.retry": "再試行",
26
+ "common.action.start": "開始",
27
+ "common.action.stop": "停止",
28
+ "common.action.pause": "一時停止",
29
+ "common.action.resume": "再開",
30
+ "common.action.upload": "アップロード",
31
+ "common.action.preview": "プレビュー",
32
+ "common.action.configure": "設定",
33
+ "common.action.continue": "続行",
34
+ "common.action.skip": "スキップ",
35
+ "common.action.select_all": "すべて選択",
36
+ "common.action.deselect_all": "すべて選択解除",
10
37
  "common.action.expand": "展開する",
11
38
  "common.action.collapse": "閉じる",
12
39
  "common.action.login": "ログイン",
@@ -7,6 +7,33 @@
7
7
  "common.action.delete": "删除",
8
8
  "common.action.download": "下载",
9
9
  "common.action.edit": "编辑",
10
+ "common.action.next": "下一步",
11
+ "common.action.previous": "上一步",
12
+ "common.action.refresh": "刷新",
13
+ "common.action.enable": "启用",
14
+ "common.action.disable": "禁用",
15
+ "common.action.export": "导出",
16
+ "common.action.import": "导入",
17
+ "common.action.copy": "复制",
18
+ "common.action.detail": "详情",
19
+ "common.action.submit": "提交",
20
+ "common.action.clear": "清空",
21
+ "common.action.filter": "筛选",
22
+ "common.action.more": "更多",
23
+ "common.action.publish": "发布",
24
+ "common.action.revoke": "撤销",
25
+ "common.action.retry": "重试",
26
+ "common.action.start": "开始",
27
+ "common.action.stop": "停止",
28
+ "common.action.pause": "暂停",
29
+ "common.action.resume": "恢复",
30
+ "common.action.upload": "上传",
31
+ "common.action.preview": "预览",
32
+ "common.action.configure": "配置",
33
+ "common.action.continue": "继续",
34
+ "common.action.skip": "跳过",
35
+ "common.action.select_all": "全选",
36
+ "common.action.deselect_all": "取消全选",
10
37
  "common.action.expand": "展开",
11
38
  "common.action.collapse": "收起",
12
39
  "common.action.login": "登录",
@@ -7,6 +7,33 @@
7
7
  "common.action.delete": "刪除",
8
8
  "common.action.download": "下載",
9
9
  "common.action.edit": "編輯",
10
+ "common.action.next": "下一步",
11
+ "common.action.previous": "上一步",
12
+ "common.action.refresh": "重新整理",
13
+ "common.action.enable": "啟用",
14
+ "common.action.disable": "停用",
15
+ "common.action.export": "匯出",
16
+ "common.action.import": "匯入",
17
+ "common.action.copy": "複製",
18
+ "common.action.detail": "詳情",
19
+ "common.action.submit": "提交",
20
+ "common.action.clear": "清空",
21
+ "common.action.filter": "篩選",
22
+ "common.action.more": "更多",
23
+ "common.action.publish": "發布",
24
+ "common.action.revoke": "撤銷",
25
+ "common.action.retry": "重試",
26
+ "common.action.start": "開始",
27
+ "common.action.stop": "停止",
28
+ "common.action.pause": "暫停",
29
+ "common.action.resume": "恢復",
30
+ "common.action.upload": "上傳",
31
+ "common.action.preview": "預覽",
32
+ "common.action.configure": "設定",
33
+ "common.action.continue": "繼續",
34
+ "common.action.skip": "跳過",
35
+ "common.action.select_all": "全選",
36
+ "common.action.deselect_all": "取消全選",
10
37
  "common.action.expand": "展開",
11
38
  "common.action.collapse": "收起",
12
39
  "common.action.login": "登入",
@@ -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.46",
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",