Без опису
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

service-register-config.component.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. import { Component, OnInit, HostListener, ChangeDetectorRef, AfterViewInit, ElementRef, Renderer2, OnDestroy, ViewChild } from '@angular/core';
  2. import { StickyHeaderComponent } from '../../components/sticky-header/sticky-header.component';
  3. import { CommonModule } from '@angular/common';
  4. import { MatCard, MatCardContent } from '@angular/material/card';
  5. import { MatTableModule } from '@angular/material/table';
  6. import { MatProgressSpinner } from '@angular/material/progress-spinner';
  7. import { ConfigMetaService } from '../../services/config-meta.service';
  8. import { ConfigService } from '../../services/config.service';
  9. import { ToastService } from '../../services/toast.service';
  10. import { ConfigMeta } from '../../models/config-meta.model';
  11. import { HttpErrorResponse } from '@angular/common/http';
  12. @Component({
  13. selector: 'app-service-register-config',
  14. imports: [CommonModule, MatCard, MatCardContent, MatTableModule, MatProgressSpinner, StickyHeaderComponent],
  15. templateUrl: './service-register-config.component.html',
  16. styleUrl: './service-register-config.component.scss'
  17. })
  18. export class ServiceRegisterConfigComponent implements OnInit, AfterViewInit, OnDestroy {
  19. title = '注册服务配置';
  20. hintText = '点击"注册"按钮将同步所有配置元信息到数据库,并显示配置列表。';
  21. displayedColumns: string[] = ['configName', 'fieldName', 'fieldType', 'yamlName', 'fieldDesc'];
  22. configMetaList: ConfigMeta[] = [];
  23. loading = false;
  24. registering = false;
  25. isScrolled = false; // 向后兼容
  26. isLocked = false; // 是否锁定在顶部
  27. isCompact = false; // 是否缩小状态
  28. scrollThreshold = 2; // 锁定阈值
  29. compactThreshold = 50; // 缩小阈值(锁定后进一步滚动多少像素开始缩小)
  30. private scrollContainer: HTMLElement | Window = window;
  31. private scrollListener: (() => void) | null = null;
  32. private rafId: number | null = null;
  33. debugInfo = {
  34. dataSource: '',
  35. recordCount: 0,
  36. lastUpdated: '',
  37. useMockData: false
  38. };
  39. @ViewChild('matCard', { read: ElementRef }) matCardRef!: ElementRef;
  40. headerWidth: number | null = null;
  41. headerLeft: number | null = null;
  42. private resizeListener: (() => void) | null = null;
  43. lastRegisterUrl = '';
  44. lastListUrl = '';
  45. constructor(
  46. private configMetaService: ConfigMetaService,
  47. private configService: ConfigService,
  48. private toastService: ToastService,
  49. private cdRef: ChangeDetectorRef,
  50. private elementRef: ElementRef,
  51. private renderer: Renderer2
  52. ) {}
  53. ngOnInit() {
  54. this.loadConfigMeta();
  55. }
  56. ngAfterViewInit() {
  57. this.findScrollContainer();
  58. this.setupScrollListener();
  59. this.checkScroll();
  60. // 添加窗口resize监听器
  61. this.resizeListener = () => this.onResize();
  62. window.addEventListener('resize', this.resizeListener, { passive: true });
  63. }
  64. private onResize() {
  65. // 只在锁定状态时更新尺寸
  66. if (this.isLocked && this.matCardRef?.nativeElement) {
  67. console.log('窗口大小变化,更新标题栏尺寸');
  68. this.updateHeaderDimensions();
  69. }
  70. }
  71. ngOnDestroy() {
  72. this.cleanupScrollListener();
  73. // 清理resize监听器
  74. if (this.resizeListener) {
  75. window.removeEventListener('resize', this.resizeListener);
  76. this.resizeListener = null;
  77. }
  78. }
  79. @HostListener('window:scroll', [])
  80. onWindowScroll() {
  81. // 保留window滚动监听作为备用
  82. if (this.scrollContainer === window) {
  83. this.onScroll();
  84. }
  85. }
  86. private findScrollContainer() {
  87. // 向上查找最近的滚动容器
  88. let element: HTMLElement | null = this.elementRef.nativeElement.parentElement;
  89. while (element && element !== document.body) {
  90. const style = getComputedStyle(element);
  91. const hasOverflow = style.overflow === 'auto' || style.overflow === 'scroll' ||
  92. style.overflowY === 'auto' || style.overflowY === 'scroll';
  93. if (hasOverflow) {
  94. console.log('找到滚动容器:', element, 'clientHeight:', element.clientHeight, 'scrollHeight:', element.scrollHeight);
  95. this.scrollContainer = element;
  96. return;
  97. }
  98. element = element.parentElement;
  99. }
  100. // 未找到合适的容器,使用window
  101. console.log('未找到滚动容器,使用window');
  102. this.scrollContainer = window;
  103. }
  104. private setupScrollListener() {
  105. if (this.scrollContainer === window) {
  106. this.scrollListener = () => this.onScroll();
  107. window.addEventListener('scroll', this.scrollListener, { passive: true });
  108. } else {
  109. this.scrollListener = () => this.onScroll();
  110. (this.scrollContainer as HTMLElement).addEventListener('scroll', this.scrollListener, { passive: true });
  111. }
  112. }
  113. private cleanupScrollListener() {
  114. if (this.scrollListener) {
  115. if (this.scrollContainer === window) {
  116. window.removeEventListener('scroll', this.scrollListener);
  117. } else {
  118. (this.scrollContainer as HTMLElement).removeEventListener('scroll', this.scrollListener);
  119. }
  120. this.scrollListener = null;
  121. }
  122. if (this.rafId !== null) {
  123. cancelAnimationFrame(this.rafId);
  124. this.rafId = null;
  125. }
  126. }
  127. private onScroll() {
  128. if (this.rafId !== null) {
  129. cancelAnimationFrame(this.rafId);
  130. }
  131. this.rafId = requestAnimationFrame(() => {
  132. this.checkScroll();
  133. });
  134. }
  135. private updateHeaderDimensions() {
  136. if (!this.matCardRef?.nativeElement) return;
  137. const matCard = this.matCardRef.nativeElement;
  138. const rect = matCard.getBoundingClientRect();
  139. // 计算mat-card在视口中的位置和宽度
  140. this.headerWidth = rect.width;
  141. this.headerLeft = rect.left;
  142. console.log('更新标题栏尺寸:', {
  143. width: this.headerWidth,
  144. left: this.headerLeft,
  145. matCardRect: rect
  146. });
  147. this.cdRef.detectChanges();
  148. }
  149. private checkScroll() {
  150. if (!this.scrollContainer) return;
  151. let scrollTop = 0;
  152. let containerType = 'unknown';
  153. if (this.scrollContainer === window) {
  154. scrollTop = window.scrollY || document.documentElement.scrollTop;
  155. containerType = 'window';
  156. } else {
  157. scrollTop = (this.scrollContainer as HTMLElement).scrollTop;
  158. containerType = 'element';
  159. }
  160. // 向后兼容:保持isScrolled逻辑
  161. const scrolled = scrollTop > this.scrollThreshold;
  162. // 阶段1: 检测是否锁定(开始滚动就锁定)
  163. const shouldLock = scrollTop > 0;
  164. // 阶段2: 检测是否缩小(锁定后继续滚动)
  165. const shouldCompact = shouldLock && scrollTop > this.compactThreshold;
  166. console.log('滚动检测:', {
  167. container: containerType,
  168. scrollTop,
  169. scrollThreshold: this.scrollThreshold,
  170. compactThreshold: this.compactThreshold,
  171. shouldLock,
  172. shouldCompact,
  173. scrolled, // 向后兼容
  174. isLocked: this.isLocked,
  175. isCompact: this.isCompact
  176. });
  177. // 更新状态
  178. let needsUpdate = false;
  179. if (this.isScrolled !== scrolled) {
  180. this.isScrolled = scrolled;
  181. needsUpdate = true;
  182. }
  183. if (this.isLocked !== shouldLock) {
  184. this.isLocked = shouldLock;
  185. needsUpdate = true;
  186. console.log('锁定状态变化:', this.isLocked);
  187. if (this.isLocked) {
  188. // 锁定状态:计算并更新标题栏尺寸
  189. this.updateHeaderDimensions();
  190. } else {
  191. // 解锁状态:清除尺寸,让标题栏恢复原始定位
  192. this.headerWidth = null;
  193. this.headerLeft = null;
  194. }
  195. }
  196. if (this.isCompact !== shouldCompact) {
  197. this.isCompact = shouldCompact;
  198. needsUpdate = true;
  199. console.log('缩小状态变化:', this.isCompact);
  200. }
  201. if (needsUpdate) {
  202. this.cdRef.detectChanges();
  203. }
  204. }
  205. onRegister() {
  206. this.lastRegisterUrl = this.configMetaService.getInitUrl();
  207. console.debug('[注册] 请求URL:', this.lastRegisterUrl);
  208. this.registering = true;
  209. this.configMetaService.initConfigMeta().subscribe({
  210. next: (response) => {
  211. this.registering = false;
  212. if (response.success) {
  213. this.toastService.success('配置元信息注册成功');
  214. this.loadConfigMeta();
  215. } else {
  216. const errorMsg = this.extractErrorMessageFromResponse(response);
  217. this.toastService.error(`注册失败:${errorMsg}`);
  218. }
  219. },
  220. error: (error) => {
  221. this.registering = false;
  222. const errorMsg = this.extractErrorMessage(error);
  223. this.toastService.error(`注册失败:${errorMsg}`);
  224. console.debug('注册配置元信息失败', error);
  225. }
  226. });
  227. }
  228. loadConfigMeta() {
  229. this.lastListUrl = this.configMetaService.getListUrl();
  230. console.debug('[加载] 请求URL:', this.lastListUrl);
  231. this.loading = true;
  232. this.configMetaService.listConfigMeta().subscribe({
  233. next: (list) => {
  234. this.configMetaList = list;
  235. this.loading = false;
  236. // 更新调试信息
  237. const source = this.configService.useMockData ? '模拟数据' : 'API数据';
  238. this.updateDebugInfo(list, source);
  239. },
  240. error: (error) => {
  241. this.loading = false;
  242. const errorMsg = this.extractErrorMessage(error);
  243. this.toastService.error(`加载配置元信息失败:${errorMsg}`);
  244. console.debug('加载配置元信息失败', error);
  245. // 错误时也更新调试信息
  246. this.updateDebugInfo([], `API调用失败: ${errorMsg}`);
  247. }
  248. });
  249. }
  250. private updateDebugInfo(list: ConfigMeta[], source: string) {
  251. // 轻量级调试信息更新,不影响性能
  252. this.debugInfo = {
  253. dataSource: source,
  254. recordCount: list.length,
  255. lastUpdated: new Date().toLocaleTimeString(),
  256. useMockData: this.configService.useMockData
  257. };
  258. // 控制台调试日志(仅在开发时查看)
  259. console.debug('[调试] 数据源:', source);
  260. console.debug('[调试] 记录数:', list.length);
  261. console.debug('[调试] 使用模拟数据:', this.configService.useMockData);
  262. if (list.length > 0) {
  263. console.debug('[调试] 首条数据示例:', list[0]);
  264. }
  265. }
  266. private extractErrorMessage(error: any): string {
  267. if (error instanceof HttpErrorResponse) {
  268. // HTTP错误
  269. const status = error.status;
  270. const errorBody = error.error;
  271. console.debug(`[错误详情] HTTP ${status} ${error.url}`);
  272. console.debug(`[错误详情] 错误响应:`, errorBody);
  273. if (errorBody?.error) {
  274. return `HTTP ${status}: ${errorBody.error}`;
  275. } else if (errorBody?.message) {
  276. return `HTTP ${status}: ${errorBody.message}`;
  277. } else if (error.message) {
  278. return `HTTP ${status}: ${error.message}`;
  279. } else {
  280. return `HTTP错误 ${status}`;
  281. }
  282. } else if (error?.error) {
  283. // 可能已经是QueryResult格式
  284. return error.error.error || error.error.message || error.message || '未知错误';
  285. } else if (error?.message) {
  286. return error.message;
  287. }
  288. return '未知错误';
  289. }
  290. private extractErrorMessageFromResponse(response: any): string {
  291. if (response?.error) {
  292. return response.error;
  293. } else if (response?.message) {
  294. return response.message;
  295. }
  296. return '未知错误';
  297. }
  298. }