feat: 问卷支持暂存功能,打开时自动恢复上次填写内容

This commit is contained in:
selfrelease
2026-07-28 14:49:18 +08:00
parent f31c9e159a
commit 7716de63f1
+68 -6
View File
@@ -1,5 +1,5 @@
import { useState, useMemo } from 'react'
import { Star, ChevronDown, ChevronRight, Check, Search, ClipboardList, Send } from 'lucide-react'
import { useState, useMemo, useEffect, useCallback } from 'react'
import { Star, ChevronDown, ChevronRight, Check, Search, ClipboardList, Send, Save, RotateCcw } from 'lucide-react'
import Modal from './ui/Modal'
import { toast } from 'sonner'
import api from '../lib/api'
@@ -11,11 +11,56 @@ interface FeatureScore {
remark: string
}
const STORAGE_KEY = 'survey-draft'
export default function SurveyModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const [expandedPage, setExpandedPage] = useState<number | null>(1)
const [scores, setScores] = useState<Record<string, FeatureScore>>({})
const [searchQuery, setSearchQuery] = useState('')
const [submitting, setSubmitting] = useState(false)
const [savedAt, setSavedAt] = useState<string | null>(null)
/** 从 localStorage 恢复暂存数据 */
const loadDraft = useCallback(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) {
const data = JSON.parse(raw)
if (data.scores && typeof data.scores === 'object') {
setScores(data.scores)
setSavedAt(data.savedAt || null)
}
}
} catch {
// 忽略解析错误
}
}, [])
/** 暂存到 localStorage */
const saveDraft = useCallback(() => {
const hasData = Object.values(scores).some(s => s.score > 0 || s.useful || s.remark)
if (!hasData) {
toast.info('暂无可保存的填写内容')
return
}
const now = new Date().toISOString()
localStorage.setItem(STORAGE_KEY, JSON.stringify({ scores, savedAt: now }))
setSavedAt(now)
toast.success('已暂存填写内容,下次打开可恢复')
}, [scores])
/** 清空暂存 */
const clearDraft = useCallback(() => {
localStorage.removeItem(STORAGE_KEY)
setScores({})
setSavedAt(null)
toast.info('已清空所有填写内容')
}, [])
/** 打开时恢复暂存 */
useEffect(() => {
if (open) loadDraft()
}, [open, loadDraft])
const setScore = (id: string, score: number) => {
setScores(prev => ({
@@ -61,10 +106,14 @@ export default function SurveyModal({ open, onClose }: { open: boolean; onClose:
try {
await api.post('/survey/submit', { items })
toast.success(`已提交 ${items.length} 项评分,感谢您的反馈!`)
localStorage.removeItem(STORAGE_KEY)
setSavedAt(null)
onClose()
} catch {
localStorage.setItem('survey-results', JSON.stringify({ items, submittedAt: new Date().toISOString() }))
toast.success(`已保存 ${items.length} 项评分到本地`)
localStorage.removeItem(STORAGE_KEY)
setSavedAt(null)
onClose()
} finally {
setSubmitting(false)
@@ -168,13 +217,26 @@ export default function SurveyModal({ open, onClose }: { open: boolean; onClose:
{/* 底部提交 */}
<div className="border-t border-gray-200 px-4 py-3 flex items-center justify-between">
<span className="text-xs text-gray-500"> {ratedCount} / {totalFeatures} </span>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-gray-500"> {ratedCount} / {totalFeatures} </span>
{savedAt && <span className="text-xs text-gray-400">{new Date(savedAt).toLocaleString('zh-CN')}</span>}
</div>
<div className="flex gap-2">
<button
onClick={onClose}
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-200 rounded-md hover:bg-gray-50"
onClick={clearDraft}
disabled={ratedCount === 0 && !savedAt}
className="px-3 py-1.5 text-sm text-gray-500 border border-gray-200 rounded-md hover:bg-gray-50 disabled:opacity-50 flex items-center gap-1"
>
<RotateCcw className="w-3.5 h-3.5" />
</button>
<button
onClick={saveDraft}
disabled={ratedCount === 0}
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-200 rounded-md hover:bg-gray-50 disabled:opacity-50 flex items-center gap-1"
>
<Save className="w-3.5 h-3.5" />
</button>
<button
onClick={handleSubmit}