From f90502b86471a47e0f4c724ae281482926e82394 Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Fri, 5 Jun 2026 14:10:23 +0800 Subject: [PATCH] feat: add bug feedback feature, fix canvas context menu on zoom, remove billing note from chat Co-Authored-By: Claude Opus 4.7 --- src/api/bugFeedbackClient.ts | 32 +++ src/components/AppShell.tsx | 9 + src/components/BugFeedbackModal.tsx | 131 ++++++++++ src/features/canvas/CanvasPage.tsx | 4 + src/features/workbench/WorkbenchPage.tsx | 5 - src/styles/components/bug-feedback-modal.css | 252 +++++++++++++++++++ src/styles/index.css | 1 + 7 files changed, 429 insertions(+), 5 deletions(-) create mode 100644 src/api/bugFeedbackClient.ts create mode 100644 src/components/BugFeedbackModal.tsx create mode 100644 src/styles/components/bug-feedback-modal.css diff --git a/src/api/bugFeedbackClient.ts b/src/api/bugFeedbackClient.ts new file mode 100644 index 0000000..84e05b7 --- /dev/null +++ b/src/api/bugFeedbackClient.ts @@ -0,0 +1,32 @@ +import { serverRequest } from "./serverConnection"; + +export interface BugFeedbackInput { + title: string; + description: string; + screenshotUrl?: string; +} + +export interface BugFeedbackItem { + id: number; + title: string; + description: string; + screenshotUrl?: string | null; + status: "pending" | "approved" | "rejected"; + adminNote?: string | null; + createdAt: string; +} + +export const bugFeedbackClient = { + async submit(input: BugFeedbackInput): Promise<{ id: number; status: string; createdAt: string }> { + const payload = await serverRequest<{ feedback: { id: number; status: string; createdAt: string } }>( + "bug-feedback", + { method: "POST", body: input }, + ); + return payload.feedback; + }, + + async listMine(): Promise { + const payload = await serverRequest<{ feedbacks?: BugFeedbackItem[] }>("bug-feedback/mine"); + return Array.isArray(payload.feedbacks) ? payload.feedbacks : []; + }, +}; diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 662c17f..a2e8b9c 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -1,6 +1,7 @@ import { ArrowDownOutlined, ArrowUpOutlined, + BugOutlined, CheckCircleOutlined, FlagOutlined, InfoCircleOutlined, @@ -14,6 +15,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import type { ReactNode } from "react"; import { publicConfigClient, type WebPublicConfig } from "../api/publicConfigClient"; import { toast } from "./toast/toastStore"; +import { BugFeedbackModal } from "./BugFeedbackModal"; import type { ServerConnectionHealth } from "../api/serverConnection"; import { ossAssets } from "../data/ossAssets"; import { canManageCommunityCases, canReviewCommunity } from "../features/community-review/communityPermissions"; @@ -68,6 +70,7 @@ function AppShell({ const [profileOpen, setProfileOpen] = useState(false); const [rechargeOpen, setRechargeOpen] = useState(false); const [infoOpen, setInfoOpen] = useState(false); + const [bugFeedbackOpen, setBugFeedbackOpen] = useState(false); const infoRef = useRef(null); const [openSubmenuKey, setOpenSubmenuKey] = useState(null); const [publicConfig, setPublicConfig] = useState({}); @@ -343,6 +346,11 @@ function AppShell({ onMarkAllRead={onMarkAllNotificationsRead} /> )} + {session && ( + + )}
); diff --git a/src/components/BugFeedbackModal.tsx b/src/components/BugFeedbackModal.tsx new file mode 100644 index 0000000..1264503 --- /dev/null +++ b/src/components/BugFeedbackModal.tsx @@ -0,0 +1,131 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { BugOutlined, CameraOutlined, CloseOutlined } from "@ant-design/icons"; +import { bugFeedbackClient, type BugFeedbackItem } from "../api/bugFeedbackClient"; +import { uploadAssetWithProgress } from "../api/uploadWithProgress"; +import { toast } from "./toast/toastStore"; + +interface BugFeedbackModalProps { + open: boolean; + onClose: () => void; +} + +export function BugFeedbackModal({ open, onClose }: BugFeedbackModalProps) { + const [tab, setTab] = useState<"submit" | "history">("submit"); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [screenshotUrl, setScreenshotUrl] = useState(null); + const [uploading, setUploading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [history, setHistory] = useState([]); + const [historyLoading, setHistoryLoading] = useState(false); + const fileInputRef = useRef(null); + const backdropRef = useRef(null); + + useEffect(() => { + if (open && tab === "history") loadHistory(); + }, [open, tab]); + + const loadHistory = useCallback(async () => { + setHistoryLoading(true); + try { + const items = await bugFeedbackClient.listMine(); + setHistory(items); + } catch { /* silent */ } + finally { setHistoryLoading(false); } + }, []); + + const handleScreenshot = useCallback(async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setUploading(true); + try { + const reader = new FileReader(); + const dataUrl = await new Promise((resolve) => { + reader.onload = () => resolve(reader.result as string); + reader.readAsDataURL(file); + }); + const result = await uploadAssetWithProgress({ dataUrl, name: file.name, scope: "bug-feedback" }); + setScreenshotUrl(result.url); + } catch { toast.error("截图上传失败"); } + finally { setUploading(false); } + }, []); + + const handleSubmit = useCallback(async () => { + if (!title.trim()) { toast.error("请输入标题"); return; } + if (!description.trim()) { toast.error("请输入问题描述"); return; } + setSubmitting(true); + try { + await bugFeedbackClient.submit({ title: title.trim(), description: description.trim(), screenshotUrl: screenshotUrl || undefined }); + toast.success("反馈提交成功,审核通过后将奖励 1 积分"); + setTitle(""); + setDescription(""); + setScreenshotUrl(null); + onClose(); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : "提交失败"); + } finally { setSubmitting(false); } + }, [title, description, screenshotUrl, onClose]); + + if (!open) return null; + + return ( +
{ if (e.target === backdropRef.current) onClose(); }}> +
+
+
+ + +
+ +
+ {tab === "submit" && ( +
+

提交有效 Bug 反馈,审核通过奖励 1 积分

+ + setTitle(e.target.value)} placeholder="简述遇到的问题" maxLength={200} /> + +