| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- (() => {
- // The graph page owns the reminder when the chat is embedded inside it.
- if (window.self !== window.top) return;
- const header = document.querySelector('header');
- if (!header) return;
- const make = (tag, className, text) => {
- const element = document.createElement(tag);
- if (className) element.className = className;
- if (text !== undefined) element.textContent = text;
- return element;
- };
- const trigger = make('button', 'dq-trigger');
- trigger.type = 'button'; trigger.hidden = true;
- trigger.id = 'dataQualityAlert';
- trigger.setAttribute('aria-haspopup', 'dialog');
- trigger.append(make('span', 'dq-icon', '!'));
- const badge = make('span', '', '数据问题'); trigger.append(badge);
- header.append(trigger);
- const dialog = make('dialog', 'dq-dialog');
- dialog.id = 'dataQualityDialog'; dialog.setAttribute('aria-labelledby', 'dqTitle');
- const heading = make('div', 'dq-head');
- const title = make('h2', '', '数据质量提醒'); title.id = 'dqTitle';
- const close = make('button', 'dq-close', '×'); close.type = 'button';
- close.setAttribute('aria-label', '关闭数据质量提醒');
- heading.append(title, close);
- const body = make('div', 'dq-body'); body.setAttribute('aria-live', 'polite');
- dialog.append(heading, body); document.body.append(dialog);
- close.onclick = () => dialog.close();
- dialog.addEventListener('click', event => { if (event.target === dialog) {
- const box = dialog.getBoundingClientRect();
- if (event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom) dialog.close();
- }});
- let summary = null, selectedFile = '', generation = 0, loading = false;
- const endpoint = () => `${typeof apiBase === 'function' ? apiBase() : location.origin}/api/data-quality`;
- async function request(parameters = '') {
- const response = await fetch(endpoint() + parameters, {cache:'no-store', signal:AbortSignal.timeout(15000)});
- const data = await response.json();
- if (!response.ok) { const error = new Error(data.detail || '读取失败,请重试'); error.status = response.status; throw error; }
- return data;
- }
- function updateBadge(data) {
- summary = data; trigger.hidden = data.total === 0;
- trigger.classList.remove('dq-unavailable');
- badge.textContent = `数据问题 ${data.total}`;
- trigger.setAttribute('aria-label', `${Object.keys(data.datasets).length} 份文件共 ${data.total} 条数据问题,点击查看`);
- trigger.title = trigger.getAttribute('aria-label');
- }
- function showError(error) {
- body.replaceChildren(make('p', 'dq-error', error.message || '读取失败,请稍后重试'));
- const retry = make('button', 'dq-file', '重新加载'); retry.onclick = loadDetails; body.append(retry);
- }
- function render(data) {
- body.replaceChildren();
- body.append(make('p', 'dq-note', '以下记录因缺少主键未进入知识图谱。请在源数据中补齐后重新更新;此处展示该次发布保留的原始记录。'));
- if (!data.total) { body.append(make('p', 'dq-status', '当前版本没有缺少主键的记录。')); return; }
- const names = Object.keys(data.datasets);
- if (!names.includes(selectedFile)) selectedFile = names[0];
- body.append(make('p', 'dq-status', `共 ${names.length} 份文件,${data.total} 条问题记录`));
- const files = make('div', 'dq-files'); files.setAttribute('aria-label', '按来源文件查看');
- const list = make('div');
- const showFile = name => {
- selectedFile = name;
- for (const button of files.children) button.setAttribute('aria-pressed', String(button.dataset.file === name));
- list.replaceChildren();
- for (const record of data.records.filter(record => record.template === name)) {
- const card = make('section', 'dq-card');
- card.append(make('h3', '', `${record.file} · ${record.sheet} · 第 ${record.row} 行`));
- card.append(make('p', 'dq-problem', `${record.message};${record.action}`));
- if (record.data_error) card.append(make('p', 'dq-problem', record.data_error));
- if (record.data) {
- const fields = make('dl', 'dq-data');
- for (const [key, value] of Object.entries(record.data)) {
- const missing = record.fields.includes(key);
- fields.append(make('dt', missing ? 'dq-missing' : '', key));
- const empty = value === null || value === undefined || (typeof value === 'string' && !value.trim());
- fields.append(make('dd', missing ? 'dq-missing' : '', empty ? '(空)' : typeof value === 'object' ? JSON.stringify(value) : String(value)));
- }
- card.append(fields);
- }
- list.append(card);
- }
- };
- for (const name of names) {
- const button = make('button', 'dq-file', `${name}.xlsx (${data.datasets[name]})`);
- button.type = 'button'; button.dataset.file = name;
- button.onclick = () => showFile(name); files.append(button);
- }
- body.append(files, list); showFile(selectedFile);
- }
- async function loadDetails() {
- const id = ++generation; loading = true;
- body.replaceChildren(make('p', 'dq-status', '正在读取问题记录…'));
- try {
- let data;
- try { data = await request('?details=true' + (summary ? '&data_version=' + encodeURIComponent(summary.data_version) : '')); }
- catch (error) { if (error.status !== 409) throw error; data = await request('?details=true'); }
- if (id !== generation) return;
- updateBadge(data); render(data);
- } catch (error) { if (id === generation) showError(error); }
- finally { if (id === generation) loading = false; }
- }
- trigger.onclick = () => { dialog.showModal(); loadDetails(); };
- async function refresh() {
- if (loading) return;
- try {
- const previous = summary?.data_version;
- const data = await request(); updateBadge(data);
- if (dialog.open && previous !== data.data_version) loadDetails();
- } catch (error) {
- trigger.hidden = false; trigger.classList.add('dq-unavailable');
- badge.textContent = '数据提醒暂不可用'; trigger.title = error.message;
- trigger.setAttribute('aria-label', '数据提醒暂不可用,点击重试');
- }
- }
- refresh(); setInterval(refresh, 30000);
- window.addEventListener('focus', refresh);
- })();
|