chore: initialize deployable website and CMS

This commit is contained in:
Codex User
2026-09-08 11:57:17 +08:00
commit 3a99ae30c5
57 changed files with 11205 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex, nofollow" />
<title>CMS后台 - 阿美巴传统官网</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1750
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "ameba-cms-admin",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 15174",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0 --port 14174"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.2"
}
}
+310
View File
@@ -0,0 +1,310 @@
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: 'password', label: '⚙ 修改密码' },
]
function useAuth(){
const [token,setToken]=useState(localStorage.getItem('cms_token')||'')
const login=async(u:string,p:string)=>{
const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})})
const j=await r.json()
if(j.code!==0) throw new Error(j.msg)
localStorage.setItem('cms_token', j.data.token)
setToken(j.data.token)
}
const logout=()=>{localStorage.removeItem('cms_token'); setToken('')}
return {token,login,logout}
}
function api(path:string, token:string, opts:RequestInit={}){
const headers:any={...(opts.headers||{})}
if(token) headers['Authorization']='Bearer '+token
if(!(opts.body instanceof FormData)) headers['Content-Type']='application/json'
return fetch(path,{...opts, headers}).then(r=>r.json())
}
export default function App(){
const {token,login,logout}=useAuth()
const [user,setUser]=useState('')
const [pwd,setPwd]=useState('')
const [err,setErr]=useState('')
const [cur,setCur]=useState('config')
if(!token) return (
<div className="login">
<h3>CMS后台登录</h3>
<input placeholder="账号" value={user} onChange={e=>setUser(e.target.value)} />
<input placeholder="密码" type="password" value={pwd} onChange={e=>setPwd(e.target.value)} />
{err && <div style={{color:'#e11d48',fontSize:12}}>{err}</div>}
<button className="btn" style={{width:'100%',marginTop:12}} onClick={async()=>{
try{ await login(user,pwd); setErr('')}catch(e:any){setErr(e.message)}
}}></button>
</div>
)
return (
<div className="layout">
<div className="sidebar">
<h2> CMS后台<br/><small style={{color:'#64748b'}}>V1.1 FINAL </small></h2>
{SIDEBAR.map(s=> <div key={s.key} className={'nav-item'+(cur===s.key?' active':'')} onClick={()=>setCur(s.key)}>{s.label}</div>)}
<div style={{padding:20}}><button className="btn btn-ghost" onClick={logout}>退</button></div>
</div>
<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==='home' && <Home token={token} />}
{cur==='ameba' && <Ameba token={token} />}
{cur==='evolution' && <Evolution token={token} />}
{cur==='methods' && <Methods token={token} />}
{cur==='services' && <Services token={token} />}
{cur==='cases' && <Cases token={token} />}
{cur==='articles' && <Articles token={token} />}
{cur==='selfcheck' && <SelfCheck token={token} />}
{cur==='diag_q' && <DiagQ token={token} />}
{cur==='diag_config' && <DiagConfig token={token} />}
{cur==='diag_reports' && <DiagReports token={token} />}
{cur==='diag_leads' && <DiagLeads token={token} />}
{cur==='contact_leads' && <ContactLeads token={token} />}
{cur==='password' && <ChangePassword token={token} />}
</div>
</div>
)
}
function ContactLeads({token}:{token:string}){
const [list,setList]=useState<any[]>([])
useEffect(()=>{api('/api/cms/contact-leads',token).then(j=> setList(j.data||[]))},[])
return <div className="card"><h3>线200</h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{list.map(l=><tr key={l.id}><td>{l.name}</td><td>{l.company}</td><td>{l.phone}</td><td>{l.need}</td><td>{l.created_at}</td></tr>)}</tbody>
</table>
</div>
}
// —— 全局配置
function Config({token}:{token:string}){
const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/config',token).then(j=> setF(j.data||{}))},[])
const save=()=> api('/api/cms/config',token,{method:'PUT',body:JSON.stringify(f)}).then(j=> alert(j.msg||'保存成功'))
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>
))}
<button className="btn" onClick={save}></button>
</div>
}
function Home({token}:{token:string}){
const [list,setList]=useState<any[]>([])
useEffect(()=>{api('/api/cms/home-sections',token).then(j=> setList(j.data||[]))},[])
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>
<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>
<button className="btn" style={{marginTop:12}} onClick={save}></button>
</div>
}
function Ameba({token}:{token:string}){
const [f,setF]=useState<any>({}); const [ex,setEx]=useState<any[]>([]); const [ne,setNe]=useState<any>({})
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>
))}
<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>
<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>
</div>
<div className="field"><label></label><textarea value={ne.bio||''} onChange={e=> setNe({...ne,bio:e.target.value})}/></div>
<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>
</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||[]))},[])
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>
</>
}
function Methods({token}:{token:string}){
const [proc,setProc]=useState<any>({}); const [sys,setSys]=useState<any[]>([])
useEffect(()=>{api('/api/cms/evolution-process',token).then(j=> setProc(j.data||{})); api('/api/cms/ten-systems',token).then(j=> setSys(j.data||[]))},[])
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>
</>
}
function Services({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/services',token).then(j=> setList(j.data||[]))},[])
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="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>
</div>
</>
}
function Cases({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/cases',token).then(j=> setList(j.data||[]))},[])
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>
{['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 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>
</>
}
function Articles({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/articles',token).then(j=> setList(j.data||[]))},[])
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="field"><label>*</label><input value={f.title||''} onChange={e=> setF({...f,title: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>
</>
}
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>)}
<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>
}
function Upload({token,onUrl}:{token:string,onUrl:(u:string)=>void}){
const up=(e:any)=>{
const file=e.target.files[0]; if(!file) return;
const fd=new FormData(); fd.append('file',file);
fetch('/api/cms/upload',{method:'POST',headers:{Authorization:'Bearer '+token},body:fd}).then(r=>r.json()).then(j=>{ if(j.code===0) onUrl(j.data.url); else alert(j.msg)})
}
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'})
useEffect(()=>{api('/api/cms/diagnosis/questions',token).then(j=> setList(j.data||[]))},[])
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>
</table>
</div>
<div className="card"><h3></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>
</div>
</>
}
function DiagConfig({token}:{token:string}){
const [cfg,setCfg]=useState<any>({weights:{},thresholds:{},templates:{},share_expire_days:30})
useEffect(()=>{api('/api/cms/diagnosis/config',token).then(j=> setCfg(j.data||cfg))},[])
return <div className="card"><h3></h3>
<div className="field"><label>JSON</label><textarea value={JSON.stringify(cfg.weights||{},null,2)} onChange={e=>{try{setCfg({...cfg,weights:JSON.parse(e.target.value)})}catch{}}} /></div>
<div className="field"><label>B_max, M_max</label><textarea value={JSON.stringify(cfg.thresholds||{},null,2)} onChange={e=>{try{setCfg({...cfg,thresholds:JSON.parse(e.target.value)})}catch{}}} /></div>
<div className="field"><label></label><textarea value={JSON.stringify(cfg.templates||{},null,2)} onChange={e=>{try{setCfg({...cfg,templates:JSON.parse(e.target.value)})}catch{}}} /></div>
<div className="field"><label></label><input type="number" value={cfg.share_expire_days||30} onChange={e=> setCfg({...cfg,share_expire_days:Number(e.target.value)})}/></div>
<button className="btn" onClick={()=> api('/api/cms/diagnosis/config',token,{method:'PUT',body:JSON.stringify(cfg)}).then(()=> alert('保存成功'))}></button>
</div>
}
function DiagReports({token}:{token:string}){
const [list,setList]=useState<any[]>([])
useEffect(()=>{api('/api/cms/diagnosis/reports',token).then(j=> setList(j.data||[]))},[])
return <div className="card"><h3>100</h3>
<table className="table"><thead><tr><th>token</th><th></th><th></th><th></th></tr></thead>
<tbody>{list.map(r=> <tr key={r.id}><td style={{fontSize:10}}>{r.token.slice(0,8)}...</td><td>{r.created_at}</td><td>{r.expires_at}</td><td>{r.share_token.slice(0,8)}...</td></tr>)}</tbody>
</table>
</div>
}
function DiagLeads({token}:{token:string}){
const [leads,setLeads]=useState<any[]>([]); const [stats,setStats]=useState<any>({})
useEffect(()=>{api('/api/cms/diagnosis/leads',token).then(j=> setLeads(j.data||[])); api('/api/cms/diagnosis/stats',token).then(j=> setStats(j.data||{}))},[])
return <>
<div className="card"><h3></h3><p>{stats.totalSessions} · {stats.totalReports} · {stats.totalLeads}</p></div>
<div className="card"><h3></h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{leads.map(l=> <tr key={l.id}><td>{l.name}</td><td>{l.company}</td><td>{l.phone}</td><td>{l.created_at}</td></tr>)}</tbody>
</table>
<button className="btn btn-ghost" onClick={()=>{
const rows=[['姓名','企业','手机','时间'], ...leads.map(l=>[l.name,l.company,l.phone,l.created_at])]
const csv=rows.map(r=> r.map(v=>`"${v||''}"`).join(',')).join('\n')
const blob=new Blob([csv],{type:'text/csv'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='leads.csv'; a.click()
}}>CSV</button>
</div>
</>
}
function ChangePassword({token}:{token:string}){
const [oldPwd,setOldPwd]=useState('')
const [newPwd,setNewPwd]=useState('')
const [confirmPwd,setConfirmPwd]=useState('')
const [msg,setMsg]=useState('')
const [err,setErr]=useState('')
const submit=async()=>{
setMsg(''); setErr('')
if(!oldPwd || !newPwd || !confirmPwd){ setErr('请填写所有字段'); return }
if(newPwd.length<6){ setErr('新密码长度至少6位'); return }
if(newPwd!==confirmPwd){ setErr('两次密码不一致'); return }
const r=await api('/api/auth/change-password',token,{method:'POST',body:JSON.stringify({oldPassword:oldPwd,newPassword:newPwd})})
if(r.code===0){ setMsg('密码修改成功!'); setOldPwd(''); setNewPwd(''); setConfirmPwd('') }
else setErr(r.msg||'修改失败')
}
return <div className="card">
<h3>修改密码</h3>
<div className="field"><label>旧密码</label><input type="password" value={oldPwd} onChange={e=>setOldPwd(e.target.value)} /></div>
<div className="field"><label>新密码至少6位</label><input type="password" value={newPwd} onChange={e=>setNewPwd(e.target.value)} /></div>
<div className="field"><label>确认新密码</label><input type="password" value={confirmPwd} onChange={e=>setConfirmPwd(e.target.value)} /></div>
{msg && <div style={{color:'#059669',fontSize:13,margin:'8px 0'}}>{msg}</div>}
{err && <div style={{color:'#e11d48',fontSize:13,margin:'8px 0'}}>{err}</div>}
<button className="btn" onClick={submit}>修改密码</button>
</div>
}
+5
View File
@@ -0,0 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './style.css'
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
+25
View File
@@ -0,0 +1,25 @@
*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#f5f7fa;color:#1e293b}
a{color:#163E70;text-decoration:none}
.login{max-width:360px;margin:80px auto;background:#fff;padding:32px;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.08)}
.login input{width:100%;padding:10px 12px;margin:8px 0;border:1px solid #e2e8f0;border-radius:8px}
.btn{padding:8px 16px;border-radius:8px;border:1px solid #163E70;background:#163E70;color:#fff;cursor:pointer}
.btn-ghost{background:#fff;color:#163E70}
.layout{display:flex;min-height:100vh}
.sidebar{width:220px;background:#0f172a;color:#cbd5e1;padding:20px 0;flex-shrink:0}
.sidebar h2{padding:0 20px 16px;font-size:14px;color:#fff;border-bottom:1px solid #1e293b;margin-bottom:12px}
.nav-item{padding:10px 20px;cursor:pointer;display:block;font-size:13px}
.nav-item.active,.nav-item:hover{background:#1e293b;color:#fff}
.main{flex:1;padding:24px;overflow:auto}
.card{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:20px;margin-bottom:16px}
.card h3{font-size:15px;margin-bottom:12px;color:#0f172a}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.form-row.single{grid-template-columns:1fr}
.field{margin-bottom:12px}
.field label{font-size:12px;color:#64748b;display:block;margin-bottom:4px}
.field input,.field textarea,.field select{width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px}
.field textarea{min-height:80px}
.table{width:100%;border-collapse:collapse;font-size:13px}
.table th,.table td{border:1px solid #e2e8f0;padding:8px;text-align:left}
.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}
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src"]
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: '/admin/',
server: { host: '0.0.0.0', port: 15174 },
build: { outDir: 'dist' }
})
+2358
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "ameba-cms-server",
"version": "1.0.0",
"description": "阿美巴传统官网1.0 CMS后台 - 轻量SQLite方案",
"type": "module",
"scripts": {
"dev": "node src/index.js",
"start": "node src/index.js",
"seed": "node src/seed.js",
"passwd": "node src/change-password.js"
},
"dependencies": {
"express": "^4.19.2",
"cors": "^2.8.5",
"multer": "^1.4.5-lts.1",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^11.0.0",
"dotenv": "^16.4.5",
"pdfkit": "^0.15.0"
},
"allowScripts": {
"better-sqlite3@11.10.0": true
}
}
+50
View File
@@ -0,0 +1,50 @@
import { initDB, getDB } from './db.js';
import bcrypt from 'bcryptjs';
import readline from 'readline';
await initDB();
const db = getDB();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (q) => new Promise(resolve => rl.question(q, resolve));
console.log('=== 修改CMS管理员密码 ===\n');
const username = await question('用户名 (默认 admin): ') || 'admin';
const newPassword = await question('新密码: ');
const confirmPassword = await question('确认密码: ');
if (newPassword !== confirmPassword) {
console.error('❌ 两次密码不一致!');
process.exit(1);
}
if (newPassword.length < 6) {
console.error('❌ 密码长度至少6位!');
process.exit(1);
}
const hash = bcrypt.hashSync(newPassword, 10);
db.get(`SELECT id FROM admins WHERE username = ?`, [username], (err, row) => {
if (err || !row) {
console.error(`❌ 用户 "${username}" 不存在!`);
process.exit(1);
}
db.run(`UPDATE admins SET password_hash = ? WHERE username = ?`, [hash, username], (err) => {
if (err) {
console.error('❌ 修改失败:', err.message);
process.exit(1);
}
console.log(`\n✅ 密码修改成功!`);
console.log(` 用户名: ${username}`);
console.log(` 新密码: ${newPassword}`);
rl.close();
setTimeout(() => process.exit(0), 500);
});
});
+348
View File
@@ -0,0 +1,348 @@
import Database from 'better-sqlite3';
import { fileURLToPath } from 'url';
import path from 'path';
import fs from 'fs';
import 'dotenv/config';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DB_PATH = process.env.CMS_DB_PATH || path.join(__dirname, '../cms.db');
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '../uploads');
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
const db = new Database(DB_PATH);
// 启用 WAL 模式提高性能
db.pragma('journal_mode = WAL');
export function initDB() {
// 全局配置
db.exec(`CREATE TABLE IF NOT EXISTS global_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
site_title TEXT,
slogan TEXT,
seo_title TEXT,
seo_desc TEXT,
seo_keywords TEXT,
hotline TEXT,
contact_email TEXT,
contact_address TEXT,
cta_text TEXT,
cta_link TEXT,
updated_at TEXT
)`);
// 首页板块
db.exec(`CREATE TABLE IF NOT EXISTS home_sections (
id TEXT PRIMARY KEY,
key TEXT UNIQUE,
title TEXT,
subtitle TEXT,
summary TEXT,
visible INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
updated_at TEXT
)`);
// 阿美巴页面
db.exec(`CREATE TABLE IF NOT EXISTS ameba_page (
id INTEGER PRIMARY KEY CHECK (id = 1),
brand_position TEXT,
mission TEXT,
philosophy TEXT,
practice TEXT,
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS experts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
title TEXT,
bio TEXT,
avatar_url TEXT,
sort_weight INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
created_at TEXT,
updated_at TEXT
)`);
// 企业进化
db.exec(`CREATE TABLE IF NOT EXISTS evolution_thought (
id INTEGER PRIMARY KEY CHECK (id = 1),
content TEXT,
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS four_values (
id TEXT PRIMARY KEY,
key TEXT UNIQUE,
name TEXT,
definition TEXT,
description TEXT,
image_url TEXT,
sort_order INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS amb_states (
id TEXT PRIMARY KEY,
key TEXT UNIQUE,
name TEXT,
definition TEXT,
description TEXT,
image_url TEXT,
sort_order INTEGER DEFAULT 0,
fixed_note TEXT,
status TEXT DEFAULT 'published',
updated_at TEXT
)`);
// 方法与能力
db.exec(`CREATE TABLE IF NOT EXISTS evolution_process (
id INTEGER PRIMARY KEY CHECK (id = 1),
steps TEXT,
scenarios TEXT,
description TEXT,
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS ten_systems (
id TEXT PRIMARY KEY,
name TEXT,
description TEXT,
sort_order INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
updated_at TEXT
)`);
// 产品服务
db.exec(`CREATE TABLE IF NOT EXISTS services (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
name TEXT,
stage TEXT,
intro TEXT,
service_content TEXT,
delivery TEXT,
suitable TEXT,
price REAL,
price_unit TEXT,
advantage TEXT,
detail_html TEXT,
status TEXT DEFAULT 'draft',
sort_weight INTEGER DEFAULT 0,
type TEXT DEFAULT 'custom',
created_at TEXT,
updated_at TEXT
)`);
// 案例 9段
db.exec(`CREATE TABLE IF NOT EXISTS cases (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
industry TEXT,
scale TEXT,
amb_tags TEXT,
value_tags TEXT,
title TEXT,
cover_url TEXT,
original_state TEXT,
goal TEXT,
gap TEXT,
root_cause TEXT,
ameba_intervention TEXT,
evolution_path TEXT,
actions TEXT,
phased_result TEXT,
continuous_iteration TEXT,
publish_time TEXT,
status TEXT DEFAULT 'draft',
sort_weight INTEGER DEFAULT 0,
created_at TEXT,
updated_at TEXT
)`);
// 认知观察
db.exec(`CREATE TABLE IF NOT EXISTS articles (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
title TEXT,
subtitle TEXT,
category TEXT,
cover_url TEXT,
content_html TEXT,
publish_time TEXT,
recommend_weight INTEGER DEFAULT 0,
status TEXT DEFAULT 'draft',
created_at TEXT,
updated_at TEXT
)`);
// 轻量自我对照
db.exec(`CREATE TABLE IF NOT EXISTS self_check (
id INTEGER PRIMARY KEY CHECK (id = 1),
guide TEXT,
questions TEXT,
logic_text TEXT,
next_guide TEXT,
updated_at TEXT
)`);
// 诊断问卷题目
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_questions (
id TEXT PRIMARY KEY,
group_key TEXT,
group_name TEXT,
type TEXT,
title TEXT,
options TEXT,
score_config TEXT,
sort_order INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
skip_logic TEXT,
created_at TEXT,
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_sessions (
id TEXT PRIMARY KEY,
token TEXT UNIQUE,
answers TEXT,
progress TEXT,
created_at TEXT,
updated_at TEXT,
completed_at TEXT,
ip TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_reports (
id TEXT PRIMARY KEY,
session_id TEXT,
token TEXT UNIQUE,
share_token TEXT UNIQUE,
scores TEXT,
amb TEXT,
gap TEXT,
root_cause TEXT,
path_suggestion TEXT,
expires_at TEXT,
created_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_leads (
id TEXT PRIMARY KEY,
session_id TEXT,
report_id TEXT,
name TEXT,
company TEXT,
phone TEXT,
created_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS contact_leads (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
company TEXT,
phone TEXT NOT NULL,
need TEXT,
created_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
weights TEXT,
thresholds TEXT,
templates TEXT,
share_expire_days INTEGER DEFAULT 30,
updated_at TEXT
)`);
// 管理员
db.exec(`CREATE TABLE IF NOT EXISTS admins (
id TEXT PRIMARY KEY,
username TEXT UNIQUE,
password_hash TEXT,
created_at TEXT
)`);
}
// 创建兼容 sqlite3 回调风格的包装器
export function getDB() {
return {
// 同步查询单行
get(sql, params, callback) {
try {
if (typeof params === 'function') {
callback = params;
params = [];
}
const row = db.prepare(sql).get(...(Array.isArray(params) ? params : []));
callback(null, row || null);
} catch (err) {
callback(err);
}
},
// 同步查询所有行
all(sql, params, callback) {
try {
if (typeof params === 'function') {
callback = params;
params = [];
}
const rows = db.prepare(sql).all(...(Array.isArray(params) ? params : []));
callback(null, rows);
} catch (err) {
callback(err);
}
},
// 同步执行
run(sql, params, callback) {
try {
if (typeof params === 'function') {
callback = params;
params = [];
}
const stmt = db.prepare(sql);
const result = stmt.run(...(Array.isArray(params) ? params : []));
// 绑定 this 上下文
const context = {
lastID: result.lastInsertRowid,
changes: result.changes
};
if (callback) callback.call(context, null);
return context;
} catch (err) {
if (callback) callback(err);
throw err;
}
},
// 序列化执行(兼容性)
serialize(callback) {
if (callback) callback();
},
// 准备语句
prepare(sql) {
const stmt = db.prepare(sql);
return {
run(...args) {
const callback = args.pop();
try {
const result = stmt.run(...args);
callback.call({ lastID: result.lastInsertRowid, changes: result.changes }, null);
} catch (err) {
callback(err);
}
},
finalize(callback) {
if (callback) callback();
}
};
}
};
}
export { DB_PATH, UPLOAD_DIR };
+554
View File
@@ -0,0 +1,554 @@
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import multer from 'multer';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import PDFDocument from 'pdfkit';
import { initDB, getDB, UPLOAD_DIR } from './db.js';
import { signToken, verifyToken, authMiddleware } from './middleware/auth.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.CMS_PORT || 13001;
await initDB();
const db = getDB();
app.use(cors({ origin: process.env.CORS_ORIGIN || true }));
app.use(express.json({ limit: '5mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(authMiddleware);
// 静态:上传文件
app.use('/uploads', express.static(UPLOAD_DIR));
// 静态:后台管理(构建后)
const adminDist = path.join(__dirname, '../../admin/dist');
if (fs.existsSync(adminDist)) {
app.use('/admin', express.static(adminDist));
app.get('/admin/*', (req, res) => res.sendFile(path.join(adminDist, 'index.html')));
}
// 防止搜索引擎索引后台
app.get('/admin', (req, res, next) => { res.set('X-Robots-Tag', 'noindex, nofollow'); next(); });
// 健康检查
app.get('/health', (req, res) => res.json({ status: 'ok', time: new Date().toISOString() }));
// 登录
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) return res.status(400).json({ code: 400, msg: '请输入账号密码' });
db.get(`SELECT * FROM admins WHERE username=?`, [username], (err, row) => {
if (err || !row) return res.status(401).json({ code: 401, msg: '账号或密码错误' });
if (!bcrypt.compareSync(password, row.password_hash)) return res.status(401).json({ code: 401, msg: '账号或密码错误' });
const token = signToken({ username: row.username, id: row.id });
res.json({ code: 0, data: { token, username } });
});
});
// 修改密码
app.post('/api/auth/change-password', (req, res) => {
const { oldPassword, newPassword } = req.body;
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) return res.status(401).json({ code: 401, msg: '未登录' });
try {
const tokenStr = authHeader.split(' ')[1];
const decoded = verifyToken(tokenStr);
const username = decoded.username;
if (!oldPassword || !newPassword) return res.status(400).json({ code: 400, msg: '请输入旧密码和新密码' });
if (newPassword.length < 6) return res.status(400).json({ code: 400, msg: '新密码长度至少6位' });
db.get(`SELECT * FROM admins WHERE username=?`, [username], (err, row) => {
if (err || !row) return res.status(404).json({ code: 404, msg: '用户不存在' });
if (!bcrypt.compareSync(oldPassword, row.password_hash)) return res.status(401).json({ code: 401, msg: '旧密码错误' });
const hash = bcrypt.hashSync(newPassword, 10);
db.run(`UPDATE admins SET password_hash=? WHERE username=?`, [hash, username], (err) => {
if (err) return res.status(500).json({ code: 500, msg: '修改失败' });
res.json({ code: 0, msg: '密码修改成功' });
});
});
} catch (e) {
return res.status(401).json({ code: 401, msg: '登录已过期' });
}
});
// 图片上传
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, UPLOAD_DIR),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
cb(null, crypto.randomBytes(16).toString('hex') + ext);
}
});
const allowedImageTypes = new Map([
['image/jpeg', '.jpg'], ['image/png', '.png'], ['image/webp', '.webp'], ['image/gif', '.gif']
]);
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024, files: 1 },
fileFilter: (req, file, cb) => {
const expectedExt = allowedImageTypes.get(file.mimetype);
const actualExt = path.extname(file.originalname).toLowerCase();
if (!expectedExt || expectedExt !== actualExt) return cb(new Error('仅支持 JPG、PNG、WEBP、GIF 图片'));
cb(null, true);
}
});
app.post('/api/cms/upload', upload.single('file'), (req, res) => {
if (!req.file) return res.status(400).json({ code: 400, msg: '未上传文件' });
const url = `/uploads/${req.file.filename}`;
res.json({ code: 0, data: { url } });
});
// 轻量内存限流:保护公开诊断与留资接口,生产环境应在 Nginx/WAF 再配置一层。
const requestBuckets = new Map();
function rateLimit({ windowMs = 60_000, max = 30 } = {}) {
return (req, res, next) => {
const forwarded = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
const key = `${req.ip || forwarded || req.socket.remoteAddress || 'unknown'}:${req.path}`;
const now = Date.now();
const old = requestBuckets.get(key);
if (!old || now - old.startedAt >= windowMs) requestBuckets.set(key, { startedAt: now, count: 1 });
else {
old.count += 1;
if (old.count > max) return res.status(429).json({ code: 429, msg: '请求过于频繁,请稍后再试' });
}
next();
};
}
const publicRateLimit = rateLimit({ windowMs: 60_000, max: 60 });
app.use('/api/diagnosis', publicRateLimit);
app.post('/api/contact/leads', (req, res) => {
const { name, company = '', phone, need = '' } = req.body || {};
if (!name || !phone) return res.status(400).json({ code: 400, msg: '姓名和手机号必填' });
if (!/^1[3-9]\d{9}$/.test(String(phone))) return res.status(400).json({ code: 400, msg: '手机号格式错误' });
const id = genId('contact');
db.run(`INSERT INTO contact_leads (id, name, company, phone, need, created_at) VALUES (?,?,?,?,?,?)`,
[id, String(name).trim().slice(0, 80), String(company).trim().slice(0, 160), String(phone), String(need).trim().slice(0, 2000), nowISO()], err => {
if (err) return res.status(500).json({ code: 500, msg: '提交失败,请稍后再试' });
res.json({ code: 0, msg: '提交成功' });
});
});
// 通用工具
const nowISO = () => new Date().toISOString();
const genId = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
// 全局配置
app.get('/api/cms/config', (req, res) => {
db.get(`SELECT * FROM global_config WHERE id=1`, (err, row) => res.json({ code: 0, data: row }));
});
app.put('/api/cms/config', (req, res) => {
const f = req.body;
db.run(`UPDATE global_config SET site_title=?, slogan=?, seo_title=?, seo_desc=?, seo_keywords=?, hotline=?, contact_email=?, contact_address=?, cta_text=?, cta_link=?, updated_at=? WHERE id=1`,
[f.site_title, f.slogan, f.seo_title, f.seo_desc, f.seo_keywords, f.hotline, f.contact_email, f.contact_address, f.cta_text, f.cta_link, nowISO()], function(err){
if(err) return res.status(500).json({code:500, msg: err.message});
res.json({code:0, msg:'保存成功'});
});
});
// 首页板块
app.get('/api/cms/home-sections', (req, res) => {
db.all(`SELECT * FROM home_sections ORDER BY sort_order DESC`, (err, rows) => res.json({code:0, data: rows}));
});
app.put('/api/cms/home-sections', (req, res) => {
const list = req.body; // array
if(!Array.isArray(list)) return res.status(400).json({code:400, msg:'格式错误'});
const stmt = db.prepare(`UPDATE home_sections SET title=?, subtitle=?, summary=?, visible=?, sort_order=?, updated_at=? WHERE id=?`);
list.forEach(it=> stmt.run(it.title, it.subtitle, it.summary, it.visible?1:0, it.sort_order, nowISO(), it.id));
stmt.finalize(()=> res.json({code:0, msg:'保存成功'}));
});
// 阿美巴页面
app.get('/api/cms/ameba', (req, res)=> db.get(`SELECT * FROM ameba_page WHERE id=1`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/ameba', (req,res)=>{
const f=req.body;
db.run(`UPDATE ameba_page SET brand_position=?, mission=?, philosophy=?, practice=?, updated_at=? WHERE id=1`, [f.brand_position,f.mission,f.philosophy,f.practice, nowISO()], e=> res.json({code:0,msg:'保存成功'}));
});
app.get('/api/cms/experts', (req,res)=> db.all(`SELECT * FROM experts ORDER BY sort_weight DESC, created_at DESC`, (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/experts', (req,res)=>{
const b=req.body; if(!b.name) return res.status(400).json({code:400,msg:'姓名必填'});
const id=genId('exp');
db.run(`INSERT INTO experts (id, name, title, bio, avatar_url, sort_weight, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)`,
[id,b.name,b.title,b.bio,b.avatar_url,b.sort_weight||0,b.status||'published',nowISO(),nowISO()], e=> res.json({code:0,data:{id}}));
});
app.put('/api/cms/experts/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE experts SET name=?, title=?, bio=?, avatar_url=?, sort_weight=?, status=?, updated_at=? WHERE id=?`,
[b.name,b.title,b.bio,b.avatar_url,b.sort_weight,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/experts/:id', (req,res)=> db.run(`DELETE FROM experts WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 企业进化
app.get('/api/cms/evolution/thought', (req,res)=> db.get(`SELECT * FROM evolution_thought WHERE id=1`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/evolution/thought', (req,res)=> db.run(`UPDATE evolution_thought SET content=?, updated_at=? WHERE id=1`,[req.body.content,nowISO()], e=> res.json({code:0,msg:'保存成功'})));
app.get('/api/cms/four-values', (req,res)=> db.all(`SELECT * FROM four_values ORDER BY sort_order DESC`, (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/four-values', (req,res)=>{
const b=req.body; if(!b.key||!b.name) return res.status(400).json({code:400,msg:'key和名称必填'});
const id=genId('fv');
db.run(`INSERT INTO four_values (id, key, name, definition, description, image_url, sort_order, status, updated_at) VALUES (?,?,?,?,?,?,?,?,?)`,
[id,b.key,b.name,b.definition,b.description,b.image_url,b.sort_order||0,b.status||'published',nowISO()], e=> res.json({code:0,data:{id}}));
});
app.put('/api/cms/four-values/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE four_values SET key=?, name=?, definition=?, description=?, image_url=?, sort_order=?, status=?, updated_at=? WHERE id=?`,
[b.key,b.name,b.definition,b.description,b.image_url,b.sort_order,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/four-values/:id', (req,res)=> db.run(`DELETE FROM four_values WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
app.get('/api/cms/amb-states', (req,res)=> db.all(`SELECT * FROM amb_states ORDER BY sort_order DESC`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/amb-states/:id', (req,res)=>{
const b=req.body;
// fixed_note 不可删但可编辑?按约束固定说明不可删
db.run(`UPDATE amb_states SET name=?, definition=?, description=?, image_url=?, sort_order=?, status=?, updated_at=? WHERE id=?`,
[b.name,b.definition,b.description,b.image_url,b.sort_order,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
// 方法与能力
app.get('/api/cms/evolution-process', (req,res)=> db.get(`SELECT * FROM evolution_process WHERE id=1`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/evolution-process', (req,res)=>{
const b=req.body;
db.run(`UPDATE evolution_process SET steps=?, scenarios=?, description=?, updated_at=? WHERE id=1`,[b.steps,b.scenarios,b.description,nowISO()], e=> res.json({code:0,msg:'保存成功'}));
});
app.get('/api/cms/ten-systems', (req,res)=> db.all(`SELECT * FROM ten_systems ORDER BY sort_order DESC`, (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/ten-systems', (req,res)=>{
const b=req.body; if(!b.name) return res.status(400).json({code:400,msg:'名称必填'});
const id=genId('sys');
db.run(`INSERT INTO ten_systems (id, name, description, sort_order, status, updated_at) VALUES (?,?,?,?,?,?)`,
[id,b.name,b.description,b.sort_order||0,b.status||'published',nowISO()], e=> res.json({code:0,data:{id}}));
});
app.put('/api/cms/ten-systems/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE ten_systems SET name=?, description=?, sort_order=?, status=?, updated_at=? WHERE id=?`,
[b.name,b.description,b.sort_order,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/ten-systems/:id', (req,res)=> db.run(`DELETE FROM ten_systems WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 产品服务
app.get('/api/cms/services', (req,res)=> db.all(`SELECT * FROM services ORDER BY sort_weight DESC, created_at DESC`, (e,r)=> res.json({code:0,data:r})));
app.get('/api/cms/services/:id', (req,res)=> db.get(`SELECT * FROM services WHERE id=?`,[req.params.id], (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/services', (req,res)=>{
const b=req.body; if(!b.name) return res.status(400).json({code:400,msg:'名称必填'});
if(b.price && isNaN(Number(b.price))) return res.status(400).json({code:400,msg:'价格格式错误'});
const id=genId('svc');
const slug = b.slug || `svc-${Date.now()}`;
db.run(`INSERT INTO services (id, slug, name, stage, intro, service_content, delivery, suitable, price, price_unit, advantage, detail_html, status, sort_weight, type, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
[id,slug,b.name,b.stage,b.intro,b.service_content,b.delivery,b.suitable,b.price?Number(b.price):null,b.price_unit,b.advantage,b.detail_html,b.status||'draft',b.sort_weight||0,b.type||'custom',nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
});
app.put('/api/cms/services/:id', (req,res)=>{
const b=req.body;
if(b.price && isNaN(Number(b.price))) return res.status(400).json({code:400,msg:'价格格式错误'});
db.run(`UPDATE services SET slug=?, name=?, stage=?, intro=?, service_content=?, delivery=?, suitable=?, price=?, price_unit=?, advantage=?, detail_html=?, status=?, sort_weight=?, type=?, updated_at=? WHERE id=?`,
[b.slug,b.name,b.stage,b.intro,b.service_content,b.delivery,b.suitable,b.price?Number(b.price):null,b.price_unit,b.advantage,b.detail_html,b.status,b.sort_weight,b.type,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/services/:id', (req,res)=> db.run(`DELETE FROM services WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 案例
app.get('/api/cms/cases', (req,res)=> db.all(`SELECT * FROM cases ORDER BY sort_weight DESC, created_at DESC`, (e,r)=> res.json({code:0,data:r})));
app.get('/api/cms/cases/:id', (req,res)=> db.get(`SELECT * FROM cases WHERE id=?`,[req.params.id], (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/cases', (req,res)=>{
const b=req.body; if(!b.title) return res.status(400).json({code:400,msg:'标题必填'});
const id=genId('case');
const slug = b.slug || `case-${Date.now()}`;
db.run(`INSERT INTO cases (id, slug, industry, scale, amb_tags, value_tags, title, cover_url, original_state, goal, gap, root_cause, ameba_intervention, evolution_path, actions, phased_result, continuous_iteration, publish_time, status, sort_weight, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
[id,slug,b.industry,b.scale,JSON.stringify(b.amb_tags||[]),JSON.stringify(b.value_tags||[]),b.title,b.cover_url,b.original_state,b.goal,b.gap,b.root_cause,b.ameba_intervention,b.evolution_path,b.actions,b.phased_result,b.continuous_iteration,b.publish_time||nowISO(),b.status||'draft',b.sort_weight||0,nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
});
app.put('/api/cms/cases/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE cases SET slug=?, industry=?, scale=?, amb_tags=?, value_tags=?, title=?, cover_url=?, original_state=?, goal=?, gap=?, root_cause=?, ameba_intervention=?, evolution_path=?, actions=?, phased_result=?, continuous_iteration=?, publish_time=?, status=?, sort_weight=?, updated_at=? WHERE id=?`,
[b.slug,b.industry,b.scale,JSON.stringify(b.amb_tags||[]),JSON.stringify(b.value_tags||[]),b.title,b.cover_url,b.original_state,b.goal,b.gap,b.root_cause,b.ameba_intervention,b.evolution_path,b.actions,b.phased_result,b.continuous_iteration,b.publish_time,b.status,b.sort_weight,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/cases/:id', (req,res)=> db.run(`DELETE FROM cases WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 文章
app.get('/api/cms/articles', (req,res)=> db.all(`SELECT * FROM articles ORDER BY recommend_weight DESC, created_at DESC`, (e,r)=> res.json({code:0,data:r})));
app.get('/api/cms/articles/:id', (req,res)=> db.get(`SELECT * FROM articles WHERE id=?`,[req.params.id], (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/articles', (req,res)=>{
const b=req.body; if(!b.title) return res.status(400).json({code:400,msg:'标题必填'});
const id=genId('art');
const slug = b.slug || `art-${Date.now()}`;
db.run(`INSERT INTO articles (id, slug, title, subtitle, category, cover_url, content_html, publish_time, recommend_weight, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
[id,slug,b.title,b.subtitle,b.category,b.cover_url,b.content_html,b.publish_time||nowISO(),b.recommend_weight||0,b.status||'draft',nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
});
app.put('/api/cms/articles/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE articles SET slug=?, title=?, subtitle=?, category=?, cover_url=?, content_html=?, publish_time=?, recommend_weight=?, status=?, updated_at=? WHERE id=?`,
[b.slug,b.title,b.subtitle,b.category,b.cover_url,b.content_html,b.publish_time,b.recommend_weight,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/articles/:id', (req,res)=> db.run(`DELETE FROM articles WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 自我对照
app.get('/api/cms/self-check', (req,res)=> db.get(`SELECT * FROM self_check WHERE id=1`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/self-check', (req,res)=>{
const b=req.body;
db.run(`UPDATE self_check SET guide=?, questions=?, logic_text=?, next_guide=?, updated_at=? WHERE id=1`,
[b.guide, b.questions, b.logic_text, b.next_guide, nowISO()], e=> res.json({code:0,msg:'保存成功'}));
});
// 前端公开聚合接口(供前端一次性拉取,不需要鉴权,使用GET放行)
app.get('/api/cms/public', (req,res)=>{
const data={};
db.serialize(()=>{
db.get(`SELECT * FROM global_config WHERE id=1`, (e,r)=> data.config=r);
db.all(`SELECT * FROM home_sections WHERE visible=1 ORDER BY sort_order DESC`, (e,r)=> data.home_sections=r);
db.get(`SELECT * FROM ameba_page WHERE id=1`, (e,r)=> data.ameba=r);
db.all(`SELECT * FROM experts WHERE status='published' ORDER BY sort_weight DESC`, (e,r)=> data.experts=r);
db.get(`SELECT * FROM evolution_thought WHERE id=1`, (e,r)=> data.evolution_thought=r);
db.all(`SELECT * FROM four_values WHERE status='published' ORDER BY sort_order DESC`, (e,r)=> data.four_values=r);
db.all(`SELECT * FROM amb_states WHERE status='published' ORDER BY sort_order DESC`, (e,r)=> data.amb_states=r);
db.get(`SELECT * FROM evolution_process WHERE id=1`, (e,r)=> data.evolution_process=r);
db.all(`SELECT * FROM ten_systems WHERE status='published' ORDER BY sort_order DESC`, (e,r)=> data.ten_systems=r);
db.all(`SELECT * FROM services WHERE status='published' ORDER BY sort_weight DESC`, (e,r)=> data.services=r);
db.all(`SELECT * FROM cases WHERE status='published' ORDER BY sort_weight DESC`, (e,r)=> data.cases=r);
db.all(`SELECT * FROM articles WHERE status='published' ORDER BY recommend_weight DESC`, (e,r)=> data.articles=r);
db.get(`SELECT * FROM self_check WHERE id=1`, (e,r)=> {
data.self_check=r;
res.json({code:0,data});
});
});
});
// ================= 诊断系统 =================
// 诊断问题公开
app.get('/api/diagnosis/questions', (req,res)=>{
db.all(`SELECT * FROM diagnosis_questions WHERE status='published' ORDER BY sort_order DESC`, (e,rows)=>{
const parsed = (rows||[]).map(r=>({ ...r, options: JSON.parse(r.options||'[]'), skip_logic: r.skip_logic?JSON.parse(r.skip_logic):null }))
res.json({code:0,data:parsed})
})
})
// CMS 管理题目
app.get('/api/cms/diagnosis/questions', (req,res)=>{
db.all(`SELECT * FROM diagnosis_questions ORDER BY sort_order DESC`, (e,rows)=> res.json({code:0,data:rows}))
})
app.post('/api/cms/diagnosis/questions', (req,res)=>{
const b=req.body; if(!b.title) return res.status(400).json({code:400,msg:'标题必填'})
const id=b.id||`dq-${Date.now()}`
db.run(`INSERT INTO diagnosis_questions (id, group_key, group_name, type, title, options, score_config, sort_order, status, skip_logic, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
[id,b.group_key,b.group_name,b.type||'single',b.title, JSON.stringify(b.options||[]), JSON.stringify(b.score_config||{}), b.sort_order||0, b.status||'published', JSON.stringify(b.skip_logic||null), nowISO(), nowISO()], e=> res.json({code:0,data:{id}}))
})
app.put('/api/cms/diagnosis/questions/:id', (req,res)=>{
const b=req.body
db.run(`UPDATE diagnosis_questions SET group_key=?, group_name=?, type=?, title=?, options=?, score_config=?, sort_order=?, status=?, skip_logic=?, updated_at=? WHERE id=?`,
[b.group_key,b.group_name,b.type,b.title, JSON.stringify(b.options||[]), JSON.stringify(b.score_config||{}), b.sort_order,b.status, JSON.stringify(b.skip_logic||null), nowISO(), req.params.id], e=> res.json({code:0,msg:'更新成功'}))
})
app.delete('/api/cms/diagnosis/questions/:id', (req,res)=> db.run(`DELETE FROM diagnosis_questions WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})))
// 诊断配置
app.get('/api/cms/diagnosis/config', (req,res)=> db.get(`SELECT * FROM diagnosis_config WHERE id=1`, (e,r)=> res.json({code:0,data:r ? {...r, weights: JSON.parse(r.weights||'{}'), thresholds: JSON.parse(r.thresholds||'{}'), templates: JSON.parse(r.templates||'{}')} : null})))
app.put('/api/cms/diagnosis/config', (req,res)=>{
const b=req.body
db.run(`UPDATE diagnosis_config SET weights=?, thresholds=?, templates=?, share_expire_days=?, updated_at=? WHERE id=1`,
[JSON.stringify(b.weights||{}), JSON.stringify(b.thresholds||{}), JSON.stringify(b.templates||{}), b.share_expire_days||30, nowISO()], e=> res.json({code:0,msg:'保存成功'}))
})
// 诊断会话
app.post('/api/diagnosis/sessions', (req,res)=>{
const id=genId('dsess')
const token=crypto.randomBytes(16).toString('hex')
const now=nowISO()
const ip=req.headers['x-forwarded-for']||req.socket.remoteAddress||''
db.run(`INSERT INTO diagnosis_sessions (id, token, answers, progress, created_at, updated_at, ip) VALUES (?,?,?,?,?,?,?)`,
[id, token, JSON.stringify({}), JSON.stringify({}), now, now, String(ip)], e=> res.json({code:0,data:{id,token}}))
})
app.get('/api/diagnosis/sessions/:token', (req,res)=>{
db.get(`SELECT * FROM diagnosis_sessions WHERE token=?`,[req.params.token], (e,row)=>{
if(!row) return res.status(404).json({code:404,msg:'会话不存在'})
res.json({code:0,data:{...row, answers: JSON.parse(row.answers||'{}'), progress: JSON.parse(row.progress||'{}')}})
})
})
app.put('/api/diagnosis/sessions/:token', (req,res)=>{
const {answers, progress}=req.body
db.run(`UPDATE diagnosis_sessions SET answers=?, progress=?, updated_at=? WHERE token=?`,
[JSON.stringify(answers||{}), JSON.stringify(progress||{}), nowISO(), req.params.token], e=> {
if (e) return res.status(500).json({code:500,msg:'保存失败'})
res.json({code:0,msg:'保存成功'})
})
})
// 分析引擎
function calcDiagnosis(answers, questions, cfg){
const weights = cfg.weights || {value1:1,value2:1,value3:1,value4:1}
const thr = cfg.thresholds || {B_max:2.5, M_max:3.8}
const groups={value1:[],value2:[],value3:[],value4:[]}
questions.forEach(q=>{
const ans = answers[q.id]
if(ans===undefined||ans===null) return
let score=0
const opts = typeof q.options==='string'? JSON.parse(q.options): q.options
if(q.type==='single' || q.type==='scale'){
const opt = opts.find(o=> String(o.label)===String(ans) || String(o.score)===String(ans) || o.label===ans)
score = opt? Number(opt.score): Number(ans)||0
} else if(q.type==='multiple'){
const arr = Array.isArray(ans)? ans:[ans]
const scores = arr.map(v=>{
const o=opts.find(x=> String(x.label)===String(v))
return o? Number(o.score):0
})
score = scores.length? scores.reduce((a,b)=>a+b,0)/scores.length:0
}
if(!groups[q.group_key]) groups[q.group_key]=[]
groups[q.group_key].push(score)
})
const scores={}
Object.keys(groups).forEach(k=>{
const arr=groups[k]
const average = arr.length ? arr.reduce((a,b)=>a+b,0)/arr.length : 0
scores[k] = Number(average.toFixed(2))
})
const amb={}
Object.keys(scores).forEach(k=>{
const s=scores[k]
let st='B'
if(s>thr.M_max) st='A'
else if(s>thr.B_max) st='M'
else st='B'
amb[k]=st
})
// 差距:以目标 A(5分) 为基准
const gap={}
Object.keys(scores).forEach(k=> gap[k]= Number((5 - scores[k]).toFixed(2)))
// 根因:最低分的维度
let minK=null, minV=Infinity
Object.keys(scores).forEach(k=>{ if(scores[k]<minV){minV=scores[k]; minK=k}})
const rootCause = minK? {dimension:minK, score:minV, label: minK==='value1'?'价值创造':minK==='value2'?'价值传递':minK==='value3'?'价值交付':'价值支持', reason: `该维度得分最低(${minV}),为当前核心瓶颈`} : null
// 路径建议
const scoreKeys = Object.keys(scores)
const weightedTotal = scoreKeys.reduce((sum, key) => sum + scores[key] * Number(weights[key] ?? 1), 0)
const weightTotal = scoreKeys.reduce((sum, key) => sum + Number(weights[key] ?? 1), 0)
const overall = weightedTotal / Math.max(1, weightTotal)
let path='B态全案'
if(overall>3.8) path='A态入门'
else if(overall>2.5) path='M态进阶'
const pathSuggestion={overall: Number(overall.toFixed(2)), stage: path, recommendation: path==='A态入门'?'聚焦单点突破与复制':path==='M态进阶'?'完善机制与协同': '系统化构建四大价值'}
return {scores, amb, gap, rootCause, pathSuggestion}
}
app.post('/api/diagnosis/sessions/:token/submit', (req,res)=>{
db.get(`SELECT * FROM diagnosis_sessions WHERE token=?`,[req.params.token], (e,sess)=>{
if(!sess) return res.status(404).json({code:404,msg:'会话不存在'})
const answers = req.body.answers || JSON.parse(sess.answers||'{}')
db.get(`SELECT * FROM diagnosis_config WHERE id=1`, (e2,cfg)=>{
const cfgObj = cfg? {weights: JSON.parse(cfg.weights||'{}'), thresholds: JSON.parse(cfg.thresholds||'{}'), templates: JSON.parse(cfg.templates||'{}'), share_expire_days: cfg.share_expire_days||30 } : {weights:{},thresholds:{},templates:{},share_expire_days:30}
db.all(`SELECT * FROM diagnosis_questions WHERE status='published' ORDER BY sort_order DESC`, (e3, qs)=>{
const result = calcDiagnosis(answers, qs||[], cfgObj)
const reportId=genId('drep')
const token=crypto.randomBytes(16).toString('hex')
const shareToken=crypto.randomBytes(16).toString('hex')
const expires=new Date(Date.now()+ (cfgObj.share_expire_days*24*3600*1000)).toISOString()
const now=nowISO()
db.run(`UPDATE diagnosis_sessions SET answers=?, completed_at=?, updated_at=? WHERE token=?`, [JSON.stringify(answers), now, now, req.params.token])
db.run(`INSERT INTO diagnosis_reports (id, session_id, token, share_token, scores, amb, gap, root_cause, path_suggestion, expires_at, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
[reportId, sess.id, token, shareToken, JSON.stringify(result.scores), JSON.stringify(result.amb), JSON.stringify(result.gap), JSON.stringify(result.rootCause), JSON.stringify(result.pathSuggestion), expires, now], e4=>{
res.json({code:0,data:{reportId, token, shareToken, ...result, expires_at: expires}})
})
})
})
})
})
// 报告查询
app.get('/api/diagnosis/reports/:token', (req,res)=>{
db.get(`SELECT * FROM diagnosis_reports WHERE token=?`,[req.params.token], (e,row)=>{
if(!row) return res.status(404).json({code:404,msg:'报告不存在'})
if(new Date(row.expires_at) < new Date()) return res.status(410).json({code:410,msg:'报告已过期,请重新诊断'})
res.json({code:0,data:{...row, scores: JSON.parse(row.scores||'{}'), amb: JSON.parse(row.amb||'{}'), gap: JSON.parse(row.gap||'{}'), root_cause: JSON.parse(row.root_cause||'{}'), path_suggestion: JSON.parse(row.path_suggestion||'{}')}})
})
})
app.get('/api/diagnosis/share/:shareToken', (req,res)=>{
db.get(`SELECT * FROM diagnosis_reports WHERE share_token=?`,[req.params.shareToken], (e,row)=>{
if(!row) return res.status(404).json({code:404,msg:'分享不存在'})
if(new Date(row.expires_at) < new Date()) return res.status(410).json({code:410,msg:'分享已过期'})
res.json({code:0,data:{...row, scores: JSON.parse(row.scores||'{}'), amb: JSON.parse(row.amb||'{}'), gap: JSON.parse(row.gap||'{}'), root_cause: JSON.parse(row.root_cause||'{}'), path_suggestion: JSON.parse(row.path_suggestion||'{}')}})
})
})
// 留资
app.post('/api/diagnosis/reports/:token/lead', (req,res)=>{
const {name, company, phone}=req.body
if(phone && !/^1[3-9]\d{9}$/.test(phone)) return res.status(400).json({code:400,msg:'手机号格式错误'})
db.get(`SELECT * FROM diagnosis_reports WHERE token=?`,[req.params.token], (e,rep)=>{
if(!rep) return res.status(404).json({code:404,msg:'报告不存在'})
if(new Date(rep.expires_at) < new Date()) return res.status(410).json({code:410,msg:'报告已过期,请重新诊断'})
const id=genId('lead')
db.run(`INSERT INTO diagnosis_leads (id, session_id, report_id, name, company, phone, created_at) VALUES (?,?,?,?,?,?,?)`,
[id, rep.session_id, rep.id, name||'', company||'', phone||'', nowISO()], e2=> res.json({code:0,msg:'提交成功', data:{id}}))
})
})
// PDF 生成
app.get('/api/diagnosis/reports/:token/pdf', (req,res)=>{
db.get(`SELECT * FROM diagnosis_reports WHERE token=?`,[req.params.token], (e,row)=>{
if(!row) return res.status(404).json({code:404,msg:'报告不存在'})
const scores=JSON.parse(row.scores||'{}')
const amb=JSON.parse(row.amb||'{}')
const gap=JSON.parse(row.gap||'{}')
const rc=JSON.parse(row.root_cause||'{}')
const ps=JSON.parse(row.path_suggestion||'{}')
res.setHeader('Content-Type','application/pdf')
res.setHeader('Content-Disposition',`attachment; filename="diagnosis-${row.token}.pdf"`)
const doc=new PDFDocument({margin:40})
doc.pipe(res)
doc.fontSize(18).text('企业进化诊断报告', {align:'center'})
doc.moveDown()
doc.fontSize(10).text('基于企业自填信息的初步分析,仅供参考,不替代人工咨询,不做绝对化结论与收益预测。', {align:'center'})
doc.moveDown()
doc.fontSize(12).text('一、进化坐标总览')
doc.fontSize(10).text(`四大价值得分:${JSON.stringify(scores)}`)
doc.text(`A/M/B定位:${JSON.stringify(amb)}`)
doc.moveDown()
doc.fontSize(12).text('二、差距分析')
doc.fontSize(10).text(JSON.stringify(gap))
doc.moveDown()
doc.fontSize(12).text('三、核心根因')
doc.fontSize(10).text(JSON.stringify(rc))
doc.moveDown()
doc.fontSize(12).text('四、进化路径建议')
doc.fontSize(10).text(JSON.stringify(ps))
doc.moveDown()
doc.fontSize(12).text('五、推荐服务:根据诊断结果匹配 A态入门/M态进阶/B态全案')
doc.moveDown()
doc.fontSize(10).text('免责声明:本报告基于企业自填信息生成,最终方案需顾问介入。', {align:'center'})
doc.end()
})
})
// CMS 后台:报告记录与统计
app.get('/api/cms/diagnosis/reports', (req,res)=>{
db.all(`SELECT r.*, s.token as session_token FROM diagnosis_reports r LEFT JOIN diagnosis_sessions s ON r.session_id=s.id ORDER BY r.created_at DESC LIMIT 100`, (e,rows)=> res.json({code:0,data:rows}))
})
app.get('/api/cms/diagnosis/leads', (req,res)=>{
db.all(`SELECT * FROM diagnosis_leads ORDER BY created_at DESC LIMIT 200`, (e,rows)=> res.json({code:0,data:rows}))
})
app.get('/api/cms/diagnosis/stats', (req,res)=>{
db.get(`SELECT COUNT(*) as totalSessions FROM diagnosis_sessions`, (e1,r1)=>{
db.get(`SELECT COUNT(*) as totalReports FROM diagnosis_reports`, (e2,r2)=>{
db.get(`SELECT COUNT(*) as totalLeads FROM diagnosis_leads`, (e3,r3)=>{
db.get(`SELECT COUNT(*) as completed FROM diagnosis_reports`, (e4,r4)=>{
res.json({code:0,data:{totalSessions:r1.totalSessions, totalReports:r2.totalReports, totalLeads:r3.totalLeads, completed:r4.completed}})
})
})
})
})
})
app.get('/api/cms/contact-leads', (req,res)=>{
db.all(`SELECT * FROM contact_leads ORDER BY created_at DESC LIMIT 200`, (e,rows)=> {
if (e) return res.status(500).json({code:500,msg:'读取失败'})
res.json({code:0,data:rows})
})
})
app.use((err, req, res, next) => {
if (res.headersSent) return next(err);
if (err?.message?.includes('仅支持') || err?.code === 'LIMIT_FILE_SIZE' || err?.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ code: 400, msg: err.message || '上传文件不符合要求' });
}
console.error('[CMS]', err);
res.status(500).json({ code: 500, msg: '服务器内部错误' });
});
app.listen(PORT, '0.0.0.0', ()=> console.log(`CMS server running http://0.0.0.0:${PORT} health /health admin /admin`));
+36
View File
@@ -0,0 +1,36 @@
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import 'dotenv/config';
const SECRET = process.env.JWT_SECRET || (process.env.NODE_ENV === 'production'
? (() => { throw new Error('JWT_SECRET must be configured in production'); })()
: crypto.randomBytes(32).toString('hex'));
export function signToken(payload) {
return jwt.sign(payload, SECRET, { expiresIn: '12h' });
}
export function verifyToken(token) {
return jwt.verify(token, SECRET);
}
export function authMiddleware(req, res, next) {
// 静态资源与诊断公开
if (req.path.startsWith('/admin') || req.path.startsWith('/uploads') || req.path === '/health' || req.path === '/') return next();
// 诊断相关全部公开(问卷、会话、报告、留资、分享、PDF)- 但CMS诊断后台需鉴权
if (req.path.startsWith('/api/diagnosis/')) return next();
// 所有 CMS 管理接口(包括 GET)都必须鉴权;只有下方聚合公开接口例外。
if (req.path === '/api/auth/login' || req.path === '/api/cms/public' || req.path === '/api/contact/leads') return next();
// 上传也需鉴权
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
if (req.path === '/api/cms/public' || req.path === '/api/contact/leads') return next();
return res.status(401).json({ code: 401, msg: '未登录' });
}
try {
const token = auth.slice(7);
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (e) {
return res.status(401).json({ code: 401, msg: '登录失效' });
}
}
+105
View File
@@ -0,0 +1,105 @@
import { initDB, getDB } from './db.js';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import 'dotenv/config';
await initDB();
const db = getDB();
const now = new Date().toISOString();
// 首次初始化密码由环境变量提供;未提供时生成一次性随机密码并打印。
const adminPassword = process.env.CMS_ADMIN_PASSWORD || crypto.randomBytes(12).toString('base64url');
const hash = bcrypt.hashSync(adminPassword, 10);
db.run(`INSERT OR IGNORE INTO admins (id, username, password_hash, created_at) VALUES (?, ?, ?, ?)`, ['admin-1', 'admin', hash, now]);
// 全局配置
db.get(`SELECT id FROM global_config WHERE id=1`, (err, row) => {
if (!row) {
db.run(`INSERT INTO global_config (id, site_title, slogan, seo_title, seo_desc, seo_keywords, hotline, contact_email, contact_address, cta_text, cta_link, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
['阿美巴咨询|企业进化陪跑', '让企业进化被看见、被度量、被落地', '阿美巴咨询|企业进化陪跑 · 体系化增长落地', '以企业进化为主线,四大价值坐标与A/M/B状态坐标为观察系', '阿美巴,企业进化,四大价值,A/M/B', '400-XXX-XXXX', 'contact@aiameiba.com', '【占位】成都市', '咨询获取方案', '/contact', now]);
}
});
// 首页板块
const sections = [
['home-hero', 'home-hero', '让企业进化被看见、被度量、被落地', '以企业进化为主线', '首屏主标题', 1, 100],
['home-ameba', 'home-ameba', '关于阿美巴', '克制理性的企业服务', '阿美巴摘要', 1, 90],
['home-evolution', 'home-evolution', '企业进化 · 观察坐标', '思想 → 价值 → 状态', '进化摘要', 1, 80],
['home-methods', 'home-methods', '方法与能力', '方法×能力×工具', '方法摘要', 1, 70],
['home-cases', 'home-cases', '实践案例', '9段固定插槽', '案例摘要', 1, 60],
['home-services', 'home-services', '产品服务 · 下一步行动', '禁止电商逻辑', '服务摘要', 1, 50],
['home-insights', 'home-insights', '认知观察', '持续观察与沉淀', '观察摘要', 1, 40],
];
sections.forEach(s => db.run(`INSERT OR IGNORE INTO home_sections (id, key, title, subtitle, summary, visible, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [...s, now]));
// 阿美巴页面
db.get(`SELECT id FROM ameba_page WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO ameba_page (id, brand_position, mission, philosophy, practice, updated_at) VALUES (1, ?, ?, ?, ?, ?)`,
['以企业进化经营哲学为底色', '陪跑企业体系化增长落地', '利他、务实、陪跑', '十年以上实战沉淀', now]);
});
// evolution_thought
db.get(`SELECT id FROM evolution_thought WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO evolution_thought (id, content, updated_at) VALUES (1, ?, ?)`, ['进化是持续迭代,而非一次性变革', now]);
});
// four_values
const fvs = [
['fv-1', 'value1', '战略价值坐标', '观察企业战略清晰度', '战略价值说明【占位】', 100],
['fv-2', 'value2', '组织价值坐标', '观察组织活力', '组织价值说明【占位】', 90],
['fv-3', 'value3', '经营价值坐标', '观察经营机制', '经营价值说明【占位】', 80],
['fv-4', 'value4', '人的价值坐标', '观察人的成长', '人的价值说明【占位】', 70],
];
fvs.forEach(f => db.run(`INSERT OR IGNORE INTO four_values (id, key, name, definition, description, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [...f, now]));
// amb_states 固定说明
const ambs = [
['amb-A', 'A', 'A 状态|自进化', '具备自驱动', 'A状态说明【占位】', 'A/M/B不绑定企业规模的说明文案固定:同一企业不同价值维度可处不同状态', 100],
['amb-M', 'M', 'M 状态|待突破', '存在卡点', 'M状态说明【占位】', '同上固定说明', 90],
['amb-B', 'B', 'B 状态|待构建', '基础待构建', 'B状态说明【占位】', '同上固定说明', 80],
];
ambs.forEach(a => db.run(`INSERT OR IGNORE INTO amb_states (id, key, name, definition, description, fixed_note, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [...a, now]));
// ten_systems
for (let i = 1; i <= 10; i++) {
db.run(`INSERT OR IGNORE INTO ten_systems (id, name, description, sort_order, updated_at) VALUES (?, ?, ?, ?, ?)`, [`sys-${i}`, `系统${i}`, `${i}系统说明【占位】`, 100 - i * 5, now]);
}
db.get(`SELECT id FROM evolution_process WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO evolution_process (id, steps, scenarios, description, updated_at) VALUES (1, ?, ?, ?, ?)`, ['步骤1→步骤2→步骤3', '适用场景【占位】', '企业进化梳理说明', now]);
});
db.get(`SELECT id FROM self_check WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO self_check (id, guide, questions, logic_text, next_guide, updated_at) VALUES (1, ?, ?, ?, ?, ?)`, ['轻量自我对照引导文案', JSON.stringify([{q:"战略是否清晰?",options:["A","M","B"]}]), '归位逻辑文案【占位】', '下一步引导:预约咨询', now]);
});
// 诊断配置
db.get(`SELECT id FROM diagnosis_config WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO diagnosis_config (id, weights, thresholds, templates, share_expire_days, updated_at) VALUES (1, ?, ?, ?, ?, ?)`, [
JSON.stringify({value1:1, value2:1, value3:1, value4:1}),
JSON.stringify({B_max:2.5, M_max:3.8}),
JSON.stringify({
disclaimer:"本报告基于企业自填信息的初步分析,仅供参考,不替代人工咨询,不做绝对化结论与收益预测。",
intro:"基于四大价值坐标与A/M/B状态坐标的初步进化分析"
}),
30, now
]);
});
// 诊断问卷 16题(每维度4题,单选/量表)
const diagQuestions = [
// 价值创造
['dq-1','value1','价值创造','single','公司未来3年战略方向是否清晰且全员认同?', JSON.stringify([{label:'非常清晰',score:5},{label:'较清晰',score:4},{label:'一般',score:3},{label:'较模糊',score:2},{label:'非常模糊',score:1}]), 100],
['dq-2','value1','价值创造','scale','战略目标分解到季度/月度的颗粒度', JSON.stringify([{label:'1分',score:1},{label:'2分',score:2},{label:'3分',score:3},{label:'4分',score:4},{label:'5分',score:5}]), 95],
['dq-3','value1','价值创造','single','是否定期进行战略复盘与迭代?', JSON.stringify([{label:'每月',score:5},{label:'每季度',score:4},{label:'每半年',score:3},{label:'每年',score:2},{label:'很少',score:1}]), 90],
['dq-4','value1','价值创造','single','核心产品/服务的差异化优势是否明确?', JSON.stringify([{label:'非常明确',score:5},{label:'较明确',score:4},{label:'一般',score:3},{label:'较模糊',score:2},{label:'无',score:1}]), 85],
// 价值传递
['dq-5','value2','价值传递','single','组织架构是否支撑战略落地?', JSON.stringify([{label:'非常支撑',score:5},{label:'较支撑',score:4},{label:'一般',score:3},{label:'较不支撑',score:2},{label:'完全不支撑',score:1}]), 80],
['dq-6','value2','价值传递','scale','跨部门协同效率自评', JSON.stringify([{label:'1分',score:1},{label:'2分',score:2},{label:'3分',score:3},{label:'4分',score:4},{label:'5分',score:5}]), 75],
['dq-7','value2','价值传递','single','人才梯队与关键岗位备份情况', JSON.stringify([{label:'非常完善',score:5},{label:'较完善',score:4},{label:'一般',score:3},{label:'较薄弱',score:2},{label:'无',score:1}]), 70],
['dq-8','value2','价值传递','single','企业文化与价值观落地程度', JSON.stringify([{label:'高度落地',score:5},{label:'较好',score:4},{label:'一般',score:3},{label:'较弱',score:2},{label:'无',score:1}]), 65],
// 价值交付
['dq-9','value3','价值交付','single','核心业务流程是否标准化且可复制?', JSON.stringify([{label:'高度标准化',score:5},{label:'较标准化',score:4},{label:'部分',score:3},{label:'较弱',score:2},{label:'无',score:1}]), 60],
['dq-10','value3','价值交付','scale','经营数据(产销存/财务)透明度', JSON.stringify([{label:'1分',score:1},{label:'2分',score:2},{label:'3分',score:3},{label:'4分',score:4},{label:'5分',score:5}]), 55],
['dq-11','value3','价值交付','single','目标→执行→复盘→改进闭环完整度', JSON.stringify([{label:'非常完整',score:5},{label:'较完整',score:4},{label:'一般',score:3},{label:'较不完整',score:2},{label:'无闭环',score:1}]), 50],
['dq-12','value3','价值交付','single','客户交付质量与一致性', JSON.stringify([{label:'非常高',score:5},{label:'较高',score:4},{label:'一般',score:3},{label:'较低',score:2},{label:'很低',score:1}]), 45],
// 价值支持
['dq-13','value4','价值支持','single','激励机制与价值创造的关联度', JSON.stringify([{label:'高度关联',score:5},{label:'较关联',score:4},{label:'一般',score:3},{label:'较弱',score:2},{label:'无关联',score:1}]), 40],
['dq-14','value4','价值支持','scale','员工成长与学习机制完善度', JSON.stringify([{label:'1分',score:1},{label:'2分',score:2},{label:'3分',score:3},{label:'4分',score:4},{label:'5分',score:5}]), 35],
['dq-15','value4','价值支持','single','信息化/数字化工具对经营的支持', JSON.stringify([{label:'非常强',score:5},{label:'较强',score:4},{label:'一般',score:3},{label:'较弱',score:2},{label:'无',score:1}]), 30],
['dq-16','value4','价值支持','single','企业抗风险与持续改进能力', JSON.stringify([{label:'非常强',score:5},{label:'较强',score:4},{label:'一般',score:3},{label:'较弱',score:2},{label:'很弱',score:1}]), 25],
];
diagQuestions.forEach(q=> db.run(`INSERT OR IGNORE INTO diagnosis_questions (id, group_key, group_name, type, title, options, sort_order, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'published', ?, ?)`, [...q, now, now]));
console.log('seed done');
console.log(`admin username: admin\nadmin password: ${adminPassword}`);
setTimeout(() => process.exit(0), 800);