11import { FormEvent , useMemo , useState } from "react" ;
22import { invoke } from "@tauri-apps/api/core" ;
33import { openUrl } from "@tauri-apps/plugin-opener" ;
4- import { Loader2 , Mail , MessageSquarePlus , Send } from "lucide-react" ;
4+ import { CheckCircle2 , Copy , ExternalLink , Loader2 , Mail , MessageSquarePlus , Send } from "lucide-react" ;
55import { version as APP_VERSION } from "../../package.json" ;
66import { Button } from "./ui/button" ;
77import {
@@ -18,6 +18,8 @@ import { toast } from "./ui/toast";
1818import { Tooltip , TooltipContent , TooltipTrigger } from "./ui/tooltip" ;
1919
2020const FEEDBACK_EMAIL = "mark@lovstudio.ai" ;
21+ const MIN_MESSAGE_CHARS = 4 ;
22+ const TICKETS_URL = "https://lovstudio.ai/account/tickets" ;
2123
2224const categoryOptions = [
2325 { value : "bug" , label : "问题" } ,
@@ -45,16 +47,23 @@ function getTimezone() {
4547 }
4648}
4749
50+ function charCount ( value : string ) {
51+ return Array . from ( value ) . length ;
52+ }
53+
4854export function FeedbackButton ( ) {
4955 const [ open , setOpen ] = useState ( false ) ;
5056 const [ category , setCategory ] = useState < FeedbackCategory > ( "idea" ) ;
5157 const [ message , setMessage ] = useState ( "" ) ;
5258 const [ contact , setContact ] = useState ( "" ) ;
5359 const [ submitting , setSubmitting ] = useState ( false ) ;
5460 const [ error , setError ] = useState ( "" ) ;
61+ const [ submittedTicket , setSubmittedTicket ] = useState < FeedbackSubmitResult | null > ( null ) ;
5562
5663 const trimmedMessage = message . trim ( ) ;
57- const canSubmit = trimmedMessage . length >= 4 && ! submitting ;
64+ const messageLength = charCount ( trimmedMessage ) ;
65+ const isMessageTooShort = messageLength > 0 && messageLength < MIN_MESSAGE_CHARS ;
66+ const canSubmit = messageLength >= MIN_MESSAGE_CHARS && ! submitting ;
5867
5968 const selectedCategoryLabel = useMemo (
6069 ( ) => categoryOptions . find ( ( item ) => item . value === category ) ?. label ?? "建议" ,
@@ -66,12 +75,15 @@ export function FeedbackButton() {
6675 setMessage ( "" ) ;
6776 setContact ( "" ) ;
6877 setError ( "" ) ;
78+ setSubmittedTicket ( null ) ;
6979 } ;
7080
7181 const handleOpenChange = ( nextOpen : boolean ) => {
7282 setOpen ( nextOpen ) ;
7383 if ( nextOpen ) {
7484 setError ( "" ) ;
85+ } else {
86+ resetForm ( ) ;
7587 }
7688 } ;
7789
@@ -99,6 +111,27 @@ export function FeedbackButton() {
99111 }
100112 } ;
101113
114+ const handleCopyTicketId = async ( ) => {
115+ if ( ! submittedTicket ?. feedbackId ) return ;
116+
117+ try {
118+ await navigator . clipboard . writeText ( submittedTicket . feedbackId ) ;
119+ toast . success ( "工单 ID 已复制" ) ;
120+ } catch ( err ) {
121+ const message = err instanceof Error ? err . message : String ( err ) ;
122+ toast . error ( `复制失败: ${ message } ` ) ;
123+ }
124+ } ;
125+
126+ const handleOpenTickets = async ( ) => {
127+ try {
128+ await openUrl ( TICKETS_URL ) ;
129+ } catch ( err ) {
130+ const message = err instanceof Error ? err . message : String ( err ) ;
131+ toast . error ( `打开用户中心失败: ${ message } ` ) ;
132+ }
133+ } ;
134+
102135 const handleSubmit = async ( event : FormEvent < HTMLFormElement > ) => {
103136 event . preventDefault ( ) ;
104137 if ( ! canSubmit ) return ;
@@ -107,7 +140,7 @@ export function FeedbackButton() {
107140 setError ( "" ) ;
108141
109142 try {
110- await invoke < FeedbackSubmitResult > ( "submit_feedback" , {
143+ const result = await invoke < FeedbackSubmitResult > ( "submit_feedback" , {
111144 payload : {
112145 category,
113146 message : trimmedMessage ,
@@ -124,9 +157,9 @@ export function FeedbackButton() {
124157 } ,
125158 } ) ;
126159
127- toast . success ( "反馈已提交" ) ;
128- resetForm ( ) ;
129- setOpen ( false ) ;
160+ setSubmittedTicket ( result ) ;
161+ setMessage ( "" ) ;
162+ toast . success ( "反馈已提交,请复制工单 ID" ) ;
130163 } catch ( err ) {
131164 const message = err instanceof Error ? err . message : String ( err ) ;
132165 setError ( message ) ;
@@ -158,11 +191,47 @@ export function FeedbackButton() {
158191 < DialogHeader >
159192 < DialogTitle className = "font-serif" > 提交反馈</ DialogTitle >
160193 < DialogDescription >
161- 同步到 lovstudio.ai,并通知 { FEEDBACK_EMAIL } 。
194+ { submittedTicket
195+ ? "反馈已进入工单系统。"
196+ : `同步到 lovstudio.ai,并通知 ${ FEEDBACK_EMAIL } 。` }
162197 </ DialogDescription >
163198 </ DialogHeader >
164199
165- < form className = "space-y-4" onSubmit = { handleSubmit } >
200+ { submittedTicket ? (
201+ < div className = "space-y-4" >
202+ < div className = "rounded-xl border border-primary/30 bg-primary/5 p-4" >
203+ < div className = "flex items-start gap-3" >
204+ < CheckCircle2 className = "mt-0.5 h-5 w-5 text-primary" />
205+ < div className = "min-w-0 flex-1 space-y-2" >
206+ < p className = "font-medium text-foreground" > 反馈已提交</ p >
207+ < p className = "text-sm text-muted-foreground" >
208+ 复制工单 ID,后续可以在用户中心查看处理状态。
209+ </ p >
210+ < div className = "flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2" >
211+ < code className = "min-w-0 flex-1 truncate font-mono text-sm text-foreground" >
212+ { submittedTicket . feedbackId }
213+ </ code >
214+ < Button type = "button" size = "sm" variant = "outline" onClick = { handleCopyTicketId } >
215+ < Copy className = "mr-2 h-4 w-4" />
216+ 复制
217+ </ Button >
218+ </ div >
219+ </ div >
220+ </ div >
221+ </ div >
222+
223+ < DialogFooter className = "gap-2 sm:space-x-0" >
224+ < Button type = "button" variant = "outline" onClick = { handleOpenTickets } >
225+ < ExternalLink className = "mr-2 h-4 w-4" />
226+ 用户中心
227+ </ Button >
228+ < Button type = "button" onClick = { ( ) => handleOpenChange ( false ) } >
229+ 完成
230+ </ Button >
231+ </ DialogFooter >
232+ </ div >
233+ ) : (
234+ < form className = "space-y-4" onSubmit = { handleSubmit } >
166235 < div className = "grid grid-cols-3 gap-2" >
167236 { categoryOptions . map ( ( item ) => (
168237 < button
@@ -190,6 +259,12 @@ export function FeedbackButton() {
190259 maxLength = { 5000 }
191260 className = "min-h-32 w-full resize-none rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
192261 />
262+ < div className = "flex items-center justify-between text-xs text-muted-foreground" >
263+ < span className = { isMessageTooShort ? "text-destructive" : "" } >
264+ { isMessageTooShort ? `至少输入 ${ MIN_MESSAGE_CHARS } 个字符` : " " }
265+ </ span >
266+ < span > { messageLength } /5000</ span >
267+ </ div >
193268 </ div >
194269
195270 < div className = "space-y-2" >
@@ -198,9 +273,12 @@ export function FeedbackButton() {
198273 id = "feedback-contact"
199274 value = { contact }
200275 onChange = { ( event ) => setContact ( event . target . value ) }
201- placeholder = "邮箱、微信或其他联系方式 (可选)"
276+ placeholder = "建议填写 lovstudio.ai 登录邮箱 (可选)"
202277 maxLength = { 200 }
203278 />
279+ < p className = "text-xs text-muted-foreground" >
280+ 使用登录邮箱提交后,可在用户中心查看已提交工单。
281+ </ p >
204282 </ div >
205283
206284 { error && (
@@ -219,7 +297,8 @@ export function FeedbackButton() {
219297 提交
220298 </ Button >
221299 </ DialogFooter >
222- </ form >
300+ </ form >
301+ ) }
223302 </ DialogContent >
224303 </ Dialog >
225304 </ >
0 commit comments