@liujitcn/kratos-admin-system 0.0.1 → 0.0.17

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.
@@ -56,7 +56,12 @@ import { defBaseMenuService } from "@liujitcn/kratos-admin-system/api/system/bas
56
56
  import { defBaseApiService } from "@liujitcn/kratos-admin-system/api/system/base_api";
57
57
  import { useAuthButtons } from "@liujitcn/kratos-admin-core/auth";
58
58
  import type { BaseApi } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_api";
59
- import type { BaseMenu, BaseMenuForm, BaseMenuMeta } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_menu";
59
+ import type {
60
+ BaseMenu,
61
+ BaseMenuAppMeta,
62
+ BaseMenuForm,
63
+ BaseMenuMeta
64
+ } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_menu";
60
65
  import { Status } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
61
66
  import { BaseMenuType } from "@liujitcn/kratos-admin-system/rpc/system/common/v1/enum";
62
67
  import { normalizeSelectedIds } from "@liujitcn/kratos-admin-core/table";
@@ -66,6 +71,8 @@ defineOptions({
66
71
  inheritAttrs: false
67
72
  });
68
73
 
74
+ const APP_MENU_ROOT_ID = 999;
75
+
69
76
  /**
70
77
  * 菜单表单状态,统一补齐 meta 字段,便于 ProForm 直接双向绑定。
71
78
  */
@@ -79,6 +86,7 @@ const proTable = ref<ProTableInstance>();
79
86
  const formDialogRef = ref<InstanceType<typeof FormDialog>>();
80
87
  const menuOptions = ref<ProFormOption[]>([]);
81
88
  const parentMenuTypeMap = ref(new Map<number, BaseMenuType>());
89
+ const appMenuIds = ref(new Set<number>([APP_MENU_ROOT_ID]));
82
90
  const apiList = ref<BaseApi[]>([]);
83
91
 
