data_quality.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. (() => {
  2. // The graph page owns the reminder when the chat is embedded inside it.
  3. if (window.self !== window.top) return;
  4. const header = document.querySelector('header');
  5. if (!header) return;
  6. const make = (tag, className, text) => {
  7. const element = document.createElement(tag);
  8. if (className) element.className = className;
  9. if (text !== undefined) element.textContent = text;
  10. return element;
  11. };
  12. const trigger = make('button', 'dq-trigger');
  13. trigger.type = 'button'; trigger.hidden = true;
  14. trigger.id = 'dataQualityAlert';
  15. trigger.setAttribute('aria-haspopup', 'dialog');
  16. trigger.append(make('span', 'dq-icon', '!'));
  17. const badge = make('span', '', '数据问题'); trigger.append(badge);
  18. header.append(trigger);
  19. const dialog = make('dialog', 'dq-dialog');
  20. dialog.id = 'dataQualityDialog'; dialog.setAttribute('aria-labelledby', 'dqTitle');
  21. const heading = make('div', 'dq-head');
  22. const title = make('h2', '', '数据质量提醒'); title.id = 'dqTitle';
  23. const close = make('button', 'dq-close', '×'); close.type = 'button';
  24. close.setAttribute('aria-label', '关闭数据质量提醒');
  25. heading.append(title, close);
  26. const body = make('div', 'dq-body'); body.setAttribute('aria-live', 'polite');
  27. dialog.append(heading, body); document.body.append(dialog);
  28. close.onclick = () => dialog.close();
  29. dialog.addEventListener('click', event => { if (event.target === dialog) {
  30. const box = dialog.getBoundingClientRect();
  31. if (event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom) dialog.close();
  32. }});
  33. let summary = null, selectedFile = '', generation = 0, loading = false;
  34. const endpoint = () => `${typeof apiBase === 'function' ? apiBase() : location.origin}/api/data-quality`;
  35. async function request(parameters = '') {
  36. const response = await fetch(endpoint() + parameters, {cache:'no-store', signal:AbortSignal.timeout(15000)});
  37. const data = await response.json();
  38. if (!response.ok) { const error = new Error(data.detail || '读取失败,请重试'); error.status = response.status; throw error; }
  39. return data;
  40. }
  41. function updateBadge(data) {
  42. summary = data; trigger.hidden = data.total === 0;
  43. trigger.classList.remove('dq-unavailable');
  44. badge.textContent = `数据问题 ${data.total}`;
  45. trigger.setAttribute('aria-label', `${Object.keys(data.datasets).length} 份文件共 ${data.total} 条数据问题,点击查看`);
  46. trigger.title = trigger.getAttribute('aria-label');
  47. }
  48. function showError(error) {
  49. body.replaceChildren(make('p', 'dq-error', error.message || '读取失败,请稍后重试'));
  50. const retry = make('button', 'dq-file', '重新加载'); retry.onclick = loadDetails; body.append(retry);
  51. }
  52. function render(data) {
  53. body.replaceChildren();
  54. body.append(make('p', 'dq-note', '以下记录因缺少主键未进入知识图谱。请在源数据中补齐后重新更新;此处展示该次发布保留的原始记录。'));
  55. if (!data.total) { body.append(make('p', 'dq-status', '当前版本没有缺少主键的记录。')); return; }
  56. const names = Object.keys(data.datasets);
  57. if (!names.includes(selectedFile)) selectedFile = names[0];
  58. body.append(make('p', 'dq-status', `共 ${names.length} 份文件,${data.total} 条问题记录`));
  59. const files = make('div', 'dq-files'); files.setAttribute('aria-label', '按来源文件查看');
  60. const list = make('div');
  61. const showFile = name => {
  62. selectedFile = name;
  63. for (const button of files.children) button.setAttribute('aria-pressed', String(button.dataset.file === name));
  64. list.replaceChildren();
  65. for (const record of data.records.filter(record => record.template === name)) {
  66. const card = make('section', 'dq-card');
  67. card.append(make('h3', '', `${record.file} · ${record.sheet} · 第 ${record.row} 行`));
  68. card.append(make('p', 'dq-problem', `${record.message};${record.action}`));
  69. if (record.data_error) card.append(make('p', 'dq-problem', record.data_error));
  70. if (record.data) {
  71. const fields = make('dl', 'dq-data');
  72. for (const [key, value] of Object.entries(record.data)) {
  73. const missing = record.fields.includes(key);
  74. fields.append(make('dt', missing ? 'dq-missing' : '', key));
  75. const empty = value === null || value === undefined || (typeof value === 'string' && !value.trim());
  76. fields.append(make('dd', missing ? 'dq-missing' : '', empty ? '(空)' : typeof value === 'object' ? JSON.stringify(value) : String(value)));
  77. }
  78. card.append(fields);
  79. }
  80. list.append(card);
  81. }
  82. };
  83. for (const name of names) {
  84. const button = make('button', 'dq-file', `${name}.xlsx (${data.datasets[name]})`);
  85. button.type = 'button'; button.dataset.file = name;
  86. button.onclick = () => showFile(name); files.append(button);
  87. }
  88. body.append(files, list); showFile(selectedFile);
  89. }
  90. async function loadDetails() {
  91. const id = ++generation; loading = true;
  92. body.replaceChildren(make('p', 'dq-status', '正在读取问题记录…'));
  93. try {
  94. let data;
  95. try { data = await request('?details=true' + (summary ? '&data_version=' + encodeURIComponent(summary.data_version) : '')); }
  96. catch (error) { if (error.status !== 409) throw error; data = await request('?details=true'); }
  97. if (id !== generation) return;
  98. updateBadge(data); render(data);
  99. } catch (error) { if (id === generation) showError(error); }
  100. finally { if (id === generation) loading = false; }
  101. }
  102. trigger.onclick = () => { dialog.showModal(); loadDetails(); };
  103. async function refresh() {
  104. if (loading) return;
  105. try {
  106. const previous = summary?.data_version;
  107. const data = await request(); updateBadge(data);
  108. if (dialog.open && previous !== data.data_version) loadDetails();
  109. } catch (error) {
  110. trigger.hidden = false; trigger.classList.add('dq-unavailable');
  111. badge.textContent = '数据提醒暂不可用'; trigger.title = error.message;
  112. trigger.setAttribute('aria-label', '数据提醒暂不可用,点击重试');
  113. }
  114. }
  115. refresh(); setInterval(refresh, 30000);
  116. window.addEventListener('focus', refresh);
  117. })();