rebuild(stage-8): deliver market insight workspaces

This commit is contained in:
leefer
2026-07-30 04:12:04 +08:00
parent a18e8e9d27
commit 976a5cac03
39 changed files with 3671 additions and 14 deletions
+53
View File
@@ -77,6 +77,41 @@ export type RotationMembersData = {
items: Record<string, unknown>[];
};
export type MarketInsightData = MarketWorkspaceData & {
requested_date: string;
previous_date?: string;
phase?: "pending" | "observing" | "selection" | "finalized" | "archive";
current_available?: boolean;
coverage?: number;
summary: Record<string, unknown>;
expectations?: Record<string, number>;
themes?: { carry: Record<string, unknown>[]; new_themes: Record<string, unknown>[] };
amount_history?: Record<string, unknown>[];
focus_rows?: Record<string, unknown>[];
rows?: Record<string, unknown>[];
one_price_rows?: Record<string, unknown>[];
watchlist_rows?: Record<string, unknown>[];
watchlist_ready?: boolean;
combined?: Record<string, unknown>[];
ths?: Record<string, unknown>[];
dc?: Record<string, unknown>[];
status?: string;
traders?: Record<string, unknown>[];
operations?: Record<string, unknown>[];
unclassified_seats?: Record<string, unknown>[];
profiles?: Record<string, unknown>[];
};
export type ThemeDetailData = {
trade_date: string;
observed_at?: string;
state?: string;
message: string;
theme: Record<string, unknown>;
summary: Record<string, unknown>;
members: Record<string, unknown>[];
};
export const marketApi = {
summary(date?: string): Promise<MarketSummary> {
const query = date ? `?date=${encodeURIComponent(date)}` : "";
@@ -103,4 +138,22 @@ export const marketApi = {
`/market/rotation-members?sector=${encodeURIComponent(sector)}&date=${encodeURIComponent(date)}`,
);
},
insight(key: string, date: string): Promise<MarketInsightData> {
return api.get<MarketInsightData>(
`/market/insights/${encodeURIComponent(key)}?date=${encodeURIComponent(date)}`,
);
},
syncInsight(key: string, date: string): Promise<MarketInsightData> {
return api.post<MarketInsightData>(
`/market/insights/${encodeURIComponent(key)}/sync?date=${encodeURIComponent(date)}`,
);
},
themeDetail(identifier: string, date: string): Promise<ThemeDetailData> {
return api.get<ThemeDetailData>(
`/market/themes/${encodeURIComponent(identifier)}?date=${encodeURIComponent(date)}`,
);
},
saveSeatAlias(seat_name: string, alias_name: string): Promise<{ seat_name: string; alias_name: string }> {
return api.put("/market/seat-aliases", { seat_name, alias_name });
},
};
+43
View File
@@ -0,0 +1,43 @@
export type SortDirection = "asc" | "desc";
export function sortRows(
rows: Record<string, unknown>[],
key: string,
direction: SortDirection,
): Record<string, unknown>[] {
const multiplier = direction === "asc" ? 1 : -1;
return [...rows].sort((left, right) => compare(left[key], right[key]) * multiplier);
}
export function formatNumber(value: unknown, digits = 2): string {
if (value === null || value === undefined || value === "") return "";
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed.toFixed(digits) : "";
}
export function formatAmount(value: unknown): string {
if (value === null || value === undefined || value === "") return "";
const parsed = Number(value);
if (!Number.isFinite(parsed)) return "";
return parsed >= 100_000_000
? `${(parsed / 100_000_000).toFixed(2)} 亿`
: `${(parsed / 10_000).toFixed(2)}`;
}
export function exportCsv(filename: string, rows: string[][]): void {
const content = rows
.map((row) => row.map((value) => `"${value.replaceAll('"', '""')}"`).join(","))
.join("\r\n");
const link = document.createElement("a");
link.href = URL.createObjectURL(new Blob(["\ufeff", content], { type: "text/csv;charset=utf-8" }));
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
function compare(left: unknown, right: unknown): number {
const a = Number(left);
const b = Number(right);
if (Number.isFinite(a) && Number.isFinite(b)) return a - b;
return String(left ?? "").localeCompare(String(right ?? ""), "zh-CN");
}
@@ -0,0 +1,187 @@
.search-control {
min-height: var(--s-32);
display: flex;
align-items: center;
gap: var(--s-8);
padding: 0 var(--s-10);
border: var(--s-1) solid var(--color-border);
border-radius: var(--control-radius);
color: var(--color-text-secondary);
background: var(--color-surface);
font-size: var(--font-11);
}
.search-control input {
min-width: 0;
flex: 1;
border: 0;
color: var(--color-text);
background: var(--c-transparent);
}
.search-control input::placeholder { color: var(--color-text-faint); }
.auction-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--s-360);
gap: var(--layout-gap);
align-items: start;
}
.auction-main,
.popularity-table-card,
.theme-detail-card,
.dragon-operations {
min-width: 0;
overflow: hidden;
}
.auction-dataset-bar {
min-height: var(--s-46);
display: flex;
align-items: stretch;
border-bottom: var(--s-1) solid var(--color-divider);
}
.auction-datasets {
align-self: center;
margin-left: var(--s-10);
}
.auction-datasets b { margin-left: var(--s-4); font-variant-numeric: tabular-nums; }
.auction-inline-summary {
min-width: var(--s-360);
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin: 0 0 0 auto;
border-left: var(--s-1) solid var(--color-divider);
}
.auction-inline-summary div {
display: grid;
align-content: center;
gap: var(--s-2);
padding: var(--s-6) var(--s-8);
border-right: var(--s-1) solid var(--color-divider);
text-align: right;
}
.auction-inline-summary div:last-child { border-right: 0; }
.auction-inline-summary dt { color: var(--color-text-faint); font-size: var(--font-10-5); }
.auction-inline-summary dd { margin: 0; font-size: var(--font-12); font-weight: var(--weight-700); font-variant-numeric: tabular-nums; }
.auction-toolbar,
.popularity-toolbar,
.dragon-toolbar {
min-height: var(--s-46);
display: flex;
align-items: center;
gap: var(--s-8);
padding: var(--s-7) var(--s-10);
border-bottom: var(--s-1) solid var(--color-divider);
}
.auction-toolbar .seg-control,
.popularity-toolbar .seg-control,
.dragon-toolbar .seg-control { margin-left: 0; }
.auction-toolbar .search-control,
.popularity-toolbar .search-control,
.dragon-toolbar .search-control { width: var(--s-260); margin-left: auto; }
.auction-side { display: grid; gap: var(--layout-gap); }
.auction-evidence-list { display: grid; gap: var(--s-1); padding: var(--s-6) var(--s-14) var(--s-10); }
.auction-evidence-list > div { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-2) var(--s-8); padding: var(--s-7) 0; border-bottom: var(--s-1) solid var(--color-divider); }
.auction-evidence-list > div:last-of-type { border-bottom: 0; }
.auction-evidence-list strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.auction-evidence-list span { color: var(--color-primary); font-size: var(--font-11); }
.auction-evidence-list small { grid-column: 1 / -1; color: var(--color-text-secondary); font-size: var(--font-10-5); }
.auction-evidence-list > p { padding: var(--s-14) 0; text-align: center; }
.auction-bars { display: grid; gap: var(--s-8); padding: var(--s-12) var(--s-14); }
.auction-bars > div { display: grid; grid-template-columns: var(--s-44) minmax(0, 1fr) var(--s-44); align-items: center; gap: var(--s-8); font-size: var(--font-10-5); }
.auction-bars i { height: var(--s-7); overflow: hidden; border-radius: var(--radius-round); background: var(--color-surface-muted); }
.auction-bars b { display: block; height: 100%; border-radius: inherit; background: var(--color-primary); }
.auction-bars strong { text-align: right; font-variant-numeric: tabular-nums; }
.themes-layout {
display: grid;
grid-template-columns: var(--s-320) minmax(0, 1fr);
gap: var(--layout-gap);
align-items: start;
}
.theme-directory { position: relative; min-width: 0; }
.theme-search { margin: var(--s-10); }
.theme-rank-list { max-height: calc(100vh - var(--s-260)); overflow-y: auto; border-top: var(--s-1) solid var(--color-divider); }
.theme-rank-list > button { width: 100%; display: grid; grid-template-columns: var(--s-28) minmax(0, 1fr) var(--s-56); align-items: center; gap: var(--s-8); padding: var(--s-8) var(--s-10); border-bottom: var(--s-1) solid var(--color-divider); background: var(--color-surface); text-align: left; }
.theme-rank-list > button:hover,
.theme-rank-list > button.active { background: var(--color-primary-soft); }
.theme-rank-list > button > b { color: var(--color-text-faint); text-align: center; font-variant-numeric: tabular-nums; }
.theme-rank-list > button:nth-child(-n+3) > b { color: var(--color-warning); font-size: var(--font-14); }
.theme-rank-list > button > span { min-width: 0; display: grid; gap: var(--s-2); }
.theme-rank-list strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.theme-rank-list small { color: var(--color-text-faint); font-size: var(--font-10-5); }
.theme-rank-list em { font-style: normal; text-align: right; font-size: var(--font-11); font-variant-numeric: tabular-nums; }
.theme-preview-popover { position: absolute; top: var(--s-64); left: calc(100% + var(--layout-gap)); z-index: var(--z-popover); width: var(--s-400); overflow: hidden; border: var(--s-1) solid var(--color-border); border-radius: var(--card-radius); background: var(--color-surface-raised); box-shadow: var(--shadow-float); }
.theme-summary-strip { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); border-bottom: var(--s-1) solid var(--color-divider); }
.theme-summary-strip > div { display: grid; gap: var(--s-4); padding: var(--s-10) var(--s-14); border-right: var(--s-1) solid var(--color-divider); }
.theme-summary-strip > div:last-child { border-right: 0; }
.theme-summary-strip span { color: var(--color-text-secondary); font-size: var(--font-11); }
.theme-summary-strip strong { font-size: var(--font-15); font-variant-numeric: tabular-nums; }
.popularity-summary-strip {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
overflow: hidden;
border: var(--s-1) solid var(--color-border);
border-radius: var(--card-radius);
background: var(--color-canvas);
}
.popularity-summary-strip article { min-width: 0; display: grid; gap: var(--s-8); padding: var(--s-12) var(--s-14); border-right: var(--s-1) solid var(--color-border); box-shadow: none; }
.popularity-summary-strip article:nth-child(1) { background: var(--color-primary-soft); }
.popularity-summary-strip article:nth-child(2) { background: var(--color-up-soft); }
.popularity-summary-strip article:nth-child(3) { border-right: 0; background: var(--color-warning-soft); }
.popularity-summary-strip header { display: flex; justify-content: space-between; gap: var(--s-8); }
.popularity-summary-strip header span { color: var(--color-text-secondary); }
.popularity-summary-strip header strong { font-variant-numeric: tabular-nums; }
.popularity-summary-strip p { overflow: hidden; color: var(--color-text); font-size: var(--font-12); text-overflow: ellipsis; white-space: nowrap; }
.dragon-summary-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--layout-gap); }
.dragon-summary-grid article { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-8); padding: var(--s-12) var(--s-14); }
.dragon-summary-grid span { color: var(--color-text-secondary); font-size: var(--font-11); }
.dragon-summary-grid strong { font-size: var(--font-15); font-variant-numeric: tabular-nums; }
.dragon-daily-layout { display: grid; grid-template-columns: var(--s-320) minmax(0, 1fr); gap: var(--layout-gap); align-items: start; }
.dragon-traders { min-width: 0; overflow: hidden; }
.dragon-traders .card-header .btn { margin-left: auto; }
.dragon-trader-list { display: grid; }
.dragon-trader-list > button { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-8); padding: var(--s-9) var(--s-12); border-bottom: var(--s-1) solid var(--color-divider); background: var(--color-surface); text-align: left; }
.dragon-trader-list > button:hover,
.dragon-trader-list > button.active { background: var(--color-primary-soft); }
.dragon-trader-list > button span { min-width: 0; display: grid; gap: var(--s-2); }
.dragon-trader-list small { overflow: hidden; color: var(--color-text-secondary); font-size: var(--font-10-5); text-overflow: ellipsis; white-space: nowrap; }
.dragon-trader-list b { align-self: center; font-variant-numeric: tabular-nums; }
.dragon-trader-list > p { padding: var(--s-20); text-align: center; }
.dragon-pending { border-top: var(--s-1) solid var(--color-divider); }
.dragon-pending > header { display: flex; justify-content: space-between; padding: var(--s-10) var(--s-12); background: var(--color-warning-soft); }
.dragon-pending > div { display: grid; gap: var(--s-6); padding: var(--s-8) var(--s-12); border-top: var(--s-1) solid var(--color-divider); }
.dragon-pending p,
.dragon-pending form { display: flex; align-items: center; gap: var(--s-8); }
.dragon-pending p span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dragon-pending input { min-width: 0; flex: 1; min-height: var(--s-32); padding: 0 var(--s-8); border: var(--s-1) solid var(--color-border); border-radius: var(--control-radius); background: var(--color-surface); }
.dragon-table-scroll { height: var(--s-400); overflow: auto; }
.dragon-table-scroll .data-table-wrap { overflow: visible; }
.dragon-profiles { min-height: var(--s-400); display: grid; grid-template-columns: var(--s-320) minmax(0, 1fr); overflow: hidden; }
.dragon-profiles > aside { border-right: var(--s-1) solid var(--color-divider); }
.dragon-profiles > aside > div { max-height: var(--s-400); overflow-y: auto; }
.dragon-profiles > aside button { width: 100%; display: grid; gap: var(--s-2); padding: var(--s-9) var(--s-14); border-bottom: var(--s-1) solid var(--color-divider); background: var(--color-surface); text-align: left; }
.dragon-profiles > aside button.active,
.dragon-profiles > aside button:hover { background: var(--color-primary-soft); }
.dragon-profiles > aside small { color: var(--color-text-secondary); font-size: var(--font-10-5); }
.dragon-profiles > article { display: grid; align-content: start; gap: var(--s-20); padding: var(--s-20); }
.dragon-profiles > article > header { display: flex; justify-content: space-between; gap: var(--s-14); }
.dragon-profiles > article > header p { margin-top: var(--s-8); color: var(--color-text-secondary); line-height: var(--s-20); }
.dragon-profiles h3 { margin: 0 0 var(--s-8); font-size: var(--font-13); }
.dragon-profiles ul { display: flex; flex-wrap: wrap; gap: var(--s-8); margin: 0; padding: 0; list-style: none; }
.dragon-profiles li { padding: var(--s-6) var(--s-8); border: var(--s-1) solid var(--color-border); border-radius: var(--control-radius); background: var(--color-surface-muted); font-size: var(--font-11); }
@@ -245,3 +245,92 @@
border-bottom: var(--s-1) solid var(--color-divider);
}
}
@media (max-width: 1399px) {
.auction-layout {
grid-template-columns: minmax(0, 1fr);
}
.auction-side {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.auction-dataset-bar {
align-items: stretch;
flex-direction: column;
}
.auction-datasets {
align-self: stretch;
overflow-x: auto;
margin: var(--s-7) var(--s-10);
}
.auction-inline-summary {
width: 100%;
min-width: 0;
margin-left: 0;
border-top: var(--s-1) solid var(--color-divider);
border-left: 0;
}
}
@media (max-width: 1023px) {
.auction-side,
.themes-layout,
.popularity-summary-strip,
.dragon-summary-grid,
.dragon-daily-layout,
.dragon-profiles {
grid-template-columns: minmax(0, 1fr);
}
.auction-toolbar,
.popularity-toolbar,
.dragon-toolbar {
align-items: stretch;
flex-direction: column;
}
.auction-toolbar .seg-control,
.popularity-toolbar .seg-control,
.dragon-toolbar .seg-control {
width: 100%;
overflow-x: auto;
}
.auction-toolbar .search-control,
.popularity-toolbar .search-control,
.dragon-toolbar .search-control {
width: 100%;
margin-left: 0;
}
.auction-inline-summary,
.theme-summary-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.theme-rank-list {
max-height: var(--s-320);
}
.theme-preview-popover {
display: none;
}
.popularity-summary-strip article,
.dragon-profiles > aside {
border-right: 0;
border-bottom: var(--s-1) solid var(--color-divider);
}
.dragon-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dragon-table-scroll {
height: auto;
overflow: visible;
}
}