chore: initialize deployable website and CMS
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
type Q = { id:string, group_key:string, group_name:string, type:string, title:string, options:any[], sort_order:number }
|
||||
const STORAGE_KEY='diagnosis_progress'
|
||||
|
||||
export default function Diagnosis(){
|
||||
const [questions,setQuestions]=useState<Q[]>([])
|
||||
const [answers,setAnswers]=useState<Record<string,any>>({})
|
||||
const [idx,setIdx]=useState(0)
|
||||
const nav=useNavigate()
|
||||
const [loading,setLoading]=useState(true)
|
||||
|
||||
useEffect(()=>{
|
||||
fetch('/api/diagnosis/questions').then(r=>r.json()).then(j=>{
|
||||
const list = (j.data||[]).sort((a:Q,b:Q)=> b.sort_order - a.sort_order)
|
||||
setQuestions(list)
|
||||
// 断点续填
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
if(saved){
|
||||
try{ const p=JSON.parse(saved); if(p.answers) setAnswers(p.answers); if(typeof p.idx==='number') setIdx(p.idx)}catch{}
|
||||
}
|
||||
setLoading(false)
|
||||
})
|
||||
},[])
|
||||
useEffect(()=>{
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({answers, idx}))
|
||||
},[answers, idx])
|
||||
|
||||
const q = questions[idx]
|
||||
const progress = questions.length? Math.round(((idx)/questions.length)*100):0
|
||||
const answeredCount = Object.keys(answers).length
|
||||
|
||||
const handleAnswer=(val:any)=>{
|
||||
const next={...answers, [q.id]:val}
|
||||
setAnswers(next)
|
||||
}
|
||||
const nextQ=()=>{
|
||||
if(idx<questions.length-1) setIdx(idx+1)
|
||||
else submit()
|
||||
}
|
||||
const prevQ=()=> setIdx(Math.max(0, idx-1))
|
||||
|
||||
const submit=async()=>{
|
||||
// 创建会话并提交
|
||||
const sess = await fetch('/api/diagnosis/sessions',{method:'POST'}).then(r=>r.json())
|
||||
const token = sess.data.token
|
||||
await fetch(`/api/diagnosis/sessions/${token}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({answers})})
|
||||
const rep = await fetch(`/api/diagnosis/sessions/${token}/submit`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({answers})}).then(r=>r.json())
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
nav(`/diagnosis/report/${rep.data.token}`)
|
||||
}
|
||||
|
||||
if(loading) return <div className="container section">加载问卷... <2s</div>
|
||||
if(!q) return <div className="container section">暂无题目,请联系管理员在CMS后台配置</div>
|
||||
|
||||
const isAnswered = answers[q.id]!==undefined
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<div className="container">
|
||||
<h1>企业进化诊断</h1>
|
||||
<p>15-20题 · 按四大价值分组 · 区间选择无需精确填写 · 免责:初步分析仅供参考</p>
|
||||
<div style={{marginTop:12, background:'rgba(255,255,255,0.2)', height:6, borderRadius:6}}>
|
||||
<div style={{width:`${progress}%`, height:'100%', background:'#fff', borderRadius:6}} />
|
||||
</div>
|
||||
<div style={{fontSize:12, color:'rgba(255,255,255,0.8)', marginTop:4}}>{idx+1}/{questions.length} · 已答 {answeredCount} 题 · 本地自动保存,刷新不丢失</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section">
|
||||
<div className="container" style={{maxWidth:720}}>
|
||||
<div className="card">
|
||||
<div style={{fontSize:12, color:'#0f6b5b', fontWeight:800}}>{q.group_name} · {q.type==='scale'?'量表 1-5':q.type==='multiple'?'多选':'单选'}</div>
|
||||
<h3 style={{marginTop:6}}>{idx+1}. {q.title}</h3>
|
||||
<div style={{marginTop:12, display:'grid', gap:8}}>
|
||||
{q.options.map((opt:any)=> {
|
||||
const selected = q.type==='multiple' ? (Array.isArray(answers[q.id]) && answers[q.id].includes(opt.label)) : answers[q.id]===opt.label
|
||||
return (
|
||||
<label key={opt.label} style={{border: selected?'2px solid #163E70':'1px solid #e2e8f0', background: selected?'#eff6ff':'#fff', padding:'10px 12px', borderRadius:8, cursor:'pointer', display:'flex', justifyContent:'space-between'}}>
|
||||
<span><input type={q.type==='multiple'?'checkbox':'radio'} checked={!!selected} onChange={()=>{
|
||||
if(q.type==='multiple'){
|
||||
const arr=Array.isArray(answers[q.id])? [...answers[q.id]]:[]
|
||||
if(selected) handleAnswer(arr.filter(v=>v!==opt.label))
|
||||
else handleAnswer([...arr, opt.label])
|
||||
} else handleAnswer(opt.label)
|
||||
}} style={{marginRight:8}} />{opt.label}</span>
|
||||
<span style={{fontSize:11,color:'#64748b'}}>{opt.score}分</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div style={{display:'flex', gap:10, marginTop:16}}>
|
||||
<button className="btn btn-ghost" onClick={prevQ} disabled={idx===0}>上一步</button>
|
||||
<button className="btn btn-primary" style={{flex:1}} onClick={nextQ} disabled={!isAnswered}>{idx===questions.length-1?'提交生成报告':'下一步'}</button>
|
||||
</div>
|
||||
<div style={{fontSize:11, color:'#94a3b8', marginTop:8}}>禁止开放题必填 · 采用区间选择 · 不收集敏感财务数据</div>
|
||||
</div>
|
||||
<div style={{marginTop:12, display:'flex', gap:10}}>
|
||||
<Link to="/self-check" className="btn btn-ghost">返回轻量对照</Link>
|
||||
<button className="btn btn-ghost" onClick={()=>{localStorage.removeItem(STORAGE_KEY); setAnswers({}); setIdx(0)}}>重置进度</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user