feat: 问卷支持暂存功能,打开时自动恢复上次填写内容
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo, useEffect, useCallback } from 'react'
|
||||||
import { Star, ChevronDown, ChevronRight, Check, Search, ClipboardList, Send } from 'lucide-react'
|
import { Star, ChevronDown, ChevronRight, Check, Search, ClipboardList, Send, Save, RotateCcw } from 'lucide-react'
|
||||||
import Modal from './ui/Modal'
|
import Modal from './ui/Modal'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import api from '../lib/api'
|
import api from '../lib/api'
|
||||||
@@ -11,11 +11,56 @@ interface FeatureScore {
|
|||||||
remark: string
|
remark: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'survey-draft'
|
||||||
|
|
||||||
export default function SurveyModal({ open, onClose }: { open: boolean; onClose: () => void }) {
|
export default function SurveyModal({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||||
const [expandedPage, setExpandedPage] = useState<number | null>(1)
|
const [expandedPage, setExpandedPage] = useState<number | null>(1)
|
||||||
const [scores, setScores] = useState<Record<string, FeatureScore>>({})
|
const [scores, setScores] = useState<Record<string, FeatureScore>>({})
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [submitting, setSubmitting] = useState(false)
|
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) => {
|
const setScore = (id: string, score: number) => {
|
||||||
setScores(prev => ({
|
setScores(prev => ({
|
||||||
@@ -61,10 +106,14 @@ export default function SurveyModal({ open, onClose }: { open: boolean; onClose:
|
|||||||
try {
|
try {
|
||||||
await api.post('/survey/submit', { items })
|
await api.post('/survey/submit', { items })
|
||||||
toast.success(`已提交 ${items.length} 项评分,感谢您的反馈!`)
|
toast.success(`已提交 ${items.length} 项评分,感谢您的反馈!`)
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
setSavedAt(null)
|
||||||
onClose()
|
onClose()
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.setItem('survey-results', JSON.stringify({ items, submittedAt: new Date().toISOString() }))
|
localStorage.setItem('survey-results', JSON.stringify({ items, submittedAt: new Date().toISOString() }))
|
||||||
toast.success(`已保存 ${items.length} 项评分到本地`)
|
toast.success(`已保存 ${items.length} 项评分到本地`)
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
setSavedAt(null)
|
||||||
onClose()
|
onClose()
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false)
|
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">
|
<div className="border-t border-gray-200 px-4 py-3 flex items-center justify-between">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
<span className="text-xs text-gray-500">已评分 {ratedCount} / {totalFeatures} 项</span>
|
<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">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={clearDraft}
|
||||||
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-200 rounded-md hover:bg-gray-50"
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
|||||||
Reference in New Issue
Block a user