rebuild(stage-6): deliver emotion and market pools

This commit is contained in:
leefer
2026-07-30 03:03:53 +08:00
parent 889963862a
commit 59f6011ae8
30 changed files with 1751 additions and 9 deletions
+19
View File
@@ -56,6 +56,17 @@ export type ChartSeries = {
points: ChartPoint[];
};
export type MarketWorkspaceData = {
trade_date: string | null;
observed_at?: string;
carried_forward: boolean;
message: string;
overview: Record<string, number>;
sentiment?: Record<string, unknown>;
history?: Record<string, unknown>[];
items?: Record<string, unknown>[];
};
export const marketApi = {
summary(date?: string): Promise<MarketSummary> {
const query = date ? `?date=${encodeURIComponent(date)}` : "";
@@ -69,4 +80,12 @@ export const marketApi = {
const identifier = encodeURIComponent(entity.identifier);
return api.get<ChartSeries>(`/market/entities/${type}/${identifier}/charts/${interval}`);
},
workspace(key: string, date: string): Promise<MarketWorkspaceData> {
return api.get<MarketWorkspaceData>(
`/market/workspaces/${encodeURIComponent(key)}?date=${encodeURIComponent(date)}`,
);
},
syncSnapshot(date: string): Promise<{ trade_date: string; temperature: number }> {
return api.post(`/market/snapshot-sync?date=${encodeURIComponent(date)}`);
},
};
@@ -0,0 +1,45 @@
<script setup lang="ts">
type TableColumn = {
key: string;
label: string;
numeric?: boolean;
wide?: boolean;
code?: boolean;
sortable?: boolean;
format?: (value: unknown, row: Record<string, unknown>) => string;
};
defineProps<{
columns: TableColumn[];
rows: Record<string, unknown>[];
sortKey: string;
sortDirection: "asc" | "desc";
}>();
const emit = defineEmits<{ sort: [key: string] }>();
</script>
<template>
<div class="data-table-wrap">
<table class="data-table">
<thead>
<tr>
<th class="index-column">序号</th>
<th v-for="column in columns" :key="column.key" :class="{ numeric: column.numeric, 'wide-column': column.wide, 'code-column': column.code }">
<button v-if="column.sortable" type="button" @click="emit('sort', column.key)">
{{ column.label }}<span v-if="sortKey === column.key">{{ sortDirection === "asc" ? "↑" : "↓" }}</span>
</button>
<span v-else>{{ column.label }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in rows" :key="String(row.identifier ?? index)">
<td class="index-column numeric">{{ index + 1 }}</td>
<td v-for="column in columns" :key="column.key" :class="{ numeric: column.numeric, 'wide-column': column.wide, 'code-column': column.code, up: column.key.includes('change') && Number(row[column.key]) > 0, down: column.key.includes('change') && Number(row[column.key]) < 0 }">
{{ column.format ? column.format(row[column.key], row) : (row[column.key] ?? "") }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
@@ -0,0 +1,118 @@
.market-workspace-page {
display: grid;
align-content: start;
gap: var(--layout-gap);
}
.market-workspace-page > .page-header {
margin-bottom: 0;
}
.market-page-header {
justify-content: space-between;
}
.workspace-state {
min-height: var(--s-200);
display: grid;
place-items: center;
color: var(--color-text-secondary);
}
.emotion-overview,
.performance-layout,
.pool-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--s-320);
gap: var(--layout-gap);
align-items: start;
}
.temperature-chart-wrap {
min-height: var(--s-200);
padding: var(--s-10) var(--s-14);
}
.temperature-chart {
width: 100%;
height: var(--s-200);
overflow: visible;
}
.temperature-grid { stroke: var(--color-divider); stroke-width: var(--s-1); }
.temperature-line { fill: none; stroke: var(--color-primary); stroke-width: var(--s-2); }
.temperature-average { fill: none; stroke: var(--color-warning); stroke-width: var(--s-2); stroke-dasharray: var(--s-6) var(--s-4); }
.temperature-point { fill: var(--color-surface); stroke: var(--color-primary); stroke-width: var(--s-2); }
.phase-body { display: grid; gap: var(--s-8); padding: var(--s-14); }
.phase-name { color: var(--color-primary); font-size: var(--font-18); font-weight: var(--weight-800); }
.phase-confidence { color: var(--color-text-secondary); font-size: var(--font-12); }
.phase-body > p { color: var(--color-text-secondary); line-height: var(--s-20); }
.phase-metrics { display: flex; flex-wrap: wrap; gap: var(--s-8) var(--s-14); color: var(--color-text-secondary); font-size: var(--font-12); }
.phase-metrics strong { color: var(--color-text); }
.phase-warning { margin-top: var(--s-4); }
.component-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--s-14);
padding: var(--s-14);
}
.component-item { display: grid; gap: var(--s-6); }
.component-item > div:first-child { display: flex; justify-content: space-between; gap: var(--s-8); }
.component-item small { color: var(--color-text-faint); font-size: var(--font-11); }
.score-track { height: var(--s-6); overflow: hidden; border-radius: var(--radius-round); background: var(--color-surface-muted); }
.score-track span { display: block; height: 100%; border-radius: inherit; background: var(--color-primary); }
.history-header { justify-content: space-between; }
.history-header .seg-control { margin-left: 0; }
.emotion-history-wrap { max-height: none; }
.pool-layout { grid-template-columns: minmax(0, 1fr) var(--s-320); }
.pool-layout-single { grid-template-columns: minmax(0, 1fr); }
.pool-table-card { min-width: 0; overflow: hidden; }
.pool-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); }
.pool-filters { margin-left: 0; }
.pool-search { width: var(--s-260); min-height: var(--s-32); margin-left: auto; }
.pool-aside { display: grid; gap: var(--layout-gap); }
.pool-stat-body { display: grid; gap: var(--s-6); padding: var(--s-16); }
.pool-stat-body strong { color: var(--color-primary); font-size: var(--font-18); }
.pool-stat-body span, .risk-note { color: var(--color-text-secondary); font-size: var(--font-12); }
.sector-list { display: grid; gap: var(--s-1); margin: 0; padding: var(--s-8) var(--s-14); list-style: none; }
.sector-list li { display: flex; justify-content: space-between; gap: var(--s-8); padding: var(--s-8) 0; border-bottom: var(--s-1) solid var(--color-divider); }
.sector-list strong { font-variant-numeric: tabular-nums; }
.risk-note { padding: 0 var(--s-14) var(--s-14); line-height: var(--s-18); }
.pool-summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--layout-gap); }
.summary-metric { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-12); padding: var(--s-12) var(--s-14); }
.summary-metric span { color: var(--color-text-secondary); }
.summary-metric strong { font-size: var(--font-17); }
.data-table-wrap { width: 100%; overflow-x: auto; }
.data-table th.numeric, .data-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; }
.data-table th.index-column, .data-table td.index-column { width: var(--s-44); }
.data-table th.code-column, .data-table td.code-column { min-width: var(--s-64); }
.data-table th.wide-column, .data-table td.wide-column { width: var(--s-200); min-width: var(--s-200); white-space: normal; }
.data-table th button { display: inline-flex; align-items: center; gap: var(--s-4); padding: 0; color: inherit; background: var(--c-transparent); font-weight: inherit; }
.performance-layout { grid-template-columns: minmax(0, 1fr) var(--s-320); }
.performance-levels { min-width: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--s-320), 1fr)); gap: var(--layout-gap); }
.performance-level-card { min-width: 0; padding: var(--s-12) var(--s-14); }
.performance-level-card > header, .performance-level-card > footer { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--s-8) var(--s-14); }
.performance-level-card > header span, .performance-level-card > footer { color: var(--color-text-secondary); font-size: var(--font-11); }
.performance-outcomes { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--s-6); margin: var(--s-12) 0; }
.performance-outcomes span { display: grid; gap: var(--s-4); padding: var(--s-8); border-radius: var(--control-radius); background: var(--color-surface-muted); color: var(--color-text-secondary); font-size: var(--font-11); text-align: center; }
.performance-outcomes b { color: var(--color-text); font-size: var(--font-15); }
.performance-conclusion { position: sticky; top: calc(var(--shell-topbar-height) + var(--shell-summary-height) + var(--s-12)); }
.performance-conclusion p { color: var(--color-text-secondary); line-height: var(--s-20); }
.performance-conclusion dl { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-8); margin: var(--s-14) 0 0; }
.performance-conclusion dl div { padding: var(--s-8); border-radius: var(--control-radius); background: var(--color-surface-muted); }
.performance-conclusion dt { color: var(--color-text-secondary); font-size: var(--font-11); }
.performance-conclusion dd { margin: var(--s-4) 0 0; font-weight: var(--weight-700); font-variant-numeric: tabular-nums; }
.breadth-body { display: grid; gap: var(--s-10); padding: var(--s-14); }
.breadth-legend { display: flex; flex-wrap: wrap; gap: var(--s-8) var(--s-20); color: var(--color-text-secondary); }
.breadth-track { height: var(--s-10); display: flex; overflow: hidden; border-radius: var(--radius-round); background: var(--color-surface-muted); }
.breadth-track span { height: 100%; }
.breadth-up { background: var(--color-up); }
.breadth-flat { background: var(--color-text-faint); }
.breadth-down { background: var(--color-down); }
@@ -163,4 +163,38 @@
.member-editor {
position: static;
}
.emotion-overview,
.pool-layout,
.performance-layout {
grid-template-columns: minmax(0, 1fr);
}
.component-grid {
grid-template-columns: 1fr 1fr;
}
.pool-toolbar {
align-items: stretch;
flex-wrap: wrap;
}
.pool-filters {
width: 100%;
overflow-x: auto;
}
.pool-search {
width: 100%;
margin-left: 0;
}
.pool-summary-grid,
.performance-levels {
grid-template-columns: minmax(0, 1fr);
}
.performance-conclusion {
position: static;
}
}