@liujitcn/kratos-admin-system 0.0.41 → 0.0.42

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
@@ -186,3 +186,5 @@ registerRuntimeConfig(notificationConfig);
186
186
 
187
187
  国际化编辑弹窗使用 Element Plus 横向表单,语言标签在左侧统一占 100px,输入框及翻译操作位于右侧;单行和多行编辑均保持左右布局。
188
188
  代码生成进度弹窗统一补齐 HTTP 和 SSE 快照中被 ProtoJSON 省略的零值计数,任务及表级进度在生成初期显示 0,避免出现 NaN。
189
+
190
+ 数据库迁移记录的模块、数据源、版本和时间仅在左侧列表展示,右侧直接展示文件 Tab。详情按文件展示:说明、升级和回退文件各占一个 Tab,标签显示原始文件名,面板显示路径;Markdown 独立渲染,SQL 独立查看和复制,内容区域填满 Tab 下方的剩余高度并在内部滚动。切换记录后选中第一个文件,仅渲染当前 Tab 的正文。
@@ -26,6 +26,10 @@ export interface RuntimeConfigField {
26
26
  labelKey: string;
27
27
  /** ProForm 控件类型。 */
28
28
  component: ProFormComponentType;
29
+ /** 栅格占位列数,默认双列;长路径等字段可设为 24 独占整行。 */
30
+ colSpan?: ProFormField["colSpan"];
31
+ /** 是否在当前字段前换行。 */
32
+ rowBreakBefore?: ProFormField["rowBreakBefore"];
29
33
  /** 控件参数。 */
30
34
  props?: ProFormField["props"];
31
35
  /** 表单项参数。 */
@@ -1,5 +1,7 @@
1
1
  export declare const LOCALE_MESSAGES: {
2
2
  readonly 'zh-CN': {
3
+ "system.base.config.tab.basic": string;
4
+ "system.base.config.tab.form": string;
3
5
  "system.ai.chat.action.collapse_sessions": string;
4
6
  "system.ai.chat.action.confirm_delete_conversation": string;
5
7
  "system.ai.chat.action.continue": string;
@@ -1521,6 +1523,8 @@ export declare const LOCALE_MESSAGES: {
1521
1523
  "system.sse.error.connection_failed": string;
1522
1524
  };
1523
1525
  readonly 'en-US': {
1526
+ "system.base.config.tab.basic": string;
1527
+ "system.base.config.tab.form": string;
1524
1528
  "system.ai.chat.action.collapse_sessions": string;
1525
1529
  "system.ai.chat.action.confirm_delete_conversation": string;
1526
1530
  "system.ai.chat.action.continue": string;
@@ -3042,6 +3046,8 @@ export declare const LOCALE_MESSAGES: {
3042
3046
  "system.sse.error.connection_failed": string;
3043
3047
  };
3044
3048
  readonly 'ja-JP': {
3049
+ "system.base.config.tab.basic": string;
3050
+ "system.base.config.tab.form": string;
3045
3051
  "system.ai.chat.action.collapse_sessions": string;
3046
3052
  "system.ai.chat.action.confirm_delete_conversation": string;
3047
3053
  "system.ai.chat.action.continue": string;
@@ -4563,6 +4569,8 @@ export declare const LOCALE_MESSAGES: {
4563
4569
  "system.sse.error.connection_failed": string;
4564
4570
  };
4565
4571
  readonly 'zh-TW': {
4572
+ "system.base.config.tab.basic": string;
4573
+ "system.base.config.tab.form": string;
4566
4574
  "system.ai.chat.action.collapse_sessions": string;
4567
4575
  "system.ai.chat.action.confirm_delete_conversation": string;
4568
4576
  "system.ai.chat.action.continue": string;
@@ -46,15 +46,18 @@ export interface BaseMigration {
46
46
  data_source: string;
47
47
  /** 迁移版本 */
48
48
  version: string;
49
- /** 升级脚本 */
50
- up_sql: string;
51
- /** 回退脚本 */
52
- down_sql: string;
53
- /** 升级描述 */
54
- description: string;
49
+ /** 迁移文件列表 */
50
+ files: BaseMigrationFile[];
55
51
  /** 创建时间 */
56
52
  created_at: string;
57
53
  }
54
+ /** 数据库迁移文件内容 */
55
+ export interface BaseMigrationFile {
56
+ /** 文件相对路径 */
57
+ path: string;
58
+ /** 文件内容 */
59
+ content: string;
60
+ }
58
61
  /** Admin数据库迁移服务。 */
59
62
  export interface BaseMigrationService {
60
63
  /** 分页查询数据库升级历史 */
@@ -28,6 +28,10 @@ export interface RuntimeConfigField {
28
28
  labelKey: string;
29
29
  /** ProForm 控件类型。 */
30
30
  component: ProFormComponentType;
31
+ /** 栅格占位列数,默认双列;长路径等字段可设为 24 独占整行。 */
32
+ colSpan?: ProFormField["colSpan"];
33
+ /** 是否在当前字段前换行。 */
34
+ rowBreakBefore?: ProFormField["rowBreakBefore"];
31
35
  /** 控件参数。 */
32
36
  props?: ProFormField["props"];
33
37
  /** 表单项参数。 */
@@ -1,4 +1,6 @@
1
1
  {
2
+ "system.base.config.tab.basic": "Basic information",
3
+ "system.base.config.tab.form": "Configuration form",
2
4
  "system.ai.chat.action.collapse_sessions": "Collapse conversations",
3
5
  "system.ai.chat.action.confirm_delete_conversation": "Delete",
4
6
  "system.ai.chat.action.continue": "Continue",
@@ -1,4 +1,6 @@
1
1
  {
2
+ "system.base.config.tab.basic": "基本情報",
3
+ "system.base.config.tab.form": "設定フォーム",
2
4
  "system.ai.chat.action.collapse_sessions": "会話バーを閉じる",
3
5
  "system.ai.chat.action.confirm_delete_conversation": "削除の確認",
4
6
  "system.ai.chat.action.continue": "続行",
@@ -1,4 +1,6 @@
1
1
  {
2
+ "system.base.config.tab.basic": "基础信息",
3
+ "system.base.config.tab.form": "配置表单",
2
4
  "system.ai.chat.action.collapse_sessions": "收起会话栏",
3
5
  "system.ai.chat.action.confirm_delete_conversation": "确认删除",
4
6
  "system.ai.chat.action.continue": "继续",
@@ -1,4 +1,6 @@
1
1
  {
2
+ "system.base.config.tab.basic": "基本資訊",
3
+ "system.base.config.tab.form": "設定表單",
2
4
  "system.ai.chat.action.collapse_sessions": "收起會話欄",
3
5
  "system.ai.chat.action.confirm_delete_conversation": "確認刪除",
4
6
  "system.ai.chat.action.continue": "繼續",
@@ -62,16 +62,20 @@ export interface BaseMigration {
62
62
  data_source: string;
63
63
  /** 迁移版本 */
64
64
  version: string;
65
- /** 升级脚本 */
66
- up_sql: string;
67
- /** 回退脚本 */
68
- down_sql: string;
69
- /** 升级描述 */
70
- description: string;
65
+ /** 迁移文件列表 */
66
+ files: BaseMigrationFile[];
71
67
  /** 创建时间 */
72
68
  created_at: string;
73
69
  }
74
70
 
71
+ /** 数据库迁移文件内容 */
72
+ export interface BaseMigrationFile {
73
+ /** 文件相对路径 */
74
+ path: string;
75
+ /** 文件内容 */
76
+ content: string;
77
+ }
78
+
75
79
  /** Admin数据库迁移服务。 */
76
80
  export interface BaseMigrationService {
77
81
  /** 分页查询数据库升级历史 */
@@ -9,95 +9,113 @@
9
9
  :request-api="requestBaseConfigTable"
10
10
  />
11
11
 
12
- <FormDialog
12
+ <ProDialog
13
13
  v-model="dialog.visible"
14
- ref="formDialogRef"
15
14
  class="system-config-dialog"
16
15
  :title="t(dialog.titleKey, { resource: t('system.base.config.resource') })"
17
- width="min(1440px, calc(100vw - 32px))"
16
+ width="min(1040px, calc(100vw - 32px))"
18
17
  top="4vh"
19
- :col-span="12"
20
- :gutter="24"
21
18
  :confirm-loading="saving"
22
- :model="formData"
23
- :fields="formFields"
24
- :rules="rules"
25
19
  @confirm="handleSubmit"
26
20
  @close="handleCloseDialog"
27
21
  >
28
- <template #textValue>
29
- <el-input
30
- v-model="formData.value"
31
- :placeholder="t('common.validation.required_input', { field: t('system.base.config.field.value') })"
32
- />
33
- </template>
34
- <template #imageValue>
35
- <UploadImg v-model:image-url="formData.value" upload-type="config" />
36
- </template>
37
- <template #richTextValue>
38
- <WangEditor v-model:value="formData.value" upload-type="config" />
39
- </template>
40
- <template #dictValue>
41
- <Dict
42
- v-if="dictValueCode"
43
- v-model="formData.value"
44
- :code="dictValueCode"
45
- code-type="string"
46
- :placeholder="t('common.validation.required_select', { field: t('system.base.config.field.value') })"
47
- />
48
- <el-input
49
- v-else
50
- v-model="formData.value"
51
- :placeholder="t('common.validation.required_input', { field: t('system.base.config.field.value') })"
52
- />
53
- </template>
54
- <template #booleanValue>
55
- <el-switch
56
- v-model="formData.value"
57
- active-value="true"
58
- inactive-value="false"
59
- active-text="true"
60
- inactive-text="false"
61
- inline-prompt
62
- />
63
- </template>
64
- <template #formValue>
65
- <div class="config-form-value">
66
- <template v-if="formDefinition">
67
- <p>
68
- <el-icon><component :is="formDefinition.icon" /></el-icon> {{ t(formDefinition.descriptionKey) }}
69
- </p>
70
- <ProForm
71
- :key="formDefinition.key"
72
- ref="valueFormRef"
73
- :model="formValue"
74
- :fields="localizedFormFields"
75
- :col-span="12"
76
- :gutter="24"
77
- label-width="180px"
78
- />
79
- </template>
80
- <el-alert v-else :title="t('system.base.config.message.form_unavailable')" type="warning" :closable="false" />
81
- </div>
82
- </template>
83
- <template #nameI18ns>
84
- <DynamicI18nEditor v-model="nameI18nValues" :source="formData.name" :maxlength="100" />
85
- </template>
86
- <template #valueI18ns>
87
- <DynamicI18nEditor v-model="valueI18nValues" :source="formData.value" :maxlength="10000" multiline />
88
- </template>
89
- </FormDialog>
22
+ <el-tabs
23
+ v-model="activeTab"
24
+ class="config-editor-tabs"
25
+ :class="{ 'config-editor-tabs--plain': formData.type !== BaseConfigType.BASE_CONFIG_TYPE_FORM }"
26
+ >
27
+ <el-tab-pane :label="t('system.base.config.tab.basic')" name="basic">
28
+ <ProForm
29
+ ref="basicFormRef"
30
+ :model="formData"
31
+ :fields="formFields"
32
+ :rules="rules"
33
+ :col-span="12"
34
+ :gutter="24"
35
+ label-position="top"
36
+ scroll-to-error
37
+ >
38
+ <template #textValue>
39
+ <el-input
40
+ v-model="formData.value"
41
+ :placeholder="t('common.validation.required_input', { field: t('system.base.config.field.value') })"
42
+ />
43
+ </template>
44
+ <template #imageValue>
45
+ <UploadImg v-model:image-url="formData.value" upload-type="config" />
46
+ </template>
47
+ <template #richTextValue>
48
+ <WangEditor v-model:value="formData.value" upload-type="config" />
49
+ </template>
50
+ <template #dictValue>
51
+ <Dict
52
+ v-if="dictValueCode"
53
+ v-model="formData.value"
54
+ :code="dictValueCode"
55
+ code-type="string"
56
+ :placeholder="t('common.validation.required_select', { field: t('system.base.config.field.value') })"
57
+ />
58
+ <el-input
59
+ v-else
60
+ v-model="formData.value"
61
+ :placeholder="t('common.validation.required_input', { field: t('system.base.config.field.value') })"
62
+ />
63
+ </template>
64
+ <template #booleanValue>
65
+ <el-switch
66
+ v-model="formData.value"
67
+ active-value="true"
68
+ inactive-value="false"
69
+ active-text="true"
70
+ inactive-text="false"
71
+ inline-prompt
72
+ />
73
+ </template>
74
+ <template #nameI18ns>
75
+ <DynamicI18nEditor v-model="nameI18nValues" :source="formData.name" :maxlength="100" />
76
+ </template>
77
+ <template #valueI18ns>
78
+ <DynamicI18nEditor v-model="valueI18nValues" :source="formData.value" :maxlength="10000" multiline />
79
+ </template>
80
+ </ProForm>
81
+ </el-tab-pane>
82
+ <el-tab-pane
83
+ v-if="formData.type === BaseConfigType.BASE_CONFIG_TYPE_FORM"
84
+ :label="t('system.base.config.tab.form')"
85
+ name="form"
86
+ >
87
+ <div class="config-form-value">
88
+ <template v-if="formDefinition">
89
+ <p>
90
+ <el-icon><component :is="formDefinition.icon" /></el-icon> {{ t(formDefinition.descriptionKey) }}
91
+ </p>
92
+ <ProForm
93
+ :key="formDefinition.key"
94
+ ref="valueFormRef"
95
+ :model="formValue"
96
+ :fields="localizedFormFields"
97
+ :col-span="12"
98
+ :gutter="24"
99
+ label-position="top"
100
+ scroll-to-error
101
+ />
102
+ </template>
103
+ <el-alert v-else :title="t('system.base.config.message.form_unavailable')" type="warning" :closable="false" />
104
+ </div>
105
+ </el-tab-pane>
106
+ </el-tabs>
107
+ </ProDialog>
90
108
  </div>
91
109
  </template>
92
110
 
93
111
  <script setup lang="ts">
94
- import { computed, h, reactive, ref, watch } from "vue";
112
+ import { computed, h, nextTick, reactive, ref, watch } from "vue";
95
113
  import { useDebounceFn } from "@vueuse/core";
96
114
  import type { ColumnProps, HeaderActionProps, ProTableInstance } from "@liujitcn/kratos-admin-core/components/ProTable/interface";
97
115
  import ProTable from "@liujitcn/kratos-admin-core/components/ProTable";
98
116
  import ProForm from "@liujitcn/kratos-admin-core/components/ProForm/index.vue";
99
117
  import { runtimeConfigDefinitions, type RuntimeConfigModel } from "@liujitcn/kratos-admin-system/config";
100
- import FormDialog from "@liujitcn/kratos-admin-core/components/Dialog/FormDialog.vue";
118
+ import ProDialog from "@liujitcn/kratos-admin-core/components/Dialog/ProDialog.vue";
101
119
  import type { ProFormField, ProFormOption, ProFormInstance } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
102
120
  import DictLabel from "@liujitcn/kratos-admin-core/components/Dict/DictLabel.vue";
103
121
  import RichTextPreview from "@liujitcn/kratos-admin-core/components/RichTextPreview/index.vue";
@@ -141,7 +159,8 @@ defineOptions({
141
159
 
142
160
  const { BUTTONS } = useAuthButtons();
143
161
  const proTable = ref<ProTableInstance>();
144
- const formDialogRef = ref<InstanceType<typeof FormDialog>>();
162
+ const basicFormRef = ref<ProFormInstance>();
163
+ const activeTab = ref("basic");
145
164
 
146
165
  const saving = ref(false);
147
166
  const valueFormRef = ref<ProFormInstance>();
@@ -155,6 +174,8 @@ const localizedFormFields = computed<ProFormField[]>(() =>
155
174
  props: field.props,
156
175
  itemProps: field.itemProps,
157
176
  options: field.options,
177
+ colSpan: field.colSpan,
178
+ rowBreakBefore: field.rowBreakBefore,
158
179
  visible: field.visible,
159
180
  labelTooltip: field.labelTooltipKey ? t(field.labelTooltipKey) : undefined,
160
181
  rules: field.rules?.map(rule => ({
@@ -200,6 +221,15 @@ const formData = reactive<BaseConfigFormState>({
200
221
  value_i18ns: []
201
222
  });
202
223
 
224
+ /** 配置类型切换后回到基础信息,避免停留在已移除的表单页签。 */
225
+ watch(
226
+ () => formData.type,
227
+ () => {
228
+ activeTab.value = "basic";
229
+ },
230
+ { flush: "sync" }
231
+ );
232
+
203
233
  /** 切换表单类型或编码时加载对应默认内容。 */
204
234
  watch(
205
235
  () => [formData.type, formData.key],
@@ -357,14 +387,6 @@ const formFields = computed<ProFormField[]>(() => [
357
387
  component: "dict",
358
388
  props: { code: "base_config_type", disabled: formData.id > 0 }
359
389
  },
360
- {
361
- prop: "formValue",
362
- label: t("system.base.config.field.value"),
363
- component: "slot",
364
- slotName: "formValue",
365
- visible: model => model.type === BaseConfigType.BASE_CONFIG_TYPE_FORM,
366
- colSpan: 24
367
- },
368
390
  {
369
391
  prop: "value",
370
392
  label: t("system.base.config.field.value"),
@@ -617,6 +639,7 @@ async function handleOpenDialog(configId?: number) {
617
639
  loadFormValue(formData.value);
618
640
  }
619
641
  }
642
+ activeTab.value = configId && formData.type === BaseConfigType.BASE_CONFIG_TYPE_FORM ? "form" : "basic";
620
643
  dialog.visible = true;
621
644
  }
622
645
 
@@ -642,8 +665,9 @@ function handleCloseDialog() {
642
665
  * 重置系统配置表单,避免新增时保留旧值。
643
666
  */
644
667
  function resetForm() {
645
- formDialogRef.value?.resetFields();
646
- formDialogRef.value?.clearValidate();
668
+ basicFormRef.value?.resetFields();
669
+ basicFormRef.value?.clearValidate();
670
+ activeTab.value = "basic";
647
671
  formData.id = 0;
648
672
  formData.site = undefined;
649
673
  formData.name = "";
@@ -661,17 +685,22 @@ function resetForm() {
661
685
  * 提交系统配置表单。
662
686
  */
663
687
  async function handleSubmit() {
688
+ if (formData.type === BaseConfigType.BASE_CONFIG_TYPE_BOOLEAN) {
689
+ formData.value = formData.value === "true" ? "true" : "false";
690
+ }
691
+ activeTab.value = "basic";
692
+ await nextTick();
693
+ if ((await basicFormRef.value?.validate()) !== true) return;
664
694
  if (formData.type === BaseConfigType.BASE_CONFIG_TYPE_FORM) {
695
+ activeTab.value = "form";
696
+ await nextTick();
665
697
  if (!formDefinition.value) {
666
698
  ElMessage.warning(t("system.base.config.message.form_unavailable"));
667
699
  return;
668
700
  }
669
701
  if ((await valueFormRef.value?.validate()) !== true) return;
670
702
  formData.value = JSON.stringify(formValue.value);
671
- } else if (formData.type === BaseConfigType.BASE_CONFIG_TYPE_BOOLEAN) {
672
- formData.value = formData.value === "true" ? "true" : "false";
673
703
  }
674
- if ((await formDialogRef.value?.validate()) !== true) return;
675
704
  saving.value = true;
676
705
  try {
677
706
  const submitData = JSON.parse(JSON.stringify(formData)) as BaseConfigForm;
@@ -858,4 +887,26 @@ function handleDelete(selected?: number | string | Array<number | string> | Base
858
887
  width: auto !important;
859
888
  }
860
889
  }
890
+ .config-editor-tabs :deep(.el-tabs__content) {
891
+ max-height: calc(92dvh - 180px);
892
+ overflow: auto;
893
+ padding: 8px 12px 0 0;
894
+ }
895
+ .config-editor-tabs--plain :deep(.el-tabs__header) {
896
+ display: none;
897
+ }
898
+ .config-editor-tabs--plain :deep(.el-tabs__content) {
899
+ max-height: calc(92dvh - 132px);
900
+ }
901
+ .config-editor-tabs :deep(.el-form-item) {
902
+ margin-bottom: 24px;
903
+ }
904
+ .config-editor-tabs :deep(.el-form-item__label) {
905
+ height: auto;
906
+ line-height: 1.6;
907
+ white-space: normal;
908
+ }
909
+ .config-form-value :deep(.el-input-number) {
910
+ width: 100%;
911
+ }
861
912
  </style>
@@ -28,8 +28,12 @@
28
28
  />
29
29
  </el-form-item>
30
30
  <el-form-item class="migration-filters__actions">
31
- <el-button type="primary" :icon="Search" native-type="submit">{{ t("common.action.search") }}</el-button>
32
- <el-button :icon="Refresh" @click="handleReset">{{ t("common.action.reset") }}</el-button>
31
+ <el-button type="primary" native-type="submit"
32
+ ><template #icon><Search /></template>{{ t("common.action.search") }}</el-button
33
+ >
34
+ <el-button @click="handleReset"
35
+ ><template #icon><Refresh /></template>{{ t("common.action.reset") }}</el-button
36
+ >
33
37
  </el-form-item>
34
38
  </el-form>
35
39
 
@@ -58,7 +62,9 @@
58
62
  </div>
59
63
 
60
64
  <div v-if="pageable.total > pageable.page_size" class="migration-pagination">
61
- <span class="migration-pagination__total">{{ t("system.base.migration.message.total", { total: pageable.total }) }}</span>
65
+ <span class="migration-pagination__total">{{
66
+ t("system.base.migration.message.total", { total: pageable.total })
67
+ }}</span>
62
68
  <el-pagination
63
69
  background
64
70
  small
@@ -77,88 +83,56 @@
77
83
  <el-skeleton :rows="8" animated />
78
84
  </div>
79
85
  <template v-else-if="selectedMigration">
80
- <header class="detail-header">
81
- <div class="detail-header__title">
82
- <span class="detail-header__data-source">
83
- {{ selectedMigration.module || t("system.base.migration.value.default_module") }} ·
84
- {{ selectedMigration.data_source || "default" }}
85
- </span>
86
- <div class="detail-header__version">
87
- <h2>{{ selectedMigration.version }}</h2>
88
- </div>
89
- </div>
90
- <div class="detail-header__meta">
91
- <time :datetime="selectedMigration.created_at">{{ selectedMigration.created_at }}</time>
92
- </div>
93
- </header>
94
-
95
86
  <div v-loading="detailLoading" class="detail-scroll">
96
87
  <template v-if="!detailLoading">
97
- <section v-if="selectedMigration.description" class="detail-section">
98
- <div class="detail-section__title">
99
- <el-icon><Document /></el-icon>
100
- <span>{{ t("system.base.migration.section.description") }}</span>
101
- </div>
102
- <MarkdownPreview
103
- class="migration-markdown"
104
- :model-value="selectedMigration.description"
105
- :is-dark="globalStore.isDark"
106
- max-code-height="360px"
107
- />
108
- </section>
109
-
110
- <section v-if="selectedMigration.up_sql || selectedMigration.down_sql" class="detail-section">
111
- <div class="detail-section__title">
112
- <el-icon><Files /></el-icon>
113
- <span>{{ t("system.base.migration.section.sql") }}</span>
114
- </div>
115
- <el-collapse class="release-sql">
116
- <el-collapse-item v-if="selectedMigration.up_sql" name="up">
117
- <template #title>
118
- <span class="sql-title">
119
- <el-icon><DocumentAdd /></el-icon>
120
- <span>{{ t("system.base.migration.field.up_script") }}</span>
121
- <code>up.sql</code>
122
- </span>
123
- </template>
124
- <div class="sql-panel">
125
- <pre class="sql-code"><code>{{ selectedMigration.up_sql }}</code></pre>
126
- <el-tooltip :content="t('system.base.migration.action.copy_up_script')" placement="top">
127
- <el-button
128
- class="sql-copy"
129
- text
130
- circle
131
- :icon="CopyDocument"
132
- :aria-label="t('system.base.migration.action.copy_up_script')"
133
- @click.stop="copySql(selectedMigration.up_sql)"
134
- />
135
- </el-tooltip>
136
- </div>
137
- </el-collapse-item>
138
- <el-collapse-item v-if="selectedMigration.down_sql" name="down">
139
- <template #title>
140
- <span class="sql-title">
141
- <el-icon><DocumentRemove /></el-icon>
142
- <span>{{ t("system.base.migration.field.down_script") }}</span>
143
- <code>down.sql</code>
144
- </span>
145
- </template>
146
- <div class="sql-panel">
147
- <pre class="sql-code"><code>{{ selectedMigration.down_sql }}</code></pre>
148
- <el-tooltip :content="t('system.base.migration.action.copy_down_script')" placement="top">
88
+ <el-tabs v-if="hasDetailContent" v-model="activeFilePath" class="migration-file-tabs">
89
+ <el-tab-pane
90
+ v-for="file in selectedMigration.files"
91
+ :key="file.path"
92
+ :name="file.path"
93
+ :label="file.path.split('/').pop()"
94
+ lazy
95
+ >
96
+ <template v-if="activeFilePath === file.path">
97
+ <div class="migration-file-path">{{ file.path }}</div>
98
+ <MarkdownPreview
99
+ v-if="file.path.endsWith('.md')"
100
+ class="migration-markdown"
101
+ :model-value="file.content"
102
+ :is-dark="globalStore.isDark"
103
+ max-code-height="360px"
104
+ />
105
+ <div v-else class="sql-panel">
106
+ <pre class="sql-code"><code>{{ file.content }}</code></pre>
107
+ <el-tooltip
108
+ :content="
109
+ t(
110
+ file.path.endsWith('.down.sql')
111
+ ? 'system.base.migration.action.copy_down_script'
112
+ : 'system.base.migration.action.copy_up_script'
113
+ )
114
+ "
115
+ placement="top"
116
+ >
149
117
  <el-button
150
118
  class="sql-copy"
151
119
  text
152
120
  circle
153
- :icon="CopyDocument"
154
- :aria-label="t('system.base.migration.action.copy_down_script')"
155
- @click.stop="copySql(selectedMigration.down_sql)"
156
- />
121
+ :aria-label="
122
+ t(
123
+ file.path.endsWith('.down.sql')
124
+ ? 'system.base.migration.action.copy_down_script'
125
+ : 'system.base.migration.action.copy_up_script'
126
+ )
127
+ "
128
+ @click="copySql(file.content)"
129
+ ><template #icon><CopyDocument /></template
130
+ ></el-button>
157
131
  </el-tooltip>
158
132
  </div>
159
- </el-collapse-item>
160
- </el-collapse>
161
- </section>
133
+ </template>
134
+ </el-tab-pane>
135
+ </el-tabs>
162
136
 
163
137
  <div v-if="!hasDetailContent" class="detail-empty">
164
138
  <el-icon><Document /></el-icon>
@@ -176,7 +150,6 @@
176
150
 
177
151
  <script setup lang="ts">
178
152
  import { computed, onMounted, reactive, ref } from "vue";
179
- import { CopyDocument, Document, DocumentAdd, DocumentRemove, Files, Refresh, Search } from "@element-plus/icons-vue";
180
153
  import MarkdownPreview from "@liujitcn/kratos-admin-core/components/MarkdownPreview/index.vue";
181
154
  import { useGlobalStore } from "@liujitcn/kratos-admin-core/stores/runtime";
182
155
  import { buildPageRequest } from "@liujitcn/kratos-admin-core/table";
@@ -220,6 +193,7 @@ const histories = ref<BaseMigrationListItem[]>([]);
220
193
  const selectedMigrationId = ref<number | null>(null);
221
194
  const selectedMigration = ref<BaseMigration | null>(null);
222
195
  const detailRequestToken = ref(0);
196
+ const activeFilePath = ref("");
223
197
  const filters = reactive<MigrationFilters>({
224
198
  version: undefined,
225
199
  module: undefined,
@@ -230,9 +204,7 @@ const pageable = reactive<MigrationPageable>({
230
204
  page_size: 10,
231
205
  total: 0
232
206
  });
233
- const hasDetailContent = computed(() =>
234
- Boolean(selectedMigration.value?.description || selectedMigration.value?.up_sql || selectedMigration.value?.down_sql)
235
- );
207
+ const hasDetailContent = computed(() => Boolean(selectedMigration.value?.files.length));
236
208
 
237
209
  /**
238
210
  * 加载数据库升级历史列表。
@@ -269,6 +241,7 @@ async function loadMigrationHistory() {
269
241
  async function selectMigration(id: number) {
270
242
  selectedMigrationId.value = id;
271
243
  selectedMigration.value = null;
244
+ activeFilePath.value = "";
272
245
  detailLoading.value = true;
273
246
  const requestToken = detailRequestToken.value + 1;
274
247
  detailRequestToken.value = requestToken;
@@ -276,6 +249,7 @@ async function selectMigration(id: number) {
276
249
  const detail = await defBaseMigrationService.GetBaseMigration({ id });
277
250
  if (detailRequestToken.value === requestToken && selectedMigrationId.value === id) {
278
251
  selectedMigration.value = detail;
252
+ activeFilePath.value = detail.files[0]?.path ?? "";
279
253
  }
280
254
  } finally {
281
255
  if (detailRequestToken.value === requestToken) {
@@ -512,147 +486,69 @@ onMounted(() => {
512
486
  white-space: nowrap;
513
487
  }
514
488
 
515
- .detail-header {
516
- display: flex;
517
- flex: 0 0 auto;
518
- align-items: flex-start;
519
- justify-content: space-between;
520
- gap: 20px;
521
- padding: 22px 24px 20px;
522
- border-bottom: 1px solid var(--admin-page-divider);
523
- }
524
-
525
- .detail-header__title {
526
- min-width: 0;
527
- }
528
-
529
- .detail-header__data-source {
530
- display: block;
531
- overflow: hidden;
532
- font-size: 12px;
533
- font-weight: 600;
534
- color: var(--admin-page-text-secondary);
535
- letter-spacing: 0.04em;
536
- text-overflow: ellipsis;
537
- white-space: nowrap;
538
- }
539
-
540
- .detail-header__version {
541
- display: flex;
542
- flex-wrap: wrap;
543
- gap: 10px;
544
- align-items: center;
545
- margin-top: 8px;
546
- }
547
-
548
- .detail-header h2 {
549
- margin: 0;
550
- font-size: 24px;
551
- font-weight: 650;
552
- line-height: 1.35;
553
- overflow-wrap: anywhere;
554
- }
555
-
556
- .detail-header__meta {
557
- display: flex;
558
- flex: 0 0 auto;
559
- flex-direction: column;
560
- align-items: flex-end;
561
- gap: 6px;
562
- padding-top: 3px;
563
- font-size: 12px;
564
- color: var(--admin-page-text-secondary);
565
- text-align: right;
566
- }
567
-
568
489
  .detail-scroll {
490
+ display: flex;
569
491
  flex: 1;
492
+ flex-direction: column;
570
493
  min-height: 0;
571
- overflow-y: auto;
494
+ overflow: hidden;
572
495
  padding: 24px;
573
496
  }
574
497
 
575
- .detail-section + .detail-section {
576
- margin-top: 26px;
577
- }
578
-
579
- .detail-section__title {
580
- display: flex;
581
- gap: 7px;
582
- align-items: center;
583
- padding-bottom: 10px;
584
- margin-bottom: 16px;
585
- font-size: 14px;
586
- font-weight: 600;
587
- color: var(--admin-page-text-primary);
588
- border-bottom: 1px solid var(--admin-page-divider);
589
- }
590
-
591
- .detail-section__title :deep(.el-icon) {
592
- color: var(--el-color-primary);
593
- }
594
-
595
498
  .migration-markdown {
499
+ flex: 1;
500
+ min-height: 0;
501
+ overflow-y: auto;
596
502
  width: 100%;
597
503
  max-width: 100%;
598
504
  overflow-wrap: anywhere;
599
505
  }
600
506
 
601
- .release-sql {
602
- border: 1px solid var(--admin-page-card-border-soft);
603
- border-radius: var(--admin-page-radius);
604
- }
605
-
606
- .release-sql :deep(.el-collapse) {
607
- border: 0;
608
- }
609
-
610
- .release-sql :deep(.el-collapse-item__header) {
611
- height: 46px;
612
- padding: 0 14px;
613
- color: var(--admin-page-text-primary);
614
- background: var(--admin-page-card-bg-soft);
615
- border-bottom-color: var(--admin-page-card-border-soft);
616
- }
617
-
618
- .release-sql :deep(.el-collapse-item__wrap) {
619
- background: var(--admin-page-card-bg);
620
- border-bottom-color: var(--admin-page-card-border-soft);
507
+ .migration-file-tabs {
508
+ display: flex;
509
+ flex: 1;
510
+ flex-direction: column;
511
+ min-width: 0;
512
+ min-height: 0;
621
513
  }
622
514
 
623
- .release-sql :deep(.el-collapse-item__content) {
624
- padding: 14px;
515
+ .migration-file-tabs :deep(.el-tabs__header) {
516
+ flex-shrink: 0;
625
517
  }
626
518
 
627
- .sql-title {
628
- display: inline-flex;
629
- gap: 8px;
630
- align-items: center;
631
- min-width: 0;
632
- font-size: 13px;
633
- font-weight: 600;
519
+ .migration-file-tabs :deep(.el-tabs__content) {
520
+ flex: 1;
521
+ min-height: 0;
522
+ overflow: hidden;
634
523
  }
635
524
 
636
- .sql-title :deep(.el-icon) {
637
- color: var(--el-color-primary);
525
+ .migration-file-tabs :deep(.el-tab-pane) {
526
+ display: flex;
527
+ flex-direction: column;
528
+ height: 100%;
529
+ min-height: 0;
638
530
  }
639
531
 
640
- .sql-title code {
641
- padding: 2px 6px;
642
- font-size: 11px;
643
- font-weight: 500;
532
+ .migration-file-path {
533
+ flex-shrink: 0;
534
+ margin-bottom: 16px;
535
+ font-size: 12px;
644
536
  color: var(--admin-page-text-secondary);
645
- background: var(--admin-page-card-bg-muted);
646
- border-radius: var(--admin-page-radius);
537
+ overflow-wrap: anywhere;
647
538
  }
648
539
 
649
540
  .sql-panel {
650
541
  position: relative;
542
+ display: flex;
543
+ flex: 1;
651
544
  min-width: 0;
545
+ min-height: 0;
652
546
  }
653
547
 
654
548
  .sql-code {
655
- max-height: 360px;
549
+ flex: 1;
550
+ min-width: 0;
551
+ min-height: 0;
656
552
  padding: 14px 48px 14px 16px;
657
553
  margin: 0;
658
554
  overflow: auto;
@@ -733,6 +629,7 @@ onMounted(() => {
733
629
  }
734
630
 
735
631
  .migration-detail-panel {
632
+ flex: 0 0 480px;
736
633
  min-height: 480px;
737
634
  border-left: 0;
738
635
  }
@@ -744,20 +641,9 @@ onMounted(() => {
744
641
  overflow-x: auto;
745
642
  }
746
643
 
747
- .detail-header,
748
644
  .detail-scroll {
749
645
  padding-right: 16px;
750
646
  padding-left: 16px;
751
647
  }
752
-
753
- .detail-header {
754
- flex-direction: column;
755
- gap: 10px;
756
- }
757
-
758
- .detail-header__meta {
759
- align-items: flex-start;
760
- text-align: left;
761
- }
762
648
  }
763
649
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liujitcn/kratos-admin-system",
3
3
  "private": false,
4
- "version": "0.0.41",
4
+ "version": "0.0.42",
5
5
  "type": "module",
6
6
  "description": "kratos-admin system management module",
7
7
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "sortablejs": "^1.15.2",
17
17
  "swagger-ui-dist": "^5.32.9",
18
18
  "vue-element-plus-x": "^2.0.3",
19
- "@liujitcn/kratos-admin-core": "0.0.41"
19
+ "@liujitcn/kratos-admin-core": "0.0.42"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "@element-plus/icons-vue": "^2.3.1",