@done-coding/admin-core 0.25.0 → 0.26.0

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.
Files changed (69) hide show
  1. package/es/components/app-layout/AppPage.vue.mjs +1 -1
  2. package/es/components/app-layout/AppPage.vue2.mjs +42 -23
  3. package/es/components/app-layout/use-content-box.mjs +17 -0
  4. package/es/components/data-view/DataGridView.vue.mjs +1 -1
  5. package/es/components/data-view/DataGridView.vue2.mjs +34 -14
  6. package/es/components/data-view/DataListView.vue.mjs +1 -1
  7. package/es/components/data-view/DataListView.vue2.mjs +27 -7
  8. package/es/components/data-view/DataListViewItem.vue.mjs +1 -1
  9. package/es/components/data-view/DataListViewItem.vue2.mjs +3 -2
  10. package/es/components/list-layout/ListLayout.vue.mjs +1 -1
  11. package/es/components/list-layout/ListLayout.vue2.mjs +34 -11
  12. package/es/components/modal/ModalConfirm.vue.mjs +1 -1
  13. package/es/components/modal/ModalConfirm.vue2.mjs +67 -49
  14. package/es/components/page-layout/AppPageDetail.vue.mjs +7 -0
  15. package/es/components/page-layout/AppPageDetail.vue2.mjs +89 -0
  16. package/es/components/page-layout/AppPageListDetailLayout.vue.mjs +153 -0
  17. package/es/components/page-layout/AppPageListDetailLayout.vue2.mjs +4 -0
  18. package/es/components/page-layout/AppPageListDetailSheet.vue.mjs +96 -0
  19. package/es/components/page-layout/AppPageListDetailSheet.vue2.mjs +4 -0
  20. package/es/components/page-layout/AppPageListDetailSplit.vue.mjs +7 -0
  21. package/es/components/page-layout/AppPageListDetailSplit.vue2.mjs +83 -0
  22. package/es/components/page-layout/use-list-detail.mjs +110 -0
  23. package/es/components/panel/PanelEditSwitch.vue.mjs +1 -1
  24. package/es/components/table/TableMain.vue.mjs +1 -1
  25. package/es/components/table/TableMain.vue2.mjs +55 -5
  26. package/es/components/view-layout/ViewLayout.vue.mjs +1 -1
  27. package/es/components/view-layout/ViewLayout.vue2.mjs +34 -4
  28. package/es/components/view-layout/ViewLayoutNavigation.vue.mjs +7 -0
  29. package/es/components/view-layout/ViewLayoutNavigation.vue2.mjs +34 -0
  30. package/es/hooks/use-active-record.mjs +100 -0
  31. package/es/index.mjs +146 -141
  32. package/es/inject/key.mjs +4 -0
  33. package/es/style.css +129 -96
  34. package/package.json +2 -2
  35. package/src/components/app-layout/README.md +8 -1
  36. package/src/components/app-layout/docs/README-AppPage.md +48 -2
  37. package/src/components/data-view/docs/README-DataGridView.md +2 -1
  38. package/src/components/data-view/docs/README-DataListView.md +13 -4
  39. package/src/components/list-layout/docs/README-ListLayout.md +1 -1
  40. package/src/components/modal/docs/README-ModalConfirm.md +2 -0
  41. package/src/components/page-layout/README.md +47 -0
  42. package/src/components/page-layout/docs/README-AppPageDetail.md +57 -0
  43. package/src/components/page-layout/docs/README-AppPageListDetailLayout.md +142 -0
  44. package/src/components/table/docs/README-TableMain.md +1 -1
  45. package/src/components/view-layout/docs/README-ViewLayout.md +14 -0
  46. package/types/components/app-layout/AppPage.vue.d.ts +14 -0
  47. package/types/components/app-layout/use-content-box.d.ts +31 -0
  48. package/types/components/data-view/DataGridView.vue.d.ts +9 -2
  49. package/types/components/data-view/DataListView.vue.d.ts +10 -2
  50. package/types/components/data-view/DataListViewItem.vue.d.ts +4 -2
  51. package/types/components/data-view/types.d.ts +5 -1
  52. package/types/components/list-layout/ListLayout.vue.d.ts +7 -2
  53. package/types/components/modal/ModalConfirm.vue.d.ts +18 -0
  54. package/types/components/page-layout/AppPageDetail.vue.d.ts +40 -0
  55. package/types/components/page-layout/AppPageListDetailLayout.vue.d.ts +39 -0
  56. package/types/components/page-layout/AppPageListDetailSheet.vue.d.ts +44 -0
  57. package/types/components/page-layout/AppPageListDetailSplit.vue.d.ts +47 -0
  58. package/types/components/page-layout/index.d.ts +4 -0
  59. package/types/components/page-layout/types.d.ts +102 -0
  60. package/types/components/page-layout/use-list-detail.d.ts +58 -0
  61. package/types/components/panel/PanelEditSwitch.vue.d.ts +30 -0
  62. package/types/components/table/TableMain.vue.d.ts +9 -2
  63. package/types/components/view-layout/ViewLayoutNavigation.vue.d.ts +13 -0
  64. package/types/components/view-layout/types.d.ts +46 -0
  65. package/types/hooks/use-active-record.d.ts +64 -0
  66. package/types/index.d.ts +1 -0
  67. package/types/inject/key.d.ts +29 -0
  68. package/types/injectInfo.json.d.ts +1 -1
  69. package/types/types/utility-types.d.ts +17 -0
