Files
group_fqcd_jr/app/static/portal/customer/dashboard/dashboard.js
T

109 lines
5.9 KiB
JavaScript

import { apiClient } from '/static/portal/common/api-client.js?v=20260913';
import { requireCustomer } from '/static/portal/common/auth.js?v=20260913';
import { mountShell } from '/static/portal/common/layout/app-shell.js';
import { applyPermissionGuard } from '/static/portal/common/permission-guard.js';
import { renderError, renderLoading } from '/static/portal/common/state-view.js?v=20260912';
import * as accountSummary from '/static/portal/customer/dashboard/modules/account-summary.js';
import * as holdingsTable from '/static/portal/customer/dashboard/modules/holdings-table.js';
import * as portfolioSummary from '/static/portal/customer/dashboard/modules/portfolio-summary.js';
import * as quickActions from '/static/portal/customer/dashboard/modules/quick-actions.js';
import { updateRefreshIndicator } from '/static/portal/customer/dashboard/modules/refresh-indicator.js';
if (requireCustomer()) {
mountShell({ active: 'dashboard', mode: 'customer' });
const state = { data: null, fetchedAt: 0, controller: null };
const containers = {
account: document.querySelector('[data-account-summary]'),
portfolio: document.querySelector('[data-portfolio-summary]'),
holdings: document.querySelector('[data-holdings-table]'),
};
const refreshButton = document.querySelector('[data-refresh]');
const refreshTime = document.querySelector('[data-refresh-time]');
const alert = document.querySelector('[data-dashboard-alert]');
const dialog = document.querySelector('[data-order-dialog]');
const quickActionsContainer = document.querySelector('[data-quick-actions]');
function showDashboardSections() {
Object.values(containers).forEach((container) => { container.hidden = false; });
quickActionsContainer.hidden = false;
}
function showLoading() {
showDashboardSections();
Object.values(containers).forEach((container) => renderLoading(container, 1));
}
function renderPageError(error) {
containers.account.hidden = false;
containers.portfolio.hidden = true;
containers.holdings.hidden = true;
quickActionsContainer.hidden = true;
renderError(containers.account, error, () => load({ force: true }));
}
function renderData(data, cached = false) {
showDashboardSections();
accountSummary.render(containers.account, data.account);
portfolioSummary.render(containers.portfolio, data);
holdingsTable.render(containers.holdings, data.holdings || []);
quickActions.render(quickActionsContainer);
updateRefreshIndicator(refreshTime, data.as_of, cached);
applyPermissionGuard();
document.querySelector('[data-open-order]')?.addEventListener('click', () => dialog.showModal());
const productCode = new URLSearchParams(window.location.search).get('product_code');
const target = productCode ? document.querySelector(`[data-product-code="${CSS.escape(productCode)}"]`) : null;
if (target) { target.classList.add('dashboard-holdings__highlight'); target.scrollIntoView({ block: 'center' }); window.setTimeout(() => target.classList.remove('dashboard-holdings__highlight'), 1500); }
if (new URLSearchParams(window.location.search).get('action') === 'trade') {
const orderForm = document.querySelector('[data-order-form]');
const productInput = orderForm?.querySelector('[name="product_code"]');
if (productInput && productCode) productInput.value = productCode;
if (dialog?.showModal && productCode && !dialog.open) dialog.showModal();
}
}
async function load({ force = false } = {}) {
if (!force && state.data && Date.now() - state.fetchedAt < 60000) { renderData(state.data, true); return; }
state.controller?.abort();
state.controller = new AbortController();
showLoading();
refreshButton.disabled = true;
const startedAt = performance.now();
try {
const response = await apiClient.get('T001', { signal: state.controller.signal });
state.data = response.data;
state.fetchedAt = Date.now();
renderData(response.data);
apiClient.track('dashboard_refresh', { source: force ? 'manual' : 'load', duration_ms: Math.round(performance.now() - startedAt) });
} catch (error) {
apiClient.reportError(error);
renderPageError(error);
} finally { refreshButton.disabled = false; }
}
refreshButton.addEventListener('click', () => load());
document.querySelector('[data-order-close]').addEventListener('click', () => dialog.close());
document.querySelector('[data-order-form]').addEventListener('submit', async (event) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const orderAlert = document.querySelector('[data-order-alert]');
const submit = form.querySelector('[type="submit"]');
orderAlert.classList.remove('form-alert--visible');
submit.disabled = true;
const body = { product_code: String(formData.get('product_code') || '').trim(), order_side: String(formData.get('order_side')), quantity: String(formData.get('quantity')), price_type: 'market' };
apiClient.track('order_submit_attempt', { product_code: body.product_code, side: body.order_side, quantity: body.quantity });
try {
const response = await apiClient.post('T002', body);
apiClient.track('order_submit_result', { order_no: response.data.order_no, status: response.data.status });
alert.textContent = `委托 ${response.data.order_no} 已${response.data.status}`;
alert.classList.add('dashboard-alert--visible');
dialog.close();
form.reset();
await load({ force: true });
} catch (error) {
apiClient.reportError(error);
apiClient.track('order_submit_result', { status: 'failed', error_code: error.code });
orderAlert.textContent = error.message;
orderAlert.classList.add('form-alert--visible');
} finally { submit.disabled = false; }
});
window.addEventListener('pagehide', () => state.controller?.abort());
apiClient.track('page_view', { page: 'dashboard', portal: 'customer' });
load();
}