rebuild(audit): complete entity detail and market preview
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import { heavenApi, type HeavenReading, type HeavenSetup, type ReadingMode } from "../../shared/api/heaven";
|
||||
import { useMarketStore } from "../../shared/stores/market";
|
||||
@@ -11,9 +12,10 @@ import InterpretDialog from "./InterpretDialog.vue";
|
||||
import TrendPanel from "./TrendPanel.vue";
|
||||
|
||||
const market = useMarketStore();
|
||||
const route = useRoute();
|
||||
const session = useSessionStore();
|
||||
const ui = useUiStore();
|
||||
const mode = ref<ReadingMode>("trend");
|
||||
const mode = ref<ReadingMode>(["trend", "fortune", "heart"].includes(String(route.query.mode)) ? String(route.query.mode) as ReadingMode : "trend");
|
||||
const setup = ref<HeavenSetup | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
@@ -75,7 +77,7 @@ watch([() => market.selectedDate, locked], load, { immediate: true });
|
||||
<div v-if="loading" class="card workspace-state">正在推演当日基础气机</div>
|
||||
<div v-else-if="error" class="notice notice-warning">{{ error }}</div>
|
||||
<div v-else :class="{ 'locked-content': locked }" :aria-disabled="locked">
|
||||
<TrendPanel v-if="mode === 'trend'" :disabled="locked" @interpret="openInterpret" />
|
||||
<TrendPanel v-if="mode === 'trend'" :disabled="locked" :initial-query="String(route.query.query || '')" @interpret="openInterpret" />
|
||||
<FortunePanel v-else-if="mode === 'fortune'" :field="setup?.fortune || null" :daily="setup?.daily_fortune || null" :disabled="locked" @interpret="openInterpret" @saved="saveFortune" />
|
||||
<HeartPanel v-else :disabled="locked" @interpret="openInterpret" />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
import { heavenApi, type HeavenReading } from "../../shared/api/heaven";
|
||||
import { useMarketStore } from "../../shared/stores/market";
|
||||
import { useUiStore } from "../../shared/stores/ui";
|
||||
import HexagramGraphic from "./HexagramGraphic.vue";
|
||||
|
||||
const props = defineProps<{ disabled: boolean }>();
|
||||
const props = defineProps<{ disabled: boolean; initialQuery?: string }>();
|
||||
const emit = defineEmits<{ interpret: [reading: HeavenReading] }>();
|
||||
const market = useMarketStore();
|
||||
const ui = useUiStore();
|
||||
@@ -49,6 +49,13 @@ function reading(): HeavenReading {
|
||||
created_at: "",
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const initial = props.initialQuery?.trim();
|
||||
if (!initial || props.disabled) return;
|
||||
query.value = initial;
|
||||
void load(false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, reactive, ref } from "vue";
|
||||
|
||||
import type { MarketWorkspaceData } from "../../../shared/api/market";
|
||||
import MarketEntityLink from "../../../shared/market/MarketEntityLink.vue";
|
||||
|
||||
const props = defineProps<{ data: MarketWorkspaceData }>();
|
||||
const sortMode = ref<"time" | "open">("time");
|
||||
@@ -88,7 +89,7 @@ function exportCsv(): void {
|
||||
<header><div><strong>{{ group.level === 1 ? "首板" : `${group.level}板` }}</strong><span>{{ group.count }}只</span></div><small v-if="group.level > 1 && Number(group.count)">{{ group.adjacentRate === null ? "相邻梯队断层" : `相邻梯队 ${group.adjacentRate.toFixed(1)}%` }}</small></header>
|
||||
<div class="ladder-stocks">
|
||||
<article v-for="stock in group.visible" :key="String(stock.identifier)" class="ladder-stock">
|
||||
<div><strong>{{ stock.name }}</strong><span>{{ stock.code }}</span></div>
|
||||
<div><strong>{{ stock.name }}</strong><MarketEntityLink v-if="stock.identifier" :entity="{ entity_type: 'stock', identifier: String(stock.identifier), code: String(stock.code), name: String(stock.name), sector: stock.sector ? String(stock.sector) : null }" /></div>
|
||||
<p><b>{{ stock.sector || "其他" }}</b><span>{{ stock.first_time || "时间待校正" }}</span></p>
|
||||
<small>开板 {{ stock.open_times }}次 · 成交 {{ (Number(stock.amount ?? 0) / 100_000_000).toFixed(2) }}亿</small>
|
||||
</article>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { onMounted, reactive, ref } from "vue";
|
||||
|
||||
import { reviewApi, type AlertItem } from "../../shared/api/review";
|
||||
import EmptyState from "../../shared/components/EmptyState.vue";
|
||||
import MarketEntityLink from "../../shared/market/MarketEntityLink.vue";
|
||||
import { useUiStore } from "../../shared/stores/ui";
|
||||
|
||||
const ui = useUiStore();
|
||||
@@ -38,7 +39,7 @@ onMounted(load);
|
||||
<div class="alerts-toolbar"><div class="segmented"><button type="button" :class="{ active: !unreadOnly }" @click="select(false)">全部</button><button type="button" :class="{ active: unreadOnly }" @click="select(true)">未读</button></div><button class="btn btn-small" type="button" @click="markAll">全部已读</button></div>
|
||||
<form class="alert-create" @submit.prevent="create"><div class="alert-create-grid"><label class="field"><span class="field-label">标题</span><input v-model="form.title" class="input" maxlength="80" required /></label><label class="field"><span class="field-label">提醒日期</span><input v-model="form.remind_date" class="input" type="date" required /></label><label class="field"><span class="field-label">股票代码(可选)</span><input v-model="form.code" class="input" maxlength="12" /></label></div><label class="field"><span class="field-label">提醒内容(可选)</span><textarea v-model="form.content" class="textarea" maxlength="500"></textarea></label><div class="form-actions"><button class="btn btn-primary" type="submit">保存提醒</button></div></form>
|
||||
<div v-if="loading" class="workspace-state">正在读取提醒</div>
|
||||
<div v-else-if="items.length" class="alert-list"><article v-for="item in items" :key="item.id" :class="{ unread: !item.is_read && item.due }"><div><strong>{{ item.title }}</strong><span class="tag">{{ item.due ? (item.is_read ? "已读" : "未读") : "未到期" }}</span></div><p>{{ item.content }}</p><small>{{ item.available_date }}<template v-if="item.code"> · {{ item.code }}</template></small><div class="table-actions"><button v-if="item.due && !item.is_read" class="btn btn-small" type="button" @click="mark(item)">标为已读</button><button class="btn btn-small" type="button" @click="remove(item)">删除</button></div></article></div>
|
||||
<div v-else-if="items.length" class="alert-list"><article v-for="item in items" :key="item.id" :class="{ unread: !item.is_read && item.due }"><div><strong>{{ item.title }}</strong><span class="tag">{{ item.due ? (item.is_read ? "已读" : "未读") : "未到期" }}</span></div><p>{{ item.content }}</p><small>{{ item.available_date }}<template v-if="item.code"> · <MarketEntityLink :entity="{ entity_type: 'stock', identifier: item.code, code: item.code, name: item.code, sector: null }" /></template></small><div class="table-actions"><button v-if="item.due && !item.is_read" class="btn btn-small" type="button" @click="mark(item)">标为已读</button><button class="btn btn-small" type="button" @click="remove(item)">删除</button></div></article></div>
|
||||
<EmptyState v-else title="暂无提醒" description="可以创建站内提醒,策略跟踪反馈也会自动汇总到这里。" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { marketApi, type MarketEntity } from "../../shared/api/market";
|
||||
import { reviewApi, type ReviewWorkspace, type TradeEntry, type WatchItem } from "../../shared/api/review";
|
||||
import BaseDialog from "../../shared/components/BaseDialog.vue";
|
||||
import EmptyState from "../../shared/components/EmptyState.vue";
|
||||
import MarketEntityLink from "../../shared/market/MarketEntityLink.vue";
|
||||
import { useMarketStore } from "../../shared/stores/market";
|
||||
import { useUiStore } from "../../shared/stores/ui";
|
||||
import TradeDialog from "./TradeDialog.vue";
|
||||
@@ -111,7 +112,7 @@ onMounted(load);
|
||||
<template v-else-if="data">
|
||||
<section class="card review-watch-card">
|
||||
<header class="card-header"><h2>自选追踪</h2><span class="tag">{{ data.watchlist.length }} 只</span></header>
|
||||
<div v-if="data.watchlist.length" class="data-table-wrap review-watch-table"><table class="data-table"><thead><tr><th>标记</th><th>代码</th><th>股票</th><th>板块</th><th class="numeric">今日涨幅(%)</th><th class="numeric">5日涨幅(%)</th><th class="numeric">竞价关注分</th><th>跟踪备注</th><th>操作</th></tr></thead><tbody><tr v-for="item in data.watchlist" :key="item.identifier"><td class="watch-star">★</td><td>{{ item.code }}</td><td>{{ item.name }}</td><td>{{ item.sector ?? "" }}</td><td class="numeric" :class="item.pct_chg !== null && item.pct_chg >= 0 ? 'up' : 'down'">{{ display(item.pct_chg) }}</td><td class="numeric" :class="item.return_5d !== null && item.return_5d >= 0 ? 'up' : 'down'">{{ display(item.return_5d) }}</td><td class="numeric">{{ display(item.attention_score) }}</td><td><input :value="item.remark" class="review-remark" maxlength="500" aria-label="跟踪备注" @change="saveRemark(item, $event)" /></td><td><div class="table-actions"><button class="btn btn-small" type="button" @click="removeWatch(item)">移除</button></div></td></tr></tbody></table></div>
|
||||
<div v-if="data.watchlist.length" class="data-table-wrap review-watch-table"><table class="data-table"><thead><tr><th>标记</th><th>代码</th><th>股票</th><th>板块</th><th class="numeric">今日涨幅(%)</th><th class="numeric">5日涨幅(%)</th><th class="numeric">竞价关注分</th><th>跟踪备注</th><th>操作</th></tr></thead><tbody><tr v-for="item in data.watchlist" :key="item.identifier"><td class="watch-star">★</td><td><MarketEntityLink :entity="{ entity_type: 'stock', identifier: item.identifier, code: item.code, name: item.name, sector: item.sector }" /></td><td>{{ item.name }}</td><td>{{ item.sector ?? "" }}</td><td class="numeric" :class="item.pct_chg !== null && item.pct_chg >= 0 ? 'up' : 'down'">{{ display(item.pct_chg) }}</td><td class="numeric" :class="item.return_5d !== null && item.return_5d >= 0 ? 'up' : 'down'">{{ display(item.return_5d) }}</td><td class="numeric">{{ display(item.attention_score) }}</td><td><input :value="item.remark" class="review-remark" maxlength="500" aria-label="跟踪备注" @change="saveRemark(item, $event)" /></td><td><div class="table-actions"><button class="btn btn-small" type="button" @click="removeWatch(item)">移除</button></div></td></tr></tbody></table></div>
|
||||
<EmptyState v-else title="暂无自选" description="添加股票后可集中查看涨幅、竞价关注分和跟踪备注。" />
|
||||
</section>
|
||||
|
||||
@@ -119,7 +120,7 @@ onMounted(load);
|
||||
<section class="card trade-card">
|
||||
<header class="card-header"><h2>交易日志</h2><span class="tag">{{ data.trades.length }} 条</span><button class="btn btn-primary btn-small review-card-action" type="button" @click="openTrade()">交易日志</button></header>
|
||||
<div class="review-summary"><div><span>总记录</span><strong>{{ summary?.total }}</strong></div><div><span>已实现</span><strong>{{ summary?.realized }}</strong></div><div><span>胜率</span><strong>{{ summary?.win_rate === null ? "" : display(summary?.win_rate, "%") }}</strong></div><div><span>累计盈亏</span><strong>{{ display(summary?.pnl_amount) }}</strong></div><div><span>平均仓位</span><strong>{{ summary?.average_position === null ? "" : display(summary?.average_position, "%") }}</strong></div></div>
|
||||
<div v-if="data.trades.length" class="trade-scroll"><table class="data-table"><thead><tr><th>日期</th><th>代码</th><th>股票</th><th>动作</th><th class="numeric">价格(元)</th><th class="numeric">仓位(%)</th><th class="numeric">盈亏(%)</th><th>操作</th></tr></thead><tbody><tr v-for="row in data.trades" :key="row.id"><td>{{ row.trade_date }}</td><td>{{ row.code }}</td><td>{{ row.name }}</td><td>{{ row.action_label }}</td><td class="numeric">{{ display(row.price) }}</td><td class="numeric">{{ display(row.position_pct) }}</td><td class="numeric" :class="row.pnl_pct !== null && row.pnl_pct >= 0 ? 'up' : 'down'">{{ display(row.pnl_pct) }}</td><td><div class="table-actions"><button class="btn btn-small" type="button" @click="openTrade(row)">编辑</button><button class="btn btn-small" type="button" @click="deleteTrade(row)">删除</button></div></td></tr></tbody></table></div>
|
||||
<div v-if="data.trades.length" class="trade-scroll"><table class="data-table"><thead><tr><th>日期</th><th>代码</th><th>股票</th><th>动作</th><th class="numeric">价格(元)</th><th class="numeric">仓位(%)</th><th class="numeric">盈亏(%)</th><th>操作</th></tr></thead><tbody><tr v-for="row in data.trades" :key="row.id"><td>{{ row.trade_date }}</td><td><MarketEntityLink :entity="{ entity_type: 'stock', identifier: row.code, code: row.code, name: row.name, sector: null }" /></td><td>{{ row.name }}</td><td>{{ row.action_label }}</td><td class="numeric">{{ display(row.price) }}</td><td class="numeric">{{ display(row.position_pct) }}</td><td class="numeric" :class="row.pnl_pct !== null && row.pnl_pct >= 0 ? 'up' : 'down'">{{ display(row.pnl_pct) }}</td><td><div class="table-actions"><button class="btn btn-small" type="button" @click="openTrade(row)">编辑</button><button class="btn btn-small" type="button" @click="deleteTrade(row)">删除</button></div></td></tr></tbody></table></div>
|
||||
<EmptyState v-else title="暂无交易日志" description="记录交易后,摘要与明细会保留在当前页面。" />
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Candidate, ScreenerRun } from "../../shared/api/screener";
|
||||
import MarketEntityLink from "../../shared/market/MarketEntityLink.vue";
|
||||
|
||||
defineProps<{ run?: ScreenerRun; locked?: boolean }>();
|
||||
const emit = defineEmits<{ track: [candidate: Candidate] }>();
|
||||
@@ -33,7 +34,7 @@ function number(value: number | null, digits = 2): string {
|
||||
<thead><tr><th>代码</th><th>股票</th><th>行业</th><th class="numeric">收盘价(元)</th><th class="numeric">涨跌幅(%)</th><th class="numeric">综合分</th><th>主要依据</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in run.items" :key="item.identifier">
|
||||
<td class="code-column">{{ item.code }}</td><td>{{ item.name }}</td><td>{{ item.sector }}</td>
|
||||
<td class="code-column"><MarketEntityLink :entity="{ entity_type: 'stock', identifier: item.identifier, code: item.code, name: item.name, sector: item.sector || null }" /></td><td>{{ item.name }}</td><td>{{ item.sector }}</td>
|
||||
<td class="numeric">{{ number(item.close) }}</td>
|
||||
<td class="numeric" :class="{ up: Number(item.pct_chg) > 0, down: Number(item.pct_chg) < 0 }">{{ number(item.pct_chg) }}</td>
|
||||
<td class="numeric"><strong>{{ number(item.score_display, 1) }}</strong></td>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
import type { Candidate, ScreenerRun, ScreenerStrategy } from "../../shared/api/screener";
|
||||
import CandidateTable from "./CandidateTable.vue";
|
||||
@@ -7,8 +7,18 @@ import CandidateTable from "./CandidateTable.vue";
|
||||
const props = defineProps<{ strategies: ScreenerStrategy[]; runs: ScreenerRun[]; locked: boolean }>();
|
||||
const emit = defineEmits<{ track: [run: ScreenerRun, candidate: Candidate] }>();
|
||||
const selected = ref(props.runs[0]?.strategy_id ?? props.strategies[0]?.id ?? "");
|
||||
const run = computed(() => props.runs.find((item) => item.strategy_id === selected.value) ?? props.runs[0]);
|
||||
const run = computed(() => props.runs.find((item) => item.strategy_id === selected.value));
|
||||
const strategy = computed(() => props.strategies.find((item) => item.id === (run.value?.strategy_id ?? selected.value)));
|
||||
const hasRun = computed(() => Boolean(run.value));
|
||||
|
||||
watch(
|
||||
() => props.runs,
|
||||
(runs) => {
|
||||
if (!runs.some((item) => item.strategy_id === selected.value)) {
|
||||
selected.value = runs[0]?.strategy_id ?? props.strategies[0]?.id ?? "";
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -19,7 +29,7 @@ const strategy = computed(() => props.strategies.find((item) => item.id === (run
|
||||
<span class="tag">盘后自动</span>
|
||||
</header>
|
||||
<div class="stage-flow" aria-label="自动选股流程">
|
||||
<span class="done">阶段识别</span><i></i><span class="done">策略匹配</span><i></i><span class="done">自动计算</span><i></i><span>结果归档</span>
|
||||
<span :class="{ done: hasRun }">阶段识别</span><i></i><span :class="{ done: hasRun }">策略匹配</span><i></i><span :class="{ done: hasRun }">自动计算</span><i></i><span :class="{ done: hasRun }">结果归档</span>
|
||||
</div>
|
||||
<nav v-if="runs.length > 1" class="stage-run-tabs" aria-label="当日阶段策略">
|
||||
<button v-for="item in runs" :key="item.id" type="button" :class="{ active: selected === item.strategy_id }" @click="selected = item.strategy_id">{{ item.strategy_name }}</button>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from "vue-router";
|
||||
|
||||
import { screenerApi, type StrategyTrack } from "../../shared/api/screener";
|
||||
import EmptyState from "../../shared/components/EmptyState.vue";
|
||||
import MarketEntityLink from "../../shared/market/MarketEntityLink.vue";
|
||||
import { useUiStore } from "../../shared/stores/ui";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -31,6 +32,6 @@ onMounted(load);
|
||||
<div v-if="loading" class="card workspace-state">正在读取跟踪记录</div>
|
||||
<EmptyState v-else-if="error" class="card" title="跟踪记录暂不可用" :description="error" />
|
||||
<EmptyState v-else-if="!rows.length" class="card" title="暂无跟踪记录" description="从任一候选结果中点击“加入跟踪”后,记录会出现在这里。" />
|
||||
<section v-else class="card tracking-table"><div class="data-table-wrap"><table class="data-table"><thead><tr><th>代码</th><th>股票</th><th>策略</th><th>入选日期</th><th class="numeric">入选价(元)</th><th class="numeric">T+1开盘(%)</th><th class="numeric">T+1收盘(%)</th><th class="numeric">T+3收盘(%)</th><th class="numeric">T+5收盘(%)</th><th class="numeric">最大涨幅(%)</th><th class="numeric">最大回撤(%)</th><th>操作</th></tr></thead><tbody><tr v-for="row in rows" :key="row.id"><td>{{ row.code }}</td><td>{{ row.name }}</td><td>{{ row.strategy_name }}</td><td>{{ row.selection_date }}</td><td class="numeric">{{ number(row.entry_price) }}</td><td class="numeric">{{ number(row.t1_open_return) }}</td><td class="numeric">{{ number(row.t1_return) }}</td><td class="numeric">{{ number(row.t3_return) }}</td><td class="numeric">{{ number(row.t5_return) }}</td><td class="numeric up">{{ number(row.max_gain) }}</td><td class="numeric down">{{ number(row.max_drawdown) }}</td><td><button class="btn btn-small" type="button" @click="remove(row)">停止</button></td></tr></tbody></table></div></section>
|
||||
<section v-else class="card tracking-table"><div class="data-table-wrap"><table class="data-table"><thead><tr><th>代码</th><th>股票</th><th>策略</th><th>入选日期</th><th class="numeric">入选价(元)</th><th class="numeric">T+1开盘(%)</th><th class="numeric">T+1收盘(%)</th><th class="numeric">T+3收盘(%)</th><th class="numeric">T+5收盘(%)</th><th class="numeric">最大涨幅(%)</th><th class="numeric">最大回撤(%)</th><th>操作</th></tr></thead><tbody><tr v-for="row in rows" :key="row.id"><td><MarketEntityLink :entity="{ entity_type: 'stock', identifier: row.identifier, code: row.code, name: row.name, sector: row.sector }" /></td><td>{{ row.name }}</td><td>{{ row.strategy_name }}</td><td>{{ row.selection_date }}</td><td class="numeric">{{ number(row.entry_price) }}</td><td class="numeric">{{ number(row.t1_open_return) }}</td><td class="numeric">{{ number(row.t1_return) }}</td><td class="numeric">{{ number(row.t3_return) }}</td><td class="numeric">{{ number(row.t5_return) }}</td><td class="numeric up">{{ number(row.max_gain) }}</td><td class="numeric down">{{ number(row.max_drawdown) }}</td><td><button class="btn btn-small" type="button" @click="remove(row)">停止</button></td></tr></tbody></table></div></section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user