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

206 lines
7.5 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, useState } from "react";
import type { Market } from "@/src/domain/types";
import { useWatchlistStore } from "@/src/stores/watchlistStore";
function normalizeCode(raw: string) {
return raw.trim();
}
export default function WatchlistPage() {
const { items, loading, error, refresh, add, update, remove } =
useWatchlistStore();
const [code, setCode] = useState("");
const [market, setMarket] = useState<Market>("SZ");
const [notes, setNotes] = useState("");
useEffect(() => {
void refresh();
}, [refresh]);
const canSubmit = useMemo(() => {
return normalizeCode(code).length > 0;
}, [code]);
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/ IndexedDB userId=anon
</p>
</div>
<section className="rounded-xl border border-black/10 p-4 dark:border-white/10">
<div className="text-sm font-medium"></div>
<form
className="mt-3 grid gap-3 md:grid-cols-6"
onSubmit={async (e) => {
e.preventDefault();
if (!canSubmit) return;
await add({
code: normalizeCode(code),
market,
notes: notes.trim() || undefined,
});
setCode("");
setNotes("");
}}
>
<label className="md:col-span-2">
<div className="text-xs text-black/60 dark:text-white/60"></div>
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="例如600519"
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-1">
<div className="text-xs text-black/60 dark:text-white/60"></div>
<select
value={market}
onChange={(e) => setMarket(e.target.value as Market)}
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"
>
<option value="SZ">SZ</option>
<option value="SH">SH</option>
<option value="BJ">BJ</option>
</select>
</label>
<label className="md:col-span-2">
<div className="text-xs text-black/60 dark:text-white/60"></div>
<input
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="可选"
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>
<div className="flex items-end md:col-span-1">
<button
type="submit"
disabled={!canSubmit || loading}
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"
>
</button>
</div>
</form>
{error ? (
<div className="mt-3 text-sm text-red-600 dark:text-red-400">
{error}
</div>
) : null}
</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>
<button
type="button"
onClick={() => void refresh()}
className="rounded-lg border border-black/10 px-3 py-2 text-xs transition-colors hover:bg-black/[.04] dark:border-white/10 dark:hover:bg-white/[.06]"
>
</button>
</div>
<div className="mt-3 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"></th>
<th className="py-2"></th>
<th className="py-2"></th>
</tr>
</thead>
<tbody className="align-top">
{items.length === 0 ? (
<tr>
<td
className="py-6 text-xs text-black/60 dark:text-white/60"
colSpan={6}
>
</td>
</tr>
) : null}
{items.map((item) => (
<tr
key={item.id}
className="border-t border-black/5 dark:border-white/10"
>
<td className="py-2">
<button
type="button"
className="rounded-md border border-black/10 px-2 py-1 text-xs dark:border-white/10"
onClick={() =>
void update(item.code, item.market, {
pinned: !item.pinned,
})
}
title="置顶"
>
{item.pinned ? "是" : "否"}
</button>
</td>
<td className="py-2 font-mono">{item.code}</td>
<td className="py-2">{item.market}</td>
<td className="py-2">
<input
defaultValue={item.notes ?? ""}
placeholder="(空)"
className="w-full rounded-md border border-black/10 bg-transparent px-2 py-1 text-xs outline-none focus:border-black/30 dark:border-white/10 dark:focus:border-white/30"
onBlur={(e) => {
const v = e.target.value.trim();
if ((item.notes ?? "") !== v) {
void update(item.code, item.market, {
notes: v || undefined,
});
}
}}
/>
</td>
<td className="py-2 text-xs text-black/60 dark:text-white/60">
{new Date(item.createdAt).toLocaleString()}
</td>
<td className="py-2">
<button
type="button"
className="rounded-md border border-black/10 px-2 py-1 text-xs transition-colors hover:bg-black/[.04] dark:border-white/10 dark:hover:bg-white/[.06]"
onClick={() => void remove(item.code, item.market)}
>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{loading ? (
<div className="mt-3 text-xs text-black/60 dark:text-white/60">
</div>
) : null}
</section>
</div>
);
}