@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.
- package/dist/package/declarations/src/api/system/base_user.d.ts +4 -2
- package/dist/package/declarations/src/rpc/system/admin/v1/base_menu.d.ts +13 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_third_account.d.ts +17 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +21 -0
- package/dist/package/declarations/src/views/ai/chat/components/AiMarkdown.vue.d.ts +1 -2
- package/dist/package/declarations/src/views/base/migration/index.vue.d.ts +0 -1
- package/dist/package/declarations/src/views/tool/project-doc/index.vue.d.ts +0 -1
- package/dist/package/src/api/system/base_user.ts +12 -1
- package/dist/package/src/rpc/system/admin/v1/base_menu.ts +14 -0
- package/dist/package/src/rpc/system/admin/v1/base_third_account.ts +29 -0
- package/dist/package/src/rpc/system/admin/v1/base_user.ts +24 -0
- package/dist/package/src/views/ai/chat/components/AiMarkdown.vue +17 -37
- package/dist/package/src/views/ai/chat/components/ChatPanel.vue +1 -1
- package/dist/package/src/views/base/menu/index.vue +188 -30
- package/dist/package/src/views/base/migration/index.vue +6 -54
- package/dist/package/src/views/tool/api-doc/index.vue +0 -17
- package/dist/package/src/views/tool/project-doc/index.vue +96 -99
- package/package.json +2 -4
- package/dist/package/src/typings/x-markdown-vue.d.ts +0 -1
|
@@ -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 {
|
|
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
|
-
{
|
|
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 =>
|
|
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:
|
|
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:
|
|
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)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
<
|
|
86
|
+
<MarkdownPreview
|
|
87
87
|
class="migration-markdown"
|
|
88
|
-
:
|
|
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
|
-
|
|
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
|
|
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;
|