分享码功能的实现,并修复了几个 bug
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { X, Copy, Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import { createShareCode, listShareCodes, queryShareCode } from "@/lib/luxsinApi";
|
||||
import type { PeqEqUi } from "../constants";
|
||||
|
||||
type PeqItem = {
|
||||
@@ -23,9 +24,11 @@ type ImportedEqData = {
|
||||
name: string;
|
||||
brand?: string;
|
||||
model?: string;
|
||||
target?: string;
|
||||
form?: string;
|
||||
filters?: any[] | string;
|
||||
filters?: unknown;
|
||||
preamp?: number;
|
||||
autoPre?: number;
|
||||
};
|
||||
|
||||
export function ShareDialog({
|
||||
@@ -35,6 +38,7 @@ export function ShareDialog({
|
||||
headphoneIdx,
|
||||
eqUi,
|
||||
peqCardLabels,
|
||||
mac,
|
||||
onImportEq,
|
||||
}: {
|
||||
open: boolean;
|
||||
@@ -43,7 +47,8 @@ export function ShareDialog({
|
||||
headphoneIdx: number;
|
||||
eqUi: PeqEqUi;
|
||||
peqCardLabels: PeqCardLabels;
|
||||
onImportEq: (data: ImportedEqData) => void;
|
||||
mac: string;
|
||||
onImportEq: (data: ImportedEqData, shareCode: string) => void | Promise<void>;
|
||||
}) {
|
||||
const [shareDialogTab, setShareDialogTab] = useState<"share" | "import" | "myShares">("share");
|
||||
const [shareSelectedIdx, setShareSelectedIdx] = useState<number>(headphoneIdx);
|
||||
@@ -51,8 +56,12 @@ export function ShareDialog({
|
||||
const [shareGenerating, setShareGenerating] = useState(false);
|
||||
const [importCodeInputs, setImportCodeInputs] = useState<string[]>(["", "", "", "", ""]);
|
||||
const [importQuerying, setImportQuerying] = useState(false);
|
||||
const [importSaving, setImportSaving] = useState(false);
|
||||
const [importPresetName, setImportPresetName] = useState("");
|
||||
const [queriedShareCode, setQueriedShareCode] = useState("");
|
||||
const [importedEqData, setImportedEqData] = useState<ImportedEqData | null>(null);
|
||||
const [mySharesList, setMySharesList] = useState<Array<{ code: string; name: string }>>([]);
|
||||
const [shareCodeExpireAt, setShareCodeExpireAt] = useState<string | null>(null);
|
||||
const [mySharesList, setMySharesList] = useState<Array<{ share_code: string; expire_at: string; eq_data: Record<string, unknown> }>>([]);
|
||||
const [mySharesLoading, setMySharesLoading] = useState(false);
|
||||
|
||||
if (!open) return null;
|
||||
@@ -131,25 +140,14 @@ export function ShareDialog({
|
||||
void (async () => {
|
||||
setMySharesLoading(true);
|
||||
try {
|
||||
// ── TODO: 伪代码 — 调用查询我的分享列表接口 ──
|
||||
// const res = await fetch("/api/eq/share/list");
|
||||
// if (!res.ok) {
|
||||
// toast.error(eqUi.mySharesLoadFail);
|
||||
// return;
|
||||
// }
|
||||
// const data = await res.json();
|
||||
// setMySharesList(data.list);
|
||||
|
||||
// 模拟接口延迟
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
// 模拟返回分享列表
|
||||
setMySharesList([
|
||||
{ code: "AK7NR", name: "Sennheiser HD 600 (AutoEQ)" },
|
||||
{ code: "P3WQM", name: "Beyerdynamic DT 880" },
|
||||
{ code: "Z5XKL", name: "Hifiman HE400i" },
|
||||
]);
|
||||
const res = await listShareCodes(mac);
|
||||
if (res.code !== 200) {
|
||||
toast.error(eqUi.mySharesLoadFail);
|
||||
return;
|
||||
}
|
||||
setMySharesList(res.share_codes ?? []);
|
||||
} catch {
|
||||
// TODO: 错误处理
|
||||
toast.error(eqUi.mySharesLoadFail);
|
||||
} finally {
|
||||
setMySharesLoading(false);
|
||||
}
|
||||
@@ -220,33 +218,31 @@ export function ShareDialog({
|
||||
if (shareGenerating) return;
|
||||
setShareGenerating(true);
|
||||
setShareCode(null);
|
||||
setShareCodeExpireAt(null);
|
||||
try {
|
||||
// ── TODO: 伪代码 — 调用分享接口 ──
|
||||
// const selectedPeq = peqItems[shareSelectedIdx];
|
||||
// const res = await fetch("/api/eq/share", {
|
||||
// method: "POST",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify({
|
||||
// brand: selectedPeq.brand,
|
||||
// model: selectedPeq.model,
|
||||
// target: selectedPeq.form,
|
||||
// filters: selectedPeq.filters,
|
||||
// preamp: selectedPeq.preamp,
|
||||
// }),
|
||||
// });
|
||||
// const data = await res.json();
|
||||
// setShareCode(data.shareCode);
|
||||
|
||||
// 模拟接口延迟
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
// 模拟返回 5 位随机分享码
|
||||
const fakeCode = Array.from(
|
||||
{ length: 5 },
|
||||
() => "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"[Math.floor(Math.random() * 32)],
|
||||
).join("");
|
||||
setShareCode(fakeCode);
|
||||
const selectedPeq = peqItems[shareSelectedIdx];
|
||||
if (!selectedPeq) {
|
||||
toast.error(eqUi.shareSelectHint);
|
||||
return;
|
||||
}
|
||||
const eqData: Record<string, unknown> = {
|
||||
name: selectedPeq.name,
|
||||
brand: selectedPeq.brand ?? "",
|
||||
model: selectedPeq.model ?? "",
|
||||
form: selectedPeq.form ?? "",
|
||||
filters: selectedPeq.filters ?? [],
|
||||
autoPre: selectedPeq.autoPre ?? 0,
|
||||
preamp: selectedPeq.preamp ?? 0,
|
||||
};
|
||||
const res = await createShareCode(mac, eqData);
|
||||
if (res.code !== 200) {
|
||||
toast.error(res.msg || eqUi.shareCreateFail);
|
||||
return;
|
||||
}
|
||||
setShareCode(res.share_code ?? "");
|
||||
setShareCodeExpireAt(res.expire_at ?? null);
|
||||
} catch {
|
||||
// TODO: 错误处理
|
||||
toast.error(eqUi.shareCreateFail);
|
||||
} finally {
|
||||
setShareGenerating(false);
|
||||
}
|
||||
@@ -293,6 +289,10 @@ export function ShareDialog({
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{/* expiration time */}
|
||||
{shareCodeExpireAt && (
|
||||
<p className="mt-2 text-[11px] text-white/30">{eqUi.shareExpireAt}: {shareCodeExpireAt}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -326,6 +326,8 @@ export function ShareDialog({
|
||||
next[i] = val;
|
||||
setImportCodeInputs(next);
|
||||
setImportedEqData(null);
|
||||
setImportPresetName("");
|
||||
setQueriedShareCode("");
|
||||
// auto-focus next box
|
||||
if (val && i < 4) {
|
||||
const el = e.target.nextElementSibling as HTMLInputElement | null;
|
||||
@@ -339,6 +341,8 @@ export function ShareDialog({
|
||||
next[i - 1] = "";
|
||||
setImportCodeInputs(next);
|
||||
setImportedEqData(null);
|
||||
setImportPresetName("");
|
||||
setQueriedShareCode("");
|
||||
const prev = (e.target as HTMLElement).previousElementSibling as HTMLInputElement | null;
|
||||
prev?.focus();
|
||||
}
|
||||
@@ -353,6 +357,8 @@ export function ShareDialog({
|
||||
}
|
||||
setImportCodeInputs(next);
|
||||
setImportedEqData(null);
|
||||
setImportPresetName("");
|
||||
setQueriedShareCode("");
|
||||
// focus last filled or last box
|
||||
const focusIdx = Math.min(text.length, 4);
|
||||
const inputs = (e.target as HTMLElement).parentElement?.querySelectorAll("input");
|
||||
@@ -380,36 +386,34 @@ export function ShareDialog({
|
||||
const code = importCodeInputs.join("");
|
||||
setImportQuerying(true);
|
||||
setImportedEqData(null);
|
||||
setImportPresetName("");
|
||||
setQueriedShareCode("");
|
||||
try {
|
||||
// ── TODO: 伪代码 — 调用导入查询接口 ──
|
||||
// const res = await fetch(`/api/eq/share/${code}`);
|
||||
// if (!res.ok) {
|
||||
// toast.error(eqUi.importCodeNotFound);
|
||||
// return;
|
||||
// }
|
||||
// const data = await res.json();
|
||||
// setImportedEqData({
|
||||
// name: data.name,
|
||||
// brand: data.brand,
|
||||
// model: data.model,
|
||||
// form: data.target,
|
||||
// filters: data.filters,
|
||||
// preamp: data.preamp,
|
||||
// });
|
||||
|
||||
// 模拟接口延迟
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
// 模拟返回 EQ 数据
|
||||
const res = await queryShareCode(code);
|
||||
if (res.code !== 200) {
|
||||
toast.error(eqUi.importCodeNotFound);
|
||||
return;
|
||||
}
|
||||
const eq = res.eq_data;
|
||||
if (!eq) {
|
||||
toast.error(eqUi.importCodeNotFound);
|
||||
return;
|
||||
}
|
||||
const fetchedName = (eq.name as string) || "";
|
||||
setImportedEqData({
|
||||
name: "Sennheiser HD 600 (AutoEQ)",
|
||||
brand: "Sennheiser",
|
||||
model: "HD 600",
|
||||
form: "over-ear",
|
||||
filters: [],
|
||||
preamp: -5.2,
|
||||
name: fetchedName,
|
||||
brand: (eq.brand as string) || undefined,
|
||||
model: (eq.model as string) || undefined,
|
||||
target: (eq.target as string) || undefined,
|
||||
form: (eq.form as string) || undefined,
|
||||
filters: eq.filters,
|
||||
preamp: eq.preamp as number | undefined,
|
||||
autoPre: eq.autoPre as number | undefined,
|
||||
});
|
||||
setImportPresetName(fetchedName);
|
||||
setQueriedShareCode(code);
|
||||
} catch {
|
||||
// TODO: 错误处理
|
||||
toast.error(eqUi.importCodeNotFound);
|
||||
} finally {
|
||||
setImportQuerying(false);
|
||||
}
|
||||
@@ -427,24 +431,47 @@ export function ShareDialog({
|
||||
|
||||
{/* imported EQ result */}
|
||||
{importedEqData && (
|
||||
<div className="mt-4 rounded-[10px] p-3" style={{ background: "rgba(0,255,246,0.06)", border: "1px solid rgba(0,255,246,0.2)" }}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[12px] text-white/40 font-medium mb-0.5">{eqUi.importEqName}</p>
|
||||
<p className="text-[14px] text-white/90 font-semibold truncate">{importedEqData.name}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 ml-3 rounded-full px-4 py-2 text-[13px] font-semibold text-black bg-[#00FFF6] hover:brightness-95 active:scale-[0.98] transition-all"
|
||||
onClick={() => {
|
||||
// ── TODO: 伪代码 — 导入 EQ 到预设列表 ──
|
||||
// onImportEq will handle the parent-level state changes
|
||||
onImportEq(importedEqData);
|
||||
}}
|
||||
>
|
||||
{eqUi.importButton}
|
||||
</button>
|
||||
<div className="mt-4 rounded-[10px] p-3 space-y-3" style={{ background: "rgba(0,255,246,0.06)", border: "1px solid rgba(0,255,246,0.2)" }}>
|
||||
<div>
|
||||
<p className="text-[12px] text-white/40 font-medium mb-1.5">{eqUi.importEqName}</p>
|
||||
<input
|
||||
value={importPresetName}
|
||||
onChange={(e) => setImportPresetName(e.target.value)}
|
||||
disabled={importSaving}
|
||||
className="h-10 w-full rounded-[10px] border border-white/12 bg-[#15171b] px-3 text-[14px] text-white/90 outline-none placeholder:text-white/30 focus:border-[#00FFF6]/40 disabled:opacity-60"
|
||||
placeholder={eqUi.importEqName}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={importSaving || !importPresetName.trim()}
|
||||
className={cn(
|
||||
"w-full rounded-full py-2.5 text-[13px] font-semibold text-black transition-all active:scale-[0.98]",
|
||||
importSaving || !importPresetName.trim()
|
||||
? "bg-[#00FFF6]/40 cursor-not-allowed"
|
||||
: "bg-[#00FFF6] hover:brightness-95",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (importSaving || !importedEqData || !queriedShareCode) return;
|
||||
const nextName = importPresetName.trim();
|
||||
if (!nextName) return;
|
||||
setImportSaving(true);
|
||||
void Promise.resolve(
|
||||
onImportEq({ ...importedEqData, name: nextName }, queriedShareCode),
|
||||
).finally(() => {
|
||||
setImportSaving(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{importSaving ? (
|
||||
<span className="flex items-center justify-center gap-1.5">
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
{eqUi.importSaving}
|
||||
</span>
|
||||
) : (
|
||||
eqUi.importButton
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -471,7 +498,7 @@ export function ShareDialog({
|
||||
>
|
||||
{mySharesList.map((item, idx) => (
|
||||
<div
|
||||
key={`${item.code}-${idx}`}
|
||||
key={`${item.share_code}-${idx}`}
|
||||
className="flex items-center gap-3 px-3 py-2.5 border-b border-white/[0.06] last:border-b-0"
|
||||
>
|
||||
{/* share code */}
|
||||
@@ -479,7 +506,7 @@ export function ShareDialog({
|
||||
className="shrink-0 flex gap-0.5"
|
||||
title={eqUi.mySharesCode}
|
||||
>
|
||||
{item.code.split("").map((ch, ci) => (
|
||||
{item.share_code.split("").map((ch: string, ci: number) => (
|
||||
<span
|
||||
key={ci}
|
||||
className="w-[18px] h-[22px] rounded-[4px] flex items-center justify-center text-[11px] font-bold text-[#00FFF6]"
|
||||
@@ -489,14 +516,23 @@ export function ShareDialog({
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* EQ name */}
|
||||
<div className="min-w-0 flex-1 truncate text-[13px] text-white/85">{item.name}</div>
|
||||
{/* EQ name + expiration */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] text-white/85">
|
||||
{(item.eq_data?.name as string) ?? "—"}
|
||||
</div>
|
||||
{item.expire_at && (
|
||||
<p className="mt-0.5 truncate text-[11px] text-white/35">
|
||||
{eqUi.shareExpireAt}: {item.expire_at}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* copy code button */}
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 w-7 h-7 rounded-[6px] flex items-center justify-center text-[#00FFF6]/70 active:scale-95 transition-transform"
|
||||
style={{ background: "rgba(0,255,246,0.08)", border: "1px solid rgba(0,255,246,0.15)" }}
|
||||
onClick={() => void copyToClipboard(item.code)}
|
||||
onClick={() => void copyToClipboard(item.share_code)}
|
||||
>
|
||||
<Copy size={13} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user