소스 검색

提出为组件和筛选,增加add方法

qdy 1 개월 전
커밋
c729b1930f

+ 56
- 86
projects/base-core/src/lib/components/sticky-header/sticky-header.component.html 파일 보기

@@ -1,100 +1,70 @@
1
-<div class="sticky-header sticky top-0 z-10 transition-all duration-300 ease-in-out" [style.min-height]="headerHeight" [class.locked]="isLocked" [class.compact]="isCompact" [ngStyle]="isLocked ? {
2
-  width: headerWidth ? (typeof headerWidth === 'number' ? headerWidth + 'px' : headerWidth) : null,
3
-  left: headerLeft ? (typeof headerLeft === 'number' ? headerLeft + 'px' : headerLeft) : null
1
+<div class="sticky-header sticky top-0 z-10 transition-all duration-300 ease-in-out" [class.locked]="isLocked()" [class.compact]="isCompact()" [style.min-height.px]="currentHeight()" [ngStyle]="isLocked() ? {
2
+  width: headerWidth() ? (typeof headerWidth() === 'number' ? headerWidth() + 'px' : headerWidth()) : null,
3
+  left: headerLeft() ? (typeof headerLeft() === 'number' ? headerLeft() + 'px' : headerLeft()) : null
4 4
 } : null">
5 5
   <!-- 主标题区域 -->
6 6
   <div class="header-content bg-white rounded-lg border border-gray-200 shadow-sm transition-all duration-300">
7 7
     <div class="flex justify-between items-center p-4 transition-all duration-300">
8 8
       <h1 class="header-title text-2xl font-bold transition-all duration-300">{{ title }}</h1>
9
-       @if (buttons.length > 0) {
10
-         <!-- 多个按钮模式 -->
11
-         <div class="flex items-center gap-2">
12
-           @for (button of buttons; track button.name) {
13
-             <button 
14
-               mat-raised-button 
15
-               [color]="button.color || 'primary'"
16
-               (click)="onButtonClick(button.name)" 
17
-               [disabled]="button.disabled || button.loading || disabled"
18
-               class="flex items-center gap-2"
19
-             >
20
-               <div class="flex items-center gap-2">
21
-                 <mat-icon *ngIf="!button.loading">{{ button.icon || 'add' }}</mat-icon>
22
-                 <mat-progress-spinner 
23
-                   *ngIf="button.loading" 
24
-                   diameter="20" 
25
-                   mode="indeterminate" 
26
-                   class="inline-block"
27
-                   [color]="button.color || 'primary'"
28
-                 ></mat-progress-spinner>
29
-                 <span>{{ button.loading ? '处理中...' : button.title }}</span>
30
-               </div>
31
-             </button>
32
-           }
33
-         </div>
34
-       } @else {
35
-         <!-- 单个按钮模式(向后兼容) -->
36
-         <button 
37
-           mat-raised-button 
38
-           [color]="buttonColor" 
39
-           (click)="onButtonClick()" 
40
-           [disabled]="disabled || loading"
41
-           class="flex items-center gap-2"
42
-         >
43
-            <div class="flex items-center gap-2">
44
-              <mat-icon *ngIf="!loading">{{ buttonIcon }}</mat-icon>
45
-              <mat-progress-spinner 
46
-                *ngIf="loading" 
47
-                diameter="20" 
48
-                mode="indeterminate" 
49
-                class="inline-block"
50
-                [color]="buttonColor"
51
-              ></mat-progress-spinner>
52
-              <span>{{ loading ? '处理中...' : buttonText }}</span>
53
-            </div>
54
-         </button>
55
-       }
9
+      
10
+      <!-- 按钮区域 -->
11
+      @if (buttons.length > 0) {
12
+        <div class="flex items-center gap-2">
13
+          @for (button of buttons; track button.name) {
14
+            <button 
15
+              mat-raised-button 
16
+              [color]="button.color || 'primary'"
17
+              (click)="onButtonClick(button.name)" 
18
+              [disabled]="button.disabled || button.loading"
19
+              class="flex items-center gap-2"
20
+            >
21
+              <div class="flex items-center gap-2">
22
+                @if (button.loading) {
23
+                  <mat-progress-spinner 
24
+                    diameter="20" 
25
+                    mode="indeterminate" 
26
+                    class="inline-block"
27
+                    [color]="button.color || 'primary'"
28
+                  ></mat-progress-spinner>
29
+                } @else {
30
+                  <mat-icon>{{ button.icon || 'add' }}</mat-icon>
31
+                }
32
+                <span>{{ button.loading ? '处理中...' : button.title }}</span>
33
+              </div>
34
+            </button>
35
+          }
36
+        </div>
37
+      }
56 38
     </div>
57 39
     
58
-    <!-- 提示信息 -->
59
-    @if (hintText) {
60
-      <div class="hint-section border-t border-gray-100 p-3 bg-blue-50/30 transition-all duration-300 overflow-hidden">
61
-        <p class="text-sm text-gray-600 m-0 transition-all duration-300">{{ hintText }}</p>
62
-      </div>
63
-    }
64
-    
65
-    <!-- 调试信息区域 -->
66
-    @if (showDebugInfo && (debugDataSource || debugRegisterUrl || debugListUrl)) {
67
-      <div class="debug-section border-t border-gray-100 overflow-hidden">
68
-        <!-- 数据源信息 -->
69
-        @if (debugDataSource) {
70
-          <div class="debug-info text-xs text-gray-500 p-2 bg-gray-50 transition-all duration-300">
71
-            <div class="flex gap-4 flex-wrap transition-all duration-300">
72
-              <span>数据源: <span class="font-mono">{{ debugDataSource }}</span></span>
73
-              <span>记录条数: <span class="font-mono">{{ debugRecordCount }}</span></span>
74
-              <span>更新时间: <span class="font-mono">{{ debugLastUpdated }}</span></span>
75
-              @if (debugUseMockData) {
76
-                <span class="text-amber-600 font-medium">⚠ 使用模拟数据</span>
77
-              } @else {
78
-                <span class="text-green-600 font-medium">✓ 使用API数据</span>
79
-              }
80
-            </div>
40
+    <!-- 动态区域 -->
41
+    @for (section of dynamicSections(); track section.id; let i = $index) {
42
+      @if (i > 0) {
43
+        <div class="section-divider border-t border-gray-100"></div>
44
+      }
45
+      @switch (section.style) {
46
+        @case ('hint') {
47
+          <div class="hint-section border-t border-gray-100 p-3 bg-blue-50/30 transition-all duration-300 overflow-hidden" [class]="section.customClass">
48
+            <p class="text-sm text-gray-600 m-0 transition-all duration-300" [innerHTML]="getRenderedContent(section)"></p>
49
+          </div>
50
+        }
51
+        @case ('debug-info') {
52
+          <div class="debug-info text-xs text-gray-500 p-2 bg-gray-50 transition-all duration-300" [class]="section.customClass">
53
+            <div class="flex gap-4 flex-wrap transition-all duration-300" [innerHTML]="getRenderedContent(section)"></div>
54
+          </div>
55
+        }
56
+        @case ('debug-urls') {
57
+          <div class="debug-urls text-xs text-gray-500 p-2 bg-gray-50 border-t border-gray-100 transition-all duration-300" [class]="section.customClass">
58
+            <div class="flex flex-col gap-1 transition-all duration-300" [innerHTML]="getRenderedContent(section)"></div>
81 59
           </div>
82 60
         }
83
-        
84
-        <!-- API URL信息 -->
85
-        @if (debugRegisterUrl || debugListUrl) {
86
-          <div class="debug-urls text-xs text-gray-500 p-2 bg-gray-50 border-t border-gray-100 transition-all duration-300">
87
-            <div class="flex flex-col gap-1 transition-all duration-300">
88
-              @if (debugRegisterUrl) {
89
-                <span>注册API: <span class="font-mono">{{ debugRegisterUrl || '未调用' }}</span></span>
90
-              }
91
-              @if (debugListUrl) {
92
-                <span>列表API: <span class="font-mono">{{ debugListUrl || '未调用' }}</span></span>
93
-              }
94
-            </div>
61
+        @default {
62
+          <div class="dynamic-section border-t border-gray-100 p-3 transition-all duration-300" [class]="section.customClass">
63
+            <div class="text-sm text-gray-600" [innerHTML]="getRenderedContent(section)"></div>
95 64
           </div>
96 65
         }
97
-      </div>
66
+      }
98 67
     }
68
+
99 69
   </div>
100 70
 </div>

+ 55
- 19
projects/base-core/src/lib/components/sticky-header/sticky-header.component.scss 파일 보기

@@ -97,17 +97,13 @@
97 97
       }
98 98
     }
99 99
     
100
-    .debug-section {
100
+    .debug-info, .debug-urls {
101 101
       max-height: 0;
102 102
       opacity: 0;
103
-      
104
-      .debug-info, .debug-urls {
105
-        padding-top: 0;
106
-        padding-bottom: 0;
107
-        font-size: 0.6875rem;
108
-        opacity: 0;
109
-        border: none;
110
-      }
103
+      padding-top: 0;
104
+      padding-bottom: 0;
105
+      font-size: 0.6875rem;
106
+      border: none;
111 107
     }
112 108
   }
113 109
 
@@ -119,14 +115,14 @@
119 115
       background: #f8fafc; /* 恢复默认背景色,与普通状态一致 */
120 116
       border: 1px solid #e2e8f0;
121 117
       border-top: none; /* 移除顶部边框,实现无缝衔接 */
122
-      height: auto; /* 改为自适应高度 */
123
-      min-height: auto; /* 移除最小高度限制 */
124
-      flex: none; /* 取消flex扩展 */
118
+      height: 100%; /* 填充父容器高度 */
119
+      min-height: 100%; /* 确保填充最小高度 */
120
+      flex: 1; /* 填充可用空间 */
125 121
       
126 122
       > div:first-child {
127 123
         padding-top: 0; /* 移除上下空白 */
128 124
         padding-bottom: 0;
129
-        min-height: 40px; /* 设置为单行高度 */
125
+        min-height: 100%; /* 填充父容器高度 */
130 126
         display: flex;
131 127
         align-items: center; /* 垂直居中 */
132 128
       }
@@ -177,18 +173,58 @@
177 173
       }
178 174
     }
179 175
     
