Files
2026-03-16 09:43:24 +08:00

215 lines
8.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useMemo } from "react";
import type { RecommendationSegment } from "@/src/recommendations/types";
import { useImportedPoolStore } from "@/src/stores/importedPoolStore";
import { useRecommendationsStore } from "@/src/stores/recommendationsStore";
const segmentOptions: { value: RecommendationSegment; label: string }[] = [
{ value: "OPEN_AUCTION", label: "集合竞价" },
{ value: "MIDDAY", label: "午盘精选" },
{ value: "AFTER_HOURS", label: "盘后分析" },
];
export default function RecommendationsPage() {
const codesText = useImportedPoolStore((s) => s.codesText);
const poolLoading = useImportedPoolStore((s) => s.loading);
const poolError = useImportedPoolStore((s) => s.error);
const loadPool = useImportedPoolStore((s) => s.load);
const savePool = useImportedPoolStore((s) => s.save);
const setPoolDraft = useImportedPoolStore((s) => s.setDraft);
const date = useRecommendationsStore((s) => s.date);
const segment = useRecommendationsStore((s) => s.segment);
const recLoading = useRecommendationsStore((s) => s.loading);
const recError = useRecommendationsStore((s) => s.error);
const snapshot = useRecommendationsStore((s) => s.snapshot);
const setDate = useRecommendationsStore((s) => s.setDate);
const setSegment = useRecommendationsStore((s) => s.setSegment);
const loadRec = useRecommendationsStore((s) => s.load);
const generate = useRecommendationsStore((s) => s.generate);
useEffect(() => {
void loadPool();
}, [loadPool]);
useEffect(() => {
void loadRec();
}, [date, segment, loadRec]);
const selectedSegmentLabel = useMemo(() => {
return segmentOptions.find((x) => x.value === segment)?.label ?? segment;
}, [segment]);
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-semibold"></h1>
<p className="mt-1 text-sm text-black/60 dark:text-white/60">
MVP + Universe v0.1+线 Top 10
</p>
</div>
<section className="rounded-xl border border-black/10 p-4 dark:border-white/10">
<div className="grid gap-3 md:grid-cols-12">
<label className="md:col-span-3">
<div className="text-xs text-black/60 dark:text-white/60"></div>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
className="mt-1 w-full rounded-lg border border-black/10 bg-white px-3 py-2 text-sm outline-none focus:border-black/30 dark:border-white/10 dark:bg-black/20 dark:focus:border-white/30"
/>
</label>
<label className="md:col-span-3">
<div className="text-xs text-black/60 dark:text-white/60"></div>
<select
value={segment}
onChange={(e) => setSegment(e.target.value as RecommendationSegment)}
className="mt-1 w-full rounded-lg border border-black/10 bg-white px-3 py-2 text-sm outline-none focus:border-black/30 dark:border-white/10 dark:bg-black/20 dark:focus:border-white/30"
>
{segmentOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
<div className="flex items-end md:col-span-3">
<button
type="button"
disabled={recLoading}
onClick={() => void loadRec()}
className="w-full rounded-lg border border-black/10 px-3 py-2 text-sm transition-colors hover:bg-black/[.04] disabled:opacity-50 dark:border-white/10 dark:hover:bg-white/[.06]"
>
</button>
</div>
<div className="flex items-end md:col-span-3">
<button
type="button"
disabled={recLoading}
onClick={async () => {
// save pool first (so generated snapshot is reproducible)
await savePool();
await generate(codesText);
}}
className="w-full rounded-lg bg-black px-3 py-2 text-sm font-medium text-white transition-opacity disabled:opacity-50 dark:bg-white dark:text-black"
>
{selectedSegmentLabel}
</button>
</div>
</div>
{(poolError || recError) ? (
<div className="mt-3 text-sm text-red-600 dark:text-red-400">
{poolError ?? recError}
</div>
) : null}
{(poolLoading || recLoading) ? (
<div className="mt-3 text-xs text-black/60 dark:text-white/60"></div>
) : null}
</section>
<section className="rounded-xl border border-black/10 p-4 dark:border-white/10">
<div className="text-sm font-medium">Universe </div>
<p className="mt-1 text-xs text-black/60 dark:text-white/60">
600519 SH:600519 / SZ:000001 / BJ:8xxxxx
</p>
<textarea
value={codesText}
onChange={(e) => setPoolDraft(e.target.value)}
rows={6}
className="mt-3 w-full rounded-lg border border-black/10 bg-white px-3 py-2 font-mono text-xs outline-none focus:border-black/30 dark:border-white/10 dark:bg-black/20 dark:focus:border-white/30"
/>
<div className="mt-3 flex justify-end">
<button
type="button"
disabled={poolLoading}
onClick={() => void savePool()}
className="rounded-lg border border-black/10 px-3 py-2 text-xs transition-colors hover:bg-black/[.04] disabled:opacity-50 dark:border-white/10 dark:hover:bg-white/[.06]"
>
</button>
</div>
</section>
<section className="rounded-xl border border-black/10 p-4 dark:border-white/10">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-medium"></div>
<div className="text-xs text-black/60 dark:text-white/60">
{snapshot ? `版本 ${snapshot.screeningVersion} · ${snapshot.generatedAt}` : "(暂无快照)"}
</div>
</div>
{snapshot ? (
<div className="mt-3 space-y-3">
<div className="text-xs text-black/60 dark:text-white/60">
Universe {snapshot.universe.watchlistCount} · {snapshot.universe.importedPoolCount} · {snapshot.universe.totalCount}
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="text-left text-xs text-black/60 dark:text-white/60">
<tr>
<th className="py-2">#</th>
<th className="py-2"></th>
<th className="py-2"></th>
<th className="py-2">Score</th>
<th className="py-2"></th>
<th className="py-2"></th>
</tr>
</thead>
<tbody>
{snapshot.items.map((it) => (
<tr
key={`${it.market}:${it.code}`}
className="border-t border-black/5 dark:border-white/10"
>
<td className="py-2 text-xs text-black/60 dark:text-white/60">
{it.rank}
</td>
<td className="py-2 font-mono">{it.code}</td>
<td className="py-2">{it.market}</td>
<td className="py-2">{it.score.toFixed(2)}</td>
<td className="py-2 text-xs">
<ul className="list-disc pl-4">
{it.reasons.map((r, idx) => (
<li key={idx}>{r}</li>
))}
</ul>
</td>
<td className="py-2 text-xs">
{it.risks.length === 0 ? (
<span className="text-black/60 dark:text-white/60">-</span>
) : (
<ul className="list-disc pl-4">
{it.risks.map((r, idx) => (
<li key={idx}>{r}</li>
))}
</ul>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="mt-3 text-sm text-black/60 dark:text-white/60">
</div>
)}
</section>
</div>
);
}