package/es/style.css CHANGED
@@ -1,11 +1,11 @@
1
- .confirm-modal__title[data-v-fca575ec] {
1
+ .confirm-modal__title[data-v-7ef0be5c] {
2
2
  font-size: 16px;
3
3
  font-weight: 600;
4
4
  line-height: 24px;
5
5
  color: var(--el-text-color-primary);
6
6
  text-align: left;
7
7
  }
8
- .confirm-modal__content[data-v-fca575ec] {
8
+ .confirm-modal__content[data-v-7ef0be5c] {
9
9
  overflow-x: hidden;
10
10
  overflow-y: auto;
11
11
  max-height: calc(100vh - 200px);
@@ -15,20 +15,20 @@
15
15
  text-align: left;
16
16
  word-break: break-word;
17
17
  }
18
- .confirm-modal__content_drawer[data-v-fca575ec] {
18
+ .confirm-modal__content_drawer[data-v-7ef0be5c] {
19
19
  max-height: none;
20
20
  height: 100%;
21
21
  }
22
- .confirm-modal__footer[data-v-fca575ec] {
22
+ .confirm-modal__footer[data-v-7ef0be5c] {
23
23
  display: flex;
24
24
  justify-content: flex-end;
25
25
  align-items: center;
26
26
  gap: 8px;
27
27
  }
28
- .confirm-modal__footer[data-v-fca575ec] .el-button {
28
+ .confirm-modal__footer[data-v-7ef0be5c] .el-button {
29
29
  min-width: 72px;
30
30
  }
31
- .confirm-modal__footer[reverse="true"][data-v-fca575ec] {
31
+ .confirm-modal__footer[reverse="true"][data-v-7ef0be5c] {
32
32
  flex-direction: row-reverse;
33
33
  }
34
34
  .dc-confirm-modal.el-drawer .el-drawer__header {
@@ -42,6 +42,9 @@
42
42
  .dc-confirm-modal.el-drawer .el-drawer__footer {
43
43
  padding: 12px 20px;
44
44
  }
45
+ .dc-confirm-modal.confirm-modal_hidden-header .el-dialog__header {
46
+ display: none;
47
+ }
45
48
  .dc-confirm-modal.confirm-modal_header-divider .el-drawer__header,
46
49
  .dc-confirm-modal.confirm-modal_header-divider .el-dialog__header {
47
50
  border-bottom: 1px solid var(--el-border-color-lighter);
@@ -70,6 +73,90 @@
70
73
  .badge-mark_middle[data-v-12868883] .el-badge__content.is-fixed {
71
74
  top: 50%;
72
75
  }
76
+ .app-page[data-v-3ef165a7] {
77
+ --dc-page-content-viewport-top: var(--v6b53f4c2);
78
+ --dc-page-content-viewport-bottom: var(--v1c770cbe);
79
+ --dc-page-content-viewport-left: var(--ff323606);
80
+ --dc-page-content-viewport-right: var(--e667c834);
81
+ --dc-page-content-viewport-width: var(--e5daf0e0);
82
+ --dc-page-content-viewport-height: var(--v09211ec6);
83
+ }
84
+ .app-page-shim[data-v-3ef165a7] {
85
+ --app-viewport-max-height: var(--v0bd4b228);
86
+ }
87
+ .app-page-slot-shim[data-v-3ef165a7] {
88
+ box-sizing: border-box;
89
+ width: 100%;
90
+ height: 100%;
91
+ }
92
+ [data-v-64f355fe] .auto-refresh-label {
93
+ display: flex;
94
+ font-size: 13px;
95
+ color: var(--el-text-color-regular);
96
+ margin-bottom: 8px;
97
+ }
98
+ /* 有根节点时(needRootNode)按钮组横向排列 + 间距(参照 TableToolbar 按钮组内 gap:10px) */
99
+ .auto-refresh-group-root[data-v-0ebeb4c7] {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 10px;
103
+ }
104
+ .auto-refresh-group-icon--rotating[data-v-0ebeb4c7] {
105
+ animation: auto-refresh-rotating-0ebeb4c7 1s linear infinite;
106
+ }
107
+ @keyframes auto-refresh-rotating-0ebeb4c7 {
108
+ from {
109
+ transform: rotate(0deg);
110
+ }
111
+ to {
112
+ transform: rotate(360deg);
113
+ }
114
+ }
115
+ .view-layout-navigation[data-v-2fcd4cb2] {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 6px;
119
+ }
120
+ .view-layout-navigation__index[data-v-2fcd4cb2] {
121
+ min-width: 44px;
122
+ color: var(--el-text-color-secondary);
123
+ font-size: 12px;
124
+ text-align: center;
125
+ font-variant-numeric: tabular-nums;
126
+ }
127
+ .view-layout[data-v-8eb550ba] {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 8px;
131
+ }
132
+ .view-layout-toolbar[data-v-8eb550ba] {
133
+ display: flex;
134
+ justify-content: space-between;
135
+ align-items: center;
136
+ padding-bottom: 8px;
137
+ gap: 20px;
138
+ }
139
+ .view-layout-toolbar__left[data-v-8eb550ba],
140
+ .view-layout-toolbar__right[data-v-8eb550ba] {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 10px;
144
+ }
145
+ .view-layout-content[data-v-8eb550ba] {
146
+ overflow: auto;
147
+ }
148
+ .dc-app-page-detail__title[data-v-8de03183] {
149
+ padding-bottom: 12px;
150
+ font-size: 16px;
151
+ font-weight: 600;
152
+ line-height: 24px;
153
+ color: var(--dc-core-text-color, var(--el-text-color-primary));
154
+ }
155
+ .dc-app-page-list-detail-split__list[data-v-19f0f56d] {
156
+ box-sizing: border-box;
157
+ height: 100%;
158
+ overflow: hidden;
159
+ }
73
160
  .form-item-tip[data-v-8b508d64] {
74
161
  text-align: left;
75
162
  line-height: 1.5;
@@ -149,20 +236,20 @@
149
236
  .form-submit-panel_bare .form-submit-panel-operation-inner[data-v-1505775a] {
150
237
  padding: 0;
151
238
  }
152
- .panel-edit-switch[data-v-65f056ab] {
239
+ .panel-edit-switch[data-v-17f3b3db] {
153
240
  position: relative;
154
241
  }
155
- .panel-edit-switch:hover .panel-edit-switch-edit-btn[data-v-65f056ab] {
242
+ .panel-edit-switch:hover .panel-edit-switch-edit-btn[data-v-17f3b3db] {
156
243
  display: block;
157
244
  }
158
- .panel-edit-switch-edit-btn[data-v-65f056ab] {
245
+ .panel-edit-switch-edit-btn[data-v-17f3b3db] {
159
246
  position: absolute;
160
247
  top: 0;
161
248
  right: 0;
162
249
  z-index: 1;
163
250
  display: none;
164
251
  }
165
- .panel-edit-switch-actions[data-v-65f056ab] {
252
+ .panel-edit-switch-actions[data-v-17f3b3db] {
166
253
  display: flex;
167
254
  justify-content: flex-end;
168
255
  gap: 8px;
@@ -617,45 +704,6 @@
617
704
  .app-layout_noAnimation[data-v-b68520cd] .app-collapse-toggle {
618
705
  transition: none !important;
619
706
  }
620
- .app-page[data-v-2359c6df] {
621
- --dc-page-content-viewport-top: var(--v55181597);
622
- --dc-page-content-viewport-bottom: var(--v5f04c6a9);
623
- --dc-page-content-viewport-left: var(--v4de6d405);
624
- --dc-page-content-viewport-right: var(--v6f4a0dde);
625
- --dc-page-content-viewport-width: var(--v6f907988);
626
- --dc-page-content-viewport-height: var(--v68afbda5);
627
- }
628
- .app-page-shim[data-v-2359c6df] {
629
- --app-viewport-max-height: var(--v2e2782e4);
630
- }
631
- .app-page-slot-shim[data-v-2359c6df] {
632
- box-sizing: border-box;
633
- width: 100%;
634
- height: 100%;
635
- }
636
- [data-v-64f355fe] .auto-refresh-label {
637
- display: flex;
638
- font-size: 13px;
639
- color: var(--el-text-color-regular);
640
- margin-bottom: 8px;
641
- }
642
- /* 有根节点时(needRootNode)按钮组横向排列 + 间距(参照 TableToolbar 按钮组内 gap:10px) */
643
- .auto-refresh-group-root[data-v-0ebeb4c7] {
644
- display: flex;
645
- align-items: center;
646
- gap: 10px;
647
- }
648
- .auto-refresh-group-icon--rotating[data-v-0ebeb4c7] {
649
- animation: auto-refresh-rotating-0ebeb4c7 1s linear infinite;
650
- }
651
- @keyframes auto-refresh-rotating-0ebeb4c7 {
652
- from {
653
- transform: rotate(0deg);
654
- }
655
- to {
656
- transform: rotate(360deg);
657
- }
658
- }
659
707
  .table-toolbar[data-v-0220e327] {
660
708
  display: flex;
661
709
  justify-content: space-between;
@@ -798,18 +846,21 @@ to {
798
846
  .infinite-list-view__pull-indicator > span[data-v-e597453f] {
799
847
  padding-bottom: 6px;
800
848
  }
801
- .data-grid-view[data-v-d9e5439d] {
849
+ .data-grid-view[data-v-16f88920] {
802
850
  width: 100%;
803
851
  }
804
- .data-grid-view__grid[data-v-d9e5439d] {
852
+ .data-grid-view__grid[data-v-16f88920] {
805
853
  display: grid;
806
854
  gap: 12px;
807
855
  padding-bottom: 12px;
808
856
  }
809
- .data-grid-view__cell[data-v-d9e5439d] {
857
+ .data-grid-view__cell.is-active[data-v-16f88920] {
858
+ /* stylelint-disable-next-line block-no-empty */
859
+ }
860
+ .data-grid-view__cell[data-v-16f88920] {
810
861
  min-width: 0;
811
862
  }
812
- .data-grid-view__card[data-v-d9e5439d] {
863
+ .data-grid-view__card[data-v-16f88920] {
813
864
  border: 1px solid var(--dc-core-border-color, var(--el-border-color-lighter));
814
865
  border-radius: var(--dc-core-radius, 4px);
815
866
  padding: 10px 12px;
@@ -817,23 +868,23 @@ to {
817
868
  box-sizing: border-box;
818
869
  overflow: hidden;
819
870
  }
820
- .data-grid-view__field[data-v-d9e5439d] {
871
+ .data-grid-view__field[data-v-16f88920] {
821
872
  display: flex;
822
873
  gap: 6px;
823
874
  align-items: baseline;
824
875
  line-height: 1.6;
825
876
  font-size: 13px;
826
877
  }
827
- .data-grid-view__field-label[data-v-d9e5439d] {
878
+ .data-grid-view__field-label[data-v-16f88920] {
828
879
  flex-shrink: 0;
829
880
  color: var(--dc-core-text-color-secondary, var(--el-text-color-secondary));
830
881
  }
831
- .data-grid-view__field-value[data-v-d9e5439d] {
882
+ .data-grid-view__field-value[data-v-16f88920] {
832
883
  overflow: hidden;
833
884
  text-overflow: ellipsis;
834
885
  white-space: nowrap;
835
886
  }
836
- .data-grid-view__empty-error[data-v-d9e5439d] {
887
+ .data-grid-view__empty-error[data-v-16f88920] {
837
888
  display: flex;
838
889
  justify-content: center;
839
890
  align-items: center;
@@ -1191,13 +1242,16 @@ to {
1191
1242
  .dc-form-upload-video__thumb_empty[data-v-5eac98bc] {
1192
1243
  background: var(--el-fill-color-light);
1193
1244
  }
1194
- .data-list-view-item.is-selected[data-v-d9b5d5ca] {
1195
- /* 选中态钩子,具体视觉由消费方覆写 */
1245
+ .data-list-view-item.is-selected[data-v-c2a6c6be] {
1246
+ /* 选中(勾选)态钩子,具体视觉由消费方覆写 */
1247
+ }
1248
+ .data-list-view-item.is-active[data-v-c2a6c6be] {
1249
+ /* 当前项(active)态钩子,具体视觉由消费方覆写 */
1196
1250
  }
1197
- .data-list-view[data-v-ce0bb05b] {
1251
+ .data-list-view[data-v-467a86a8] {
1198
1252
  width: 100%;
1199
1253
  }
1200
- .data-list-view-empty[data-v-ce0bb05b] {
1254
+ .data-list-view-empty[data-v-467a86a8] {
1201
1255
  display: flex;
1202
1256
  justify-content: center;
1203
1257
  align-items: center;
@@ -1205,13 +1259,13 @@ to {
1205
1259
  min-height: 60px;
1206
1260
  grid-column: 1 / -1;
1207
1261
  }
1208
- .table-main-pagination[data-v-6a8d4637] {
1262
+ .table-main-pagination[data-v-ab2d031c] {
1209
1263
  margin-top: 10px;
1210
1264
  display: flex;
1211
1265
  justify-content: flex-end;
1212
1266
  }
1213
- .table-main_fill[data-v-6a8d4637] .el-table__body-wrapper {
1214
- min-height: var(--ed56f300);
1267
+ .table-main_fill[data-v-ab2d031c] .el-table__body-wrapper {
1268
+ min-height: var(--v4d2ea4ab);
1215
1269
  }
1216
1270
  .dc-slot-layout-flow-aside_separate[data-v-5c6ef74b] {
1217
1271
  position: relative;
@@ -1241,40 +1295,40 @@ to {
1241
1295
  .dc-slot-layout-flow-aside-main[data-v-5c6ef74b] {
1242
1296
  width: var(--v7e1d11ca);
1243
1297
  }
1244
- .list-layout[data-v-a47ec619] {
1245
- min-height: var(--v37fdcdf2);
1298
+ .list-layout[data-v-84c13797] {
1299
+ min-height: var(--d1ae0a92);
1246
1300
  }
1247
- .list-layout-header[data-v-a47ec619] {
1301
+ .list-layout-header[data-v-84c13797] {
1248
1302
  margin-bottom: 10px;
1249
1303
  }
1250
- .list-layout-operation[data-v-a47ec619] {
1304
+ .list-layout-operation[data-v-84c13797] {
1251
1305
  margin-bottom: 10px;
1252
1306
  }
1253
- .list-layout_search-sticky[data-v-a47ec619] .form-search {
1307
+ .list-layout_search-sticky[data-v-84c13797] .form-search {
1254
1308
  position: sticky;
1255
- top: var(--v5bcb0a52);
1309
+ top: var(--e3e22ffc);
1256
1310
  z-index: 10;
1257
1311
  background: var(--dc-core-surface, var(--el-bg-color));
1258
1312
  box-shadow: 0 1px 0 0 var(--dc-core-border-color, var(--el-border-color-lighter));
1259
1313
  }
1260
- .list-layout_toolbar-sticky[data-v-a47ec619] .dc-table-main__toolbar {
1314
+ .list-layout_toolbar-sticky[data-v-84c13797] .dc-table-main__toolbar {
1261
1315
  position: sticky;
1262
- top: var(--v489faec3);
1316
+ top: var(--v5ed78713);
1263
1317
  z-index: 9;
1264
1318
  padding-top: 10px;
1265
1319
  background: var(--dc-core-surface, var(--el-bg-color));
1266
1320
  box-shadow: 0 1px 0 0 var(--dc-core-border-color, var(--el-border-color-lighter));
1267
1321
  }
1268
- .list-layout_pager-sticky[data-v-a47ec619] .dc-table-main__pagination {
1322
+ .list-layout_pager-sticky[data-v-84c13797] .dc-table-main__pagination {
1269
1323
  position: sticky;
1270
- bottom: var(--v17e649c5);
1324
+ bottom: var(--v6ead0916);
1271
1325
  z-index: 9;
1272
1326
  margin-top: 0;
1273
1327
  padding-top: 10px;
1274
1328
  background: var(--dc-core-surface, var(--el-bg-color));
1275
1329
  box-shadow: 0 -1px 0 0 var(--dc-core-border-color, var(--el-border-color-lighter));
1276
1330
  }
1277
- .list-layout[data-v-a47ec619] .el-loading-mask {
1331
+ .list-layout[data-v-84c13797] .el-loading-mask {
1278
1332
  left: -10px;
1279
1333
  top: -10px;
1280
1334
  right: -10px;
@@ -1286,24 +1340,3 @@ to {
1286
1340
  gap: 8px;
1287
1341
  margin-top: 8px;
1288
1342
  }
1289
- .view-layout[data-v-c8a1cb2d] {
1290
- display: flex;
1291
- flex-direction: column;
1292
- gap: 8px;
1293
- }
1294
- .view-layout-toolbar[data-v-c8a1cb2d] {
1295
- display: flex;
1296
- justify-content: space-between;
1297
- align-items: center;
1298
- padding-bottom: 8px;
1299
- gap: 20px;
1300
- }
1301
- .view-layout-toolbar__left[data-v-c8a1cb2d],
1302
- .view-layout-toolbar__right[data-v-c8a1cb2d] {
1303
- display: flex;
1304
- align-items: center;
1305
- gap: 10px;
1306
- }
1307
- .view-layout-content[data-v-c8a1cb2d] {
1308
- overflow: auto;
1309
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@done-coding/admin-core",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "内部前端库",
5
5
  "private": false,
6
6
  "main": "es/index.mjs",
@@ -83,5 +83,5 @@
83
83
  "dependencies": {
84
84
  "@tanstack/vue-virtual": "^3.13.35"
85
85
  },
86
- "gitHead": "bb7ff7c309925c6b9738383a60e3a62a80ee99b4"
86
+ "gitHead": "57af30147986e57cbf53b07e1470448ddfc13661"
87
87
  }
@@ -8,7 +8,7 @@
8
8
  | 组件 | 一句话 | 核心 props | 文档 |
9
9
  | --- | --- | --- | --- |
10
10
  | `AppLayout` | 应用布局总控:bridge/appStore/userStore 三注入 + 5 内容锚 + 内置默认经典布局 shell | `bridge`(必填)`appStore`(必填)`userStore`(必填)`watermarkContent` `sidebarAboveHeader` | [README-AppLayout.md](./docs/README-AppLayout.md) |
11
- | `AppPage` | 页面主体容器:撑满视口 + 四向悬浮 fixed 插槽 | `fullViewport` `fullMode` `gap` `topBg`/`bottomBg`/`leftBg`/`rightBg` `nextViewportMaxHeight` | [README-AppPage.md](./docs/README-AppPage.md) |
11
+ | `AppPage` | 页面主体容器:撑满视口 + 四向悬浮 fixed 插槽 + 超宽屏内容限宽居中 | `fullViewport` `fullMode` `gap` `contentCentered`/`contentMaxWidth` `topBg`/`bottomBg`/`leftBg`/`rightBg` `nextViewportMaxHeight` | [README-AppPage.md](./docs/README-AppPage.md) |
12
12
  | `AppBody` | 内容主体:内置 RouterView + KeepAlive + 面包屑 affix + #footer 槽 | `menuFlatList` | [README-AppBody.md](./docs/README-AppBody.md) |
13
13
  | `AppHeader` | 顶栏积木:#left 品牌 + 路由模块菜单(按 path 查 bridge.badge 显徽标)+ #right 操作区 | `menus` `menuFlatList`(必填)`show`(未消费) | [README-AppHeader.md](./docs/README-AppHeader.md) |
14
14
  | `AppSidebar` | 侧边栏积木:折叠把手 + MenuTree 菜单(树形/分组双形态)+ #top/#bottom 高度自撑 | `show` `menus`(必填)`menuFlatList`(必填)`aboveHeader`;菜单形态读 bridge `APP_LAYOUT_SIDEBAR_CONFIG.menuGroupMode`(分组态无折叠把手) | [README-AppSidebar.md](./docs/README-AppSidebar.md) |
@@ -39,6 +39,13 @@
39
39
 
40
40
  9 组件 + `export * from "./types"` + `./viewport`(`CORE_VIEWPORT_DECOR_ZINDEX_BELOW`=800 弹窗下档 / `CORE_VIEWPORT_DECOR_ZINDEX_ABOVE`=10000 弹窗上档)+ `./app-page-geometry`(`computeAppPageReducedViewportHeight`/`computeAppPageReducedViewportWidth`、`computeAppPageContentInset`、`computeAppPageSlotStyles`、`computeAppPageSlotShimStyles` + `APP_PAGE_SLOT_Z_INDEX`=1)
41
41
 
42
+ ## 相邻族
43
+
44
+ - **`page-layout`**(同级新族):一整页级别的编排组件,准入标准 = **DOM 根节点是 `AppPage`**。
45
+ 列表 / 详情同页范式(`AppPageListDetailLayout` / `AppPageDetail`)已迁至该族,见
46
+ [`../page-layout/README.md`](../page-layout/README.md)。`AppPage` 本身留在本族——它是**被**
47
+ 那个族使用的页面原语。
48
+
42
49
  ## 关联
43
50
 
44
51
  - 布局插件自供 shell 契约:`AppShellProps` / `AppShellSlots`(types.ts)
@@ -1,6 +1,6 @@
1
1
  # AppPage(页面容器·撑满视口 + 四向悬浮插槽)
2
2
 
3
- > 页面主体容器:默认 slot 按视口高变量撑满(min-height/height 双模式),#top/#bottom/#left/#right 四向 fixed 悬浮插槽(shim 呼吸 + gap 间距)。
3
+ > 页面主体容器:默认 slot 按视口高变量撑满(min-height/height 双模式),#top/#bottom/#left/#right 四向 fixed 悬浮插槽(shim 呼吸 + gap 间距),超宽屏内容限宽居中。
4
4
  > 范式页:`apps/reference/src/pages/app-layout/page-slots/`(四向悬浮槽 + ChainReadout 高度链互动面板)
5
5
 
6
6
  ## 定位
@@ -9,6 +9,7 @@
9
9
  - **何时不用**:无需容器几何(纯组件局部)时直接用裸组件
10
10
  - 默认 slot 内容经 ModalShelf page 层包裹(弹层挂载点)
11
11
  - 四向悬浮插槽为 fixed 定位(z-index `APP_PAGE_SLOT_Z_INDEX`=1),top/bottom 通栏量高、left/right 量宽,尺寸几何由 `app-page-geometry` 纯函数计算
12
+ - `contentCentered` 开启后默认 slot 内容限宽居中(超宽屏适配,默认 1200px)——**四向悬浮槽与 `background` 仍通栏**(chrome / 底色通栏、只有内容列限宽,超宽下两侧留白露出页面底色)
12
13
 
13
14
  ## 快速上手(最小可用)
14
15
 
@@ -28,9 +29,18 @@
28
29
  </AppPage>
29
30
  ```
30
31
 
32
+ 超宽屏(2K / 4K)下内容限宽居中(默认 1200px,窄屏自动无感):
33
+
34
+ ```vue
35
+ <AppPage content-centered>
36
+ <FormMain ... />
37
+ </AppPage>
38
+ ```
39
+
31
40
  ## 能力边界 / 按需使用
32
41
 
33
42
  - **四向悬浮插槽(`#top`/`#bottom`/`#left`/`#right`):默认全关、常规页面不需要。** 仅当页面需要「悬浮于内容之上的固定补充区」(通栏操作条 / 侧边抽屉 / 浮动提示)才开对应槽——判据:内容必须 fixed 悬浮且不参与文档流(top/bottom 通栏量高 → padding 让位,left/right 量宽)。列表/表单/详情常规页一律不开。**`#left`/`#right` 内放列表时用 scope 的 `listLayoutRecommended`(见 API)拿窄栏推荐配置,[MUST NOT] 自行拍脑袋配窄栏列表形态。**
43
+ - **`contentCentered`(超宽屏限宽居中):默认关,按页面类型开。** 表单页 / 详情页 / 阅读型页开(超宽屏下内容整行铺满,label↔控件、行首↔行尾视线跨度过长);**列表 / 表格页 [MUST NOT] 开**——表格列本就挤,限宽后更挤(业界同解:GitHub 对 diff / 表格页保留全宽)。`contentMaxWidth` 仅在需要非 1200 时设。
34
44
  - `topBg`/`bottomBg`/`leftBg`/`rightBg`:仅悬浮槽需要着色时设,默认无背景即够用。
35
45
  - `*ObserveResize`:仅当槽内内容有 CSS 过渡折叠 / 异步撑开(不触发重渲染的尺寸变化)才开对应 RO;普通槽内容(v-if/v-show/静态)不开。
36
46
  - `fullMode`:默认 `"min-height"` 即够用;`"height"` 仅当页面内子级依赖硬高撑满(如顶级自撑 TabsMain)时选。
@@ -46,7 +56,9 @@
46
56
  | `fullViewport` | `boolean` | `false` | 撑满视口 |
47
57
  | `fullMode` | `"min-height" \| "height"` | `"min-height"` | 撑满方式 |
48
58
  | `nextViewportMaxHeight` | `string` | `var(--app-viewport-max-height)` | shim 内视口高变量覆写 |
49
- | `background` | `string` | 无 | 根盒背景 |
59
+ | `background` | `string` | 无 | 根盒背景(通栏,不受限宽影响) |
60
+ | `contentCentered` | `boolean` | `false` | 默认 slot 内容限宽居中(超宽屏适配);同步收窄对后代公布的内容几何(见下) |
61
+ | `contentMaxWidth` | `number` | `1200` | 限宽值(px),仅 `contentCentered` 开启时生效 |
50
62
  | `gap` | `number` | `8` | 悬浮槽与内容间隔 px |
51
63
  | `topBg` / `bottomBg` / `leftBg` / `rightBg` | `string` | 无 | 各槽 shim 背景 |
52
64
  | `topObserveResize` / `bottomObserveResize` / `leftObserveResize` / `rightObserveResize` | `boolean` | 均 `false` | 逐槽 WatchSize RO 观测(CSS 过渡折叠等非重渲染尺寸变化时开) |
@@ -100,6 +112,34 @@
100
112
 
101
113
  **类型**:`AppPageListLayoutRecommended`(`app-layout/types.ts` 导出,非泛型——与 ListLayout 泛型 props 无关的平铺子集)。
102
114
 
115
+ ### `contentCentered` / `contentMaxWidth`(超宽屏内容限宽居中)
116
+
117
+ **意图**:超宽屏(2K / 4K / 带鱼屏)下默认 slot 内容整行铺满会让视线跨度过长(表单 label↔控件、正文行首↔行尾),开启后内容列限宽居中、两侧留白露出页面底色。窄屏(可用宽 ≤ `contentMaxWidth`)留白为 0,**与关闭态逐字节一致**,无需断点判断。
118
+
119
+ **机制**:**不新增 DOM 节点**——在既有 `.app-page-shim` 的左右 padding 上各叠一份「超宽留白」`gutter`:
120
+
121
+ ```
122
+ gutter = max(0, (内容可用宽 − contentMaxWidth) / 2)
123
+ paddingLeft = 左槽让位量 + gutter
124
+ paddingRight = 右槽让位量 + gutter
125
+ ```
126
+
127
+ 「内容可用宽」= AppPage 盒宽扣四向槽让位(即 `computeAppPageReducedViewportWidth`)。因此**居中基准是「两个侧槽之间那段区域」而非整个窗口**——`#left`/`#right` 只开单侧时内容仍精确居中于剩余可用区,不会被槽压偏。
128
+
129
+ > 选型:本能力**刻意不套限宽容器**。`.app-page-shim` 上挂着 `data-dc-slot`(默认槽区域标记),`useContextDefaults` 读的是**直系父 DOM**——中间插一层容器会让 ListLayout 窄栏默认、ViewLayout/TabsMain `refine` 上下文默认静默失效。padding 方案与套容器行为等价,且零 DOM 变更、零失效风险。
130
+
131
+ **对后代公布的几何同步收窄**(开启后下列三者一致,下游零改动即对):
132
+
133
+ | 出口 | 开启后 |
134
+ | --- | --- |
135
+ | `.app-page-shim` 左右 padding | 各 `+ gutter`(内容盒宽 = `contentMaxWidth`) |
136
+ | `APP_PAGE_CONTENT_INSET` 的 `left`/`right` | 各 `+ gutter`(fixed / 悬浮元素贴真实内容列边,而非页面边) |
137
+ | `--dc-page-content-viewport-width` | `min(原值, contentMaxWidth)` |
138
+
139
+ 高度链(`SCOPE_VIEWPORT_MAX_HEIGHT` / `BODY_CONTENT_VIEWPORT_HEIGHT`)、四向悬浮槽几何、`background` 均**不受影响**——限宽是纯横向能力,chrome 与底色通栏。
140
+
141
+ **`contentMaxWidth` 只接受 `number`(px)**:几何链三处出口都要拿它参与数值运算(gutter 求差、`min` 比较),CSS 字符串(`80rem` / `50%`)无法参与,会让样式与公布几何脱钩。需要相对单位请自行换算成 px 传入。
142
+
103
143
  ### Expose
104
144
 
105
145
 
@@ -108,13 +148,19 @@
108
148
 
109
149
  - **悬浮槽尺寸变化来自 CSS 过渡/异步撑开 → 必须开对应 `observeResize`**(onUpdated 只读渲染后瞬时值,过渡动画读起点漏最终)
110
150
  - 四向槽 fixed 定位——内容滚动时悬浮不动,属设计语义,勿误当普通 inline 槽
151
+ - **运行时动态增减插槽(在槽模板上挂 `v-if`)不生效**——`hasTop/hasBottom/hasLeft/hasRight` 取 `!!slots.x`,`slots` 非响应式对象、computed 无依赖恒吃首次结果。插槽集 [MUST] 当静态结构写死;确需切换整套槽形态由调用方给 AppPage 换 `:key` 重挂载
111
152
  - 槽尺寸/inset 几何由 `app-page-geometry` 纯函数计算导出,[MUST NOT] 手写自造
112
153
  - `listLayoutRecommended` 开 `customView` 后 [MUST] 配套 ListLayout `#custom-view-item`,否则窄栏卡片渲染为空
154
+ - **列表 / 表格页开 `contentCentered`** —— 表格列本就挤,限宽后更挤;限宽只服务表单 / 详情 / 阅读型页
155
+ - `contentMaxWidth` 传 CSS 字符串(`"80rem"`)—— 只收 `number`(px),字符串会让样式与公布几何脱钩
156
+ - [MUST NOT] 在业务侧改写 `.app-page-shim` padding 自造限宽 —— 会与槽让位量打架且不同步 `APP_PAGE_CONTENT_INSET`;限宽一律走 `contentCentered`。子级确需破框全宽(full-bleed)时用负 margin 主动逃逸,属逃生舱
113
157
  - `listLayoutRecommended` 是 AppPage → ListLayout 的**定向耦合**(AppPage 持有 ListLayout prop 形状子集),属本能力设计使然;若 ListLayout 该子集发生 breaking,[MUST] 同步更新 `app-layout/types.ts` 与本 README
114
158
 
115
159
  ## 关联
116
160
 
161
+ - 超宽限宽留白:`use-content-box.ts`(`useAppPageContentBox` → `contentGutter`,core 内部组合函数不出桶)
117
162
  - 几何纯函数:`app-page-geometry.ts`(computeAppPageReducedViewportHeight/Width、computeAppPageContentInset、computeAppPageSlotStyles、computeAppPageSlotShimStyles + `APP_PAGE_SLOT_Z_INDEX`=1)
118
163
  - 高度链:AppBody provide 的 `BODY_CONTENT_VIEWPORT_HEIGHT` / `BODY_CONTENT_VIEWPORT_RECT` / `BODY_CONTENT_VIEWPORT_INSET` / `SCOPE_VIEWPORT_MAX_HEIGHT`
119
164
  - 范式页:`apps/reference/src/pages/app-layout/page-slots/`(含 ChainReadout 高度链互动面板)
165
+ - 超宽限宽范式页:`apps/reference/src/pages/app-layout/content-box/`(开关 / 限宽值 / 槽组合 + 公布几何实时读数)
120
166
  - 窄栏列表范式:`apps/reference/src/pages/app-layout/narrow-list/`(#left/#right 放 ListLayout + `listLayoutRecommended`,与裸用对照)
@@ -97,7 +97,8 @@
97
97
  | `toolbar` | `TableToolbarConfig<T,SQ,F>` | — | toolbar 配置(按钮 / 刷新 / 自动刷新 / 导出) |
98
98
  | `infiniteListProps` | `InfiniteListViewConfig` | — | InfiniteListView 配置透传(distance/endText/errorText/pullRefresh/estimateSize/overscan) |
99
99
 
100
- **v-model**:`isAutoRefresh`(boolean)、`refreshInterval`(number,秒)。
100
+ **v-model**:`isAutoRefresh`(boolean)、`refreshInterval`(number,秒)、`activeId`(`string | number | undefined`)/ `activeData`(行对象 | undefined)—— **「当前项」双 model**(= 用户点击/激活的那一条,[MUST NOT] 与勾选 selection 混同)。两者**同生同灭**:`activeId` 有值 ⇒ `activeData` 必有值且恒为**列表数据原样**;列表刷新后当前项不在新数据内 → 两者一并清空;外部写入不在列表内的 id → 忽略 + `console.warn`(不清空既有选中)。⚠️ `activeId` 的 `string` / `number` **两种都是一等公民**:出口值**保业务数据里的原始类型**(`rowKey` 为字段名时即 `row[rowKey]`,数字 id 出数字);外部用另一种类型写入(如 `"1"` 对数字 id)同样命中——比对时内部统一 `String()` 归一,且**保留调用方写入的原值**不回写覆盖。点击卡片即设为当前项,卡片容器挂 `.is-active` 钩子类(视觉由消费方覆写)。⚠️ 该能力**止步于本组件**,[MUST NOT] 下沉 `InfiniteListView`——它的「一项」是网格**整行**(多张卡片),粒度是可滚动单元而非业务记录。
101
+
101
102
 
102
103
  ### Emits
103
104
 
@@ -53,17 +53,24 @@
53
53
  | `selectable` | `boolean` | `false` | 启用选中能力 |
54
54
  | `getRenderCtxParams` | `() => Pick<TableColumnDefaultScope<T, F>, "injectInfo" \| "exposeInfo">`(必填) | 无 | 列 render 上下文(必填契约) |
55
55
 
56
+ ### v-model
57
+
58
+ | 名 | type | 语义 |
59
+ | --- | --- | --- |
60
+ | `activeId` | `string \| number \| undefined` | **「当前项」唯一键**(= 用户点击/激活的那一条;[MUST NOT] 与勾选 `selected` 混同)。点击任一项即写入。`string` / `number` 两种都是一等公民:出口值**保原始类型**(`rowKey` 为字段名时即 `row[rowKey]`,数字 id 出数字),外部用另一种类型写入也命中(比对内部 `String()` 归一,且保留调用方原值) |
61
+ | `activeData` | `T \| undefined` | 当前项对应的**列表数据原样**。与 `activeId` **同生同灭**:`activeId` 有值 ⇒ 本值必有值;`data` 刷新后当前项不在新数据内 → 两者一并清空;外部写入不在 `data` 内的 id → 忽略 + `console.warn`(不清空既有选中) |
62
+
56
63
  ### Emits
57
64
 
58
65
  | 事件 | 载荷 | 语义 |
59
66
  | --- | --- | --- |
60
- | `selectionChange` | `rows: T[]` | 选中项变更 |
67
+ | `selectionChange` | `rows: T[]` | 选中项变更(勾选语义,与 active 无关) |
61
68
 
62
69
  ### Slots
63
70
 
64
71
  | 槽 | scope | 语义 |
65
72
  | --- | --- | --- |
66
- | `#item` | `DataListViewItemScope<T>`(`fieldComponentMap` / `row` / `index` / `selected` / `toggleSelect`) | 单项渲染(customView 卡片内容) |
73
+ | `#item` | `DataListViewItemScope<T>`(`fieldComponentMap` / `row` / `index` / `selected` / `toggleSelect` / `active` / `setActive`) | 单项渲染(customView 卡片内容)。`active` = 是否当前项、`setActive()` = 设为当前项(等价点击该项) |
67
74
  | `#empty` | 无 | 空态(默认 `<el-empty />`,消费方可覆写) |
68
75
 
69
76
  ### Expose(`DataListViewInstance` = TableInstance 对齐)
@@ -77,7 +84,8 @@
77
84
  - **不对外导出**:本组件 [MUST NOT] 对外导出 / 全局注册 / 提供 install 插件;消费方 [MUST] 走 `TableMain` `customView` 模式
78
85
  - **无请求/分页**:`data` 须外部传入(当前页数据),勿期待组件内部取数
79
86
  - **刷新清选中**:`data` 引用变化即清空选中——跨页 / 刷新后选中状态不保留
80
- - **未实现方法抛错**:Expose 面与 TableInstance 对齐但 10 个方法为「not supported」桩(表格特有行为在自定义视图无意义)
87
+ - **未实现方法抛错**:Expose 面与 TableInstance 对齐但 10 个方法为「not supported」桩(表格特有行为在自定义视图无意义)——含 `setCurrentRow`,故 TableMain 在 `customView` 态 [MUST NOT] 调它同步高亮(active 由本组件自持)
88
+ - **`.is-active` / `.is-selected` 是空钩子类**:本族 [MUST NOT] 写死卡片视觉,选中 / 当前项的样式由消费方覆写
81
89
  - **列键依赖归一规则**:消费 `fieldComponentMap` 只用 `prop` / `$type_index` / `$type_selection` 三系键,`type: "expand"` 列不产出
82
90
 
83
91
  ## DataListViewItem(列表项容器)
@@ -88,7 +96,8 @@
88
96
 
89
97
  | name | type | 默认 | 语义 |
90
98
  | --- | --- | --- | --- |
91
- | `selected` | `boolean`(必填) | 无 | 是否选中(由 DataListView 传入) |
99
+ | `selected` | `boolean`(必填) | 无 | 是否选中(勾选语义,由 DataListView 传入)→ `.is-selected` |
100
+ | `active` | `boolean` | `false` | 是否为「当前项」(active,与勾选无关)→ `.is-active` |
92
101
 
93
102
  ### Emits / Expose
94
103
 
@@ -109,7 +109,7 @@ const { listPageConfig } = useSpaceConfig({
109
109
  | `headerObserveResize` / `operationObserveResize` | `boolean` | 均 `false` | header/操作槽量高 ResizeObserver |
110
110
  | `initialSearch` | `boolean` | `true` | 挂载后是否立即触发初始化查询(默认查);关闭后表格空态显示「欢迎触发查询」、KeepAlive 激活不自动刷新(TableMain 经「已发生过查询」自判,见 table docs) |
111
111
 
112
- **v-model**:`isAutoRefresh`、`refreshInterval`、`customView`(配合 `#custom-view-item` 卡片渲染)
112
+ **v-model**:`isAutoRefresh`、`refreshInterval`、`customView`(配合 `#custom-view-item` 卡片渲染)、`activeId`(`string | number | undefined`)/ `activeData`(行对象 | undefined)—— **「当前项」双 model**(= 用户点击/激活的那一条,[MUST NOT] 与勾选 selection 混同)。两者**同生同灭**:`activeId` 有值 ⇒ `activeData` 必有值且恒为**列表数据原样**;列表刷新后当前项不在新数据内 → 两者一并清空;外部写入不在列表内的 id → 忽略 + `console.warn`(不清空既有选中)。⚠️ `activeId` 的 `string` / `number` **两种都是一等公民**:出口值**保业务数据里的原始类型**(`rowKey` 为字段名时即 `row[rowKey]`,数字 id 出数字);外部用另一种类型写入(如 `"1"` 对数字 id)同样命中——比对时内部统一 `String()` 归一,且**保留调用方写入的原值**不回写覆盖。本组件是**纯透传层**,状态机在下游(table 态 → TableMain / DataListView;grid 态 → DataGridView);⚠️ `viewMode` 切换会换载下游组件 → 当前项随之丢失(既有换载语义)
113
113
 
114
114
  ### Emits
115
115
 
@@ -54,6 +54,8 @@
54
54
  | `cancelText` | `string \| (() => VNode)` | `"取消"` | 取消按钮文案 |
55
55
  | `confirmText` | `string \| (() => VNode)` | `"确定"` | 确认按钮文案 |
56
56
  | `hiddenCancel` | `boolean` | `false` | 隐藏取消按钮 |
57
+ | `hiddenFooter` | `boolean` | `false` | **整块隐藏 footer**(操作区容器一并不渲染)。用于「只要弹层容器、不要确认-取消」的场景(如列表详情范式的详情抽屉)。EP 的 dialog / drawer 都按 footer 插槽存在与否渲染容器,故这里是**真不渲染**;[MUST NOT] 改用「传空 `#footer` 插槽」绕——那会留一条带 padding 的空条(drawer 实测 56px) |
58
+ | `hiddenHeader` | `boolean` | `false` | **整块隐藏 header**(标题区 + 关闭按钮)。两形态实现不同(EP 使然):`drawer` 走 EP 原生 `with-header=false` → 节点真不渲染;`modal` 的 `el-dialog__header` **无条件渲染**,只能由 `dc-confirm-modal_hidden-header` 类 `display:none` 兜。⚠️ 隐藏后失去关闭按钮,[MUST] 自备关闭入口(遮罩 / ESC / 自绘) |
57
59
  | `width` | `number \| string` | `420` | modal = 弹窗宽度 / drawer = 抽屉尺寸(size) |
58
60
  | `reverse` | `boolean` | `false` | 反转按钮位置 |
59
61
  | `closeOnPressEscape` | `boolean` | `true` | ESC 关闭 |
@@ -0,0 +1,47 @@
1
+ # page-layout(页面布局族)
2
+
3
+ > **准入标准:组件的 DOM 根节点是 `AppPage`。** 想进本族的组件 [MUST] 满足它。
4
+ > 标准由 `__tests__/family-contract.test.ts` 守卫(逐个挂载断言根元素带 `.app-page`,
5
+ > 并校验族内每个 `.vue` 都在清单里,防漏登记绕过)。
6
+
7
+ ## 这个族是什么
8
+
9
+ 「**一整页**」级别的编排组件:使用方把它当路由页的根来用,页面的四向 chrome、内容视口几何、
10
+ 弹层挂载点等由它内含的 `AppPage` 统一提供。故本族组件与 `AppPage` 是**二选一**关系
11
+ ——[MUST NOT] 出现「业务写 `AppPage` 再套本族组件」的嵌套。
12
+
13
+ **`AppPage` 自己不在本族**:它是**被**本族使用的页面原语(留在 `app-layout`,与 `AppLayout` /
14
+ `AppBody` 同族)。本族是「用 AppPage 搭出来的成品页型」。
15
+
16
+ ## 组件清单
17
+
18
+ | 组件 | 对外 | 一句话 |
19
+ | --- | --- | --- |
20
+ | `AppPageListDetailLayout` | ✅ | 列表 / 详情同页范式(master-detail)门面:一根 `activeId` 状态线打通列表与详情,宽屏并排 / 窄屏叠加两形态按视口宽定档 |
21
+ | `AppPageDetail` | ✅ | 详情区。默认自带 `AppPage`、可**独立成详情页**;`embedded` 开启后走透传态供门面内嵌 |
22
+ | `AppPageListDetailSplit` | ✗ | 宽屏并排形态(门面实现细节) |
23
+ | `AppPageListDetailSheet` | ✗ | 窄屏叠加形态(抽屉 / 弹窗,门面实现细节) |
24
+
25
+ 两个形态件与 `use-list-detail.ts` [MUST NOT] 出桶——换承载体不算 breaking。
26
+
27
+ ## 标准的唯一例外
28
+
29
+ `AppPageDetail` 的 **`embedded=true`** 是**显式 opt-out**:宿主(Split / Sheet)已经有一层
30
+ `AppPage`,再套就成两层,故内嵌态走 `SlotLayoutTemplate` 透传、不产生宿主元素。
31
+ 守卫测试单独锁住了这条语义(开了就不该有 `.app-page`)。
32
+
33
+ ⚠️ 用 `SlotLayoutTemplate :disabled` 做透传是**有前提**的:它的透传态只渲染默认槽、
34
+ **具名槽无落点**。`AppPageDetail` 只用 `AppPage` 的默认槽,故成立;两个形态件要用
35
+ `#left` / `#right` 等具名槽,[MUST NOT] 照抄。
36
+
37
+ ## 文档
38
+
39
+ - [README-AppPageListDetailLayout.md](./docs/README-AppPageListDetailLayout.md)
40
+ - [README-AppPageDetail.md](./docs/README-AppPageDetail.md)
41
+
42
+ ## 关联
43
+
44
+ - 页面原语:`AppPage`(app-layout 族)
45
+ - 不变式内核:`hooks/use-active-record.ts`
46
+ - 上下文 key:`APP_PAGE_LIST_DETAIL_CONTEXT`
47
+ - spec:`docs/specs/2026-08-31-025041-moderate-列表详情同页范式AppPageListDetailLayout/spec.md`