84
92
  const dialog = reactive({
@@ -88,6 +96,16 @@ const dialog = reactive({
88
96
  parentLocked: true
89
97
  });
90
98
 
99
+ /** 创建默认移动端页面配置。 */
100
+ function createDefaultAppMenuMeta(): BaseMenuAppMeta {
101
+ return {
102
+ view_key: "",
103
+ access: "AUTHENTICATED",
104
+ in_tab_bar: false,
105
+ selected_icon: undefined
106
+ };
107
+ }
108
+
91
109
  /** 创建默认菜单元信息。 */
92
110
  function createDefaultMenuMeta(): BaseMenuMeta {
93
111
  return {
@@ -98,7 +116,8 @@ function createDefaultMenuMeta(): BaseMenuMeta {
98
116
  keep_alive: false,
99
117
  full: false,
100
118
  affix: false,
101
- params: []
119
+ params: [],
120
+ app: undefined
102
121
  };
103
122
  }
104
123
 
@@ -136,6 +155,16 @@ const menuTypeOptions: ProFormOption[] = [
136
155
  { label: "外链", value: BaseMenuType.EXT_LINK }
137
156
  ];
138
157
 
158
+ /** 判断父节点是否属于固定移动端菜单树。 */
159
+ function isAppMenu(parentId?: number) {
160
+ return parentId !== undefined && appMenuIds.value.has(parentId);
161
+ }
162
+
163
+ /** 判断页面是否为固定移动端根目录的直属 tab。 */
164
+ function isAppTab(parentId?: number) {
165
+ return parentId === APP_MENU_ROOT_ID;
166
+ }
167
+
139
168
  /** 根据三、五、七、九位编号识别菜单层级。 */
140
169
  function getMenuLevel(menuId: number) {
141
170
  if (menuId >= 100 && menuId <= 999) return 1;
@@ -148,6 +177,7 @@ function getMenuLevel(menuId: number) {
148
177
  /** 判断当前菜单是否还能继续新增下级节点。 */
149
178
  function canCreateChild(menu: BaseMenu) {
150
179
  const level = getMenuLevel(menu.id);
180
+ if (menu.id === APP_MENU_ROOT_ID || isAppMenu(menu.parent_id)) return level >= 1 && level < 4;
151
181
  if (menu.type === BaseMenuType.FOLDER) return level === 1 || level === 2;
152
182
  if (menu.type === BaseMenuType.MENU) return level === 2 || level === 3;
153
183
  return false;
@@ -157,6 +187,8 @@ function canCreateChild(menu: BaseMenu) {
157
187
  const availableMenuTypeOptions = computed(() => {
158
188
  const parentLevel = getMenuLevel(formData.parent_id ?? 0);
159
189
 
190
+ if (formData.id === APP_MENU_ROOT_ID) return menuTypeOptions.filter(item => item.value === BaseMenuType.FOLDER);
191
+ if (isAppMenu(formData.parent_id)) return menuTypeOptions.filter(item => item.value === BaseMenuType.MENU);
160
192
  if (formData.id > 0 && parentLevel === 0) return menuTypeOptions.filter(item => item.value === BaseMenuType.FOLDER);
161
193
  if (dialog.parentType === BaseMenuType.FOLDER && parentLevel === 1)
162
194
  return menuTypeOptions.filter(
@@ -172,6 +204,12 @@ const availableMenuTypeOptions = computed(() => {
172
204
  watch(
173
205
  () => formData.parent_id,
174
206
  () => {
207
+ if (isAppMenu(formData.parent_id)) {
208
+ formData.meta.app ??= createDefaultAppMenuMeta();
209
+ formData.meta.app.in_tab_bar = isAppTab(formData.parent_id);
210
+ } else if (formData.id === 0) {
211
+ formData.meta.app = undefined;
212
+ }
175
213
  dialog.parentType = parentMenuTypeMap.value.get(formData.parent_id ?? 0) ?? BaseMenuType.UNKNOWN_MT;
176
214
  if (formData.id > 0 || availableMenuTypeOptions.value.some(item => item.value === formData.type)) return;
177
215
  formData.type = (availableMenuTypeOptions.value[0]?.value as BaseMenuType) ?? BaseMenuType.UNKNOWN_MT;
@@ -183,11 +221,18 @@ const statusOptions: ProFormOption[] = [
183
221
  { label: "禁用", value: Status.DISABLE }
184
222
  ];
185
223
 
224
+ const appAccessOptions: ProFormOption[] = [
225
+ { label: "公开访问", value: "PUBLIC" },
226
+ { label: "仅游客", value: "GUEST_ONLY" },
227
+ { label: "登录访问", value: "AUTHENTICATED" }
228
+ ];
229
+
186
230
  /**
187
231
  * 渲染菜单图标单元格,统一兼容 Element Plus 图标和本地 svg 图标。
188
232
  */
189
233
  function renderMenuIconCell(scope: RenderScope<BaseMenu>) {
190
234
  const icon = scope.row.meta?.icon;
235
+ if (isAppMenu(scope.row.parent_id)) return icon || "--";
191
236
  const iconName = resolveElementIcon(icon);
192
237
  if (iconName) {
193
238
  return h(
@@ -212,7 +257,14 @@ function renderHiddenCell(scope: RenderScope<BaseMenu>) {
212
257
 
213
258
  /** 菜单表格列配置。 */
214
259
  const columns = computed<ColumnProps[]>(() => [
215
- { type: "selection", width: 55, selectable: row => (row as BaseMenu).parent_id !== 0 },
260
+ {
261
+ type: "selection",
262
+ width: 55,
263
+ selectable: row => {
264
+ const menu = row as BaseMenu;
265
+ return menu.parent_id !== 0 && menu.id !== APP_MENU_ROOT_ID;
266
+ }
267
+ },
216
268
  {
217
269
  prop: "meta.title",
218
270
  label: "菜单名称",
@@ -284,7 +336,10 @@ const columns = computed<ColumnProps[]>(() => [
284
336
  type: "danger",
285
337
  link: true,
286
338
  icon: Delete,
287
- hidden: scope => !BUTTONS.value["base:menu:delete"] || (scope.row as BaseMenu).parent_id === 0,
339
+ hidden: scope => {
340
+ const menu = scope.row as BaseMenu;
341
+ return !BUTTONS.value["base:menu:delete"] || menu.parent_id === 0 || menu.id === APP_MENU_ROOT_ID;
342
+ },
288
343
  onClick: scope => handleDeleteMenu(scope.row as BaseMenu)
289
344
  }
290
345
  ]
@@ -388,29 +443,74 @@ const formFields = computed<ProFormField[]>(() => [
388
443
  itemProps: model => ({
389
444
  required: model.type !== BaseMenuType.BUTTON
390
445
  }),
391
- visible: model => model.type !== BaseMenuType.BUTTON
446
+ visible: model => !isAppMenu(model.parent_id) && model.type !== BaseMenuType.BUTTON
447
+ },
448
+ {
449
+ prop: "meta.icon",
450
+ label: "默认图标",
451
+ component: "input",
452
+ labelTooltip: "填写移动端模块注册的图标键或 HTTPS 图片地址。",
453
+ itemProps: model => ({
454
+ required: Boolean(model.meta?.app?.in_tab_bar)
455
+ }),
456
+ props: { placeholder: "例如 HOME_DEFAULT" },
457
+ visible: model => isAppMenu(model.parent_id)
392
458
  },
393
459
  {
394
460
  prop: "name",
395
- label: "路由名称",
461
+ label: isAppMenu(formData.parent_id) ? "逻辑名称" : "路由名称",
396
462
  component: "input",
397
- labelTooltip: "如果需要开启缓存,需保证页面 defineOptions 中的 name 与此处一致,建议使用驼峰。",
463
+ labelTooltip: isAppMenu(formData.parent_id)
464
+ ? "移动端页面使用 App 前缀的唯一逻辑名称。"
465
+ : "如果需要开启缓存,需保证页面 defineOptions 中的 name 与此处一致,建议使用驼峰。",
398
466
  itemProps: model => ({
399
467
  required: model.type === BaseMenuType.MENU
400
468
  }),
401
- props: { placeholder: "请输入路由名称" },
469
+ props: () => ({
470
+ placeholder: isAppMenu(formData.parent_id) ? "例如 AppHome" : "请输入路由名称"
471
+ }),
402
472
  visible: model => model.type === BaseMenuType.MENU
403
473
  },
474
+ {
475
+ prop: "meta.app.view_key",
476
+ label: "视图键",
477
+ component: "input",
478
+ labelTooltip: "必须与移动端模块注册的稳定 viewKey 一致,不填写组件文件路径。",
479
+ itemProps: { required: true },
480
+ props: { placeholder: "例如 HOME" },
481
+ visible: model => isAppMenu(model.parent_id)
482
+ },
483
+ {
484
+ prop: "meta.app.access",
485
+ label: "访问模式",
486
+ component: "select",
487
+ options: appAccessOptions,
488
+ itemProps: { required: true },
489
+ props: {
490
+ clearable: false,
491
+ placeholder: "请选择访问模式"
492
+ },
493
+ visible: model => isAppMenu(model.parent_id)
494
+ },
495
+ {
496
+ prop: "meta.app.selected_icon",
497
+ label: "选中图标",
498
+ component: "input",
499
+ labelTooltip: "填写移动端模块注册的图标键或 HTTPS 图片地址。",
500
+ itemProps: { required: true },
501
+ props: { placeholder: "例如 HOME_SELECTED" },
502
+ visible: model => isAppTab(model.parent_id)
503
+ },
404
504
  {
405
505
  prop: "component",
406
506
  label: "组件路径",
407
507
  component: "input",
408
508
  labelTooltip: "组件页面完整路径,格式为模块名/模块内 views 路径,例如 system/base/user/index,缺省后缀 .vue。",
409
509
  itemProps: model => ({
410
- required: model.type === BaseMenuType.MENU
510
+ required: model.type === BaseMenuType.MENU && !isAppMenu(model.parent_id)
411
511
  }),
412
512
  props: { placeholder: "system/base/user/index" },
413
- visible: model => model.type === BaseMenuType.MENU
513
+ visible: model => model.type === BaseMenuType.MENU && !isAppMenu(model.parent_id)
414
514
  },
415
515
  {
416
516
  prop: "meta.hidden",
@@ -423,7 +523,7 @@ const formFields = computed<ProFormField[]>(() => [
423
523
  activeValue: true,
424
524
  inactiveValue: false
425
525
  },
426
- visible: model => model.type !== BaseMenuType.BUTTON
526
+ visible: model => !isAppMenu(model.parent_id) && model.type !== BaseMenuType.BUTTON
427
527
  },
428
528
  {
429
529
  prop: "meta.always_show",
@@ -437,7 +537,7 @@ const formFields = computed<ProFormField[]>(() => [
437
537
  activeValue: true,
438
538
  inactiveValue: false
439
539
  },
440
- visible: model => model.type === BaseMenuType.FOLDER || model.type === BaseMenuType.MENU
540
+ visible: model => !isAppMenu(model.parent_id) && (model.type === BaseMenuType.FOLDER || model.type === BaseMenuType.MENU)
441
541
  },
442
542
  {
443
543
  prop: "meta.keep_alive",
@@ -450,7 +550,7 @@ const formFields = computed<ProFormField[]>(() => [
450
550
  activeValue: true,
451
551
  inactiveValue: false
452
552
  },
453
- visible: model => model.type === BaseMenuType.MENU
553
+ visible: model => !isAppMenu(model.parent_id) && model.type === BaseMenuType.MENU
454
554
  },
455
555
  {
456
556
  prop: "meta.full",
@@ -463,7 +563,7 @@ const formFields = computed<ProFormField[]>(() => [
463
563
  activeValue: true,
464
564
  inactiveValue: false
465
565
  },
466
- visible: model => model.type === BaseMenuType.MENU
566
+ visible: model => !isAppMenu(model.parent_id) && model.type === BaseMenuType.MENU
467
567
  },
468
568
  {
469
569
  prop: "meta.affix",
@@ -476,7 +576,7 @@ const formFields = computed<ProFormField[]>(() => [
476
576
  activeValue: true,
477
577
  inactiveValue: false
478
578
  },
479
- visible: model => model.type === BaseMenuType.MENU
579
+ visible: model => !isAppMenu(model.parent_id) && model.type === BaseMenuType.MENU
480
580
  },
481
581
  {
482
582
  prop: "meta.params",
@@ -491,7 +591,7 @@ const formFields = computed<ProFormField[]>(() => [
491
591
  itemProps: {
492
592
  class: "menu-form__params"
493
593
  },
494
- visible: model => model.type === BaseMenuType.MENU
594
+ visible: model => !isAppMenu(model.parent_id) && model.type === BaseMenuType.MENU
495
595
  },
496
596
  {
497
597
  prop: "api",
@@ -503,7 +603,7 @@ const formFields = computed<ProFormField[]>(() => [
503
603
  filterable: true,
504
604
  titles: ["可选 API", "已选 API"]
505
605
  },
506
- visible: model => model.type === BaseMenuType.MENU || model.type === BaseMenuType.BUTTON,
606
+ visible: model => model.id === APP_MENU_ROOT_ID || model.type === BaseMenuType.MENU || model.type === BaseMenuType.BUTTON,
507
607
  colSpan: 24
508
608
  },
509
609
  {
@@ -548,21 +648,54 @@ const rules = computed<FormRules>(() => ({
548
648
  {
549
649
  validator: (_rule, value, callback) => {
550
650
  if (formData.type === BaseMenuType.BUTTON) return callback();
651
+ if (isAppMenu(formData.parent_id) && !isAppTab(formData.parent_id)) return callback();
551
652
  if (value) return callback();
552
- callback(new Error("请选择菜单图标"));
653
+ callback(new Error(isAppMenu(formData.parent_id) ? "请输入默认图标" : "请选择菜单图标"));
654
+ },
655
+ trigger: "change"
656
+ }
657
+ ],
658
+ "meta.app.view_key": [
659
+ {
660
+ validator: (_rule, value, callback) => {
661
+ if (!isAppMenu(formData.parent_id) || value) return callback();
662
+ callback(new Error("请输入视图键"));
663
+ },
664
+ trigger: "blur"
665
+ }
666
+ ],
667
+ "meta.app.access": [
668
+ {
669
+ validator: (_rule, value, callback) => {
670
+ if (!isAppMenu(formData.parent_id) || value) return callback();
671
+ callback(new Error("请选择访问模式"));
553
672
  },
554
673
  trigger: "change"
555
674
  }
556
675
  ],
676
+ "meta.app.selected_icon": [
677
+ {
678
+ validator: (_rule, value, callback) => {
679
+ if (!isAppTab(formData.parent_id) || value) return callback();
680
+ callback(new Error("请输入选中图标"));
681
+ },
682
+ trigger: "blur"
683
+ }
684
+ ],
557
685
  path: [
558
686
  { max: 1024, message: "路径不能超过 1024 个字符", trigger: "blur" },
559
687
  {
560
688
  validator: (_rule, value, callback) => {
561
689
  if (formData.type === BaseMenuType.FOLDER) return callback();
562
- if (value) return callback();
563
- if (formData.type === BaseMenuType.BUTTON) return callback(new Error("请输入权限标识"));
564
- if (formData.type === BaseMenuType.EXT_LINK) return callback(new Error("请输入完整外链地址"));
565
- callback(new Error("请输入路由路径"));
690
+ if (!value) {
691
+ if (formData.type === BaseMenuType.BUTTON) return callback(new Error("请输入权限标识"));
692
+ if (formData.type === BaseMenuType.EXT_LINK) return callback(new Error("请输入完整外链地址"));
693
+ return callback(new Error(isAppMenu(formData.parent_id) ? "请输入逻辑路径" : "请输入路由路径"));
694
+ }
695
+ if (isAppMenu(formData.parent_id) && !String(value).startsWith("app/")) {
696
+ return callback(new Error("移动端逻辑路径必须使用 app/ 前缀"));
697
+ }
698
+ callback();
566
699
  },
567
700
  trigger: "blur"
568
701
  }
@@ -583,8 +716,11 @@ const rules = computed<FormRules>(() => ({
583
716
  {
584
717
  validator: (_rule, value, callback) => {
585
718
  if (formData.type !== BaseMenuType.MENU) return callback();
586
- if (value) return callback();
587
- callback(new Error("请输入路由名称"));
719
+ if (!value) return callback(new Error(isAppMenu(formData.parent_id) ? "请输入逻辑名称" : "请输入路由名称"));
720
+ if (isAppMenu(formData.parent_id) && !String(value).startsWith("App")) {
721
+ return callback(new Error("移动端逻辑名称必须使用 App 前缀"));
722
+ }
723
+ callback();
588
724
  },
589
725
  trigger: "blur"
590
726
  }
@@ -593,7 +729,7 @@ const rules = computed<FormRules>(() => ({
593
729
  { max: 255, message: "组件路径不能超过 255 个字符", trigger: "blur" },
594
730
  {
595
731
  validator: (_rule, value, callback) => {
596
- if (formData.type !== BaseMenuType.MENU) return callback();
732
+ if (formData.type !== BaseMenuType.MENU || isAppMenu(formData.parent_id)) return callback();
597
733
  if (value) return callback();
598
734
  callback(new Error("请输入组件路径"));
599
735
  },
@@ -606,6 +742,7 @@ const rules = computed<FormRules>(() => ({
606
742
 
607
743
  /** 计算当前路径字段文案。 */
608
744
  function getPathFieldLabel() {
745
+ if (isAppMenu(formData.parent_id)) return "逻辑路径";
609
746
  if (formData.type === BaseMenuType.BUTTON) return "权限标识";
610
747
  if (formData.type === BaseMenuType.EXT_LINK) return "内部路径";
611
748
  return "路由路径";
@@ -613,6 +750,7 @@ function getPathFieldLabel() {
613
750
 
614
751
  /** 计算当前路径字段占位文案。 */
615
752
  function getPathFieldPlaceholder() {
753
+ if (isAppMenu(formData.parent_id)) return "例如 app/home";
616
754
  if (formData.type === BaseMenuType.BUTTON) return "请输入按钮权限标识";
617
755
  if (formData.type === BaseMenuType.EXT_LINK) return "external/baidu";
618
756
  if (formData.type === BaseMenuType.FOLDER) return "base";
@@ -621,6 +759,7 @@ function getPathFieldPlaceholder() {
621
759
 
622
760
  /** 计算当前路径字段提示文案。 */
623
761
  function getPathFieldTooltip() {
762
+ if (isAppMenu(formData.parent_id)) return "使用 app/ 前缀的稳定逻辑路径,不填写 uni-app 物理页面路径。";
624
763
  if (formData.type === BaseMenuType.BUTTON) return "按钮类型菜单使用权限标识,例如 base:user:create。";
625
764
  if (formData.type === BaseMenuType.EXT_LINK) return "外链使用内部唯一路径,例如 external/baidu;实际地址填写在外链地址中。";
626
765
  if (formData.type === BaseMenuType.FOLDER) return "目录不填写路径,仅用于组织下级菜单。";
@@ -679,16 +818,18 @@ function normalizeMenuApiSelection(api?: unknown[]) {
679
818
  /** 将服务端菜单表单补齐为前端可编辑结构。 */
680
819
  function normalizeMenuForm(data?: Partial<BaseMenuForm>): MenuFormState {
681
820
  const defaultForm = createDefaultMenuForm();
821
+ const parentId = data?.parent_id === 0 ? undefined : data?.parent_id;
682
822
  const normalizedMeta = {
683
823
  ...createDefaultMenuMeta(),
684
824
  ...(data?.meta ?? {}),
685
- params: data?.meta?.params ?? []
825
+ params: data?.meta?.params ?? [],
826
+ app: data?.meta?.app ?? (isAppMenu(parentId) ? createDefaultAppMenuMeta() : undefined)
686
827
  };
687
828
 
688
829
  return {
689
830
  ...defaultForm,
690
831
  ...data,
691
- parent_id: data?.parent_id === 0 ? undefined : data?.parent_id,
832
+ parent_id: parentId,
692
833
  type: data?.type ?? BaseMenuType.FOLDER,
693
834
  status: data?.status ?? Status.ENABLE,
694
835
  api: normalizeMenuApiSelection(data?.api),
@@ -700,11 +841,13 @@ function normalizeMenuForm(data?: Partial<BaseMenuForm>): MenuFormState {
700
841
  /** 构建可选父级菜单树,并记录父节点类型用于约束下级菜单类型。 */
701
842
  function buildMenuOptions(menuList: BaseMenu[] = []) {
702
843
  const typeMap = new Map<number, BaseMenuType>();
844
+ const mobileIds = new Set<number>([APP_MENU_ROOT_ID]);
703
845
  const convert = (list: BaseMenu[]): ProFormOption[] =>
704
846
  list
705
847
  .filter(item => item.type === BaseMenuType.FOLDER || item.type === BaseMenuType.MENU)
706
848
  .map(item => {
707
849
  typeMap.set(item.id, item.type);
850
+ if (mobileIds.has(item.parent_id ?? 0)) mobileIds.add(item.id);
708
851
  return {
709
852
  label: item.meta?.title || item.name || item.path,
710
853
  value: item.id,
@@ -713,6 +856,7 @@ function buildMenuOptions(menuList: BaseMenu[] = []) {
713
856
  });
714
857
  const options = convert(menuList);
715
858
  parentMenuTypeMap.value = typeMap;
859
+ appMenuIds.value = mobileIds;
716
860
  return options;
717
861
  }
718
862
 
@@ -723,6 +867,20 @@ function buildSubmitPayload(): BaseMenuForm {
723
867
  if (payload.id > 0 && payload.parent_id === undefined) payload.parent_id = 0;
724
868
  payload.meta.params = (payload.meta.params ?? []).filter(item => item.key || item.value);
725
869
 
870
+ if (isAppMenu(payload.parent_id)) {
871
+ payload.component = "";
872
+ payload.redirect = "";
873
+ payload.meta.app ??= createDefaultAppMenuMeta();
874
+ payload.meta.always_show = false;
875
+ payload.meta.hidden = false;
876
+ payload.meta.keep_alive = false;
877
+ payload.meta.full = false;
878
+ payload.meta.affix = false;
879
+ payload.meta.params = [];
880
+ payload.meta.app.in_tab_bar = isAppTab(payload.parent_id);
881
+ if (!payload.meta.app.in_tab_bar) payload.meta.app.selected_icon = undefined;
882
+ }
883
+
726
884
  if (payload.type === BaseMenuType.BUTTON) {
727
885
  payload.name = "";
728
886
  payload.component = "";
@@ -740,7 +898,7 @@ function buildSubmitPayload(): BaseMenuForm {
740
898
  payload.path = "";
741
899
  payload.name = "";
742
900
  payload.component = "Layout";
743
- payload.api = [];
901
+ if (payload.id !== APP_MENU_ROOT_ID) payload.api = [];
744
902
  payload.meta.keep_alive = false;
745
903
  payload.meta.full = false;
746
904
  payload.meta.affix = false;
@@ -897,8 +1055,8 @@ function handleDeleteMenu(selected?: number | string | Array<number | string> |
897
1055
  : selected && typeof selected === "object"
898
1056
  ? [selected as BaseMenu]
899
1057
  : [];
900
- if (menuList.some(item => item.parent_id === 0)) {
901
- ElMessage.warning("一级菜单不允许删除");
1058
+ if (menuList.some(item => item.parent_id === 0 || item.id === APP_MENU_ROOT_ID)) {
1059
+ ElMessage.warning("固定菜单不允许删除");
902
1060
  return;
903
1061
  }
904
1062
  const menuIds = (
@@ -83,22 +83,11 @@
83
83
  <el-icon><Document /></el-icon>
84
84
  <span>升级说明</span>
85
85
  </div>
86
- <MarkdownRenderer
86
+ <MarkdownPreview
87
87
  class="migration-markdown"
88
- :markdown="selectedMigration.description"
89
- :allow-html="false"
90
- :sanitize="true"
91
- :enable-breaks="true"
92
- :enable-latex="false"
93
- :enable-animate="false"
94
- :enable-shiki="false"
95
- :enable-mermaid="false"
88
+ :model-value="selectedMigration.description"
96
89
  :is-dark="globalStore.isDark"
97
- :show-code-block-header="true"
98
- :sticky-code-block-header="false"
99
- :enable-code-line-number="false"
100
- code-max-height="360px"
101
- :style="markdownRootStyle"
90
+ max-code-height="360px"
102
91
  />
103
92
  </section>
104
93
 
@@ -173,12 +162,11 @@
173
162
  <script setup lang="ts">
174
163
  import { computed, onMounted, reactive, ref } from "vue";
175
164
  import { CopyDocument, Document, DocumentAdd, DocumentRemove, Files, Refresh, Search } from "@element-plus/icons-vue";
176
- import { MarkdownRenderer } from "x-markdown-vue";
177
- import "x-markdown-vue/style";
178
- import { defBaseMigrationService } from "@liujitcn/kratos-admin-system/api/system/base_migration";
179
- import type { BaseMigration, BaseMigrationListItem, PageBaseMigrationRequest } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_migration";
165
+ import MarkdownPreview from "@liujitcn/kratos-admin-core/components/MarkdownPreview/index.vue";
180
166
  import { useGlobalStore } from "@liujitcn/kratos-admin-core/stores/runtime";
181
167
  import { buildPageRequest } from "@liujitcn/kratos-admin-core/table";
168
+ import { defBaseMigrationService } from "@liujitcn/kratos-admin-system/api/system/base_migration";
169
+ import type { BaseMigration, BaseMigrationListItem, PageBaseMigrationRequest } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_migration";
182
170
 
183
171
  defineOptions({
184
172
  name: "BaseMigration",
@@ -222,12 +210,6 @@ const pageable = reactive<MigrationPageable>({
222
210
  page_size: 10,
223
211
  total: 0
224
212
  });
225
- /** 覆盖 MarkdownRenderer 默认容器样式,保持内容与详情面板对齐。 */
226
- const markdownRootStyle = {
227
- padding: "0",
228
- color: "inherit",
229
- backgroundColor: "transparent"
230
- };
231
213
  const hasDetailContent = computed(() =>
232
214
  Boolean(selectedMigration.value?.description || selectedMigration.value?.up_sql || selectedMigration.value?.down_sql)
233
215
  );
@@ -596,36 +578,6 @@ onMounted(() => {
596
578
  overflow-wrap: anywhere;
597
579
  }
598
580
 
599
- .migration-markdown :deep(.x-md-core > :first-child) {
600
- margin-top: 0;
601
- }
602
-
603
- .migration-markdown :deep(.x-md-core > :last-child) {
604
- margin-bottom: 0;
605
- }
606
-
607
- .migration-markdown :deep(h1),
608
- .migration-markdown :deep(h2),
609
- .migration-markdown :deep(h3),
610
- .migration-markdown :deep(h4),
611
- .migration-markdown :deep(h5),
612
- .migration-markdown :deep(h6) {
613
- color: var(--admin-page-text-primary);
614
- }
615
-
616
- .migration-markdown :deep(a) {
617
- color: var(--el-color-primary);
618
- }
619
-
620
- .migration-markdown :deep(blockquote) {
621
- border-left-color: var(--el-border-color);
622
- }
623
-
624
- .migration-markdown :deep(table) {
625
- max-width: 100%;
626
- overflow-x: auto;
627
- }
628
-
629
581
  .release-sql {
630
582
  border: 1px solid var(--admin-page-card-border-soft);
631
583
  border-radius: var(--admin-page-radius);
@@ -2,9 +2,6 @@
2
2
  <div v-loading="loading" class="app-container code-gen-sub-page">
3
3
  <div class="api-doc-page__layout">
4
4
  <el-card class="code-gen-sub-card api-doc-page__navigation" shadow="never">
5
- <template #header>
6
- <div class="api-doc-page__panel-title">文档服务</div>
7
- </template>
8
5
  <div v-if="documents.length" class="api-doc-page__document-list">
9
6
  <button
10
7
  v-for="document in documents"
@@ -21,9 +18,6 @@
21
18
  <el-empty v-else :image-size="56" description="暂无文档" />
22
19
  </el-card>
23
20
  <el-card class="code-gen-sub-card api-doc-page__card" shadow="never">
24
- <template #header>
25
- <div class="api-doc-page__panel-title">{{ selectedDocumentName }}</div>
26
- </template>
27
21
  <div ref="swaggerRootRef" class="api-doc-page__swagger" />
28
22
  </el-card>
29
23
  </div>
@@ -41,9 +35,6 @@ const swaggerRootRef = ref<HTMLElement>();
41
35
  const loading = ref(true);
42
36
  const documents = ref<OpenApiServiceOption[]>([]);
43
37
  const selectedDocumentKey = ref("");
44
- const selectedDocumentName = computed(
45
- () => documents.value.find(document => document.key === selectedDocumentKey.value)?.name ?? "API 文档"
46
- );
47
38
 
48
39
  /** 初始化携带当前登录令牌的 Swagger UI。 */
49
40
  function initializeSwaggerUI(documentKey: string) {
@@ -142,14 +133,6 @@ onBeforeUnmount(() => {
142
133
  min-width: 0;
143
134
  min-height: 0;
144
135
  }
145
- .api-doc-page__panel-title {
146
- overflow: hidden;
147
- text-overflow: ellipsis;
148
- font-size: 14px;
149
- font-weight: 600;
150
- color: var(--admin-page-text-primary);
151
- white-space: nowrap;
152
- }
153
136
  .api-doc-page__document-list {
154
137
  display: flex;
155
138
  flex-direction: column;