Files
Jyotisha/frontend/src/components/feedback-dialog.tsx
T
jesse-uxandGrok (agent loop) 352d45b2ef
Independent Staging Quality Gate / validate (push) Failing after 9m12s
Independent Staging Quality Gate / publish (push) Skipped
外壳换成组件库:侧栏可拖宽,会话可排序(#105)。
桌面侧栏可在 200 到 360 像素之间拖宽,拖到最左收起,宽度记在本机。手机从左边滑出。会话在同一组里拖动排序,顺序写入可空的 sidebar_order,收藏仍置顶。账户菜单、设置、反馈和退出确认改用组件库。顶栏点数只显示图标和数字。启程清单还在。菜单、拖宽、拖动排序和翻页在首屏之后再载入。

自测:tsc 退出码 0;npm run lint 0 error、121 warning;改到的前端测试 205 项全过;npm run build 后首页仍是 static;首屏 gzip 630350,相对 8f7c0f7d 的 621226 为 +1.47%。test:db 92 项里 79 过、13 败,失败名与 Windows 已知清单一致,排序列在通过的 ok 37。没有跑全量 npm test,没有跑 Python 快速门。没有登录态,没有在浏览器里拖侧栏。

Co-Authored-By: Grok (agent loop)
2026-10-25 01:41:19 +08:00

145 lines
5.5 KiB
TypeScript

"use client";
import { X } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { LEGAL_ENTITY } from "@/lib/legal-entity";
import {
FEEDBACK_BODY_MAX,
FEEDBACK_RESPONSE_WORKING_DAYS,
FEEDBACK_TYPE_LABELS,
FEEDBACK_TYPES,
feedbackBodyProblem,
type FeedbackType,
} from "@/lib/user-feedback";
import { notifyError } from "@/lib/notify";
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/**
* 反馈与投诉 (compliance round 2026-09-30): opened from the account menu on
* every page. Type, a description, an optional contact and, when a saved
* conversation is open, an opt-in to attach it (its id only). Submitting
* stores the row for the admin console; the success view promises a reply
* window and gives the contact email.
*/
export function FeedbackDialogBody({ accountEmail, sessionId, onClose }: Readonly<{
accountEmail?: string | null;
sessionId?: string | null;
onClose: () => void;
}>) {
const [type, setType] = useState<FeedbackType>("problem");
const [body, setBody] = useState("");
const [contact, setContact] = useState(accountEmail ?? "");
const [attach, setAttach] = useState(false);
const [sending, setSending] = useState(false);
const [sent, setSent] = useState(false);
const canAttach = Boolean(sessionId && UUID.test(sessionId));
async function submit() {
const problem = feedbackBodyProblem(body);
if (problem) {
notifyError(problem);
return;
}
setSending(true);
try {
const response = await fetch("/api/feedback", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type,
body,
...(contact.trim() ? { contact: contact.trim() } : {}),
...(attach && canAttach && sessionId ? { sessionId } : {}),
}),
});
if (!response.ok) {
const json = await response.json().catch(() => null) as { error?: string } | null;
notifyError(json?.error ?? "暂时没能提交,请稍后再试。");
return;
}
setSent(true);
} catch {
notifyError("网络不太稳,没能提交,请稍后再试。");
} finally {
setSending(false);
}
}
return (
<>
{sent ? (
<div className="feedback-dialog-body feedback-dialog-done" role="status">
<p>已收到,我们会在 {FEEDBACK_RESPONSE_WORKING_DAYS} 个工作日内处理。</p>
<p className="feedback-dialog-caption">急事也可以发邮件到 {LEGAL_ENTITY.contactEmail}。</p>
<Button onClick={onClose}>好的</Button>
</div>
) : (
<form className="feedback-dialog-body" onSubmit={(event) => { event.preventDefault(); void submit(); }}>
<Tabs value={type} onValueChange={(next) => setType(next as FeedbackType)}>
<TabsList className="feedback-dialog-types" aria-label="反馈类型">
{FEEDBACK_TYPES.map((option) => (
<TabsTrigger key={option} value={option}>
{FEEDBACK_TYPE_LABELS[option]}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<label className="feedback-dialog-field">
<span>发生了什么</span>
<Textarea
value={body}
maxLength={FEEDBACK_BODY_MAX}
rows={5}
placeholder={type === "content_report" ? "哪一段内容有问题,为什么" : type === "refund" ? "哪一次扣点或付款,希望怎么处理" : "尽量写清楚你做了什么、看到了什么"}
onChange={(event) => setBody(event.target.value)}
/>
</label>
<label className="feedback-dialog-field">
<span>联系方式(选填)</span>
<Input value={contact} maxLength={200} onChange={(event) => setContact(event.target.value)} placeholder="邮箱或手机号" />
</label>
{canAttach ? (
<label className="feedback-dialog-check">
<Checkbox checked={attach} onCheckedChange={(checked) => setAttach(checked === true)} />
<span>附上当前对话(只附编号,便于我们查看)</span>
</label>
) : null}
<Button type="submit" disabled={sending}>{sending ? "正在提交…" : "提交"}</Button>
</form>
)}
</>
);
}
export function FeedbackDialog(props: Readonly<{
accountEmail?: string | null;
sessionId?: string | null;
onClose: () => void;
}>) {
return (
<Dialog open onOpenChange={(next) => { if (!next) props.onClose(); }}>
<DialogContent className="feedback-dialog sm:max-w-md" showCloseButton={false}>
<DialogHeader className="feedback-dialog-header">
<DialogTitle>反馈与投诉</DialogTitle>
<Button variant="ghost" size="icon" onClick={props.onClose} aria-label="关闭反馈与投诉" title="关闭"><X aria-hidden="true" /></Button>
</DialogHeader>
<FeedbackDialogBody {...props} />
</DialogContent>
</Dialog>
);
}