fix: make CMS content live and migrate to MySQL

This commit is contained in:
Codex User
2026-09-08 16:03:38 +08:00
parent 3a99ae30c5
commit 7608dc5415
31 changed files with 614 additions and 962 deletions
+84 -53
View File
@@ -3,19 +3,20 @@ import { useState, useEffect } from 'react'
const API_BASE = '' // 同域,相对路径
const SIDEBAR = [
{ key: 'config', label: '1 全局配置' },
{ key: 'home', label: '2 首页模块' },
{ key: 'ameba', label: '3 阿美巴页面' },
{ key: 'evolution', label: '4 企业进化' },
{ key: 'methods', label: '5 方法与能力' },
{ key: 'services', label: '6 产品服务' },
{ key: 'cases', label: '7 案例管理' },
{ key: 'articles', label: '8 认知观察' },
{ key: 'selfcheck', label: '9 自我对照' },
{ key: 'diag_q', label: '10 诊断问卷' },
{ key: 'diag_config', label: '11 诊断配置' },
{ key: 'diag_reports', label: '12 诊断记录' },
{ key: 'diag_leads', label: '13 留资统计' },
{ key: 'contact_leads', label: '14 咨询线索' },
{ key: 'texts', label: '2 全站文案' },
{ key: 'home', label: '3 首页模块' },
{ key: 'ameba', label: '4 阿美巴页面' },
{ key: 'evolution', label: '5 企业进化' },
{ key: 'methods', label: '6 方法与能力' },
{ key: 'services', label: '7 产品服务' },
{ key: 'cases', label: '8 案例管理' },
{ key: 'articles', label: '9 认知观察' },
{ key: 'selfcheck', label: '10 自我对照' },
{ key: 'diag_q', label: '11 诊断问卷' },
{ key: 'diag_config', label: '12 诊断配置' },
{ key: 'diag_reports', label: '13 诊断记录' },
{ key: 'diag_leads', label: '14 留资统计' },
{ key: 'contact_leads', label: '15 咨询线索' },
{ key: 'password', label: '⚙ 修改密码' },
]
@@ -39,6 +40,15 @@ function api(path:string, token:string, opts:RequestInit={}){
return fetch(path,{...opts, headers}).then(r=>r.json())
}
const FIELD_LABELS:Record<string,string> = {
site_title:'网站标题', slogan:'网站标语', seo_title:'SEO标题', seo_desc:'SEO描述', seo_keywords:'SEO关键词', hotline:'热线电话', contact_email:'联系邮箱', contact_address:'联系地址', cta_text:'行动按钮文字', cta_link:'行动按钮链接',
brand_position:'品牌定位', mission:'使命/介绍', philosophy:'经营哲学', practice:'服务理念', steps:'步骤', scenarios:'适用场景', description:'描述',
original_state:'原始状态', goal:'目标', gap:'差距', root_cause:'根因', ameba_intervention:'阿美巴介入', evolution_path:'进化路径', actions:'行动', phased_result:'阶段性结果', continuous_iteration:'持续迭代',
stage:'适用阶段', intro:'简介', service_content:'服务内容', delivery:'交付内容', suitable:'适合对象', price_unit:'价格单位', advantage:'核心优势', detail_html:'详情正文',
guide:'引导语', questions:'问题配置', logic_text:'结果逻辑说明', next_guide:'下一步引导',
}
const fieldLabel=(key:string)=>FIELD_LABELS[key] ? `${FIELD_LABELS[key]}${key}` : key
export default function App(){
const {token,login,logout}=useAuth()
const [user,setUser]=useState('')
@@ -66,6 +76,7 @@ export default function App(){
<div className="main">
<div className="topbar"><h2>{SIDEBAR.find(s=>s.key===cur)?.label}</h2><span style={{fontSize:12,color:'#94a3b8'}}> · </span></div>
{cur==='config' && <Config token={token} />}
{cur==='texts' && <Texts token={token} />}
{cur==='home' && <Home token={token} />}
{cur==='ameba' && <Ameba token={token} />}
{cur==='evolution' && <Evolution token={token} />}
@@ -85,6 +96,19 @@ export default function App(){
)
}
function Texts({token}:{token:string}){
const [list,setList]=useState<any[]>([])
useEffect(()=>{api('/api/cms/texts',token).then(j=>setList(j.data||[]))},[])
const update=(i:number,key:string,value:any)=>{const n=[...list]; n[i]={...n[i],[key]:value}; setList(n)}
const save=()=>api('/api/cms/texts',token,{method:'PUT',body:JSON.stringify(list)}).then(j=>alert(j.msg||'保存成功'))
const groups=list.reduce((acc:any,it:any)=>{const group=it.id.split('.')[0]; (acc[group] ||= []).push(it); return acc},{})
const groupNames:any={home:'首页(Home',global_cta:'全站行动区(Global CTA',ameba:'阿美巴页面(Ameba',evolution:'企业进化页面(Evolution',methods:'方法与能力(Methods',services:'产品服务(Services',cases:'实践案例(Cases',insights:'认知观察(Insights',contact:'联系页(Contact',diagnosis:'诊断页(Diagnosis',selfcheck:'轻量对照(Self Check',footer:'页脚(Footer'}
return <div className="card"><h3></h3><p className="hint"></p>
{Object.entries(groups).map(([group,items]:any)=><div key={group} className="editor-block"><h4>{groupNames[group] || group}</h4>{items.map((it:any)=><div key={it.id} className="field"><label>{it.label} <small>({it.id})</small></label><textarea value={it.value||''} onChange={e=>update(list.findIndex(x=>x.id===it.id),'value',e.target.value)} /></div>)}</div>)}
<button className="btn" onClick={save}></button>
</div>
}
function ContactLeads({token}:{token:string}){
const [list,setList]=useState<any[]>([])
useEffect(()=>{api('/api/cms/contact-leads',token).then(j=> setList(j.data||[]))},[])
@@ -103,7 +127,7 @@ function Config({token}:{token:string}){
return <div className="card">
<h3></h3>
{['site_title','slogan','seo_title','seo_desc','seo_keywords','hotline','contact_email','contact_address','cta_text','cta_link'].map(k=>(
<div key={k} className="field"><label>{k}</label><input value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
<div key={k} className="field"><label>{fieldLabel(k)}</label><input value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
))}
<button className="btn" onClick={save}></button>
</div>
@@ -114,7 +138,7 @@ function Home({token}:{token:string}){
const save=()=> api('/api/cms/home-sections',token,{method:'PUT',body:JSON.stringify(list)}).then(()=> alert('保存成功'))
return <div className="card">
<h3>visible/sort_order</h3>
<table className="table"><thead><tr><th>key</th><th></th><th></th><th></th><th></th></tr></thead>
<table className="table"><thead><tr><th> keyKey</th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{list.map((it,i)=>(
<tr key={it.id}><td>{it.key}</td><td><input value={it.title} onChange={e=>{const n=[...list]; n[i].title=e.target.value; setList(n)}}/></td><td><input value={it.subtitle} onChange={e=>{const n=[...list]; n[i].subtitle=e.target.value; setList(n)}}/></td><td><input type="checkbox" checked={!!it.visible} onChange={e=>{const n=[...list];n[i].visible=e.target.checked?1:0; setList(n)}}/></td><td><input type="number" value={it.sort_order} onChange={e=>{const n=[...list];n[i].sort_order=Number(e.target.value); setList(n)}} /></td></tr>
))}</tbody></table>
@@ -122,18 +146,18 @@ function Home({token}:{token:string}){
</div>
}
function Ameba({token}:{token:string}){
const [f,setF]=useState<any>({}); const [ex,setEx]=useState<any[]>([]); const [ne,setNe]=useState<any>({})
const [f,setF]=useState<any>({}); const [ex,setEx]=useState<any[]>([]); const [ne,setNe]=useState<any>({}); const [editingExpert,setEditingExpert]=useState(false)
useEffect(()=>{api('/api/cms/ameba',token).then(j=> setF(j.data||{})); api('/api/cms/experts',token).then(j=> setEx(j.data||[]))},[])
return <>
<div className="card"><h3></h3>
{['brand_position','mission','philosophy','practice'].map(k=>(
<div key={k} className="field"><label>{k}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
<div key={k} className="field"><label>{fieldLabel(k)}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
))}
<button className="btn" onClick={()=> api('/api/cms/ameba',token,{method:'PUT',body:JSON.stringify(f)}).then(()=> alert('保存成功'))}></button>
</div>
<div className="card"><h3></h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{ex.map(e=> <tr key={e.id}><td>{e.name}</td><td>{e.title}</td><td>{e.sort_weight}</td><td>{e.status}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/experts/'+e.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table>
<tbody>{ex.map(e=> <tr key={e.id}><td>{e.name}</td><td>{e.title}</td><td>{e.sort_weight}</td><td>{e.status}</td><td><button onClick={()=>{setNe({...e});setEditingExpert(true)}}></button> <button onClick={()=>{if(confirm('删除?')) api('/api/cms/experts/'+e.id,token,{method:'DELETE'}).then(j=>{if(j.code===0)setEx(ex.filter(x=>x.id!==e.id))})}}></button></td></tr>)}</tbody></table>
<div className="form-row" style={{marginTop:12}}>
<div className="field"><label></label><input value={ne.name||''} onChange={e=> setNe({...ne,name:e.target.value})}/></div>
<div className="field"><label></label><input value={ne.title||''} onChange={e=> setNe({...ne,title:e.target.value})}/></div>
@@ -142,68 +166,78 @@ function Ameba({token}:{token:string}){
<div className="form-row"><div className="field"><label></label><input type="number" value={ne.sort_weight||0} onChange={e=> setNe({...ne,sort_weight:Number(e.target.value)})}/></div><div className="field"><label></label><select value={ne.status||'published'} onChange={e=> setNe({...ne,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div></div>
<Upload token={token} onUrl={url=> setNe({...ne,avatar_url:url})} />
{ne.avatar_url && <img src={ne.avatar_url} style={{width:80,height:80,objectFit:'cover',borderRadius:8}}/>}
<button className="btn" onClick={()=> api('/api/cms/experts',token,{method:'POST',body:JSON.stringify(ne)}).then(()=> location.reload())}></button>
<button className="btn" onClick={()=> api(editingExpert?'/api/cms/experts/'+ne.id:'/api/cms/experts',token,{method:editingExpert?'PUT':'POST',body:JSON.stringify(ne)}).then(j=>{if(j.code===0){const saved={...ne,id:editingExpert?ne.id:j.data?.id};setEx(editingExpert?ex.map(x=>x.id===saved.id?saved:x):[...ex,saved]);setNe({});setEditingExpert(false);alert('保存成功')}else alert(j.msg||'保存失败')})}>{editingExpert?'保存专家':'新增专家'}</button>{editingExpert&&<button className="btn btn-ghost" style={{marginLeft:8}} onClick={()=>{setNe({});setEditingExpert(false)}}></button>}
</div>
</>
}
function Evolution({token}:{token:string}){
const [thought,setThought]=useState(''); const [fvs,setFvs]=useState<any[]>([]); const [ambs,setAmbs]=useState<any[]>([])
useEffect(()=>{api('/api/cms/evolution/thought',token).then(j=> setThought(j.data?.content||'')); api('/api/cms/four-values',token).then(j=> setFvs(j.data||[])); api('/api/cms/amb-states',token).then(j=> setAmbs(j.data||[]))},[])
const update=(setter:any,list:any[],i:number,key:string,value:any)=>{const n=[...list]; n[i]={...n[i],[key]:value}; setter(n)}
const saveValues=()=>Promise.all(fvs.map(v=>api('/api/cms/four-values/'+v.id,token,{method:'PUT',body:JSON.stringify(v)}))).then(()=>alert('四大价值保存成功'))
const saveAmbs=()=>Promise.all(ambs.map(a=>api('/api/cms/amb-states/'+a.id,token,{method:'PUT',body:JSON.stringify(a)}))).then(()=>alert('A/M/B 三态保存成功'))
return <>
<div className="card"><h3></h3><div className="field"><textarea value={thought} onChange={e=> setThought(e.target.value)} /></div><button className="btn" onClick={()=> api('/api/cms/evolution/thought',token,{method:'PUT',body:JSON.stringify({content:thought})}).then(()=> alert('保存成功'))}></button></div>
<div className="card"><h3></h3><table className="table"><thead><tr><th>key</th><th></th><th></th><th></th><th></th></tr></thead><tbody>{fvs.map(v=> <tr key={v.id}><td>{v.key}</td><td>{v.name}</td><td>{v.sort_order}</td><td>{v.status}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/four-values/'+v.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>A/M/B </h3><table className="table"><thead><tr><th>key</th><th></th><th></th></tr></thead><tbody>{ambs.map(a=> <tr key={a.id}><td>{a.key}</td><td>{a.name}</td><td style={{fontSize:11}}>{a.fixed_note}</td></tr>)}</tbody></table><p style={{fontSize:11,color:'#92400e',background:'#fff7e6',padding:8,borderRadius:6}}>A/M/B不绑定企业规模的说明文案固定不可删</p></div>
<div className="card"><h3>Enterprise Evolution Thought</h3><div className="field"><label>Content</label><textarea value={thought} onChange={e=> setThought(e.target.value)} /></div><button className="btn" onClick={()=> api('/api/cms/evolution/thought',token,{method:'PUT',body:JSON.stringify({content:thought})}).then(()=> alert('保存成功'))}></button></div>
<div className="card"><h3>Four Values</h3><p className="hint"></p>{fvs.map((v,i)=><div key={v.id} className="editor-block"><div className="form-row"><div className="field"><label> keyKey</label><input value={v.key||''} onChange={e=>update(setFvs,fvs,i,'key',e.target.value)}/></div><div className="field"><label>Name</label><input value={v.name||''} onChange={e=>update(setFvs,fvs,i,'name',e.target.value)}/></div></div><div className="field"><label>/Definition</label><textarea value={v.definition||''} onChange={e=>update(setFvs,fvs,i,'definition',e.target.value)}/></div><div className="field"><label>Description</label><textarea value={v.description||''} onChange={e=>update(setFvs,fvs,i,'description',e.target.value)}/></div><div className="form-row"><div className="field"><label>Sort Order</label><input type="number" value={v.sort_order||0} onChange={e=>update(setFvs,fvs,i,'sort_order',Number(e.target.value))}/></div><div className="field"><label>Status</label><select value={v.status||'published'} onChange={e=>update(setFvs,fvs,i,'status',e.target.value)}><option value="published">published</option><option value="draft">稿draft</option></select></div></div><div className="hint">/evolution/values/{v.key || 'key'}</div></div>)}<button className="btn" onClick={saveValues}></button></div>
<div className="card"><h3>A/M/B A/M/B States</h3><p className="hint"></p>{ambs.map((a,i)=><div key={a.id} className="editor-block"><div className="form-row"><div className="field"><label> keyKey</label><input value={a.key||''} disabled /></div><div className="field"><label>Name</label><input value={a.name||''} onChange={e=>update(setAmbs,ambs,i,'name',e.target.value)}/></div></div><div className="field"><label>Definition</label><textarea value={a.definition||''} onChange={e=>update(setAmbs,ambs,i,'definition',e.target.value)}/></div><div className="field"><label>Description</label><textarea value={a.description||''} onChange={e=>update(setAmbs,ambs,i,'description',e.target.value)}/></div><div className="field"><label>Fixed Note</label><textarea value={a.fixed_note||''} onChange={e=>update(setAmbs,ambs,i,'fixed_note',e.target.value)}/></div><div className="hint">/evolution/amb/{a.key}</div></div>)}<button className="btn" onClick={saveAmbs}> A/M/B </button></div>
</>
}
function Methods({token}:{token:string}){
const [proc,setProc]=useState<any>({}); const [sys,setSys]=useState<any[]>([])
const [proc,setProc]=useState<any>({}); const [sys,setSys]=useState<any[]>([]); const [f,setF]=useState<any>({status:'published'}); const [editing,setEditing]=useState(false)
useEffect(()=>{api('/api/cms/evolution-process',token).then(j=> setProc(j.data||{})); api('/api/cms/ten-systems',token).then(j=> setSys(j.data||[]))},[])
const edit=(item:any)=>{setF({...item}); setEditing(true)}
const save=()=>api(editing?'/api/cms/ten-systems/'+f.id:'/api/cms/ten-systems',token,{method:editing?'PUT':'POST',body:JSON.stringify(f)}).then(j=>{if(j.code===0){const saved={...f,id:editing?f.id:j.data?.id};setSys(editing?sys.map(x=>x.id===saved.id?saved:x):[...sys,saved]);setF({status:'published'});setEditing(false);alert('保存成功')}else alert(j.msg||'保存失败')})
return <>
<div className="card"><h3></h3>{['steps','scenarios','description'].map(k=> <div key={k} className="field"><label>{k}</label><textarea value={proc[k]||''} onChange={e=> setProc({...proc,[k]:e.target.value})} /></div>)}<button className="btn" onClick={()=> api('/api/cms/evolution-process',token,{method:'PUT',body:JSON.stringify(proc)}).then(()=> alert('保存成功'))}></button></div>
<div className="card"><h3></h3><table className="table"><thead><tr><th></th><th></th><th></th></tr></thead><tbody>{sys.map(s=> <tr key={s.id}><td>{s.name}</td><td>{s.sort_order}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/ten-systems/'+s.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3></h3>{['steps','scenarios','description'].map(k=> <div key={k} className="field"><label>{fieldLabel(k)}</label><textarea value={proc[k]||''} onChange={e=> setProc({...proc,[k]:e.target.value})} /></div>)}<button className="btn" onClick={()=> api('/api/cms/evolution-process',token,{method:'PUT',body:JSON.stringify(proc)}).then(()=> alert('保存成功'))}></button></div>
<div className="card"><h3></h3><p className="hint"></p><table className="table"><thead><tr><th></th><th></th><th></th><th></th></tr></thead><tbody>{sys.map(s=> <tr key={s.id}><td>{s.name}</td><td>{s.sort_order}</td><td>{s.status}</td><td><button onClick={()=>edit(s)}></button> <button onClick={()=>{if(confirm('删除?')) api('/api/cms/ten-systems/'+s.id,token,{method:'DELETE'}).then(j=>{if(j.code===0)setSys(sys.filter(x=>x.id!==s.id))})}}></button></td></tr>)}</tbody></table><div className="editor-block"><h4>{editing?'编辑系统':'新增系统'}</h4><div className="field"><label>Name</label><input value={f.name||''} onChange={e=>setF({...f,name:e.target.value})}/></div><div className="field"><label>Description</label><textarea value={f.description||''} onChange={e=>setF({...f,description:e.target.value})}/></div><div className="form-row"><div className="field"><label>Sort Order</label><input type="number" value={f.sort_order||0} onChange={e=>setF({...f,sort_order:Number(e.target.value)})}/></div><div className="field"><label>Status</label><select value={f.status||'published'} onChange={e=>setF({...f,status:e.target.value})}><option value="published"></option><option value="draft">稿</option></select></div></div><button className="btn" onClick={save}>{editing?'保存修改':'新增系统'}</button>{editing&&<button className="btn btn-ghost" style={{marginLeft:8}} onClick={()=>{setF({status:'published'});setEditing(false)}}></button>}</div></div>
</>
}
function Services({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({status:'published',type:'custom'}); const [editing,setEditing]=useState(false)
useEffect(()=>{api('/api/cms/services',token).then(j=> setList(j.data||[]))},[])
const save=()=>api(editing?'/api/cms/services/'+f.id:'/api/cms/services',token,{method:editing?'PUT':'POST',body:JSON.stringify(f)}).then(j=>{if(j.code===0){const saved={...f,id:editing?f.id:j.data?.id};setList(editing?list.map(x=>x.id===saved.id?saved:x):[...list,saved]);setF({status:'published',type:'custom'});setEditing(false);alert('保存成功')}else alert(j.msg||'保存失败')})
return <>
<div className="card"><h3></h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(s=> <tr key={s.id}><td>{s.name}</td><td>{s.type}</td><td>{s.price||'-'}</td><td>{s.status}</td><td>{s.sort_weight}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/services/'+s.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>/</h3>
<div className="form-row"><div className="field"><label>*</label><input value={f.name||''} onChange={e=> setF({...f,name:e.target.value})}/></div><div className="field"><label>slug</label><input value={f.slug||''} onChange={e=> setF({...f,slug:e.target.value})}/></div></div>
<div className="card"><h3></h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(s=> <tr key={s.id}><td>{s.name}</td><td>{s.type}</td><td>{s.price||'-'}</td><td>{s.status}</td><td>{s.sort_weight}</td><td><button onClick={()=>{setF({...s});setEditing(true)}}></button> <button onClick={()=>{if(confirm('删除?')) api('/api/cms/services/'+s.id,token,{method:'DELETE'}).then(j=>{if(j.code===0)setList(list.filter(x=>x.id!==s.id))})}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>{editing?'编辑产品':'新增产品'}</h3>
<div className="form-row"><div className="field"><label>*</label><input value={f.name||''} onChange={e=> setF({...f,name:e.target.value})}/></div><div className="field"><label>slug</label><input value={f.slug||''} onChange={e=> setF({...f,slug:e.target.value})}/></div></div>
<div className="form-row"><div className="field"><label></label><select value={f.type||'custom'} onChange={e=> setF({...f,type:e.target.value})}><option value="standard"></option><option value="custom">/</option></select></div><div className="field"><label></label><input value={f.price||''} onChange={e=> setF({...f,price:e.target.value})} placeholder="仅标准化需要"/></div></div>
<div className="field"><label></label><textarea value={f.intro||''} onChange={e=> setF({...f,intro:e.target.value})}/></div>
<div className="form-row"><div className="field"><label></label><select value={f.status||'draft'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.sort_weight||0} onChange={e=> setF({...f,sort_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={()=> api('/api/cms/services',token,{method:'POST',body:JSON.stringify(f)}).then(j=> {if(j.code===0) location.reload(); else alert(j.msg)})}></button>
{['stage','intro','service_content','delivery','suitable','price_unit','advantage','detail_html'].map(k=><div key={k} className="field"><label>{fieldLabel(k)}</label><textarea value={f[k]||''} onChange={e=>setF({...f,[k]:e.target.value})}/></div>)}
<div className="form-row"><div className="field"><label></label><select value={f.status||'published'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.sort_weight||0} onChange={e=> setF({...f,sort_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={save}>{editing?'保存修改':'新增产品'}</button>{editing&&<button className="btn btn-ghost" style={{marginLeft:8}} onClick={()=>{setF({status:'published',type:'custom'});setEditing(false)}}></button>}
</div>
</>
}
function Cases({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({status:'published'}); const [editing,setEditing]=useState(false)
useEffect(()=>{api('/api/cms/cases',token).then(j=> setList(j.data||[]))},[])
const save=()=>api(editing?'/api/cms/cases/'+f.id:'/api/cms/cases',token,{method:editing?'PUT':'POST',body:JSON.stringify(f)}).then(j=>{if(j.code===0){const saved={...f,id:editing?f.id:j.data?.id};setList(editing?list.map(x=>x.id===saved.id?saved:x):[...list,saved]);setF({status:'published'});setEditing(false);alert('保存成功')}else alert(j.msg||'保存失败')})
return <>
<div className="card"><h3>9</h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(c=> <tr key={c.id}><td>{c.title}</td><td>{c.industry}</td><td>{c.status}</td><td>{c.sort_weight}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/cases/'+c.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>9</h3>
<div className="field"><label>*</label><input value={f.title||''} onChange={e=> setF({...f,title:e.target.value})}/></div>
<div className="card"><h3>9</h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(c=> <tr key={c.id}><td>{c.title}</td><td>{c.industry}</td><td>{c.status}</td><td>{c.sort_weight}</td><td><button onClick={()=>{setF({...c});setEditing(true)}}></button> <button onClick={()=>{if(confirm('删除?')) api('/api/cms/cases/'+c.id,token,{method:'DELETE'}).then(j=>{if(j.code===0)setList(list.filter(x=>x.id!==c.id))})}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>{editing?'编辑案例':'新增案例'}</h3>
<div className="form-row"><div className="field"><label>*</label><input value={f.title||''} onChange={e=> setF({...f,title:e.target.value})}/></div><div className="field"><label></label><input value={f.industry||''} onChange={e=> setF({...f,industry:e.target.value})}/></div></div>
{['original_state','goal','gap','root_cause','ameba_intervention','evolution_path','actions','phased_result','continuous_iteration'].map(k=>(
<div key={k} className="field"><label>{k}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
<div key={k} className="field"><label>{fieldLabel(k)}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
))}
<div className="form-row"><div className="field"><label></label><select value={f.status||'draft'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.sort_weight||0} onChange={e=> setF({...f,sort_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={()=> api('/api/cms/cases',token,{method:'POST',body:JSON.stringify(f)}).then(j=> {if(j.code===0) location.reload(); else alert(j.msg)})}></button>
<div className="form-row"><div className="field"><label></label><select value={f.status||'published'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.sort_weight||0} onChange={e=> setF({...f,sort_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={save}>{editing?'保存修改':'新增案例'}</button>{editing&&<button className="btn btn-ghost" style={{marginLeft:8}} onClick={()=>{setF({status:'published'});setEditing(false)}}></button>}
</div>
</>
}
function Articles({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({status:'published'}); const [editing,setEditing]=useState(false)
useEffect(()=>{api('/api/cms/articles',token).then(j=> setList(j.data||[]))},[])
const save=()=>api(editing?'/api/cms/articles/'+f.id:'/api/cms/articles',token,{method:editing?'PUT':'POST',body:JSON.stringify(f)}).then(j=>{if(j.code===0){const saved={...f,id:editing?f.id:j.data?.id};setList(editing?list.map(x=>x.id===saved.id?saved:x):[...list,saved]);setF({status:'published'});setEditing(false);alert('保存成功')}else alert(j.msg||'保存失败')})
return <>
<div className="card"><h3></h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(a=> <tr key={a.id}><td>{a.title}</td><td>{a.category}</td><td>{a.recommend_weight}</td><td>{a.status}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/articles/'+a.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3></h3>
<div className="card"><h3></h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(a=> <tr key={a.id}><td>{a.title}</td><td>{a.category}</td><td>{a.recommend_weight}</td><td>{a.status}</td><td><button onClick={()=>{setF({...a});setEditing(true)}}></button> <button onClick={()=>{if(confirm('删除?')) api('/api/cms/articles/'+a.id,token,{method:'DELETE'}).then(j=>{if(j.code===0)setList(list.filter(x=>x.id!==a.id))})}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>{editing?'编辑文章':'新增文章'}</h3>
<div className="field"><label>*</label><input value={f.title||''} onChange={e=> setF({...f,title:e.target.value})}/></div>
<div className="form-row"><div className="field"><label>slug</label><input value={f.slug||''} onChange={e=>setF({...f,slug:e.target.value})}/></div><div className="field"><label></label><input value={f.category||''} onChange={e=>setF({...f,category:e.target.value})}/></div></div>
<div className="field"><label></label><textarea value={f.subtitle||''} onChange={e=>setF({...f,subtitle:e.target.value})}/></div>
<div className="field"><label> HTML</label><textarea value={f.content_html||''} onChange={e=> setF({...f,content_html:e.target.value})} /></div>
<Upload token={token} onUrl={url=> setF({...f,cover_url:url})} />
{f.cover_url && <img src={f.cover_url} style={{width:120,height:80,objectFit:'cover'}}/>}
<div className="form-row"><div className="field"><label></label><select value={f.status||'draft'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.recommend_weight||0} onChange={e=> setF({...f,recommend_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={()=> api('/api/cms/articles',token,{method:'POST',body:JSON.stringify(f)}).then(j=> {if(j.code===0) location.reload(); else alert(j.msg)})}></button>
<div className="form-row"><div className="field"><label></label><select value={f.status||'published'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.recommend_weight||0} onChange={e=> setF({...f,recommend_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={save}>{editing?'保存修改':'新增文章'}</button>{editing&&<button className="btn btn-ghost" style={{marginLeft:8}} onClick={()=>{setF({status:'published'});setEditing(false)}}></button>}
</div>
</>
}
@@ -211,7 +245,7 @@ function SelfCheck({token}:{token:string}){
const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/self-check',token).then(j=> setF(j.data||{}))},[])
return <div className="card"><h3></h3>
{['guide','questions','logic_text','next_guide'].map(k=> <div key={k} className="field"><label>{k}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>)}
{['guide','questions','logic_text','next_guide'].map(k=> <div key={k} className="field"><label>{fieldLabel(k)}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>)}
<p style={{fontSize:11,color:'#92400e'}}></p>
<button className="btn" onClick={()=> api('/api/cms/self-check',token,{method:'PUT',body:JSON.stringify(f)}).then(()=> alert('保存成功'))}></button>
</div>
@@ -225,24 +259,21 @@ function Upload({token,onUrl}:{token:string,onUrl:(u:string)=>void}){
return <div className="field"><label></label><input type="file" onChange={up} accept="image/*" /></div>
}
function DiagQ({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({group_key:'value1',type:'single',sort_order:0,status:'published'})
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({group_key:'value1',type:'single',sort_order:0,status:'published'}); const [editing,setEditing]=useState(false)
useEffect(()=>{api('/api/cms/diagnosis/questions',token).then(j=> setList(j.data||[]))},[])
const save=()=>{let opts=[]; try{opts=f.options_text?JSON.parse(f.options_text):(typeof f.options==='string'?JSON.parse(f.options||'[]'):(f.options||[]))}catch{return alert('选项JSON格式错误')} const path=editing?'/api/cms/diagnosis/questions/'+f.id:'/api/cms/diagnosis/questions'; return api(path,token,{method:editing?'PUT':'POST',body:JSON.stringify({...f,options:opts})}).then(j=>{if(j.code===0){const saved={...f,options:JSON.stringify(opts),options_text:JSON.stringify(opts,null,2),id:editing?f.id:j.data?.id};setList(editing?list.map(x=>x.id===saved.id?saved:x):[...list,saved]);setF({group_key:'value1',type:'single',sort_order:0,status:'published'});setEditing(false);alert('保存成功')}else alert(j.msg||'保存失败')})}
return <>
<div className="card"><h3></h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{list.map(q=> <tr key={q.id}><td>{q.group_name}</td><td style={{maxWidth:200,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{q.title}</td><td>{q.type}</td><td>{q.sort_order}</td><td>{q.status}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/diagnosis/questions/'+q.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody>
<tbody>{list.map(q=> <tr key={q.id}><td>{q.group_name}</td><td style={{maxWidth:200,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{q.title}</td><td>{q.type}</td><td>{q.sort_order}</td><td>{q.status}</td><td><button onClick={()=>{setF({...q,options_text:typeof q.options==='string'?q.options:JSON.stringify(q.options||[],null,2)});setEditing(true)}}></button> <button onClick={()=>{if(confirm('删除?')) api('/api/cms/diagnosis/questions/'+q.id,token,{method:'DELETE'}).then(j=>{if(j.code===0)setList(list.filter(x=>x.id!==q.id))})}}></button></td></tr>)}</tbody>
</table>
</div>
<div className="card"><h3></h3>
<div className="card"><h3>{editing?'编辑题目':'新增题目'}</h3>
<div className="form-row"><div className="field"><label>key</label><select value={f.group_key} onChange={e=> setF({...f,group_key:e.target.value})}><option value="value1">value1 </option><option value="value2">value2 </option><option value="value3">value3 </option><option value="value4">value4 </option></select></div><div className="field"><label></label><select value={f.type} onChange={e=> setF({...f,type:e.target.value})}><option value="single"></option><option value="multiple"></option><option value="scale">1-5</option></select></div></div>
<div className="field"><label>*</label><input value={f.title||''} onChange={e=> setF({...f,title:e.target.value})} /></div>
<div className="field"><label>JSON数组 [{label,score}]</label><textarea value={f.options_text||''} onChange={e=> setF({...f,options_text:e.target.value})} placeholder='[{"label":"非常清晰","score":5}]' /></div>
<div className="form-row"><div className="field"><label></label><input type="number" value={f.sort_order||0} onChange={e=> setF({...f,sort_order:Number(e.target.value)})}/></div><div className="field"><label></label><select value={f.status} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div></div>
<button className="btn" onClick={()=>{
let opts=[]
try{ opts=f.options_text? JSON.parse(f.options_text):[]}catch{ return alert('选项JSON格式错误')}
api('/api/cms/diagnosis/questions',token,{method:'POST',body:JSON.stringify({...f, options:opts})}).then(j=>{ if(j.code===0) location.reload(); else alert(j.msg)})
}}></button>
<button className="btn" onClick={save}>{editing?'保存修改':'新增题目'}</button>{editing&&<button className="btn btn-ghost" style={{marginLeft:8}} onClick={()=>{setF({group_key:'value1',type:'single',sort_order:0,status:'published'});setEditing(false)}}></button>}
</div>
</>
}
+2
View File
@@ -23,3 +23,5 @@ a{color:#163E70;text-decoration:none}
.table th{background:#f8fafc}
.tag{font-size:11px;padding:2px 6px;border-radius:4px;background:#e0f2fe;color:#0369a1}
.topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
.hint{font-size:12px;color:#64748b;line-height:1.7;margin:-4px 0 12px}
.editor-block{border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin:12px 0;background:#fbfdff}