digital-human-monitor-v2/components/dashboard-actions.tsx
2026-04-18 11:03:34 +08:00

359 lines
12 KiB
TypeScript
Raw 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 Link from "next/link";
import { useRouter } from "next/navigation";
import { startTransition, useRef, useState } from "react";
import { useNavigationProgress } from "@/components/navigation-progress";
export function BindDeviceForm() {
const router = useRouter();
const { beginNavigationProgress } = useNavigationProgress();
const [rawCode, setRawCode] = useState("");
const [submitting, setSubmitting] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!rawCode.trim()) {
setNotice("请先粘贴长辈手机上的设备码。\n");
return;
}
setSubmitting(true);
setNotice("正在前往绑定页面,请稍候……");
startTransition(() => {
beginNavigationProgress();
router.push(`/bind?deviceUuid=${encodeURIComponent(rawCode.trim())}`);
});
}
return (
<div className="rounded-[28px] border border-white/70 bg-white/80 p-5 shadow-[0_24px_60px_rgba(125,80,46,0.12)] backdrop-blur">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
</p>
</div>
<Link
href="/scan"
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
>
</Link>
</div>
<form className="mt-5 flex flex-col gap-3 lg:flex-row" onSubmit={handleSubmit}>
<input
value={rawCode}
onChange={(event) => setRawCode(event.target.value)}
placeholder="粘贴设备码或二维码链接"
className="h-13 flex-1 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-5 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
/>
<button
type="submit"
disabled={submitting}
className="inline-flex h-13 min-w-36 items-center justify-center rounded-full bg-[var(--olive)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--olive-deep)] disabled:cursor-not-allowed disabled:opacity-60"
>
{submitting ? "正在连接" : "立即绑定"}
</button>
</form>
{notice ? (
<p className="mt-3 whitespace-pre-line text-sm leading-7 text-[var(--muted)]">
{notice}
</p>
) : null}
</div>
);
}
type SendMessageFormProps = {
deviceUuid: string;
};
export function SendMessageForm({ deviceUuid }: SendMessageFormProps) {
const router = useRouter();
const [content, setContent] = useState("");
const [importance, setImportance] = useState("NORMAL");
const [submitting, setSubmitting] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
const [createdMessagePublicId, setCreatedMessagePublicId] = useState<number | null>(null);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!content.trim()) {
setNotice("请先写下您想说的话。\n");
setCreatedMessagePublicId(null);
return;
}
setSubmitting(true);
setNotice("正在送达这条留言……");
try {
const response = await fetch("/api/family/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
deviceUuid,
content,
importance,
}),
});
const payload = (await response.json().catch(() => null)) as
| { error?: string; publicId?: number }
| null;
if (!response.ok) {
throw new Error(payload?.error || "留言发送失败,请稍后再试。");
}
setNotice("留言已发出,长辈下次使用时就能看到。\n");
setCreatedMessagePublicId(
typeof payload?.publicId === "number" ? payload.publicId : null,
);
setContent("");
setImportance("NORMAL");
startTransition(() => {
router.refresh();
});
} catch (error) {
setCreatedMessagePublicId(null);
setNotice(
error instanceof Error ? error.message : "留言发送失败,请稍后再试。",
);
} finally {
setSubmitting(false);
}
}
return (
<form className="rounded-[28px] border border-white/70 bg-white/88 p-5" onSubmit={handleSubmit}>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h4 className="mt-2 font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h4>
</div>
<select
value={importance}
onChange={(event) => setImportance(event.target.value)}
className="h-11 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none"
>
<option value="LOW"></option>
<option value="NORMAL"></option>
<option value="HIGH"></option>
<option value="URGENT"></option>
</select>
</div>
<textarea
value={content}
onChange={(event) => setContent(event.target.value)}
rows={4}
placeholder="例如:妈,晚上天气凉,记得添件衣服。晚饭后我给您打电话。"
className="mt-4 w-full rounded-[24px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-4 text-sm leading-7 text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
/>
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm leading-7 text-[var(--muted)]">
</p>
<button
type="submit"
disabled={submitting}
className="inline-flex h-12 min-w-32 items-center justify-center rounded-full bg-[var(--ink)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--ink-soft)] disabled:cursor-not-allowed disabled:opacity-60"
>
{submitting ? "正在送达" : "写好并送出"}
</button>
</div>
{notice ? (
<div className="mt-3 space-y-2 text-sm leading-7 text-[var(--muted)]">
<p className="whitespace-pre-line">{notice}</p>
{createdMessagePublicId ? (
<Link
href={`/messages/${createdMessagePublicId}`}
className="inline-flex h-10 items-center justify-center rounded-full border border-[var(--line)] bg-white px-4 font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
>
#{createdMessagePublicId}
</Link>
) : null}
</div>
) : null}
</form>
);
}
type ManageDeviceFormProps = {
deviceUuid: string;
initialAlias?: string | null;
deviceLabel: string;
};
export function ManageDeviceForm({
deviceUuid,
initialAlias,
deviceLabel,
}: ManageDeviceFormProps) {
const router = useRouter();
const { beginNavigationProgress } = useNavigationProgress();
const aliasInputRef = useRef<HTMLInputElement | null>(null);
const [saving, setSaving] = useState(false);
const [unbinding, setUnbinding] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
async function handleSaveAlias(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const alias = aliasInputRef.current?.value ?? "";
setSaving(true);
setNotice("正在保存设备别名……");
try {
const response = await fetch("/api/family/device-binding", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
deviceUuid,
alias,
}),
});
const payload = (await response.json().catch(() => null)) as
| { error?: string; alias?: string | null; deviceName?: string }
| null;
if (!response.ok) {
throw new Error(payload?.error || "设备别名保存失败。");
}
if (aliasInputRef.current) {
aliasInputRef.current.value = payload?.alias?.trim() || "";
}
setNotice(
payload?.alias?.trim()
? `已将这台设备备注为“${payload.alias.trim()}”。\n`
: "已恢复默认设备名称。\n",
);
startTransition(() => {
router.refresh();
});
} catch (error) {
setNotice(
error instanceof Error ? error.message : "设备别名保存失败。",
);
} finally {
setSaving(false);
}
}
async function handleUnbind() {
if (!window.confirm(`解绑后将不再收到“${deviceLabel}”的留言和推送提醒,确定继续吗?`)) {
return;
}
setUnbinding(true);
setNotice("正在解绑这台设备……");
try {
const response = await fetch("/api/family/device-binding", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
deviceUuid,
}),
});
const payload = (await response.json().catch(() => null)) as
| { error?: string }
| null;
if (!response.ok) {
throw new Error(payload?.error || "解绑设备失败。",);
}
startTransition(() => {
beginNavigationProgress();
router.push("/devices");
router.refresh();
});
} catch (error) {
setNotice(
error instanceof Error ? error.message : "解绑设备失败。",
);
setUnbinding(false);
}
}
return (
<section className="rounded-[28px] border border-white/70 bg-white/88 p-5">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h4 className="mt-2 font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h4>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
</p>
<form className="mt-4 flex flex-col gap-3 sm:flex-row" onSubmit={handleSaveAlias}>
<input
key={`${deviceUuid}:${initialAlias ?? ""}`}
ref={aliasInputRef}
defaultValue={initialAlias ?? ""}
maxLength={24}
placeholder="给这台设备起个好记的名字"
className="h-12 flex-1 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-5 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
/>
<button
type="submit"
disabled={saving || unbinding}
className="inline-flex h-12 min-w-32 items-center justify-center rounded-full bg-[var(--olive)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--olive-deep)] disabled:cursor-not-allowed disabled:opacity-60"
>
{saving ? "正在保存" : "保存别名"}
</button>
</form>
<div className="mt-4 flex flex-wrap gap-3">
<button
type="button"
onClick={() => {
void handleUnbind();
}}
disabled={saving || unbinding}
className="inline-flex h-11 items-center justify-center rounded-full border border-[rgba(160,74,48,0.28)] bg-white px-4 text-sm font-semibold text-[var(--ink)] transition hover:bg-[rgba(199,103,51,0.08)] disabled:cursor-not-allowed disabled:opacity-60"
>
{unbinding ? "正在解绑" : "解绑这台设备"}
</button>
</div>
{notice ? (
<p className="mt-3 whitespace-pre-line text-sm leading-7 text-[var(--muted)]">
{notice}
</p>
) : null}
</section>
);
}