180
-    .debug-section {
176
+    .debug-info, .debug-urls {
181 177
       max-height: 0;
182 178
       opacity: 0;
179
+      padding-top: 0;
180
+      padding-bottom: 0;
181
+      font-size: 0.6875rem; /* 比text-xs更小 */
182
+      border: none;
183
+    }
184
+  }
185
+}
186
+
187
+// 动态区域基础样式
188
+.sticky-header {
189
+  .dynamic-section {
190
+    transition: all 0.3s ease-in-out;
191
+    border-top: 1px solid #e2e8f0;
192
+    padding: 0.75rem;
193
+    
194
+    .text-sm {
195
+      font-size: 0.875rem;
196
+      line-height: 1.25rem;
197
+      color: #4b5563;
198
+    }
199
+  }
200
+  
201
+  // 分隔线样式
202
+  .section-divider {
203
+    border-top: 1px solid #e2e8f0;
204
+    margin: 0;
205
+    transition: all 0.3s ease-in-out;
206
+  }
207
+  
208
+  // 锁定状态下隐藏动态区域和分隔线
209
+  &.locked, &.compact {
210
+    .dynamic-section {
211
+      max-height: 0;
212
+      opacity: 0;
213
+      padding-top: 0;
214
+      padding-bottom: 0;
215
+      border: none;
183 216
       
184
-      .debug-info, .debug-urls {
185
-        padding-top: 0;
186
-        padding-bottom: 0;
187
-        font-size: 0.6875rem; /* 比text-xs更小 */
217
+      .text-sm {
218
+        font-size: 0.8125rem;
188 219
         opacity: 0;
189
-        border: none;
190 220
       }
191 221
     }
222
+    
223
+    .section-divider {
224
+      max-height: 0;
225
+      opacity: 0;
226
+      border: none;
227
+    }
192 228
   }
193 229
 }
194 230
 

+ 418
- 314
projects/base-core/src/lib/components/sticky-header/sticky-header.component.ts 파일 보기

@@ -1,18 +1,25 @@
1
-import { Component, Input, Output, EventEmitter, AfterViewInit, OnDestroy, Renderer2, Inject, ElementRef } from '@angular/core';
1
+import { Component, Input, Output, EventEmitter, AfterViewInit, OnDestroy, Renderer2, ElementRef, signal, computed, effect } from '@angular/core';
2 2
 import { CommonModule } from '@angular/common';
3 3
 import { MatButtonModule } from '@angular/material/button';
4 4
 import { MatIconModule } from '@angular/material/icon';
5 5
 import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
6 6
 
7 7
 export interface StickyHeaderButton {
8
-  title: string;    // 显示文本
9
-  name: string;     // 标识符(事件返回)
10
-  icon?: string;    // 可选图标
8
+  title: string;
9
+  name: string;
10
+  icon?: string;
11 11
   color?: 'primary' | 'accent' | 'warn';
12 12
   disabled?: boolean;
13 13
   loading?: boolean;
14 14
 }
15 15
 
16
+export interface DynamicSection {
17
+  id: number;
18
+  content: string;
19
+  style?: 'hint' | 'debug-info' | 'debug-urls' | 'custom';
20
+  customClass?: string;
21
+}
22
+
16 23
 @Component({
17 24
   selector: 'app-sticky-header',
18 25
   standalone: true,
@@ -25,36 +32,31 @@ export class StickyHeaderComponent implements AfterViewInit, OnDestroy {
25 32
     private renderer: Renderer2,
26 33
     private elementRef: ElementRef
27 34
   ) {}
28
-  
35
+
29 36
   // 滚动优化相关属性
30 37
   private scrollRafId: number | null = null;
31 38
   private lastScrollTop = 0;
32
-  private lastIsLocked = false;
33
-  private lastIsCompact = false;
34 39
   
40
+  // 尺寸缓存
41
+  private initialHeight = 0;
42
+  private initialTop = 0;
43
+  private maxHeight = 0;          // header最大高度
44
+  private distanceFromTop = 0;    // header距离文档顶部距离
45
+  private minHeight = 40; // 最小高度(与CSS中compact状态一致)
46
+  private resizeObserver: ResizeObserver | null = null;
47
+  private currentScrollContainer: HTMLElement | null = null;
48
+  
49
+  // 滚动检测和状态缓存
50
+  private scrollListener: (() => void) | null = null;
51
+  private isAutoDetecting = false;
52
+  private lastIsLocked = false;   // 状态变化检测
53
+  private lastIsCompact = false;
54
+
35 55
   /** 标题文本 */
36 56
   @Input() title = '';
37
-  
38
-  /** 按钮文本 */
39
-  @Input() buttonText = '操作';
40
-  
41
-  /** 按钮图标 */
42
-  @Input() buttonIcon = 'add';
43
-  
44
-  /** 按钮颜色,默认primary */
45
-  @Input() buttonColor: 'primary' | 'accent' | 'warn' = 'primary';
46
-  
47
-  /** 多个按钮配置,如果设置则忽略单个按钮属性 */
57
+
58
+  /** 按钮配置数组 */
48 59
   @Input() buttons: StickyHeaderButton[] = [];
49
-  
50
-  /** 是否禁用按钮 */
51
-  @Input() disabled = false;
52
-  
53
-  /** 是否显示加载状态 */
54
-  @Input() loading = false;
55
-  
56
-  /** 滚动阈值(像素),默认为5 */
57
-  @Input() scrollThreshold = 5;
58 60
 
59 61
   /** 是否启用自动滚动检测 */
60 62
   @Input() autoDetect = false;
@@ -62,77 +64,78 @@ export class StickyHeaderComponent implements AfterViewInit, OnDestroy {
62 64
   /** 滚动容器选择器,默认为'.content-area',设为'window'监听窗口滚动 */
63 65
   @Input() scrollContainer = '.content-area';
64 66
 
65
-  /** 紧凑模式阈值(像素),默认为50 */
66
-  @Input() compactThreshold = 50;
67
-
68
-  /** 是否显示调试信息框 */
69
-  @Input() showDebugInfo = false;
70
-  
71
-  /** 调试信息数据源 */
72
-  @Input() debugDataSource = '';
73
-  
74
-  /** 调试信息记录数 */
75
-  @Input() debugRecordCount = 0;
76
-  
77
-  /** 调试信息更新时间 */
78
-  @Input() debugLastUpdated = '';
79
-  
80
-  /** 调试信息是否使用模拟数据 */
81
-  @Input() debugUseMockData = false;
82
-  
83
-  /** 调试信息注册API URL */
84
-  @Input() debugRegisterUrl = '';
85
-  
86
-  /** 调试信息列表API URL */
87
-  @Input() debugListUrl = '';
88
-
89
-  /** 提示文本,显示在标题下方 */
90
-  @Input() hintText = '';
91
-
92
-  /** 是否已滚动(由父组件控制,向后兼容) */
93
-  @Input() isScrolled = false;
67
+  /** 宽度参考目标选择器(用于锁定状态宽度匹配),如未设置则自动检测 */
68
+  @Input() widthTarget: string | null = null;
94 69
 
95
-  /** 是否锁定在顶部(到达滚动容器顶部) */
96
-  @Input() isLocked = false;
70
+  /** 按钮点击事件 */
71
+  @Output() buttonAction = new EventEmitter<string>();
97 72
 
98
-  /** 是否缩小状态(锁定后进一步缩小) */
99
-  @Input() isCompact = false;
73
+  // 信号状态
74
+  isScrolled = signal(false);
75
+  isLocked = signal(false);
76
+  isCompact = signal(false);
77
+  currentHeight = signal<number | null>(null);  // 动态高度
78
+  headerWidth = signal<string | number | null>(null);
79
+  headerLeft = signal<string | number | null>(null);
80
+  dynamicSections = signal<DynamicSection[]>([]);
100 81
 
101
-  /** 锁定时的宽度(像素或百分比) */
102
-  @Input() headerWidth: string | number | null = null;
82
+  // 计算属性:当前压缩比例和高度
83
+  compressionRatio = computed(() => {
84
+    if (!this.isLocked()) return 0;
85
+    const height = this.currentHeight();
86
+    if (height === null || this.maxHeight <= this.minHeight) return 0;
87
+    
88
+    // 计算压缩比例:1 - (当前高度 - 最小高度) / (最大高度 - 最小高度)
89
+    const ratio = 1 - (height - this.minHeight) / (this.maxHeight - this.minHeight);
90
+    return Math.max(0, Math.min(1, ratio)); // 限制在0-1之间
91
+  });
103 92
 
104
-  /** 锁定时的左侧偏移(像素) */
105
-  @Input() headerLeft: string | number | null = null;
93
+  // 添加动态区域的方法
94
+  add(index: number, content: string, style?: string, customClass?: string): void {
95
+    const sections = this.dynamicSections();
96
+    const existingIndex = sections.findIndex(s => s.id === index);
97
+    
98
+    const newSection: DynamicSection = {
99
+      id: index,
100
+      content,
101
+      style: style as any || 'hint',
102
+      customClass
103
+    };
106 104
 
107
-  /** 宽度参考目标选择器(用于锁定状态宽度匹配),如未设置则使用滚动容器 */
108
-  @Input() widthTarget: string | null = null;
105
+    if (existingIndex >= 0) {
106
+      const updated = [...sections];
107
+      updated[existingIndex] = newSection;
108
+      this.dynamicSections.set(updated);
109
+    } else {
110
+      this.dynamicSections.set([...sections, newSection].sort((a, b) => a.id - b.id));
111
+    }
112
+  }
109 113
 
110
-  /** 标题区域最小高度(用于测试或固定高度布局),如 '200px', '10rem',未设置则由内容决定 */
111
-  @Input() headerHeight: string | null = null;
114
+  // 移除动态区域
115
+  remove(index: number): void {
116
+    this.dynamicSections.set(this.dynamicSections().filter(s => s.id !== index));
117
+  }
112 118
 
113
-  /** 按钮点击事件 */
114
-  @Output() buttonClick = new EventEmitter<void>();
115
-  
116
-  /** 多个按钮点击事件,返回按钮name标识符 */
117
-  @Output() buttonAction = new EventEmitter<string>();
119
+  // 清除所有动态区域
120
+  clear(): void {
121
+    this.dynamicSections.set([]);
122
+  }
118 123
 
119
-  // 自动滚动检测的私有属性
120
-  private scrollListener: (() => void) | null = null;
121
-  private canScroll = false;
122
-  private isAutoDetecting = false;
124
+  // 获取渲染内容(支持变量绑定)
125
+  getRenderedContent(section: DynamicSection): string {
126
+    const content = section.content;
127
+    // 简单变量绑定:{propertyName} 替换为组件属性值
128
+    return content.replace(/\{(\w+)\}/g, (match, key) => {
129
+      // 从组件实例中获取值
130
+      const value = (this as any)[key];
131
+      return value !== undefined ? String(value) : match;
132
+    });
133
+  }
123 134
 
124
-  onButtonClick(name?: string) {
125
-    if (this.buttons.length > 0) {
126
-      // 多个按钮模式
127
-      const button = this.buttons.find(b => b.name === name);
128
-      if (button && !button.disabled && !button.loading) {
129
-        this.buttonAction.emit(name);
130
-      }
131
-    } else {
132
-      // 单个按钮模式(向后兼容)
133
-      if (!this.disabled && !this.loading) {
134
-        this.buttonClick.emit();
135
-      }
135
+  onButtonClick(name: string) {
136
+    const button = this.buttons.find(b => b.name === name);
137
+    if (button && !button.disabled && !button.loading) {
138
+      this.buttonAction.emit(name);
136 139
     }
137 140
   }
138 141
 
@@ -142,70 +145,53 @@ export class StickyHeaderComponent implements AfterViewInit, OnDestroy {
142 145
     }
143 146
   }
144 147
 
145
-   ngOnDestroy() {
148
+  ngOnDestroy() {
146 149
     this.cleanupScrollListener();
147 150
     if (this.scrollRafId) {
148 151
       cancelAnimationFrame(this.scrollRafId);
149 152
       this.scrollRafId = null;
150 153
     }
154
+    if (this.resizeObserver) {
155
+      this.resizeObserver.disconnect();
156
+      this.resizeObserver = null;
157
+    }
151 158
   }
152 159
 
153
-   private setupAutoScrollDetection() {
160
+  private setupAutoScrollDetection() {
154 161
     if (this.isAutoDetecting) return;
155 162
     
156 163
     this.isAutoDetecting = true;
164
+    
157 165
     const container = this.getScrollContainer();
158 166
     const isWindowScroll = this.scrollContainer === 'window';
167
+    this.currentScrollContainer = isWindowScroll ? null : container;
168
+    
169
+    // 初始化尺寸缓存(根据滚动类型传递正确的容器)
170
+    this.cacheInitialDimensions(this.currentScrollContainer || undefined);
171
+    this.setupResizeObserver();
159 172
     
160 173
     if (isWindowScroll) {
161
-      // 窗口滚动
162
-      this.canScroll = this.checkWindowScrollable();
163
-      const scrollHeight = document.body.scrollHeight || document.documentElement.scrollHeight || 0;
164
-      const clientHeight = window.innerHeight || document.documentElement.clientHeight || 0;
174
+      console.log(`sticky-header: 启用窗口滚动检测`);
165 175
       
166
-      console.log(`sticky-header: 窗口滚动性检查 - scrollHeight: ${scrollHeight}, clientHeight: ${clientHeight}, canScroll: ${this.canScroll}`);
176
+      this.scrollListener = this.renderer.listen(
177
+        'window',
178
+        'scroll',
179
+        this.handleWindowScroll.bind(this)
180
+      );
167 181
       
168
-      if (this.canScroll) {
169
-        this.scrollListener = this.renderer.listen(
170
-          'window',
171
-          'scroll',
172
-          this.handleWindowScroll.bind(this)
173
-        );
174
-        console.log('sticky-header: 自动滚动检测已启用,窗口滚动');
175
-        
176
-        // 初始检查窗口滚动位置
177
-        this.checkInitialWindowScrollPosition();
178
-      } else {
179
-        console.log('sticky-header: 窗口内容不足,禁用自动检测,设置isLocked=false, isCompact=false');
180
-        this.isLocked = false;
181
-        this.isCompact = false;
182
-        this.isScrolled = false;
183
-      }
182
+      // 初始检查窗口滚动位置
183
+      this.checkInitialWindowScrollPosition();
184 184
     } else if (container) {
185
-      // 容器滚动
186
-      this.canScroll = this.checkScrollable(container);
187
-      const scrollHeight = container.scrollHeight || 0;
188
-      const clientHeight = container.clientHeight || 0;
185
+      console.log(`sticky-header: 启用容器滚动检测,容器: ${this.scrollContainer}`);
189 186
       
190
-      console.log(`sticky-header: 容器滚动性检查 - scrollHeight: ${scrollHeight}, clientHeight: ${clientHeight}, canScroll: ${this.canScroll}, selector: ${this.scrollContainer}`);
187
+      this.scrollListener = this.renderer.listen(
188
+        container,
189
+        'scroll',
190
+        this.handleScroll.bind(this)
191
+      );
191 192
       
192
-      if (this.canScroll) {
193
-        this.scrollListener = this.renderer.listen(
194
-          container,
195
-          'scroll',
196
-          this.handleScroll.bind(this)
197
-        );
198
-        console.log('sticky-header: 自动滚动检测已启用,容器:', this.scrollContainer);
199
-        
200
-        // 初始检查滚动位置
201
-        this.checkInitialScrollPosition(container);
202
-      } else {
203
-        console.log('sticky-header: 滚动容器内容不足,禁用自动检测,设置isLocked=false, isCompact=false');
204
-        // 确保状态为false
205
-        this.isLocked = false;
206
-        this.isCompact = false;
207
-        this.isScrolled = false;
208
-      }
193
+      // 初始检查滚动位置
194
+      this.checkInitialScrollPosition(container);
209 195
     } else {
210 196
       console.warn('sticky-header: 未找到滚动容器:', this.scrollContainer);
211 197
     }
@@ -213,7 +199,7 @@ export class StickyHeaderComponent implements AfterViewInit, OnDestroy {
213 199
 
214 200
   private getScrollContainer(): HTMLElement | null {
215 201
     if (this.scrollContainer === 'window') {
216
-      return null; // 表示窗口滚动
202
+      return null;
217 203
     }
218 204
     
219 205
     // 从当前元素向上查找最近的匹配容器
@@ -226,95 +212,124 @@ export class StickyHeaderComponent implements AfterViewInit, OnDestroy {
226 212
     return document.querySelector(this.scrollContainer) as HTMLElement;
227 213
   }
228 214
 
229
-  private checkScrollable(container: HTMLElement): boolean {
230
-    if (!container) return false;
215
+  /**
216
+   * 自动获取宽度参考目标元素
217
+   * 优先级:1. widthTarget设置 2. 滚动容器 3. 自动查找合适父级容器
218
+   */
219
+  private getAutoWidthTarget(): HTMLElement | null {
220
+    // 情况1: widthTarget已设置,使用指定元素
221
+    if (this.widthTarget) {
222
+      const target = document.querySelector(this.widthTarget) as HTMLElement;
223
+      if (target) {
224
+        console.log(`sticky-header: 使用指定宽度目标: ${this.widthTarget}`);
225
+        return target;
226
+      }
227
+      console.warn(`sticky-header: 未找到宽度目标元素: ${this.widthTarget}`);
228
+    }
231 229
     
232
-    const scrollHeight = container.scrollHeight || 0;
233
-    const clientHeight = container.clientHeight || 0;
230
+    // 情况2: 容器滚动模式,使用滚动容器
231
+    if (this.scrollContainer !== 'window') {
232
+      const container = this.getScrollContainer();
233
+      if (container) {
234
+        console.log(`sticky-header: 自动使用滚动容器作为宽度目标`);
235
+        return container;
236
+      }
237
+    }
234 238
     
235
-    // 如果内容高度大于可视区域高度,说明可以滚动
236
-    // 添加5px容差,避免因计算误差导致的误判
237
-    const canScroll = scrollHeight > clientHeight + 5;
238
-    return canScroll;
239
+    // 情况3: 窗口滚动模式,自动查找合适的父级容器
240
+    return this.findSuitableParentContainer();
239 241
   }
240 242
 
241
-   private checkWindowScrollable(): boolean {
242
-    const scrollHeight = document.body.scrollHeight || document.documentElement.scrollHeight || 0;
243
-    const clientHeight = window.innerHeight || document.documentElement.clientHeight || 0;
243
+  /**
244
+   * 查找合适的父级容器(用于窗口滚动模式)
245
+   */
246
+  private findSuitableParentContainer(): HTMLElement | null {
247
+    // 算法1: 查找最近的滚动容器祖先
248
+    const scrollContainer = this.findClosestScrollableAncestor();
249
+    if (scrollContainer) return scrollContainer;
244 250
     
245
-    const canScroll = scrollHeight > clientHeight + 5;
246
-    return canScroll;
247
-  }
248
-
249
-  private findContentContainer(): HTMLElement | null {
250
-    // 尝试查找内容区域容器,优先级:
251
-    // 1. .content-area 类 (我们在app.component.html中添加的)
252
-    // 2. 包含router-outlet的容器
253
-    // 3. 右侧内容区域
254
-    const selectors = [
255
-      '.content-area',
256
-      '.flex-1.overflow-auto',
251
+    // 算法2: 查找常见的内容区域选择器
252
+    const commonSelectors = [
253
+      '.content-area',        // ng-configure应用结构
254
+      '.flex-1.overflow-auto', // 常见flex布局
257 255
       'div[class*="content"]',
258 256
       'div[class*="main"]',
259 257
       'div[class*="container"]'
260 258
     ];
261 259
     
262
-    for (const selector of selectors) {
263
-      const container = document.querySelector(selector) as HTMLElement;
264
-      if (container) {
265
-        console.log(`sticky-header: 找到内容容器: ${selector}, 位置: ${container.getBoundingClientRect().left}px, 宽度: ${container.getBoundingClientRect().width}px`);
266
-        return container;
260
+    for (const selector of commonSelectors) {
261
+      const element = document.querySelector(selector) as HTMLElement;
262
+      if (element && this.elementRef.nativeElement.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_CONTAINS) {
263
+        console.log(`sticky-header: 找到内容容器: ${selector}`);
264
+        return element;
267 265
       }
268 266
     }
269 267
     
270
-    console.warn('sticky-header: 未找到内容容器');
271
-    return null;
268
+    // 算法3: 回退到视口
269
+    console.log('sticky-header: 未找到合适容器,使用视口宽度');
270
+    return null; // null表示使用视口
272 271
   }
273 272
 
274
-  private getWidthTargetElement(): HTMLElement | null {
275
-    // 如果设置了widthTarget,优先使用它
276
-    if (this.widthTarget) {
277
-      const target = document.querySelector(this.widthTarget) as HTMLElement;
278
-      if (target) {
279
-        console.log(`sticky-header: 找到宽度目标元素: ${this.widthTarget}, 宽度: ${target.getBoundingClientRect().width}px`);
280
-        return target;
273
+  /**
274
+   * 查找最近的可滚动祖先容器
275
+   */
276
+  private findClosestScrollableAncestor(): HTMLElement | null {
277
+    let element = this.elementRef.nativeElement.parentElement;
278
+    
279
+    while (element && element !== document.body) {
280
+      const style = window.getComputedStyle(element);
281
+      const overflow = style.overflow + style.overflowY + style.overflowX;
282
+      
283
+      if (overflow.includes('auto') || overflow.includes('scroll')) {
284
+        console.log(`sticky-header: 找到可滚动祖先容器`);
285
+        return element;
281 286
       }
282
-      console.warn(`sticky-header: 未找到宽度目标元素: ${this.widthTarget}`);
287
+      
288
+      element = element.parentElement;
283 289
     }
284 290
     
285
-    // 否则根据滚动容器类型返回相应元素
286
-    if (this.scrollContainer === 'window') {
287
-      return this.findContentContainer();
288
-    } else {
289
-      const container = this.getScrollContainer();
290
-      return container;
291
-    }
291
+    return null;
292 292
   }
293 293
 
294
-  private getSidebarWidth(): number {
295
-    // 从localStorage读取侧边栏宽度,默认320px
296
-    try {
297
-      const savedWidth = localStorage.getItem('sidebarWidth');
298
-      if (savedWidth) {
299
-        const width = parseInt(savedWidth, 10);
300
-        if (!isNaN(width) && width >= 200 && width <= 600) {
301
-          return width;
302
-        }
303
-      }
304
-    } catch (e) {
305
-      console.warn('sticky-header: 读取侧边栏宽度失败', e);
294
+  private cacheInitialDimensions(container?: HTMLElement): void {
295
+    const element = this.elementRef.nativeElement;
296
+    const rect = element.getBoundingClientRect();
297
+    this.initialHeight = rect.height;
298
+    this.maxHeight = rect.height; // 设置最大高度
299
+    
300
+    if (container) {
301
+      // 容器滚动:计算相对于容器的偏移量
302
+      const containerRect = container.getBoundingClientRect();
303
+      this.initialTop = rect.top - containerRect.top + container.scrollTop;
304
+      console.log(`sticky-header: 容器滚动尺寸缓存 - 高度: ${this.initialHeight}px, 最大高度: ${this.maxHeight}px, 相对容器顶部: ${this.initialTop}px, 容器scrollTop: ${container.scrollTop}`);
305
+    } else if (typeof window !== 'undefined') {
306
+      // 窗口滚动:计算绝对距离
307
+      this.initialTop = rect.top + (window.scrollY || document.documentElement.scrollTop || 0);
308
+      this.distanceFromTop = this.initialTop; // 保存距离文档顶部距离
309
+      console.log(`sticky-header: 窗口滚动尺寸缓存 - 高度: ${this.initialHeight}px, 最大高度: ${this.maxHeight}px, 距离文档顶部: ${this.distanceFromTop}px`);
310
+    } else {
311
+      this.initialTop = rect.top;
312
+      console.log(`sticky-header: 尺寸缓存(无window)- 高度: ${this.initialHeight}px, 最大高度: ${this.maxHeight}px, 距离顶部: ${this.initialTop}px`);
306 313
     }
307
-    return 320; // 默认宽度
308 314
   }
309 315
 
310
-   private handleWindowScroll() {
311
-    // 如果窗口不可滚动,不处理滚动事件
312
-    if (!this.canScroll) {
313
-      console.log('sticky-header: 窗口不可滚动,跳过处理');
316
+  private setupResizeObserver(): void {
317
+    if (typeof ResizeObserver === 'undefined') {
318
+      console.warn('sticky-header: ResizeObserver 不支持,尺寸变化将不会自动更新');
314 319
       return;
315 320
     }
316
-    
317
-    // 使用requestAnimationFrame防抖,避免频繁更新
321
+
322
+    this.resizeObserver = new ResizeObserver(() => {
323
+      console.log('sticky-header: header尺寸变化,重新缓存');
324
+      this.cacheInitialDimensions(this.currentScrollContainer || undefined);
325
+    });
326
+
327
+    this.resizeObserver.observe(this.elementRef.nativeElement);
328
+    console.log('sticky-header: ResizeObserver 已启动');
329
+  }
330
+
331
+  private handleWindowScroll() {
332
+    // 使用requestAnimationFrame防抖
318 333
     if (this.scrollRafId) {
319 334
       cancelAnimationFrame(this.scrollRafId);
320 335
     }
@@ -326,104 +341,92 @@ export class StickyHeaderComponent implements AfterViewInit, OnDestroy {
326 341
       
327 342
       // 检查滚动位置是否显著变化,避免微小滚动触发状态更新
328 343
       const scrollDelta = Math.abs(scrollTop - this.lastScrollTop);
329
-      if (scrollDelta < 1) { // 小于1像素的变化忽略
344
+      if (scrollDelta < 3) {
330 345
         return;
331 346
       }
332 347
       
333 348
       this.lastScrollTop = scrollTop;
334 349
       
335
-      // 计算新状态
336
-      const newIsScrolled = scrollTop > this.scrollThreshold;
337
-      const newIsLocked = scrollTop > this.scrollThreshold;
338
-      const newIsCompact = scrollTop > this.compactThreshold;
350
+      // 计算压缩状态
351
+      const { ratio, height, isLocked, isCompact } = this.calculateCompression(scrollTop);
352
+      const isScrolled = ratio > 0;
339 353
       
340
-      // 检查状态是否真正变化,避免不必要更新
354
+      // 检查状态或高度是否真正变化,避免不必要更新
355
+      const currentHeightValue = this.currentHeight();
356
+      const heightChanged = currentHeightValue === null || Math.abs(height - currentHeightValue) > 1;
341 357
       const stateChanged = 
342
-        newIsLocked !== this.lastIsLocked || 
343
-        newIsCompact !== this.lastIsCompact ||
344
-        newIsScrolled !== this.isScrolled;
358
+        isLocked !== this.lastIsLocked || 
359
+        isCompact !== this.lastIsCompact ||
360
+        isScrolled !== this.isScrolled();
345 361
       
346
-      if (!stateChanged) {
347
-        console.log(`sticky-header: 滚动位置变化但状态未变,scrollTop=${scrollTop}, isLocked=${newIsLocked}, isCompact=${newIsCompact}`);
362
+      if (!stateChanged && !heightChanged) {
363
+        console.log(`sticky-header: 窗口滚动位置变化但状态和高度未变,scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, isLocked=${isLocked}, isCompact=${isCompact}, height=${height.toFixed(0)}px`);
348 364
         return;
349 365
       }
350 366
       
351
-      this.lastIsLocked = newIsLocked;
352
-      this.lastIsCompact = newIsCompact;
367
+      this.lastIsLocked = isLocked;
368
+      this.lastIsCompact = isCompact;
353 369
       
354
-      // 更新滚动状态
355
-      this.isScrolled = newIsScrolled;
356
-      this.isLocked = newIsLocked;
357
-      this.isCompact = newIsCompact;
370
+      // 更新状态
371
+      this.currentHeight.set(height);
372
+      this.isScrolled.set(isScrolled);
373
+      this.isLocked.set(isLocked);
374
+      this.isCompact.set(isCompact);
358 375
       
359
-      console.log(`sticky-header: 滚动状态更新,scrollTop=${scrollTop}, isLocked=${this.isLocked}, isCompact=${this.isCompact}`);
376
+      console.log(`sticky-header: 窗口滚动状态更新,scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, height=${height.toFixed(0)}px, isLocked=${isLocked}, isCompact=${isCompact}`);
360 377
       
361 378
       // 设置宽度匹配容器
362
-      if (this.isLocked) {
363
-        // 使用宽度目标元素计算宽度
364
-        const widthTargetElement = this.getWidthTargetElement();
365
-        if (widthTargetElement) {
366
-          const rect = widthTargetElement.getBoundingClientRect();
367
-          this.headerWidth = rect.width;
368
-          this.headerLeft = rect.left;
369
-          console.log(`sticky-header: 窗口滚动锁定,目标宽度: ${rect.width}px, 左侧位置: ${rect.left}px, 目标元素: ${this.widthTarget || '默认'}`);
379
+      if (isLocked) {
380
+        const widthTarget = this.getAutoWidthTarget();
381
+        if (widthTarget) {
382
+          const rect = widthTarget.getBoundingClientRect();
383
+          this.headerWidth.set(rect.width);
384
+          this.headerLeft.set(rect.left);
385
+          console.log(`sticky-header: 窗口滚动锁定,使用自动宽度目标,宽度: ${rect.width}px, 左侧位置: ${rect.left}px`);
370 386
         } else {
371
-          // 回退:使用视口宽度减去实际侧边栏宽度
372
-          const sidebarWidth = this.getSidebarWidth();
373
-          this.headerWidth = `calc(100% - ${sidebarWidth}px)`;
374
-          this.headerLeft = sidebarWidth;
375
-          console.log(`sticky-header: 未找到宽度目标元素,使用回退宽度: ${this.headerWidth}, 侧边栏宽度: ${sidebarWidth}px`);
387
+          // 未找到合适容器,使用视口
388
+          this.headerWidth.set('100%');
389
+          this.headerLeft.set(0);
390
+          console.log(`sticky-header: 窗口滚动锁定,未找到宽度目标,使用视口宽度`);
376 391
         }
377 392
       } else {
378
-        this.headerWidth = null;
379
-        this.headerLeft = null;
393
+        this.headerWidth.set(null);
394
+        this.headerLeft.set(null);
380 395
       }
381 396
     });
382 397
   }
383 398
 
384
-   private checkInitialWindowScrollPosition() {
399
+  private checkInitialWindowScrollPosition() {
385 400
     const scrollTop = window.scrollY || document.documentElement.scrollTop || 0;
386 401
     this.lastScrollTop = scrollTop;
387 402
     
388
-    this.isScrolled = scrollTop > this.scrollThreshold;
389
-    this.isLocked = scrollTop > this.scrollThreshold;
390
-    this.isCompact = scrollTop > this.compactThreshold;
391
-    
392
-    // 记录初始状态
393
-    this.lastIsLocked = this.isLocked;
394
-    this.lastIsCompact = this.isCompact;
395
-    
396
-    if (this.isLocked) {
397
-      // 使用宽度目标元素计算宽度
398
-      const widthTargetElement = this.getWidthTargetElement();
399
-      if (widthTargetElement) {
400
-        const rect = widthTargetElement.getBoundingClientRect();
401
-        this.headerWidth = rect.width;
402
-        this.headerLeft = rect.left;
403
-        console.log(`sticky-header: 初始窗口滚动位置检查: scrollTop=${scrollTop}, isLocked=${this.isLocked}, isCompact=${this.isCompact}, 目标宽度: ${rect.width}px, 目标元素: ${this.widthTarget || '默认'}`);
403
+    const { ratio, height, isLocked, isCompact } = this.calculateCompression(scrollTop);
404
+    const isScrolled = ratio > 0;
405
+    
406
+    this.currentHeight.set(height);
407
+    this.isScrolled.set(isScrolled);
408
+    this.isLocked.set(isLocked);
409
+    this.isCompact.set(isCompact);
410
+    
411
+    if (isLocked) {
412
+      const widthTarget = this.getAutoWidthTarget();
413
+      if (widthTarget) {
414
+        const rect = widthTarget.getBoundingClientRect();
415
+        this.headerWidth.set(rect.width);
416
+        this.headerLeft.set(rect.left);
417
+        console.log(`sticky-header: 初始窗口滚动锁定,使用自动宽度目标,宽度: ${rect.width}px, 左侧位置: ${rect.left}px`);
404 418
       } else {
405
-        // 回退:使用视口宽度减去实际侧边栏宽度
406
-        const sidebarWidth = this.getSidebarWidth();
407
-        this.headerWidth = `calc(100% - ${sidebarWidth}px)`;
408
-        this.headerLeft = sidebarWidth;
409
-        console.log(`sticky-header: 初始窗口滚动位置检查: scrollTop=${scrollTop}, isLocked=${this.isLocked}, isCompact=${this.isCompact}, 使用回退宽度`);
419
+        this.headerWidth.set('100%');
420
+        this.headerLeft.set(0);
421
+        console.log(`sticky-header: 初始窗口滚动锁定,未找到宽度目标,使用视口宽度`);
410 422
       }
411
-    } else {
412
-      this.headerWidth = null;
413
-      this.headerLeft = null;
414 423
     }
415 424
     
416
-    console.log(`sticky-header: 初始窗口滚动位置检查完成: scrollTop=${scrollTop}, isLocked=${this.isLocked}, isCompact=${this.isCompact}, canScroll=${this.canScroll}`);
425
+    console.log(`sticky-header: 初始窗口滚动位置检查完成: scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, height=${height.toFixed(0)}px, isLocked=${isLocked}, isCompact=${isCompact}`);
417 426
   }
418 427
 
419
-   private handleScroll(event: Event) {
420
-    // 如果容器不可滚动,不处理滚动事件
421
-    if (!this.canScroll) {
422
-      console.log('sticky-header: 容器不可滚动,跳过处理');
423
-      return;
424
-    }
425
-    
426
-    // 使用requestAnimationFrame防抖,避免频繁更新
428
+  private handleScroll(event: Event) {
429
+    // 使用requestAnimationFrame防抖
427 430
     if (this.scrollRafId) {
428 431
       cancelAnimationFrame(this.scrollRafId);
429 432
     }
@@ -436,80 +439,181 @@ export class StickyHeaderComponent implements AfterViewInit, OnDestroy {
436 439
       
437 440
       // 检查滚动位置是否显著变化,避免微小滚动触发状态更新
438 441
       const scrollDelta = Math.abs(scrollTop - this.lastScrollTop);
439
-      if (scrollDelta < 1) { // 小于1像素的变化忽略
442
+      if (scrollDelta < 3) {
440 443
         return;
441 444
       }
442 445
       
443 446
       this.lastScrollTop = scrollTop;
444 447
       
445
-      // 计算新状态
446
-      const newIsScrolled = scrollTop > this.scrollThreshold;
447
-      const newIsLocked = scrollTop > this.scrollThreshold;
448
-      const newIsCompact = scrollTop > this.compactThreshold;
448
+      // 计算压缩状态
449
+      const { ratio, height, isLocked, isCompact } = this.calculateCompression(scrollTop);
450
+      const isScrolled = ratio > 0;
449 451
       
450
-      // 检查状态是否真正变化,避免不必要更新
452
+      // 检查状态或高度是否真正变化,避免不必要更新
453
+      const currentHeightValue = this.currentHeight();
454
+      const heightChanged = currentHeightValue === null || Math.abs(height - currentHeightValue) > 1;
451 455
       const stateChanged = 
452
-        newIsLocked !== this.lastIsLocked || 
453
-        newIsCompact !== this.lastIsCompact ||
454
-        newIsScrolled !== this.isScrolled;
456
+        isLocked !== this.lastIsLocked || 
457
+        isCompact !== this.lastIsCompact ||
458
+        isScrolled !== this.isScrolled();
455 459
       
456
-      if (!stateChanged) {
457
-        console.log(`sticky-header: 容器滚动位置变化但状态未变,scrollTop=${scrollTop}, isLocked=${newIsLocked}, isCompact=${newIsCompact}`);
460
+      if (!stateChanged && !heightChanged) {
461
+        console.log(`sticky-header: 容器滚动位置变化但状态和高度未变,scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, isLocked=${isLocked}, isCompact=${isCompact}, height=${height.toFixed(0)}px`);
458 462
         return;
459 463
       }
460 464
       
461
-      this.lastIsLocked = newIsLocked;
462
-      this.lastIsCompact = newIsCompact;
465
+      this.lastIsLocked = isLocked;
466
+      this.lastIsCompact = isCompact;
463 467
       
464
-      // 更新滚动状态
465
-      this.isScrolled = newIsScrolled;
466
-      this.isLocked = newIsLocked;
467
-      this.isCompact = newIsCompact;
468
+      // 更新状态
469
+      this.currentHeight.set(height);
470
+      this.isScrolled.set(isScrolled);
471
+      this.isLocked.set(isLocked);
472
+      this.isCompact.set(isCompact);
468 473
       
469
-      console.log(`sticky-header: 容器滚动状态更新,scrollTop=${scrollTop}, isLocked=${this.isLocked}, isCompact=${this.isCompact}`);
474
+      console.log(`sticky-header: 容器滚动状态更新,scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, height=${height.toFixed(0)}px, isLocked=${isLocked}, isCompact=${isCompact}`);
470 475
       
471 476
       // 设置宽度匹配容器
472
-      if (this.isLocked) {
473
-        // 优先使用宽度目标元素,否则使用滚动容器
474
-        const widthTargetElement = this.getWidthTargetElement();
475
-        const targetElement = widthTargetElement || container;
476
-        const rect = targetElement.getBoundingClientRect();
477
-        this.headerWidth = rect.width;
478
-        this.headerLeft = rect.left;
479
-        console.log(`sticky-header: 容器滚动锁定,目标宽度: ${rect.width}px, 左侧位置: ${rect.left}px, 目标元素: ${widthTargetElement ? (this.widthTarget || '默认') : '滚动容器'}`);
477
+      if (isLocked) {
478
+        // 容器滚动模式优先使用滚动容器作为宽度目标
479
+        const widthTarget = this.getAutoWidthTarget();
480
+        if (widthTarget) {
481
+          const rect = widthTarget.getBoundingClientRect();
482
+          this.headerWidth.set(rect.width);
483
+          this.headerLeft.set(rect.left);
484
+          console.log(`sticky-header: 容器滚动锁定,使用自动宽度目标,宽度: ${rect.width}px, 左侧位置: ${rect.left}px`);
485
+        } else {
486
+          // 未找到合适容器,使用滚动容器本身
487
+          const rect = container.getBoundingClientRect();
488
+          this.headerWidth.set(rect.width);
489
+          this.headerLeft.set(rect.left);
490
+          console.log(`sticky-header: 容器滚动锁定,使用滚动容器宽度,宽度: ${rect.width}px, 左侧位置: ${rect.left}px`);
491
+        }
480 492
       } else {
481
-        this.headerWidth = null;
482
-        this.headerLeft = null;
493
+        this.headerWidth.set(null);
494
+        this.headerLeft.set(null);
483 495
       }
484 496
     });
485 497
   }
486 498
 
487
-   private checkInitialScrollPosition(container: HTMLElement) {
499
+  private checkInitialScrollPosition(container: HTMLElement) {
488 500
     const scrollTop = container.scrollTop || 0;
489 501
     this.lastScrollTop = scrollTop;
490 502
     
491
-    this.isScrolled = scrollTop > this.scrollThreshold;
492
-    this.isLocked = scrollTop > this.scrollThreshold;
493
-    this.isCompact = scrollTop > this.compactThreshold;
494
-    
495
-    // 记录初始状态
496
-    this.lastIsLocked = this.isLocked;
497
-    this.lastIsCompact = this.isCompact;
498
-    
499
-    if (this.isLocked) {
500
-      // 优先使用宽度目标元素,否则使用滚动容器
501
-      const widthTargetElement = this.getWidthTargetElement();
502
-      const targetElement = widthTargetElement || container;
503
-      const rect = targetElement.getBoundingClientRect();
504
-      this.headerWidth = rect.width;
505
-      this.headerLeft = rect.left;
506
-      console.log(`sticky-header: 初始容器滚动位置检查: scrollTop=${scrollTop}, isLocked=${this.isLocked}, isCompact=${this.isCompact}, 目标宽度: ${rect.width}px, 目标元素: ${widthTargetElement ? (this.widthTarget || '默认') : '滚动容器'}`);
503
+    const { ratio, height, isLocked, isCompact } = this.calculateCompression(scrollTop);
504
+    const isScrolled = ratio > 0;
505
+    
506
+    this.currentHeight.set(height);
507
+    this.isScrolled.set(isScrolled);
508
+    this.isLocked.set(isLocked);
509
+    this.isCompact.set(isCompact);
510
+    
511
+    if (isLocked) {
512
+      const widthTarget = this.getAutoWidthTarget();
513
+      if (widthTarget) {
514
+        const rect = widthTarget.getBoundingClientRect();
515
+        this.headerWidth.set(rect.width);
516
+        this.headerLeft.set(rect.left);
517
+        console.log(`sticky-header: 初始容器滚动锁定,使用自动宽度目标,宽度: ${rect.width}px, 左侧位置: ${rect.left}px`);
518
+      } else {
519
+        const rect = container.getBoundingClientRect();
520
+        this.headerWidth.set(rect.width);
521
+        this.headerLeft.set(rect.left);
522
+        console.log(`sticky-header: 初始容器滚动锁定,使用滚动容器宽度,宽度: ${rect.width}px, 左侧位置: ${rect.left}px`);
523
+      }
524
+    }
525
+    
526
+    console.log(`sticky-header: 初始容器滚动位置检查完成: scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, height=${height.toFixed(0)}px, isLocked=${isLocked}, isCompact=${isCompact}`);
527
+  }
528
+
529
+  /**
530
+   * 智能压缩计算 - 基于备份文件逻辑,自动计算最大滚动距离
531
+   * @param scrollTop 当前滚动位置
532
+   * @returns 压缩比例、高度和状态
533
+   */
534
+  private calculateCompression(scrollTop: number): {
535
+    ratio: number;      // 压缩比例 0-1
536
+    height: number;     // 当前动态高度
537
+    isLocked: boolean;  // 是否锁定
538
+    isCompact: boolean; // 是否缩小
539
+  } {
540
+    // 如果尚未缓存尺寸或高度无效,返回默认值
541
+    if (!this.maxHeight || this.maxHeight <= this.minHeight) {
542
+      console.log(`sticky-header: 压缩计算 - 未缓存尺寸或高度无效,maxHeight=${this.maxHeight}, minHeight=${this.minHeight}`);
543
+      return {
544
+        ratio: 0,
545
+        height: this.maxHeight || 0,
546
+        isLocked: false,
547
+        isCompact: false
548
+      };
549
+    }
550
+
551
+    // 计算有效滚动距离
552
+    const effectiveScroll = Math.max(scrollTop - this.initialTop, 0);
553
+    const availableScroll = this.maxHeight - this.minHeight;
554
+    
555
+    // 自动计算最大可能的滚动距离
556
+    let maxPossibleScroll = availableScroll; // 默认值
557
+    
558
+    if (this.scrollContainer === 'window') {
559
+      // 窗口滚动:使用文档高度 - 视口高度
560
+      const scrollHeight = document.body.scrollHeight || document.documentElement.scrollHeight || 0;
561
+      const clientHeight = window.innerHeight || document.documentElement.clientHeight || 0;
562
+      maxPossibleScroll = Math.max(scrollHeight - clientHeight, 0);
507 563
     } else {
508
-      this.headerWidth = null;
509
-      this.headerLeft = null;
564
+      // 容器滚动:使用容器实际可滚动距离
565
+      const container = this.getScrollContainer();
566
+      if (container) {
567
+        maxPossibleScroll = Math.max(container.scrollHeight - container.clientHeight, 0);
568
+      }
510 569
     }
511 570
     
512
-    console.log(`sticky-header: 初始容器滚动位置检查完成: scrollTop=${scrollTop}, isLocked=${this.isLocked}, isCompact=${this.isCompact}, canScroll=${this.canScroll}`);
571
+    // 如果容器实际可滚动距离为0(内容未完全加载或高度不足),使用虚拟高度确保压缩计算能进行
572
+    if (maxPossibleScroll <= 0) {
573
+      const virtualHeight = typeof window !== 'undefined' ? window.innerHeight + 200 : availableScroll;
574
+      maxPossibleScroll = Math.max(virtualHeight, availableScroll);
575
+      console.log(`sticky-header: 使用虚拟高度计算可滚动距离: ${virtualHeight}px, maxPossibleScroll=${maxPossibleScroll}`);
576
+    }
577
+    
578
+    // 确定实际可用的压缩距离:取理论压缩距离和实际最大滚动距离的较小值
579
+    const compressionDistance = Math.min(availableScroll, maxPossibleScroll);
580
+    
581
+    console.log(`sticky-header: 压缩计算参数:
582
+  scrollTop=${scrollTop},
583
+  initialTop=${this.initialTop},
584
+  effectiveScroll=${effectiveScroll},
585
+  maxHeight=${this.maxHeight},
586
+  minHeight=${this.minHeight},
587
+  availableScroll=${availableScroll},
588
+  maxPossibleScroll=${maxPossibleScroll},
589
+  compressionDistance=${compressionDistance}`);
590
+    
591
+    // 避免除零错误
592
+    if (compressionDistance <= 0) {
593
+      console.log(`sticky-header: 压缩计算 - compressionDistance<=0 (${compressionDistance}),跳过计算`);
594
+      return {
595
+        ratio: 0,
596
+        height: this.maxHeight,
597
+        isLocked: false,
598
+        isCompact: false
599
+      };
600
+    }
601
+
602
+    // 线性比例计算:滚动距离 / 实际压缩距离
603
+    const ratio = Math.min(effectiveScroll / compressionDistance, 1);
604
+    const currentHeight = this.maxHeight - (ratio * availableScroll);
605
+    
606
+    console.log(`sticky-header: 压缩计算结果:
607
+  ratio=${ratio.toFixed(3)},
608
+  currentHeight=${currentHeight.toFixed(0)}px,
609
+  需要滚动${compressionDistance * 0.5}px才能进入compact状态`);
610
+    
611
+    return {
612
+      ratio,
613
+      height: currentHeight,
614
+      isLocked: ratio > 0,           // 开始滚动即锁定
615
+      isCompact: ratio > 0.5         // 压缩超过50%视为缩小状态
616
+    };
513 617
   }
514 618
 
515 619
   private cleanupScrollListener() {

+ 644
- 0
projects/base-core/src/lib/components/sticky-header/sticky-header.component.ts.backup 파일 보기

@@ -0,0 +1,644 @@
1
+import { Component, Input, Output, EventEmitter, AfterViewInit, OnDestroy, Renderer2, Inject, ElementRef } from '@angular/core';
2
+import { CommonModule } from '@angular/common';
3
+import { MatButtonModule } from '@angular/material/button';
4
+import { MatIconModule } from '@angular/material/icon';
5
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
6
+
7
+export interface StickyHeaderButton {
8
+  title: string;    // 显示文本
9
+  name: string;     // 标识符(事件返回)
10
+  icon?: string;    // 可选图标
11
+  color?: 'primary' | 'accent' | 'warn';
12
+  disabled?: boolean;
13
+  loading?: boolean;
14
+}
15
+
16
+@Component({
17
+  selector: 'app-sticky-header',
18
+  standalone: true,
19
+  imports: [CommonModule, MatButtonModule, MatIconModule, MatProgressSpinnerModule],
20
+  templateUrl: './sticky-header.component.html',
21
+  styleUrl: './sticky-header.component.scss'
22
+})
23
+export class StickyHeaderComponent implements AfterViewInit, OnDestroy {
24
+  constructor(
25
+    private renderer: Renderer2,
26
+    private elementRef: ElementRef
27
+  ) {}
28
+  
29
+  // 滚动优化相关属性
30
+  private scrollRafId: number | null = null;
31
+  private lastScrollTop = 0;
32
+  private lastIsLocked = false;
33
+  private lastIsCompact = false;
34
+  
35
+  // 智能滚动检测属性
36
+  private headerRect: DOMRect | null = null;
37
+  private maxHeight = 0;
38
+  private minHeight = 40; // 最小高度(与CSS中compact状态一致)
39
+  private distanceFromTop = 0; // header初始距离文档顶部的距离
40
+  private resizeObserver: ResizeObserver | null = null;
41
+  
42
+  /** 标题文本 */
43
+  @Input() title = '';
44
+  
45
+  /** 按钮文本 */
46
+  @Input() buttonText = '操作';
47
+  
48
+  /** 按钮图标 */
49
+  @Input() buttonIcon = 'add';
50
+  
51
+  /** 按钮颜色,默认primary */
52
+  @Input() buttonColor: 'primary' | 'accent' | 'warn' = 'primary';
53
+  
54
+  /** 多个按钮配置,如果设置则忽略单个按钮属性 */
55
+  @Input() buttons: StickyHeaderButton[] = [];
56
+  
57
+  /** 是否禁用按钮 */
58
+  @Input() disabled = false;
59
+  
60
+  /** 是否显示加载状态 */
61
+  @Input() loading = false;
62
+  
63
+  /** 滚动阈值(像素),默认为5 */
64
+  @Input() scrollThreshold = 5;
65
+
66
+  /** 是否启用自动滚动检测 */
67
+  @Input() autoDetect = false;
68
+
69
+  /** 滚动容器选择器,默认为'.content-area',设为'window'监听窗口滚动 */
70
+  @Input() scrollContainer = '.content-area';
71
+
72
+  /** 紧凑模式阈值(像素),默认为50 */
73
+  @Input() compactThreshold = 50;
74
+
75
+  /** 是否显示调试信息框 */
76
+  @Input() showDebugInfo = false;
77
+  
78
+  /** 调试信息数据源 */
79
+  @Input() debugDataSource = '';
80
+  
81
+  /** 调试信息记录数 */
82
+  @Input() debugRecordCount = 0;
83
+  
84
+  /** 调试信息更新时间 */
85
+  @Input() debugLastUpdated = '';
86
+  
87
+  /** 调试信息是否使用模拟数据 */
88
+  @Input() debugUseMockData = false;
89
+  
90
+  /** 调试信息注册API URL */
91
+  @Input() debugRegisterUrl = '';
92
+  
93
+  /** 调试信息列表API URL */
94
+  @Input() debugListUrl = '';
95
+
96
+  /** 提示文本,显示在标题下方 */
97
+  @Input() hintText = '';
98
+
99
+  /** 是否已滚动(由父组件控制,向后兼容) */
100
+  @Input() isScrolled = false;
101
+
102
+  /** 是否锁定在顶部(到达滚动容器顶部) */
103
+  @Input() isLocked = false;
104
+
105
+  /** 是否缩小状态(锁定后进一步缩小) */
106
+  @Input() isCompact = false;
107
+
108
+  /** 锁定时的宽度(像素或百分比) */
109
+  @Input() headerWidth: string | number | null = null;
110
+
111
+  /** 锁定时的左侧偏移(像素) */
112
+  @Input() headerLeft: string | number | null = null;
113
+
114
+  /** 宽度参考目标选择器(用于锁定状态宽度匹配),如未设置则使用滚动容器 */
115
+  @Input() widthTarget: string | null = null;
116
+
117
+  /** 标题区域最小高度(用于测试或固定高度布局),如 '200px', '10rem',未设置则由内容决定 */
118
+  @Input() headerHeight: string | null = null;
119
+
120
+  /** 按钮点击事件 */
121
+  @Output() buttonClick = new EventEmitter<void>();
122
+  
123
+  /** 多个按钮点击事件,返回按钮name标识符 */
124
+  @Output() buttonAction = new EventEmitter<string>();
125
+
126
+  // 自动滚动检测的私有属性
127
+  private scrollListener: (() => void) | null = null;
128
+  private isAutoDetecting = false;
129
+
130
+  onButtonClick(name?: string) {
131
+    if (this.buttons.length > 0) {
132
+      // 多个按钮模式
133
+      const button = this.buttons.find(b => b.name === name);
134
+      if (button && !button.disabled && !button.loading) {
135
+        this.buttonAction.emit(name);
136
+      }
137
+    } else {
138
+      // 单个按钮模式(向后兼容)
139
+      if (!this.disabled && !this.loading) {
140
+        this.buttonClick.emit();
141
+      }
142
+    }
143
+  }
144
+
145
+  ngAfterViewInit() {
146
+    if (this.autoDetect) {
147
+      this.setupAutoScrollDetection();
148
+    }
149
+  }
150
+
151
+   ngOnDestroy() {
152
+    this.cleanupScrollListener();
153
+    if (this.scrollRafId) {
154
+      cancelAnimationFrame(this.scrollRafId);
155
+      this.scrollRafId = null;
156
+    }
157
+    if (this.resizeObserver) {
158
+      this.resizeObserver.disconnect();
159
+      this.resizeObserver = null;
160
+      console.log('sticky-header: ResizeObserver 已清理');
161
+    }
162
+  }
163
+
164
+    private setupAutoScrollDetection() {
165
+    if (this.isAutoDetecting) return;
166
+    
167
+    this.isAutoDetecting = true;
168
+    
169
+    // 初始化尺寸缓存和ResizeObserver
170
+    this.cacheHeaderDimensions();
171
+    this.setupResizeObserver();
172
+    
173
+    const container = this.getScrollContainer();
174
+    const isWindowScroll = this.scrollContainer === 'window';
175
+    
176
+    if (isWindowScroll) {
177
+      // 窗口滚动 - 始终监听,不检查可滚动性
178
+      console.log(`sticky-header: 启用窗口滚动检测(智能模式)`);
179
+      
180
+      this.scrollListener = this.renderer.listen(
181
+        'window',
182
+        'scroll',
183
+        this.handleWindowScroll.bind(this)
184
+      );
185
+      console.log('sticky-header: 自动滚动检测已启用,窗口滚动');
186
+      
187
+      // 初始检查窗口滚动位置
188
+      this.checkInitialWindowScrollPosition();
189
+    } else if (container) {
190
+      // 容器滚动 - 始终监听,不检查可滚动性
191
+      console.log(`sticky-header: 启用容器滚动检测(智能模式),容器: ${this.scrollContainer}`);
192
+      
193
+      // 添加容器可滚动性调试信息
194
+      const scrollHeight = container.scrollHeight || 0;
195
+      const clientHeight = container.clientHeight || 0;
196
+      const maxScrollTop = Math.max(scrollHeight - clientHeight, 0);
197
+      const canScroll = scrollHeight > clientHeight + 5;
198
+      
199
+      console.log(`sticky-header: 容器滚动性检测:
200
+  scrollHeight=${scrollHeight}, 
201
+  clientHeight=${clientHeight},
202
+  最大滚动距离=${maxScrollTop}px,
203
+  可滚动=${canScroll ? '是' : '否'}`);
204
+      
205
+      this.scrollListener = this.renderer.listen(
206
+        container,
207
+        'scroll',
208
+        this.handleScroll.bind(this)
209
+      );
210
+      console.log('sticky-header: 自动滚动检测已启用,容器:', this.scrollContainer);
211
+      
212
+      // 初始检查滚动位置
213
+      this.checkInitialScrollPosition(container);
214
+    } else {
215
+      console.warn('sticky-header: 未找到滚动容器:', this.scrollContainer);
216
+    }
217
+  }
218
+
219
+  private getScrollContainer(): HTMLElement | null {
220
+    if (this.scrollContainer === 'window') {
221
+      return null; // 表示窗口滚动
222
+    }
223
+    
224
+    // 从当前元素向上查找最近的匹配容器
225
+    const container = this.elementRef.nativeElement.closest(this.scrollContainer);
226
+    if (container) {
227
+      return container;
228
+    }
229
+    
230
+    // 全局查找
231
+    return document.querySelector(this.scrollContainer) as HTMLElement;
232
+  }
233
+
234
+  private checkScrollable(container: HTMLElement): boolean {
235
+    if (!container) return false;
236
+    
237
+    const scrollHeight = container.scrollHeight || 0;
238
+    const clientHeight = container.clientHeight || 0;
239
+    
240
+    // 如果内容高度大于可视区域高度,说明可以滚动
241
+    // 添加5px容差,避免因计算误差导致的误判
242
+    const canScroll = scrollHeight > clientHeight + 5;
243
+    return canScroll;
244
+  }
245
+
246
+   private checkWindowScrollable(): boolean {
247
+    const scrollHeight = document.body.scrollHeight || document.documentElement.scrollHeight || 0;
248
+    const clientHeight = window.innerHeight || document.documentElement.clientHeight || 0;
249
+    
250
+    const canScroll = scrollHeight > clientHeight + 5;
251
+    return canScroll;
252
+  }
253
+
254
+  private findContentContainer(): HTMLElement | null {
255
+    // 尝试查找内容区域容器,优先级:
256
+    // 1. .content-area 类 (我们在app.component.html中添加的)
257
+    // 2. 包含router-outlet的容器
258
+    // 3. 右侧内容区域
259
+    const selectors = [
260
+      '.content-area',
261
+      '.flex-1.overflow-auto',
262
+      'div[class*="content"]',
263
+      'div[class*="main"]',
264
+      'div[class*="container"]'
265
+    ];
266
+    
267
+    for (const selector of selectors) {
268
+      const container = document.querySelector(selector) as HTMLElement;
269
+      if (container) {
270
+        console.log(`sticky-header: 找到内容容器: ${selector}, 位置: ${container.getBoundingClientRect().left}px, 宽度: ${container.getBoundingClientRect().width}px`);
271
+        return container;
272
+      }
273
+    }
274
+    
275
+    console.warn('sticky-header: 未找到内容容器');
276
+    return null;
277
+  }
278
+
279
+  private getWidthTargetElement(): HTMLElement | null {
280
+    // 如果设置了widthTarget,优先使用它
281
+    if (this.widthTarget) {
282
+      const target = document.querySelector(this.widthTarget) as HTMLElement;
283
+      if (target) {
284
+        console.log(`sticky-header: 找到宽度目标元素: ${this.widthTarget}, 宽度: ${target.getBoundingClientRect().width}px`);
285
+        return target;
286
+      }
287
+      console.warn(`sticky-header: 未找到宽度目标元素: ${this.widthTarget}`);
288
+    }
289
+    
290
+    // 否则根据滚动容器类型返回相应元素
291
+    if (this.scrollContainer === 'window') {
292
+      return this.findContentContainer();
293
+    } else {
294
+      const container = this.getScrollContainer();
295
+      return container;
296
+    }
297
+  }
298
+
299
+  private getSidebarWidth(): number {
300
+    // 从localStorage读取侧边栏宽度,默认320px
301
+    try {
302
+      const savedWidth = localStorage.getItem('sidebarWidth');
303
+      if (savedWidth) {
304
+        const width = parseInt(savedWidth, 10);
305
+        if (!isNaN(width) && width >= 200 && width <= 600) {
306
+          return width;
307
+        }
308
+      }
309
+    } catch (e) {
310
+      console.warn('sticky-header: 读取侧边栏宽度失败', e);
311
+    }
312
+    return 320; // 默认宽度
313
+  }
314
+
315
+    private handleWindowScroll() {
316
+    // 使用requestAnimationFrame防抖,避免频繁更新
317
+    if (this.scrollRafId) {
318
+      cancelAnimationFrame(this.scrollRafId);
319
+    }
320
+    
321
+    this.scrollRafId = requestAnimationFrame(() => {
322
+      this.scrollRafId = null;
323
+      
324
+      const scrollTop = window.scrollY || document.documentElement.scrollTop || 0;
325
+      
326
+      // 检查滚动位置是否显著变化,避免微小滚动触发状态更新
327
+      const scrollDelta = Math.abs(scrollTop - this.lastScrollTop);
328
+      if (scrollDelta < 1) { // 小于1像素的变化忽略
329
+        return;
330
+      }
331
+      
332
+      this.lastScrollTop = scrollTop;
333
+      
334
+      // 智能计算压缩状态
335
+      const { ratio, height, isLocked, isCompact } = this.calculateCompression(scrollTop);
336
+      const newIsScrolled = ratio > 0;
337
+      
338
+      // 检查状态是否真正变化,避免不必要更新
339
+      const stateChanged = 
340
+        isLocked !== this.lastIsLocked || 
341
+        isCompact !== this.lastIsCompact ||
342
+        newIsScrolled !== this.isScrolled;
343
+      
344
+      if (!stateChanged) {
345
+        console.log(`sticky-header: 滚动位置变化但状态未变,scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, isLocked=${isLocked}, isCompact=${isCompact}`);
346
+        return;
347
+      }
348
+      
349
+      this.lastIsLocked = isLocked;
350
+      this.lastIsCompact = isCompact;
351
+      
352
+      // 更新滚动状态
353
+      this.isScrolled = newIsScrolled;
354
+      this.isLocked = isLocked;
355
+      this.isCompact = isCompact;
356
+      
357
+      console.log(`sticky-header: 滚动状态更新,scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, height=${height.toFixed(0)}px, isLocked=${this.isLocked}, isCompact=${this.isCompact}`);
358
+      
359
+      // 设置宽度匹配容器
360
+      if (this.isLocked) {
361
+        // 使用宽度目标元素计算宽度
362
+        const widthTargetElement = this.getWidthTargetElement();
363
+        if (widthTargetElement) {
364
+          const rect = widthTargetElement.getBoundingClientRect();
365
+          this.headerWidth = rect.width;
366
+          this.headerLeft = rect.left;
367
+          console.log(`sticky-header: 窗口滚动锁定,目标宽度: ${rect.width}px, 左侧位置: ${rect.left}px, 目标元素: ${this.widthTarget || '默认'}`);
368
+        } else {
369
+          // 回退:使用视口宽度减去实际侧边栏宽度
370
+          const sidebarWidth = this.getSidebarWidth();
371
+          this.headerWidth = `calc(100% - ${sidebarWidth}px)`;
372
+          this.headerLeft = sidebarWidth;
373
+          console.log(`sticky-header: 未找到宽度目标元素,使用回退宽度: ${this.headerWidth}, 侧边栏宽度: ${sidebarWidth}px`);
374
+        }
375
+      } else {
376
+        this.headerWidth = null;
377
+        this.headerLeft = null;
378
+      }
379
+    });
380
+  }
381
+
382
+    private checkInitialWindowScrollPosition() {
383
+    const scrollTop = window.scrollY || document.documentElement.scrollTop || 0;
384
+    this.lastScrollTop = scrollTop;
385
+    
386
+    // 智能计算初始压缩状态
387
+    const { ratio, height, isLocked, isCompact } = this.calculateCompression(scrollTop);
388
+    const isScrolled = ratio > 0;
389
+    
390
+    this.isScrolled = isScrolled;
391
+    this.isLocked = isLocked;
392
+    this.isCompact = isCompact;
393
+    
394
+    // 记录初始状态
395
+    this.lastIsLocked = isLocked;
396
+    this.lastIsCompact = isCompact;
397
+    
398
+    if (this.isLocked) {
399
+      // 使用宽度目标元素计算宽度
400
+      const widthTargetElement = this.getWidthTargetElement();
401
+      if (widthTargetElement) {
402
+        const rect = widthTargetElement.getBoundingClientRect();
403
+        this.headerWidth = rect.width;
404
+        this.headerLeft = rect.left;
405
+        console.log(`sticky-header: 初始窗口滚动位置检查: scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, isLocked=${this.isLocked}, isCompact=${this.isCompact}, 目标宽度: ${rect.width}px, 目标元素: ${this.widthTarget || '默认'}`);
406
+      } else {
407
+        // 回退:使用视口宽度减去实际侧边栏宽度
408
+        const sidebarWidth = this.getSidebarWidth();
409
+        this.headerWidth = `calc(100% - ${sidebarWidth}px)`;
410
+        this.headerLeft = sidebarWidth;
411
+        console.log(`sticky-header: 初始窗口滚动位置检查: scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, isLocked=${this.isLocked}, isCompact=${this.isCompact}, 使用回退宽度`);
412
+      }
413
+    } else {
414
+      this.headerWidth = null;
415
+      this.headerLeft = null;
416
+    }
417
+    
418
+    console.log(`sticky-header: 初始窗口滚动位置检查完成: scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, height=${height.toFixed(0)}px, isLocked=${this.isLocked}, isCompact=${this.isCompact}`);
419
+  }
420
+
421
+    private handleScroll(event: Event) {
422
+    // 使用requestAnimationFrame防抖,避免频繁更新
423
+    if (this.scrollRafId) {
424
+      cancelAnimationFrame(this.scrollRafId);
425
+    }
426
+    
427
+    this.scrollRafId = requestAnimationFrame(() => {
428
+      this.scrollRafId = null;
429
+      
430
+       const container = event.target as HTMLElement;
431
+       const scrollTop = container.scrollTop || 0;
432
+       
433
+       // 容器滚动详情调试信息
434
+       const scrollHeight = container.scrollHeight || 0;
435
+       const clientHeight = container.clientHeight || 0;
436
+       const maxScrollTop = Math.max(scrollHeight - clientHeight, 0);
437
+       
438
+       console.log(`sticky-header: 容器滚动详情:
439
+  scrollTop=${scrollTop},
440
+  scrollHeight=${scrollHeight},
441
+  clientHeight=${clientHeight},
442
+  最大滚动距离=${maxScrollTop}px`);
443
+       
444
+       // 检查滚动位置是否显著变化,避免微小滚动触发状态更新
445
+       const scrollDelta = Math.abs(scrollTop - this.lastScrollTop);
446
+       if (scrollDelta < 1) { // 小于1像素的变化忽略
447
+         console.log(`sticky-header: 滚动变化小于1像素 (${scrollDelta}),跳过处理`);
448
+         return;
449
+       }
450
+       
451
+       this.lastScrollTop = scrollTop;
452
+      
453
+       // 智能计算压缩状态(容器滚动时,header通常位于容器顶部,使用scrollTop作为有效滚动距离)
454
+       // 注意:这里假设header在容器顶部,如果需要更精确,可以计算header相对于容器顶部的偏移
455
+       // 对于容器滚动,使用基准偏移0,因为header通常位于容器顶部
456
+       // 传递最大滚动距离,确保压缩比例基于实际可滚动空间计算
457
+       const { ratio, height, isLocked, isCompact } = this.calculateCompression(scrollTop, 0, maxScrollTop);
458
+      const newIsScrolled = ratio > 0;
459
+      
460
+      // 检查状态是否真正变化,避免不必要更新
461
+      const stateChanged = 
462
+        isLocked !== this.lastIsLocked || 
463
+        isCompact !== this.lastIsCompact ||
464
+        newIsScrolled !== this.isScrolled;
465
+      
466
+      if (!stateChanged) {
467
+        console.log(`sticky-header: 容器滚动位置变化但状态未变,scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, isLocked=${isLocked}, isCompact=${isCompact}`);
468
+        return;
469
+      }
470
+      
471
+      this.lastIsLocked = isLocked;
472
+      this.lastIsCompact = isCompact;
473
+      
474
+      // 更新滚动状态
475
+      this.isScrolled = newIsScrolled;
476
+      this.isLocked = isLocked;
477
+      this.isCompact = isCompact;
478
+      
479
+      console.log(`sticky-header: 容器滚动状态更新,scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, height=${height.toFixed(0)}px, isLocked=${this.isLocked}, isCompact=${this.isCompact}`);
480
+      
481
+      // 设置宽度匹配容器
482
+      if (this.isLocked) {
483
+        // 优先使用宽度目标元素,否则使用滚动容器
484
+        const widthTargetElement = this.getWidthTargetElement();
485
+        const targetElement = widthTargetElement || container;
486
+        const rect = targetElement.getBoundingClientRect();
487
+        this.headerWidth = rect.width;
488
+        this.headerLeft = rect.left;
489
+        console.log(`sticky-header: 容器滚动锁定,目标宽度: ${rect.width}px, 左侧位置: ${rect.left}px, 目标元素: ${widthTargetElement ? (this.widthTarget || '默认') : '滚动容器'}`);
490
+      } else {
491
+        this.headerWidth = null;
492
+        this.headerLeft = null;
493
+      }
494
+    });
495
+  }
496
+
497
+    private checkInitialScrollPosition(container: HTMLElement) {
498
+    const scrollTop = container.scrollTop || 0;
499
+    this.lastScrollTop = scrollTop;
500
+    
501
+    // 智能计算初始压缩状态(容器滚动使用基准偏移0)
502
+    const { ratio, height, isLocked, isCompact } = this.calculateCompression(scrollTop, 0);
503
+    const isScrolled = ratio > 0;
504
+    
505
+    this.isScrolled = isScrolled;
506
+    this.isLocked = isLocked;
507
+    this.isCompact = isCompact;
508
+    
509
+    // 记录初始状态
510
+    this.lastIsLocked = isLocked;
511
+    this.lastIsCompact = isCompact;
512
+    
513
+    if (this.isLocked) {
514
+      // 优先使用宽度目标元素,否则使用滚动容器
515
+      const widthTargetElement = this.getWidthTargetElement();
516
+      const targetElement = widthTargetElement || container;
517
+      const rect = targetElement.getBoundingClientRect();
518
+      this.headerWidth = rect.width;
519
+      this.headerLeft = rect.left;
520
+      console.log(`sticky-header: 初始容器滚动位置检查: scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, isLocked=${this.isLocked}, isCompact=${this.isCompact}, 目标宽度: ${rect.width}px, 目标元素: ${widthTargetElement ? (this.widthTarget || '默认') : '滚动容器'}`);
521
+    } else {
522
+      this.headerWidth = null;
523
+      this.headerLeft = null;
524
+    }
525
+    
526
+    console.log(`sticky-header: 初始容器滚动位置检查完成: scrollTop=${scrollTop}, ratio=${ratio.toFixed(2)}, height=${height.toFixed(0)}px, isLocked=${this.isLocked}, isCompact=${this.isCompact}`);
527
+  }
528
+
529
+  /**
530
+   * 缓存header尺寸信息
531
+   */
532
+  private cacheHeaderDimensions(): void {
533
+    const element = this.elementRef.nativeElement;
534
+    this.headerRect = element.getBoundingClientRect();
535
+    this.maxHeight = this.headerRect!.height;
536
+    
537
+    // 计算距离文档顶部的距离(考虑窗口滚动)
538
+    if (typeof window !== 'undefined') {
539
+      this.distanceFromTop = this.headerRect!.top + (window.scrollY || document.documentElement.scrollTop || 0);
540
+    } else {
541
+      this.distanceFromTop = this.headerRect!.top;
542
+    }
543
+    
544
+    console.log(`sticky-header: 尺寸缓存完成 - 最大高度: ${this.maxHeight}px, 距离顶部: ${this.distanceFromTop}px`);
545
+  }
546
+
547
+  /**
548
+   * 设置ResizeObserver监听header自身尺寸变化
549
+   */
550
+  private setupResizeObserver(): void {
551
+    if (typeof ResizeObserver === 'undefined') {
552
+      console.warn('sticky-header: ResizeObserver 不支持,尺寸变化将不会自动更新');
553
+      return;
554
+    }
555
+
556
+    this.resizeObserver = new ResizeObserver(() => {
557
+      console.log('sticky-header: header尺寸变化,重新缓存');
558
+      this.cacheHeaderDimensions();
559
+    });
560
+
561
+    this.resizeObserver.observe(this.elementRef.nativeElement);
562
+    console.log('sticky-header: ResizeObserver 已启动');
563
+  }
564
+
565
+  /**
566
+   * 智能压缩计算
567
+   * @param scrollTop 当前滚动位置
568
+   * @param baseOffset 基准偏移量(对于窗口滚动使用distanceFromTop,对于容器滚动使用0或header相对于容器顶部的距离)
569
+   * @param maxPossibleScroll 最大可能的滚动距离(对于容器滚动是scrollHeight-clientHeight,对于窗口滚动使用availableScroll)
570
+   * @returns 压缩比例和状态
571
+   */
572
+  private calculateCompression(scrollTop: number, baseOffset?: number, maxPossibleScroll?: number): {
573
+    ratio: number;      // 压缩比例 0-1
574
+    height: number;     // 当前高度
575
+    isLocked: boolean;  // 是否锁定
576
+    isCompact: boolean; // 是否缩小
577
+  } {
578
+    // 如果尚未缓存尺寸或高度无效,返回默认值
579
+    if (!this.headerRect || this.maxHeight <= this.minHeight) {
580
+      console.log(`sticky-header: 压缩计算 - 未缓存尺寸或高度无效,headerRect=${!!this.headerRect}, maxHeight=${this.maxHeight}, minHeight=${this.minHeight}`);
581
+      return {
582
+        ratio: 0,
583
+        height: this.maxHeight || 0,
584
+        isLocked: false,
585
+        isCompact: false
586
+      };
587
+    }
588
+
589
+    // 计算有效滚动距离(考虑基准偏移)
590
+    const offset = baseOffset !== undefined ? baseOffset : this.distanceFromTop;
591
+    const effectiveScroll = Math.max(scrollTop - offset, 0);
592
+    const availableScroll = this.maxHeight - this.minHeight;
593
+    
594
+    // 确定实际可用的压缩距离:取理论压缩距离和实际最大滚动距离的较小值
595
+    const actualMaxScroll = maxPossibleScroll !== undefined ? maxPossibleScroll : availableScroll;
596
+    const compressionDistance = Math.min(availableScroll, actualMaxScroll);
597
+    
598
+    console.log(`sticky-header: 压缩计算参数:
599
+  scrollTop=${scrollTop},
600
+  offset=${offset} (${baseOffset !== undefined ? '自定义基准' : 'distanceFromTop'}),
601
+  effectiveScroll=${effectiveScroll},
602
+  maxHeight=${this.maxHeight},
603
+  minHeight=${this.minHeight},
604
+  availableScroll=${availableScroll},
605
+  maxPossibleScroll=${maxPossibleScroll !== undefined ? maxPossibleScroll : '未设置'},
606
+  compressionDistance=${compressionDistance}`);
607
+    
608
+    // 避免除零错误
609
+    if (compressionDistance <= 0) {
610
+      console.log(`sticky-header: 压缩计算 - compressionDistance<=0 (${compressionDistance}),跳过计算`);
611
+      return {
612
+        ratio: 0,
613
+        height: this.maxHeight,
614
+        isLocked: false,
615
+        isCompact: false
616
+      };
617
+    }
618
+
619
+    // 线性比例计算:滚动距离 / 实际压缩距离
620
+    const ratio = Math.min(effectiveScroll / compressionDistance, 1);
621
+    const currentHeight = this.maxHeight - (ratio * availableScroll);
622
+    
623
+    console.log(`sticky-header: 压缩计算结果:
624
+  ratio=${ratio.toFixed(3)},
625
+  currentHeight=${currentHeight.toFixed(0)}px,
626
+  需要滚动${compressionDistance * 0.5}px才能进入compact状态`);
627
+    
628
+    return {
629
+      ratio,
630
+      height: currentHeight,
631
+      isLocked: ratio > 0,           // 开始滚动即锁定
632
+      isCompact: ratio > 0.5         // 压缩超过50%视为缩小状态
633
+    };
634
+  }
635
+
636
+  private cleanupScrollListener() {
637
+    if (this.scrollListener) {
638
+      this.scrollListener();
639
+      this.scrollListener = null;
640
+      console.log('sticky-header: 滚动监听器已清理');
641
+    }
642
+    this.isAutoDetecting = false;
643
+  }
644
+}

+ 1
- 1
projects/base-core/src/lib/components/tabulator-grid/tabulator-grid.component.html 파일 보기

@@ -1 +1 @@
1
-<div [id]="containerId" class="tabulator-grid-container"></div>
1
+<div [id]="containerId" ></div>

+ 2
- 2
projects/base-core/src/lib/components/tabulator-grid/tabulator-grid.component.ts 파일 보기

@@ -571,8 +571,8 @@ export class TabulatorGridComponent implements OnInit, AfterViewInit, OnDestroy
571 571
           console.log('📦 原始参数值 (安全显示):', safeStringify(params, 2));
572 572
          
573 573
          // 调试:记录调用堆栈以确认函数被 Tabulator 调用
574
-         console.trace('TabulatorGrid ajaxRequestFunc 调用堆栈');
575
-         console.groupEnd();
574
+         //console.trace('TabulatorGrid ajaxRequestFunc 调用堆栈');
575
+         //console.groupEnd();
576 576
         
577 577
         return new Promise((resolve, reject) => {
578 578
           if (!dataLoader) {

Loading…
취소
저장