fix: make CMS content live and migrate to MySQL
This commit is contained in:
+84
-53
@@ -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>标识 key(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>
|
||||
@@ -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>标识 key(Key)</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>状态 key(Key)</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>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
NODE_ENV=production
|
||||
CMS_PORT=13001
|
||||
JWT_SECRET=replace-with-a-long-random-secret
|
||||
CMS_ADMIN_PASSWORD=change-on-first-login
|
||||
|
||||
# MySQL 生产配置
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=33306
|
||||
DB_USER=admin
|
||||
DB_PASSWORD=replace-with-database-password
|
||||
DB_NAME=ameiba_cms
|
||||
DB_CONNECTION_LIMIT=10
|
||||
|
||||
UPLOAD_DIR=/var/www/ameiba-cn-new/cms/server/uploads
|
||||
CORS_ORIGIN=https://www.ameiba.cn
|
||||
Generated
+123
-377
@@ -9,12 +9,12 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.11.3",
|
||||
"pdfkit": "^0.15.0"
|
||||
}
|
||||
},
|
||||
@@ -27,6 +27,16 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz",
|
||||
"integrity": "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -83,6 +93,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -109,51 +128,6 @@
|
||||
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.6",
|
||||
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.6.tgz",
|
||||
@@ -196,30 +170,6 @@
|
||||
"pako": "~1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
@@ -299,12 +249,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz",
|
||||
@@ -404,21 +348,6 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-equal": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/deep-equal/-/deep-equal-2.2.3.tgz",
|
||||
@@ -451,15 +380,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
@@ -513,15 +433,6 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dfa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/dfa/-/dfa-1.2.0.tgz",
|
||||
@@ -578,15 +489,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@@ -652,15 +554,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz",
|
||||
@@ -707,12 +600,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
@@ -781,12 +668,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -805,6 +686,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -842,12 +732,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -955,38 +839,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@@ -1130,6 +988,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@@ -1358,6 +1222,27 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz",
|
||||
"integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=1.30.0",
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -1427,18 +1312,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -1460,12 +1333,6 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
|
||||
@@ -1491,11 +1358,54 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
|
||||
"license": "MIT"
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.24.4",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.4.tgz",
|
||||
"integrity": "sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.2",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.7.3",
|
||||
"long": "^5.3.2",
|
||||
"lru.min": "^1.1.4",
|
||||
"named-placeholders": "^1.1.6",
|
||||
"sql-escaper": "^1.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2/node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru.min": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
@@ -1506,18 +1416,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.96.0",
|
||||
"resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.96.0.tgz",
|
||||
"integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -1596,15 +1494,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
|
||||
@@ -1656,32 +1545,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.2.tgz",
|
||||
"integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^1.0.1",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -1701,16 +1564,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.3.tgz",
|
||||
@@ -1751,21 +1604,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
@@ -2029,49 +1867,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"node_modules/sql-escaper": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.1.tgz",
|
||||
"integrity": "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=2.0.0",
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
@@ -2119,57 +1927,6 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.5.tgz",
|
||||
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
@@ -2191,18 +1948,6 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -2222,6 +1967,13 @@
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||
@@ -2339,12 +2091,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ameba-cms-server",
|
||||
"version": "1.0.0",
|
||||
"description": "阿美巴传统官网1.0 CMS后台 - 轻量SQLite方案",
|
||||
"description": "阿美巴传统官网 CMS 服务",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node src/index.js",
|
||||
@@ -15,7 +15,7 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"mysql2": "^3.11.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"pdfkit": "^0.15.0"
|
||||
},
|
||||
|
||||
+59
-335
@@ -1,348 +1,72 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { fileURLToPath } from 'url';
|
||||
import mysql from 'mysql2/promise';
|
||||
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');
|
||||
const DB_HOST = process.env.DB_HOST || '127.0.0.1';
|
||||
const DB_PORT = Number(process.env.DB_PORT || 33306);
|
||||
const DB_USER = process.env.DB_USER || 'admin';
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD || '';
|
||||
const DB_NAME = process.env.DB_NAME || 'ameiba_cms';
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.resolve(process.cwd(), 'uploads');
|
||||
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
let pool;
|
||||
|
||||
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
|
||||
)`);
|
||||
async function ensureDatabase() {
|
||||
const connection = await mysql.createConnection({ host: DB_HOST, port: DB_PORT, user: DB_USER, password: DB_PASSWORD, charset: 'utf8mb4' });
|
||||
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${DB_NAME.replace(/`/g, '')}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
||||
await connection.end();
|
||||
}
|
||||
|
||||
// 创建兼容 sqlite3 回调风格的包装器
|
||||
function mysqlSchema(sql) {
|
||||
return sql.replace(/INTEGER PRIMARY KEY CHECK \(id = 1\)/gi, 'INT PRIMARY KEY')
|
||||
.replace(/TEXT PRIMARY KEY/gi, 'VARCHAR(191) PRIMARY KEY')
|
||||
.replace(/TEXT UNIQUE/gi, 'VARCHAR(191) UNIQUE')
|
||||
.replace(/\bTEXT\b/gi, 'LONGTEXT').replace(/LONGTEXT UNIQUE/gi, 'VARCHAR(191) UNIQUE')
|
||||
.replace(/LONGTEXT DEFAULT '([^']*)'/gi, "VARCHAR(191) DEFAULT '$1'")
|
||||
.replace(/\bREAL\b/gi, 'DECIMAL(18,4)').replace(/\bINTEGER\b/gi, 'INT');
|
||||
}
|
||||
|
||||
export async function initDB() {
|
||||
await ensureDatabase();
|
||||
pool = mysql.createPool({ host: DB_HOST, port: DB_PORT, user: DB_USER, password: DB_PASSWORD, database: DB_NAME, charset: 'utf8mb4', waitForConnections: true, connectionLimit: Number(process.env.DB_CONNECTION_LIMIT || 10), timezone: 'Z' });
|
||||
const tables = [
|
||||
`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)`,
|
||||
`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)`,
|
||||
`CREATE TABLE IF NOT EXISTS site_texts (id VARCHAR(191) PRIMARY KEY, label TEXT, value LONGTEXT, updated_at TEXT)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`CREATE TABLE IF NOT EXISTS evolution_thought (id INTEGER PRIMARY KEY CHECK (id = 1), content TEXT, updated_at TEXT)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`CREATE TABLE IF NOT EXISTS evolution_process (id INTEGER PRIMARY KEY CHECK (id = 1), steps TEXT, scenarios TEXT, description TEXT, updated_at TEXT)`,
|
||||
`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)`,
|
||||
`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 DECIMAL(18,4), 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)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`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)`,
|
||||
`CREATE TABLE IF NOT EXISTS admins (id TEXT PRIMARY KEY, username TEXT UNIQUE, password_hash TEXT, created_at TEXT)`,
|
||||
];
|
||||
for (const table of tables) await pool.query(mysqlSchema(table));
|
||||
}
|
||||
|
||||
function normalizeSql(sql) { return sql.replace(/INSERT OR IGNORE/gi, 'INSERT IGNORE'); }
|
||||
|
||||
export function getDB() {
|
||||
if (!pool) throw new Error('Database is not initialized');
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
get(sql, params, callback) { if (typeof params === 'function') { callback = params; params = []; } pool.query(normalizeSql(sql), params || []).then(([rows]) => callback(null, rows[0] || null)).catch(callback); },
|
||||
all(sql, params, callback) { if (typeof params === 'function') { callback = params; params = []; } pool.query(normalizeSql(sql), params || []).then(([rows]) => callback(null, rows)).catch(callback); },
|
||||
run(sql, params, callback) { if (typeof params === 'function') { callback = params; params = []; } return pool.query(normalizeSql(sql), params || []).then(([result]) => { const context = { lastID: result.insertId, changes: result.affectedRows }; if (callback) callback.call(context, null); return context; }).catch(err => { if (callback) callback(err); else throw err; }); },
|
||||
serialize(callback) { if (callback) callback(); },
|
||||
prepare(sql) { return { run(...args) { const callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined; return pool.query(normalizeSql(sql), args).then(([result]) => { if (callback) callback.call({ lastID: result.insertId, changes: result.affectedRows }, null); }).catch(err => callback && callback(err)); }, finalize(callback) { if (callback) callback(); } }; },
|
||||
};
|
||||
}
|
||||
|
||||
export { DB_PATH, UPLOAD_DIR };
|
||||
export { UPLOAD_DIR, DB_HOST, DB_PORT, DB_NAME };
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import fs from 'fs';
|
||||
|
||||
const sqlite = new Database(process.env.SQLITE_DB_PATH || './cms.db', { readonly: true });
|
||||
const output = process.env.SQLITE_EXPORT_PATH || './sqlite-export.json';
|
||||
const tables = ['global_config', 'home_sections', 'site_texts', 'ameba_page', 'experts', 'evolution_thought', 'four_values', 'amb_states', 'evolution_process', 'ten_systems', 'services', 'cases', 'articles', 'self_check', 'diagnosis_questions', 'diagnosis_sessions', 'diagnosis_reports', 'diagnosis_leads', 'contact_leads', 'diagnosis_config', 'admins'];
|
||||
const data = {};
|
||||
for (const table of tables) { try { data[table] = sqlite.prepare(`SELECT * FROM ${table}`).all(); } catch { data[table] = []; } }
|
||||
fs.writeFileSync(output, JSON.stringify(data));
|
||||
sqlite.close();
|
||||
console.log(`SQLite export complete: ${output}`);
|
||||
@@ -0,0 +1,19 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import fs from 'fs';
|
||||
import 'dotenv/config';
|
||||
|
||||
const input = process.env.SQLITE_EXPORT_PATH || './sqlite-export.json';
|
||||
const data = JSON.parse(fs.readFileSync(input, 'utf8'));
|
||||
const connection = await mysql.createConnection({ host: process.env.DB_HOST || '127.0.0.1', port: Number(process.env.DB_PORT || 33306), user: process.env.DB_USER || 'admin', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'ameiba_cms', charset: 'utf8mb4' });
|
||||
for (const [table, rows] of Object.entries(data)) {
|
||||
for (const row of rows) {
|
||||
const columns = Object.keys(row);
|
||||
const escaped = columns.map(c => `\`${c}\``).join(',');
|
||||
const values = columns.map(() => '?').join(',');
|
||||
const updates = columns.filter(c => c !== 'id').map(c => `\`${c}\`=VALUES(\`${c}\`)`).join(',');
|
||||
await connection.query(`INSERT INTO \`${table}\` (${escaped}) VALUES (${values}) ON DUPLICATE KEY UPDATE ${updates || '`id`=VALUES(`id`)'}`, columns.map(c => row[c]));
|
||||
}
|
||||
console.log(`${table}: ${(rows || []).length} rows`);
|
||||
}
|
||||
await connection.end();
|
||||
console.log('MySQL import complete');
|
||||
+39
-27
@@ -21,6 +21,7 @@ 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((req, res, next) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); next(); });
|
||||
app.use(authMiddleware);
|
||||
|
||||
// 静态:上传文件
|
||||
@@ -139,6 +140,8 @@ app.post('/api/contact/leads', (req, res) => {
|
||||
// 通用工具
|
||||
const nowISO = () => new Date().toISOString();
|
||||
const genId = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
||||
const queryOne = (sql, params=[]) => new Promise((resolve, reject) => db.get(sql, params, (err, row) => err ? reject(err) : resolve(row || null)));
|
||||
const queryAll = (sql, params=[]) => new Promise((resolve, reject) => db.all(sql, params, (err, rows) => err ? reject(err) : resolve(rows || [])));
|
||||
|
||||
// 全局配置
|
||||
app.get('/api/cms/config', (req, res) => {
|
||||
@@ -153,6 +156,16 @@ app.put('/api/cms/config', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 可配置站点文案。页面只读取公开聚合中的 texts,后台通过此处集中维护。
|
||||
app.get('/api/cms/texts', (req, res) => db.all(`SELECT * FROM site_texts ORDER BY id`, (e, rows) => res.json({ code: 0, data: rows || [] })));
|
||||
app.put('/api/cms/texts', (req, res) => {
|
||||
const items = Array.isArray(req.body) ? req.body : [];
|
||||
const stmt = db.prepare(`INSERT INTO site_texts (id, label, value, updated_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE label=VALUES(label), value=VALUES(value), updated_at=VALUES(updated_at)`);
|
||||
Promise.all(items.filter(it => it?.id).map(it => stmt.run(it.id, it.label || it.id, it.value || '', nowISO())))
|
||||
.then(() => stmt.finalize(() => res.json({ code: 0, msg: '保存成功' })))
|
||||
.catch(err => res.status(500).json({ code: 500, msg: err.message }));
|
||||
});
|
||||
|
||||
// 首页板块
|
||||
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}));
|
||||
@@ -192,21 +205,20 @@ app.get('/api/cms/four-values', (req,res)=> db.all(`SELECT * FROM four_values OR
|
||||
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 (?,?,?,?,?,?,?,?,?)`,
|
||||
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=?`,
|
||||
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:'更新成功'}));
|
||||
db.run(`UPDATE amb_states SET name=?, definition=?, description=?, image_url=?, sort_order=?, fixed_note=?, status=?, updated_at=? WHERE id=?`,
|
||||
[b.name,b.definition,b.description,b.image_url,b.sort_order,b.fixed_note,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
|
||||
});
|
||||
|
||||
// 方法与能力
|
||||
@@ -238,7 +250,7 @@ app.post('/api/cms/services', (req,res)=>{
|
||||
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}}));
|
||||
[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||'published',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;
|
||||
@@ -256,7 +268,7 @@ app.post('/api/cms/cases', (req,res)=>{
|
||||
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}}));
|
||||
[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||'published',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;
|
||||
@@ -273,7 +285,7 @@ app.post('/api/cms/articles', (req,res)=>{
|
||||
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}}));
|
||||
[id,slug,b.title,b.subtitle,b.category,b.cover_url,b.content_html,b.publish_time||nowISO(),b.recommend_weight||0,b.status||'published',nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
|
||||
});
|
||||
app.put('/api/cms/articles/:id', (req,res)=>{
|
||||
const b=req.body;
|
||||
@@ -292,25 +304,25 @@ app.put('/api/cms/self-check', (req,res)=>{
|
||||
|
||||
// 前端公开聚合接口(供前端一次性拉取,不需要鉴权,使用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});
|
||||
});
|
||||
});
|
||||
Promise.all([
|
||||
queryOne(`SELECT * FROM global_config WHERE id=1`),
|
||||
queryAll(`SELECT * FROM home_sections WHERE visible=1 ORDER BY sort_order DESC`),
|
||||
queryOne(`SELECT * FROM ameba_page WHERE id=1`),
|
||||
queryAll(`SELECT * FROM experts WHERE status='published' ORDER BY sort_weight DESC`),
|
||||
queryOne(`SELECT * FROM evolution_thought WHERE id=1`),
|
||||
queryAll(`SELECT * FROM four_values WHERE status='published' ORDER BY sort_order DESC`),
|
||||
queryAll(`SELECT * FROM amb_states WHERE status='published' ORDER BY sort_order DESC`),
|
||||
queryOne(`SELECT * FROM evolution_process WHERE id=1`),
|
||||
queryAll(`SELECT * FROM ten_systems WHERE status='published' ORDER BY sort_order DESC`),
|
||||
queryAll(`SELECT * FROM services WHERE status='published' ORDER BY sort_weight DESC`),
|
||||
queryAll(`SELECT * FROM cases WHERE status='published' ORDER BY sort_weight DESC`),
|
||||
queryAll(`SELECT * FROM articles WHERE status='published' ORDER BY recommend_weight DESC`),
|
||||
queryOne(`SELECT * FROM self_check WHERE id=1`),
|
||||
queryAll(`SELECT id, label, value FROM site_texts ORDER BY id`),
|
||||
]).then(([config, home_sections, ameba, experts, evolution_thought, four_values, amb_states, evolution_process, ten_systems, services, cases, articles, self_check, texts]) => {
|
||||
const textMap = Object.fromEntries(texts.map(item => [item.id, item.value]));
|
||||
res.json({ code: 0, data: { config, home_sections, ameba, experts, evolution_thought, four_values, amb_states, evolution_process, ten_systems, services, cases, articles, self_check, texts: textMap } });
|
||||
}).catch(err => res.status(500).json({ code: 500, msg: err.message }));
|
||||
});
|
||||
|
||||
// ================= 诊断系统 =================
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/* 一次性迁移工具:保留服务器原 SQLite 内容到 MySQL。
|
||||
* 在服务器 npm install 新依赖前执行,SQLITE_DB_PATH 指向旧 cms.db。
|
||||
*/
|
||||
import Database from 'better-sqlite3';
|
||||
import mysql from 'mysql2/promise';
|
||||
import 'dotenv/config';
|
||||
|
||||
const sqlitePath = process.env.SQLITE_DB_PATH || './cms.db';
|
||||
const sqlite = new Database(sqlitePath, { readonly: true });
|
||||
const mysqlConfig = { host: process.env.DB_HOST || '127.0.0.1', port: Number(process.env.DB_PORT || 33306), user: process.env.DB_USER || 'admin', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'ameiba_cms', charset: 'utf8mb4' };
|
||||
const connection = await mysql.createConnection(mysqlConfig);
|
||||
|
||||
const tables = ['global_config', 'home_sections', 'site_texts', 'ameba_page', 'experts', 'evolution_thought', 'four_values', 'amb_states', 'evolution_process', 'ten_systems', 'services', 'cases', 'articles', 'self_check', 'diagnosis_questions', 'diagnosis_sessions', 'diagnosis_reports', 'diagnosis_leads', 'contact_leads', 'diagnosis_config', 'admins'];
|
||||
for (const table of tables) {
|
||||
let rows = [];
|
||||
try { rows = sqlite.prepare(`SELECT * FROM ${table}`).all(); } catch { continue; }
|
||||
for (const row of rows) {
|
||||
const columns = Object.keys(row);
|
||||
const escaped = columns.map(c => `\`${c}\``).join(',');
|
||||
const placeholders = columns.map(() => '?').join(',');
|
||||
await connection.query(`INSERT IGNORE INTO \`${table}\` (${escaped}) VALUES (${placeholders})`, columns.map(c => row[c]));
|
||||
}
|
||||
console.log(`${table}: ${rows.length} rows`);
|
||||
}
|
||||
await connection.end();
|
||||
sqlite.close();
|
||||
console.log('SQLite -> MySQL migration complete');
|
||||
+92
-4
@@ -16,7 +16,7 @@ db.run(`INSERT OR IGNORE INTO admins (id, username, password_hash, created_at) V
|
||||
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]);
|
||||
['阿美巴咨询|企业进化陪跑', '让企业进化被看见、被度量、被落地', '阿美巴咨询|企业进化陪跑 · 体系化增长落地', '以企业进化为主线,四大价值坐标与A/M/B状态坐标为观察系', '阿美巴,企业进化,四大价值,A/M/B', '400-XXX-XXXX', 'contact@aiameiba.com', '成都', '咨询获取方案', '/contact', now]);
|
||||
}
|
||||
});
|
||||
// 首页板块
|
||||
@@ -29,7 +29,95 @@ const sections = [
|
||||
['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]));
|
||||
sections.forEach(s => db.run(`INSERT OR IGNORE INTO home_sections (id, \`key\`, title, subtitle, summary, visible, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [...s, now]));
|
||||
|
||||
const siteTexts = [
|
||||
['home.eyebrow', '首页首屏眉题', '阿美巴 · 企业进化陪跑'],
|
||||
['home.title', '首页首屏标题', '让企业进化\n被看见、被度量、被落地'],
|
||||
['home.description', '首页首屏描述', '以“企业进化”为主线,以四大价值坐标与 A/M/B 状态坐标为观察系,陪跑企业构建可复制、可持续的增长经营体系。'],
|
||||
['home.trust', '首页首屏信任说明', '30秒理解主张:进化思想 → 价值坐标 → 状态坐标 → 行动路径'],
|
||||
['home.primary_cta', '首页主按钮', '开始企业进化诊断'],
|
||||
['home.secondary_cta', '首页咨询按钮', '咨询获取方案'],
|
||||
['home.light_cta', '首页轻量对照按钮', '轻量对照'],
|
||||
['home.notice', '首页提示', ''],
|
||||
['home.about_title', '首页关于标题', '我们是谁'],
|
||||
['home.about_text', '首页关于描述', '围绕企业进化经营哲学,陪跑企业把战略、组织、机制与人的成长转化为可执行、可复盘的经营系统。'],
|
||||
['home.about_note', '首页关于补充说明', '我们持续沉淀品牌故事、使命愿景与真实实践。'],
|
||||
['home.cases_title', '首页案例标题', '实践案例'],
|
||||
['home.services_title', '首页服务标题', '产品服务 · 下一步行动'],
|
||||
['home.insights_title', '首页洞察标题', '认知观察'],
|
||||
['global_cta.title', '全站行动标题', '下一步如何行动?'],
|
||||
['global_cta.description', '全站行动描述', '任意页面均可直接发起咨询,选择轻量对照或深度陪跑路径。'],
|
||||
['global_cta.primary', '全站主按钮', '咨询获取方案'],
|
||||
['global_cta.secondary', '全站次按钮', '看看你的企业'],
|
||||
['ameba.header_title', '阿美巴页标题', '阿美巴'],
|
||||
['ameba.header_description', '阿美巴页简介', '以企业进化经营哲学为底色,坚持利他、务实、陪跑,帮助企业把方法落到经营现场。'],
|
||||
['ameba.philosophy_title', '阿美巴哲学标题', '经营哲学'],
|
||||
['ameba.practice_title', '阿美巴实践标题', '服务理念'],
|
||||
['ameba.about_title', '阿美巴介绍标题', '我们是谁'],
|
||||
['ameba.about_text', '阿美巴介绍正文', '围绕企业进化与体系化增长,提供长期、务实、可复盘的企业管理咨询服务。'],
|
||||
['footer.note', '页脚说明', ''],
|
||||
['footer.meta', '页脚底部说明', ''],
|
||||
['home.services_note', '首页服务说明', '围绕下一步行动提供标准产品与定制陪跑服务。'],
|
||||
['cases.header_description', '案例页说明', '通过原状、目标、差距、根因、介入、路径、行动、结果与迭代,看见企业进化过程。'],
|
||||
['cases.detail_link', '案例详情链接文字', '查看完整叙事'],
|
||||
['cases.detail_note', '案例详情提示', '原状、目标、差距、根因、介入、路径、行动、结果与迭代,构成完整的案例叙事。'],
|
||||
['cases.value_placeholder', '案例关联价值提示', '暂未关联价值坐标'],
|
||||
['cases.service_placeholder', '案例关联服务提示', '暂未关联服务'],
|
||||
['contact.company_placeholder', '联系人企业选填提示', '选填'],
|
||||
['contact.need_placeholder', '咨询需求输入提示', '请简述你的企业现状与期望'],
|
||||
['contact.privacy_note', '咨询隐私说明', '提交即视为同意隐私与合规说明。'],
|
||||
['contact.info_placeholder', '联系信息提示', '欢迎通过电话、邮箱或在线表单与我们联系。'],
|
||||
['diagnosis.input_note', '诊断填写提示', '采用区间选择,不收集敏感财务数据。'],
|
||||
['methods.header_description', '方法页说明', '方法决定路径,能力决定落地。二者闭环,支撑企业进化。'],
|
||||
['methods.title', '方法页标题', '方法与能力'],
|
||||
['methods.method_label', '方法卡片标签', '方法'],
|
||||
['methods.capability_label', '能力卡片标签', '能力'],
|
||||
['methods.detail_steps_title', '方法详情步骤标题', '关键步骤'],
|
||||
['methods.placeholder', '方法页补充说明', '从判断到行动,让方法真正进入经营现场。'],
|
||||
['methods.detail_note', '方法详情补充说明', '围绕方法、工具与交付物,形成可复盘的行动路径。'],
|
||||
['services.header_description', '服务页说明', '围绕下一步行动展示标准产品与定制陪跑服务。'],
|
||||
['services.title', '服务页标题', '产品服务'],
|
||||
['services.standard_label', '标准服务标签', '标准产品'],
|
||||
['services.custom_label', '定制服务标签', '深度定制'],
|
||||
['services.custom_cta', '定制服务行动文字', '咨询/获取方案(无价格)'],
|
||||
['services.placeholder', '服务页补充说明', '围绕企业当前阶段,选择合适的行动路径。'],
|
||||
['services.detail_note', '服务详情补充说明', '明确交付物、周期、适合对象与服务流程,让合作预期清晰可见。'],
|
||||
['insights.header_placeholder', '洞察页补充说明', '围绕企业进化、价值坐标与经营现场,持续记录真实观察。'],
|
||||
['insights.title', '洞察页标题', '认知观察'],
|
||||
['insights.header_description', '洞察页说明', '围绕企业进化、价值坐标、A/M/B 与方法论的持续观察与沉淀。'],
|
||||
['insights.detail_note', '洞察详情补充说明', '从真实经营现场出发,持续记录可验证、可复盘的观察。'],
|
||||
['selfcheck.header_note', '轻量对照页头部说明', '仅做轻量对照,深度诊断需线下陪跑。'],
|
||||
['selfcheck.storage_note', '轻量对照结果说明', '结果仅本地展示,不存储。'],
|
||||
['selfcheck.result_title', '轻量对照结果标题', '对照结果'],
|
||||
['selfcheck.result_note', '轻量对照结果补充说明', '此处仅为轻量对照提示,不生成诊断报告。'],
|
||||
['evolution.header_title', '企业进化页标题', '企业进化'],
|
||||
['evolution.header_description', '企业进化页头部说明', '内部顺序强制:企业进化思想 → 四大价值坐标 → A/M/B 状态坐标,顺序不可调换。'],
|
||||
['evolution.header_note', '企业进化页头部补充', '从进化思想到价值坐标,再到状态坐标,逐层看清企业位置。'],
|
||||
['evolution.idea_eyebrow', '企业进化思想眉题', '01 — 企业进化思想'],
|
||||
['evolution.idea_title', '企业进化思想标题', '进化是持续迭代,而非一次性变革'],
|
||||
['evolution.idea_description', '企业进化思想正文', '企业进化强调在不确定环境中,通过机制与人的双重迭代,实现组织的自适应与自进化。拒绝规模决定论,强调价值维度独立观察。'],
|
||||
['evolution.idea_core_title', '核心主张标题', '核心主张'],
|
||||
['evolution.idea_bullet_1', '核心主张一', '经营即进化:战略、组织、机制、人的协同进化'],
|
||||
['evolution.idea_bullet_2', '核心主张二', '坐标而非标签:用价值与状态观察,而非给企业贴标签'],
|
||||
['evolution.idea_bullet_3', '核心主张三', '陪跑而非交付:共创机制,陪跑落地,复盘迭代'],
|
||||
['evolution.idea_bullet_4', '核心主张四', '持续复盘迭代,让每一次行动都沉淀为组织能力'],
|
||||
['evolution.idea_note', '核心主张补充说明', '此处用于补充企业进化思想、原则和实践说明。'],
|
||||
['evolution.values_eyebrow', '四大价值眉题', '02 — 四大价值坐标'],
|
||||
['evolution.values_title', '四大价值标题', '观察坐标,非售卖产品'],
|
||||
['evolution.values_description', '四大价值说明', '四大价值定位为观察坐标,用于看清企业在各维度的真实位置。'],
|
||||
['evolution.amb_eyebrow', 'A/M/B眉题', '03 — A/M/B 状态坐标'],
|
||||
['evolution.amb_title', 'A/M/B标题', 'A / M / B 不绑定规模'],
|
||||
['evolution.amb_description', 'A/M/B说明', '支持同一企业不同价值维度处于不同状态,拒绝规模决定状态的包装。'],
|
||||
['evolution.amb_example_title', '状态示例标题', '跨维度状态示例'],
|
||||
['evolution.amb_example_description', '状态示例正文', '示例:某企业 · 战略价值 A / 组织价值 M / 经营价值 B / 人的价值 M。说明同一企业可在不同坐标处不同状态,需分维度干预。'],
|
||||
['evolution.amb_example_note', '状态示例补充说明', '真实示例可在此处配置,发布前请完成脱敏。'],
|
||||
['evolution.value_detail_meta', '四大价值详情页头部补充', '观察坐标 · 非售卖产品 · 用于理解企业真实位置'],
|
||||
['evolution.value_detail_note', '四大价值详情页说明', '从定义、表现与行动三个层面,理解该价值坐标的真实含义。'],
|
||||
['evolution.amb_detail_meta', 'A/M/B详情页头部补充', '不绑定规模 · 支持跨维度不同状态'],
|
||||
['evolution.amb_detail_note', 'A/M/B详情页说明', '从状态特征、关键风险与进化建议,理解企业当前所处位置。'],
|
||||
];
|
||||
siteTexts.forEach(([id, label, value]) => db.run(`INSERT OR IGNORE INTO site_texts (id, label, value, updated_at) VALUES (?, ?, ?, ?)`, [id, label, value, 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, ?, ?, ?, ?, ?)`,
|
||||
@@ -46,14 +134,14 @@ const fvs = [
|
||||
['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]));
|
||||
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]));
|
||||
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]);
|
||||
|
||||
@@ -9,10 +9,7 @@ export default function Footer() {
|
||||
<div className="footer-brand">成都阿美巴企业管理咨询有限公司</div>
|
||||
<p className="footer-desc">
|
||||
以“企业进化”为核心主张,以四大价值坐标与 A/M/B 状态坐标为观察系,陪跑企业体系化增长落地。
|
||||
<br />
|
||||
<span className="footer-note">一期为前端骨架与占位内容,不虚构案例与数据。</span>
|
||||
</p>
|
||||
<p className="footer-meta">V1.1 FINAL 冻结基线 · 导航6项冻结 · CMS预留</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>一级导航(冻结6项)</h4>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useCMSTexts } from '@/services/cms'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
@@ -17,6 +18,11 @@ export default function GlobalCTA({
|
||||
secondaryLabel = '看看你的企业',
|
||||
secondaryTo = '/self-check',
|
||||
}: Props) {
|
||||
const { data: texts } = useCMSTexts()
|
||||
title = texts['global_cta.title'] || title
|
||||
desc = texts['global_cta.description'] || desc
|
||||
primaryLabel = texts['global_cta.primary'] || primaryLabel
|
||||
secondaryLabel = texts['global_cta.secondary'] || secondaryLabel
|
||||
return (
|
||||
<section className="cta-band">
|
||||
<div className="container">
|
||||
|
||||
+11
-20
@@ -1,10 +1,12 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
import { useCMS, getCMSAmeba } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSAmeba } from '@/services/cms'
|
||||
|
||||
export default function Ameba() {
|
||||
const { data } = useCMS(getCMSAmeba, null)
|
||||
const { data: texts } = useCMSTexts()
|
||||
const content = data || {}
|
||||
const text = (key: string, fallback: string) => texts[key] || fallback
|
||||
return (
|
||||
<div>
|
||||
<div className="page-header">
|
||||
@@ -13,13 +15,10 @@ export default function Ameba() {
|
||||
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
首页
|
||||
</Link>{' '}
|
||||
/ 阿美巴
|
||||
</div>
|
||||
<h1>阿美巴</h1>
|
||||
<p>{content.brand_position || '以企业进化经营哲学为底色,坚持“利他、务实、陪跑”。不做纸上谈兵,只做可落地、可量化、有结果的实操辅导。'}</p>
|
||||
<div className="placeholder" style={{ marginTop: 16 }}>
|
||||
【占位|待业务确认】团队介绍、资质、里程碑、价值观等由知识母盘灌入。专家网络仅作辅助信任证据,不作为核心支柱。
|
||||
/ {text('ameba.header_title', '阿美巴')}
|
||||
</div>
|
||||
<h1>{text('ameba.header_title', '阿美巴')}</h1>
|
||||
<p>{content.brand_position || text('ameba.header_description', '以企业进化经营哲学为底色,坚持利他、务实、陪跑,帮助企业把方法落到经营现场。')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,18 +26,12 @@ export default function Ameba() {
|
||||
<div className="container">
|
||||
<div className="grid-2">
|
||||
<div className="card">
|
||||
<h3>经营哲学</h3>
|
||||
<h3>{text('ameba.philosophy_title', '经营哲学')}</h3>
|
||||
<p>{content.philosophy || '阿米巴哲学本土化实践,强调人人经营、数据透明、持续改善。'}</p>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位】哲学主张、原则、行为准则待确认。
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h3>服务理念</h3>
|
||||
<h3>{text('ameba.practice_title', '服务理念')}</h3>
|
||||
<p>{content.practice || '教练式陪跑:顾问入企,手把手带教,体系化交付 SOP 与工具包。'}</p>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位】陪跑模式、交付标准待确认。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,14 +39,12 @@ export default function Ameba() {
|
||||
|
||||
<section className="section section-muted">
|
||||
<div className="container">
|
||||
<h2 className="h2">我们是谁 · 占位</h2>
|
||||
<h2 className="h2">{text('ameba.about_title', '我们是谁')}</h2>
|
||||
<div className="card">
|
||||
<p className="desc">
|
||||
成都阿美巴企业管理咨询有限公司,专注体系化增长落地。核心团队十年以上实战经验,覆盖制造、服务、科技、商贸等领域。
|
||||
{text('ameba.about_text', '围绕企业进化与体系化增长,提供长期、务实、可复盘的企业管理咨询服务。')}
|
||||
</p>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
{content.mission || '【占位|待业务确认】公司简介、团队、资质证书、合作客户 LOGO 墙均待业务方提供定稿素材。'}
|
||||
</div>
|
||||
{content.mission && <div className="placeholder" style={{ marginTop: 12 }}>{content.mission}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -53,7 +53,6 @@ export default function CaseDetail() {
|
||||
<div className="container">
|
||||
<div className="card" style={{ borderLeft: '4px solid #163E70' }}>
|
||||
<h3>案例叙事 · 9段固定插槽</h3>
|
||||
<p style={{ fontSize: 12, color: '#64748b' }}>CMS 仅替换插槽文本,不改结构。未确认字段统一占位。</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20, display: 'grid', gap: 16 }}>
|
||||
@@ -67,8 +66,6 @@ export default function CaseDetail() {
|
||||
|
||||
<div className="card" style={{ marginTop: 16, background: '#f8fafc' }}>
|
||||
<h3>关联维度(预留)</h3>
|
||||
<p style={{ fontSize: 13, color: '#64748b' }}>关联价值:{c.fourValuesInvolved?.join(', ') || '【占位】待关联'}</p>
|
||||
<p style={{ fontSize: 13, color: '#64748b' }}>关联服务:{c.serviceIds?.join(', ') || '【占位】待关联'}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { CASES as FALLBACK_CASES } from '@/data/mock'
|
||||
import { useCMS, getCMSCases } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSCases } from '@/services/cms'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
|
||||
export default function Cases() {
|
||||
const { data: CASES } = useCMS(getCMSCases, FALLBACK_CASES)
|
||||
const { data: texts } = useCMSTexts()
|
||||
return (
|
||||
<div>
|
||||
<div className="page-header">
|
||||
@@ -16,7 +17,7 @@ export default function Cases() {
|
||||
/ 实践案例
|
||||
</div>
|
||||
<h1>实践案例</h1>
|
||||
<p>固定叙事插槽 9 段式结构,业务数据与布局解耦,未来由 CMS 接管。无虚构数据,未确认统一占位。</p>
|
||||
<p>{texts['cases.header_description'] || '通过原状、目标、差距、根因、介入、路径、行动、结果与迭代,看见企业进化过程。'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section">
|
||||
@@ -27,20 +28,10 @@ export default function Cases() {
|
||||
<div style={{ fontSize: 11, color: '#0f6b5b', fontWeight: 800, letterSpacing: '0.08em' }}>CASE · {c.status.toUpperCase()}</div>
|
||||
<h3 style={{ marginTop: 6 }}>{c.title}</h3>
|
||||
<p>{c.summary}</p>
|
||||
<div className="placeholder" style={{ marginTop: 10, fontSize: 12 }}>
|
||||
权重 {c.sortWeight} · 上下架 {c.status} · 预留排序
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#163E70', fontWeight: 700, marginTop: 10, display: 'inline-block' }}>查看完整叙事 →</span>
|
||||
<span style={{ fontSize: 12, color: '#163E70', fontWeight: 700, marginTop: 10, display: 'inline-block' }}>{texts['cases.detail_link'] || '查看完整叙事'} →</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="card" style={{ marginTop: 20, background: '#fff7e6', borderColor: '#f59e0b' }}>
|
||||
<strong style={{ color: '#92400e' }}>案例叙事模板插槽(固定 9 段)</strong>
|
||||
<p style={{ fontSize: 13, color: '#78350f', marginTop: 6 }}>
|
||||
企业原状 → 企业目标 → 现存差距 → 根因 → 阿美巴介入 → 进化路径 → 落地动作 → 阶段结果 → 持续迭代
|
||||
</p>
|
||||
<p style={{ fontSize: 12, color: '#92400e', marginTop: 4 }}>CMS 接管后,编辑仅替换插槽内容,不改页面结构。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<GlobalCTA />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useCMSTexts } from '@/services/cms'
|
||||
|
||||
export default function Contact() {
|
||||
const { data: texts } = useCMSTexts()
|
||||
const [form, setForm] = useState({ name: '', company: '', phone: '', need: '' })
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@@ -64,7 +66,7 @@ export default function Contact() {
|
||||
<input
|
||||
value={form.company}
|
||||
onChange={e => setForm({ ...form, company: e.target.value })}
|
||||
placeholder="【占位】选填"
|
||||
placeholder={texts['contact.company_placeholder'] || '选填'}
|
||||
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
|
||||
/>
|
||||
</label>
|
||||
@@ -83,7 +85,7 @@ export default function Contact() {
|
||||
<textarea
|
||||
value={form.need}
|
||||
onChange={e => setForm({ ...form, need: e.target.value })}
|
||||
placeholder="请简述你的企业现状与期望(占位)"
|
||||
placeholder={texts['contact.need_placeholder'] || '请简述你的企业现状与期望'}
|
||||
rows={3}
|
||||
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
|
||||
/>
|
||||
@@ -93,16 +95,12 @@ export default function Contact() {
|
||||
</button>
|
||||
{submitted && <div style={{ fontSize: 13, color: '#0f6b5b', background: '#ecfdf5', padding: 10, borderRadius: 8 }}>已提交,顾问将尽快联系。</div>}
|
||||
{error && <div style={{ fontSize: 13, color: '#be123c', background: '#fff1f2', padding: 10, borderRadius: 8 }}>{error}</div>}
|
||||
<p style={{ fontSize: 11, color: '#94a3b8' }}>提交即视为同意隐私与合规说明(占位)。</p>
|
||||
<p style={{ fontSize: 11, color: '#94a3b8' }}>{texts['contact.privacy_note'] || '提交即视为同意隐私与合规说明。'}</p>
|
||||
</form>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 16, alignContent: 'start' }}>
|
||||
<div className="card">
|
||||
<h3>其他通道</h3>
|
||||
<p style={{ fontSize: 13, color: '#475569' }}>电话、邮箱、地址等待业务确认后灌入。</p>
|
||||
<div className="placeholder" style={{ marginTop: 10 }}>
|
||||
【占位|待业务确认】商务电话 / 企业邮箱 / 办公地址 / 企业微信二维码
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h3>快速路径</h3>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useCMSTexts } from '@/services/cms'
|
||||
|
||||
type Q = { id:string, group_key:string, group_name:string, type:string, title:string, options:any[], sort_order:number }
|
||||
const STORAGE_KEY='diagnosis_progress'
|
||||
|
||||
export default function Diagnosis(){
|
||||
const { data: texts } = useCMSTexts()
|
||||
const [questions,setQuestions]=useState<Q[]>([])
|
||||
const [answers,setAnswers]=useState<Record<string,any>>({})
|
||||
const [idx,setIdx]=useState(0)
|
||||
@@ -52,7 +54,7 @@ export default function Diagnosis(){
|
||||
}
|
||||
|
||||
if(loading) return <div className="container section">加载问卷... <2s</div>
|
||||
if(!q) return <div className="container section">暂无题目,请联系管理员在CMS后台配置</div>
|
||||
if(!q) return <div className="container section">暂无可用题目,请稍后再试。</div>
|
||||
|
||||
const isAnswered = answers[q.id]!==undefined
|
||||
|
||||
@@ -94,7 +96,7 @@ export default function Diagnosis(){
|
||||
<button className="btn btn-ghost" onClick={prevQ} disabled={idx===0}>上一步</button>
|
||||
<button className="btn btn-primary" style={{flex:1}} onClick={nextQ} disabled={!isAnswered}>{idx===questions.length-1?'提交生成报告':'下一步'}</button>
|
||||
</div>
|
||||
<div style={{fontSize:11, color:'#94a3b8', marginTop:8}}>禁止开放题必填 · 采用区间选择 · 不收集敏感财务数据</div>
|
||||
<div style={{fontSize:11, color:'#94a3b8', marginTop:8}}>{texts['diagnosis.input_note'] || '采用区间选择,不收集敏感财务数据。'}</div>
|
||||
</div>
|
||||
<div style={{marginTop:12, display:'flex', gap:10}}>
|
||||
<Link to="/self-check" className="btn btn-ghost">返回轻量对照</Link>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { AMB_STATUS_META } from '@/data/mock'
|
||||
import { useCMS, getCMSAmb } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSAmb } from '@/services/cms'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
|
||||
export default function AmbDetail() {
|
||||
const { status } = useParams()
|
||||
const { data: ambMeta } = useCMS(getCMSAmb, AMB_STATUS_META)
|
||||
const { data: texts } = useCMSTexts()
|
||||
const meta = ambMeta[status as string]
|
||||
|
||||
if (!meta) {
|
||||
@@ -35,7 +36,7 @@ export default function AmbDetail() {
|
||||
</div>
|
||||
<h1>{meta.label}</h1>
|
||||
<p>{meta.description}</p>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}>不绑定规模 · 支持跨维度不同状态</p>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}>{texts['evolution.amb_detail_meta'] || '不绑定规模 · 支持跨维度不同状态'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section">
|
||||
@@ -47,9 +48,6 @@ export default function AmbDetail() {
|
||||
<li key={t}>{t}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位|待业务确认】该状态的完整特征、风险、进化建议、案例映射待业务确认后灌入。
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 10 }}>
|
||||
<Link to="/evolution" className="btn btn-ghost">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { FOUR_VALUES as FALLBACK_FOUR } from '@/data/mock'
|
||||
import { useCMS, getCMSFourValues } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSFourValues } from '@/services/cms'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
|
||||
export default function ValuesDetail() {
|
||||
const { data: FOUR_VALUES } = useCMS(getCMSFourValues, FALLBACK_FOUR)
|
||||
const { data: texts } = useCMSTexts()
|
||||
const { key } = useParams()
|
||||
const value = FOUR_VALUES.find(v => v.key === key)
|
||||
|
||||
@@ -35,7 +36,7 @@ export default function ValuesDetail() {
|
||||
</div>
|
||||
<h1>{value.name}</h1>
|
||||
<p>{value.summary}</p>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}>观察坐标 · 非售卖产品 · CMS预留字段已就绪</p>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}>{texts['evolution.value_detail_meta'] || '观察坐标 · 非售卖产品 · 用于理解企业真实位置'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section">
|
||||
@@ -49,11 +50,7 @@ export default function ValuesDetail() {
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="placeholder" style={{ marginTop: 16 }}>
|
||||
{value.description}
|
||||
<br />
|
||||
【占位|待业务确认】此价值坐标的完整阐述、评估要点、典型表现需由知识母盘灌入。
|
||||
</div>
|
||||
<div className="placeholder" style={{ marginTop: 16 }}>{value.description}</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 10 }}>
|
||||
<Link to="/evolution" className="btn btn-ghost">
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FOUR_VALUES as FALLBACK_FOUR, AMB_STATUS_META } from '@/data/mock'
|
||||
import { useCMS, getCMSFourValues, getCMSAmb } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSFourValues, getCMSAmb } from '@/services/cms'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
|
||||
export default function Evolution() {
|
||||
const { data: FOUR_VALUES } = useCMS(getCMSFourValues, FALLBACK_FOUR)
|
||||
const { data: AMB_META } = useCMS(getCMSAmb, AMB_STATUS_META)
|
||||
const { data: texts } = useCMSTexts()
|
||||
const text = (key: string, fallback: string) => texts[key] || fallback
|
||||
const [foldValues, setFoldValues] = useState(false)
|
||||
const [foldAmb, setFoldAmb] = useState(false)
|
||||
|
||||
@@ -20,12 +22,12 @@ export default function Evolution() {
|
||||
</Link>{' '}
|
||||
/ 企业进化
|
||||
</div>
|
||||
<h1>企业进化</h1>
|
||||
<h1>{text('evolution.header_title', '企业进化')}</h1>
|
||||
<p>
|
||||
内部顺序强制:<strong>企业进化思想 → 四大价值坐标 → A/M/B 状态坐标</strong>,顺序不可调换。
|
||||
{text('evolution.header_description', '内部顺序强制:企业进化思想 → 四大价值坐标 → A/M/B 状态坐标,顺序不可调换。')}
|
||||
</p>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}>
|
||||
V1.1 FINAL 冻结顺序,任何调换需登记台账延后二期。
|
||||
{text('evolution.header_note', '从进化思想到价值坐标,再到状态坐标,逐层看清企业位置。')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,22 +35,17 @@ export default function Evolution() {
|
||||
{/* 1. 企业进化思想 */}
|
||||
<section className="section" id="idea">
|
||||
<div className="container">
|
||||
<p className="eyebrow">01 — 企业进化思想</p>
|
||||
<h2 className="h2">进化是持续迭代,而非一次性变革</h2>
|
||||
<p className="desc">
|
||||
企业进化强调在不确定环境中,通过机制与人的双重迭代,实现组织的自适应与自进化。拒绝“规模决定论”,强调价值维度独立观察。
|
||||
</p>
|
||||
<p className="eyebrow">{text('evolution.idea_eyebrow', '01 — 企业进化思想')}</p>
|
||||
<h2 className="h2">{text('evolution.idea_title', '进化是持续迭代,而非一次性变革')}</h2>
|
||||
<p className="desc">{text('evolution.idea_description', '企业进化强调在不确定环境中,通过机制与人的双重迭代,实现组织的自适应与自进化。拒绝规模决定论,强调价值维度独立观察。')}</p>
|
||||
<div className="card" style={{ marginTop: 16 }}>
|
||||
<h3>核心主张(占位)</h3>
|
||||
<h3>{text('evolution.idea_core_title', '核心主张')}</h3>
|
||||
<ul style={{ paddingLeft: 18, fontSize: 14, color: '#475569', lineHeight: 2 }}>
|
||||
<li>经营即进化:战略、组织、机制、人的协同进化</li>
|
||||
<li>坐标而非标签:用价值与状态观察,而非给企业贴标签</li>
|
||||
<li>陪跑而非交付:共创机制,陪跑落地,复盘迭代</li>
|
||||
<li>【占位|待业务确认】更多思想阐述由知识母盘灌入</li>
|
||||
<li>{text('evolution.idea_bullet_1', '经营即进化:战略、组织、机制、人的协同进化')}</li>
|
||||
<li>{text('evolution.idea_bullet_2', '坐标而非标签:用价值与状态观察,而非给企业贴标签')}</li>
|
||||
<li>{text('evolution.idea_bullet_3', '陪跑而非交付:共创机制,陪跑落地,复盘迭代')}</li>
|
||||
<li>{text('evolution.idea_bullet_4', '持续复盘迭代,让每一次行动都沉淀为组织能力')}</li>
|
||||
</ul>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位|待业务确认】此段需业务方确认最终表述,一期仅保留结构。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -58,9 +55,9 @@ export default function Evolution() {
|
||||
<div className="container">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<div>
|
||||
<p className="eyebrow">02 — 四大价值坐标</p>
|
||||
<h2 className="h2">观察坐标,非售卖产品</h2>
|
||||
<p className="desc">四大价值定位为观察坐标,不能包装成售卖产品。用于看清企业在各维度的真实位置。</p>
|
||||
<p className="eyebrow">{text('evolution.values_eyebrow', '02 — 四大价值坐标')}</p>
|
||||
<h2 className="h2">{text('evolution.values_title', '观察坐标,非售卖产品')}</h2>
|
||||
<p className="desc">{text('evolution.values_description', '四大价值定位为观察坐标,用于看清企业在各维度的真实位置。')}</p>
|
||||
</div>
|
||||
<button className="foldable-trigger" onClick={() => setFoldValues(!foldValues)} style={{ width: 'auto' }}>
|
||||
{foldValues ? '收起' : '展开'}价值详情 <span>{foldValues ? '−' : '+'}</span>
|
||||
@@ -123,9 +120,9 @@ export default function Evolution() {
|
||||
<div className="container">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<div>
|
||||
<p className="eyebrow">03 — A/M/B 状态坐标</p>
|
||||
<h2 className="h2">A / M / B 不绑定规模</h2>
|
||||
<p className="desc">支持同一企业不同价值维度处于不同状态;拒绝“规模决定状态”的包装。</p>
|
||||
<p className="eyebrow">{text('evolution.amb_eyebrow', '03 — A/M/B 状态坐标')}</p>
|
||||
<h2 className="h2">{text('evolution.amb_title', 'A / M / B 不绑定规模')}</h2>
|
||||
<p className="desc">{text('evolution.amb_description', '支持同一企业不同价值维度处于不同状态,拒绝规模决定状态的包装。')}</p>
|
||||
</div>
|
||||
<button className="foldable-trigger" onClick={() => setFoldAmb(!foldAmb)} style={{ width: 'auto' }}>
|
||||
{foldAmb ? '收起' : '展开'}状态详情 <span>{foldAmb ? '−' : '+'}</span>
|
||||
@@ -150,13 +147,8 @@ export default function Evolution() {
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: 20, background: '#f8fafc' }}>
|
||||
<h3>跨维度状态示例(占位)</h3>
|
||||
<p style={{ fontSize: 13, color: '#64748b' }}>
|
||||
示例:某企业 · 战略价值 A / 组织价值 M / 经营价值 B / 人的价值 M。说明同一企业可在不同坐标处不同状态,需分维度干预。
|
||||
</p>
|
||||
<div className="placeholder" style={{ marginTop: 10 }}>
|
||||
【占位|待业务确认】真实示例需脱敏后灌入,一期仅示意结构。
|
||||
</div>
|
||||
<h3>{text('evolution.amb_example_title', '跨维度状态示例')}</h3>
|
||||
<p style={{ fontSize: 13, color: '#64748b' }}>{text('evolution.amb_example_description', '示例:某企业 · 战略价值 A / 组织价值 M / 经营价值 B / 人的价值 M。说明同一企业可在不同坐标处不同状态,需分维度干预。')}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
.hero-copy h1 { font-size: 40px; font-weight: 900; line-height: 1.15; margin: 8px 0 14px; }
|
||||
.hero-desc { color: rgba(255,255,255,0.82); font-size: 15px; line-height: 1.8; max-width: 560px; }
|
||||
.hero-actions { display: flex; gap: 12px; margin-top: 20px; flex-wrap: wrap; }
|
||||
.hero-actions .btn-ghost { color: #fff; border-color: rgba(255,255,255,0.55); background: rgba(255,255,255,0.08); }
|
||||
.hero-actions .btn-ghost:hover { color: #163E70; background: #fff; border-color: #fff; }
|
||||
.hero-trust { margin-top: 16px; font-size: 12px; color: rgba(255,255,255,0.6); border-top: 1px solid rgba(255,255,255,0.12); padding-top: 12px; }
|
||||
.visual-card { background: #fff; color: var(--text); border-radius: 16px; padding: 24px; box-shadow: 0 20px 60px rgba(0,0,0,0.2); }
|
||||
.visual-title { font-weight: 800; color: var(--primary); margin-bottom: 16px; }
|
||||
|
||||
+18
-19
@@ -1,7 +1,7 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
import { FOUR_VALUES as FALLBACK_FOUR, SERVICES as FALLBACK_SERVICES, CASES as FALLBACK_CASES, INSIGHTS as FALLBACK_INSIGHTS } from '@/data/mock'
|
||||
import { useCMS, getCMSFourValues, getCMSServices, getCMSCases, getCMSInsights } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSFourValues, getCMSServices, getCMSCases, getCMSInsights } from '@/services/cms'
|
||||
import './home.css'
|
||||
|
||||
export default function Home() {
|
||||
@@ -9,36 +9,35 @@ export default function Home() {
|
||||
const { data: services } = useCMS(getCMSServices, FALLBACK_SERVICES)
|
||||
const { data: cases } = useCMS(getCMSCases, FALLBACK_CASES)
|
||||
const { data: insights } = useCMS(getCMSInsights, FALLBACK_INSIGHTS)
|
||||
const { data: texts } = useCMSTexts()
|
||||
const text = (key: string, fallback: string) => texts[key] || fallback
|
||||
const heroTitle = text('home.title', '让企业进化\n被看见、被度量、被落地').split('\n')
|
||||
return (
|
||||
<div className="home">
|
||||
{/* 首屏:3秒识别品牌 */}
|
||||
<section className="hero-home">
|
||||
<div className="container hero-home-inner">
|
||||
<div className="hero-copy">
|
||||
<p className="eyebrow">阿美巴 · 企业进化陪跑</p>
|
||||
<p className="eyebrow">{text('home.eyebrow', '阿美巴 · 企业进化陪跑')}</p>
|
||||
<h1>
|
||||
让企业进化
|
||||
<br />
|
||||
被看见、被度量、被落地
|
||||
{heroTitle.map((line, i) => <span key={i}>{i > 0 && <br />}{line}</span>)}
|
||||
</h1>
|
||||
<p className="hero-desc">
|
||||
以“企业进化”为主线,以<strong>四大价值坐标</strong>与<strong> A/M/B 状态坐标</strong>为观察系,陪跑企业构建可复制、可持续的增长经营体系。
|
||||
<br />
|
||||
<span style={{ color: '#f59e0b' }}>一期为骨架与占位,不虚构业务数据。</span>
|
||||
{text('home.description', '以“企业进化”为主线,以四大价值坐标与 A/M/B 状态坐标为观察系,陪跑企业构建可复制、可持续的增长经营体系。')}
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<Link to="/diagnosis" className="btn btn-primary">
|
||||
开始企业进化诊断
|
||||
{text('home.primary_cta', '开始企业进化诊断')}
|
||||
</Link>
|
||||
<Link to="/contact" className="btn btn-secondary">
|
||||
咨询获取方案
|
||||
{text('home.secondary_cta', '咨询获取方案')}
|
||||
</Link>
|
||||
<Link to="/self-check" className="btn btn-ghost">
|
||||
轻量对照
|
||||
{text('home.light_cta', '轻量对照')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hero-trust">
|
||||
<span>30秒理解主张:进化思想 → 价值坐标 → 状态坐标 → 行动路径</span>
|
||||
<span>{text('home.trust', '30秒理解主张:进化思想 → 价值坐标 → 状态坐标 → 行动路径')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-visual">
|
||||
@@ -75,15 +74,15 @@ export default function Home() {
|
||||
<div className="container">
|
||||
<div className="section-heading">
|
||||
<p className="eyebrow">Ameba</p>
|
||||
<h2 className="h2">关于阿美巴</h2>
|
||||
<h2 className="h2">{text('home.about_title', '关于阿美巴')}</h2>
|
||||
<p className="desc">克制理性的企业服务风格,以方法与陪跑见长,非培训话术与AI噱头。</p>
|
||||
</div>
|
||||
<div className="card" style={{ display: 'flex', justifyContent: 'space-between', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<p className="desc" style={{ maxWidth: 640 }}>
|
||||
阿美巴以企业进化经营哲学为底色,结合中国中小企业实际,提供从诊断到落地陪跑的全链路服务。
|
||||
{text('home.about_text', '阿美巴以企业进化经营哲学为底色,结合中国中小企业实际,提供从诊断到落地陪跑的全链路服务。')}
|
||||
<br />
|
||||
<span className="placeholder" style={{ display: 'inline-block', marginTop: 8 }}>
|
||||
【占位|待业务确认】品牌故事、创始团队、使命愿景等由知识母盘灌入。
|
||||
{text('home.about_note', '我们持续沉淀品牌故事、使命愿景与真实实践。')}
|
||||
</span>
|
||||
</p>
|
||||
<Link to="/ameba" className="btn btn-secondary">
|
||||
@@ -152,7 +151,7 @@ export default function Home() {
|
||||
<div className="container">
|
||||
<div className="section-heading">
|
||||
<p className="eyebrow">Cases</p>
|
||||
<h2 className="h2">实践案例(占位)</h2>
|
||||
<h2 className="h2">{text('home.cases_title', '实践案例')}</h2>
|
||||
<p className="desc">固定叙事插槽:原状 → 目标 → 差距 → 根因 → 介入 → 路径 → 动作 → 结果 → 迭代。无虚构数据。</p>
|
||||
</div>
|
||||
<div className="grid-2">
|
||||
@@ -175,8 +174,8 @@ export default function Home() {
|
||||
<div className="container">
|
||||
<div className="section-heading">
|
||||
<p className="eyebrow">Services</p>
|
||||
<h2 className="h2">产品服务 · 下一步行动</h2>
|
||||
<p className="desc">禁止电商商城逻辑;重心是“下一步如何行动”。标准产品预留价格,定制展示“咨询/获取方案”。</p>
|
||||
<h2 className="h2">{text('home.services_title', '产品服务 · 下一步行动')}</h2>
|
||||
<p className="desc">{text('home.services_note', '围绕下一步行动提供标准产品与定制陪跑服务。')}</p>
|
||||
</div>
|
||||
<div className="grid-3">
|
||||
{services.map(s => (
|
||||
@@ -204,7 +203,7 @@ export default function Home() {
|
||||
<div className="container">
|
||||
<div className="section-heading">
|
||||
<p className="eyebrow">Insights</p>
|
||||
<h2 className="h2">认知观察</h2>
|
||||
<h2 className="h2">{text('home.insights_title', '认知观察')}</h2>
|
||||
<p className="desc">围绕进化思想、价值坐标、A/M/B 的观察与思考。</p>
|
||||
</div>
|
||||
<div className="grid-2">
|
||||
|
||||
@@ -54,9 +54,6 @@ export default function InsightDetail() {
|
||||
{b.type === 'placeholder' && <div className="placeholder">{b.placeholderNote}</div>}
|
||||
</div>
|
||||
))}
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位|待业务确认】正文由 CMS 接管,编辑仅替换 contentBlocks,不改布局。
|
||||
</div>
|
||||
</article>
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 10 }}>
|
||||
<Link to="/insights" className="btn btn-ghost">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { INSIGHTS as FALLBACK_INSIGHTS } from '@/data/mock'
|
||||
import { useCMS, getCMSInsights } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSInsights } from '@/services/cms'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
|
||||
export default function Insights() {
|
||||
const { data: INSIGHTS } = useCMS(getCMSInsights, FALLBACK_INSIGHTS)
|
||||
const { data: texts } = useCMSTexts()
|
||||
return (
|
||||
<div>
|
||||
<div className="page-header">
|
||||
@@ -13,13 +14,10 @@ export default function Insights() {
|
||||
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
首页
|
||||
</Link>{' '}
|
||||
/ 认知观察
|
||||
</div>
|
||||
<h1>认知观察</h1>
|
||||
<p>围绕企业进化、价值坐标、A/M/B 与方法论的持续观察与沉淀。</p>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位|待业务确认】文章由知识母盘定稿后灌入,一期仅保留结构与占位。
|
||||
/ {texts['insights.title'] || '认知观察'}
|
||||
</div>
|
||||
<h1>{texts['insights.title'] || '认知观察'}</h1>
|
||||
<p>{texts['insights.header_description'] || '围绕企业进化、价值坐标、A/M/B 与方法论的持续观察与沉淀。'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { METHODS as FALLBACK_METHODS } from '@/data/mock'
|
||||
import { useCMS, getCMSMethods } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSMethods } from '@/services/cms'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
|
||||
export default function MethodDetail() {
|
||||
const { data: METHODS } = useCMS(getCMSMethods, FALLBACK_METHODS)
|
||||
const { data: texts } = useCMSTexts()
|
||||
const { slug } = useParams()
|
||||
const item = METHODS.find(m => m.slug === slug)
|
||||
if (!item)
|
||||
@@ -26,7 +27,7 @@ export default function MethodDetail() {
|
||||
</Link>{' '}
|
||||
/{' '}
|
||||
<Link to="/methods" style={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
方法与能力
|
||||
{texts['methods.title'] || '方法与能力'}
|
||||
</Link>{' '}
|
||||
/ {item.title}
|
||||
</div>
|
||||
@@ -37,22 +38,19 @@ export default function MethodDetail() {
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<div className="card">
|
||||
<h3>关键步骤</h3>
|
||||
<h3>{texts['methods.detail_steps_title'] || '关键步骤'}</h3>
|
||||
<ol style={{ paddingLeft: 18, lineHeight: 2, fontSize: 14 }}>
|
||||
{item.steps?.map(s => (
|
||||
<li key={s}>{s}</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位|待业务确认】完整方法阐述、工具清单、交付物清单待灌入。
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 10 }}>
|
||||
<Link to="/methods" className="btn btn-ghost">
|
||||
← 返回
|
||||
</Link>
|
||||
<Link to="/contact" className="btn btn-primary">
|
||||
咨询获取方案
|
||||
{texts['global_cta.primary'] || '咨询获取方案'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { METHODS as FALLBACK_METHODS } from '@/data/mock'
|
||||
import { useCMS, getCMSMethods } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSMethods } from '@/services/cms'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
|
||||
export default function Methods() {
|
||||
const { data: METHODS } = useCMS(getCMSMethods, FALLBACK_METHODS)
|
||||
const { data: texts } = useCMSTexts()
|
||||
return (
|
||||
<div>
|
||||
<div className="page-header">
|
||||
@@ -15,11 +16,8 @@ export default function Methods() {
|
||||
</Link>{' '}
|
||||
/ 方法与能力
|
||||
</div>
|
||||
<h1>方法与能力</h1>
|
||||
<p>方法决定路径,能力决定落地。二者闭环,支撑企业进化。</p>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位|待业务确认】方法论体系、工具、交付标准待知识母盘灌入。
|
||||
</div>
|
||||
<h1>{texts['methods.title'] || '方法与能力'}</h1>
|
||||
<p>{texts['methods.header_description'] || '方法决定路径,能力决定落地。二者闭环,支撑企业进化。'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section">
|
||||
@@ -27,7 +25,7 @@ export default function Methods() {
|
||||
<div className="grid-2">
|
||||
{METHODS.map(m => (
|
||||
<Link key={m.id} to={`/methods/${m.slug}`} className="card" style={{ display: 'block' }}>
|
||||
<p className="eyebrow">{m.group === 'method' ? '方法' : '能力'}</p>
|
||||
<p className="eyebrow">{m.group === 'method' ? (texts['methods.method_label'] || '方法') : (texts['methods.capability_label'] || '能力')}</p>
|
||||
<h3>{m.title}</h3>
|
||||
<p>{m.summary}</p>
|
||||
{m.steps && (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
import { useCMS, useCMSTexts, getCMSSelfCheck } from '@/services/cms'
|
||||
|
||||
type CheckItem = { id: string; question: string; dimension: string }
|
||||
type CheckItem = { id: string; question: string; dimension: string; options?: string[] }
|
||||
|
||||
const CHECK_ITEMS: CheckItem[] = [
|
||||
{ id: 'q1', question: '战略方向在团队中是否清晰一致?', dimension: '战略价值' },
|
||||
@@ -12,6 +13,15 @@ const CHECK_ITEMS: CheckItem[] = [
|
||||
]
|
||||
|
||||
export default function SelfCheck() {
|
||||
const { data: cmsCheck } = useCMS(getCMSSelfCheck, null)
|
||||
const { data: texts } = useCMSTexts()
|
||||
const items = useMemo(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(cmsCheck?.questions || '[]')
|
||||
if (!Array.isArray(parsed) || !parsed.length) return CHECK_ITEMS
|
||||
return parsed.map((item: any, index: number) => ({ id: item.id || `q${index + 1}`, question: item.q || item.question || '', dimension: item.dimension || `价值维度 ${index + 1}`, options: item.options }))
|
||||
} catch { return CHECK_ITEMS }
|
||||
}, [cmsCheck])
|
||||
const [answers, setAnswers] = useState<Record<string, string>>({})
|
||||
const [showResult, setShowResult] = useState(false)
|
||||
|
||||
@@ -29,9 +39,9 @@ export default function SelfCheck() {
|
||||
</div>
|
||||
<h1>看看你的企业</h1>
|
||||
<p>
|
||||
轻量自我对照工具,帮助你快速定位在四大价值 × A/M/B 的大致位置。
|
||||
{cmsCheck?.guide || '轻量自我对照工具,帮助你快速定位在四大价值 × A/M/B 的大致位置。'}
|
||||
<br />
|
||||
<strong style={{ color: '#fde68a' }}>一期禁止长问卷、复杂评分、自动诊断报告。</strong>仅做轻量对照,深度诊断需线下陪跑。
|
||||
<strong style={{ color: '#fde68a' }}>{texts['selfcheck.header_note'] || '仅做轻量对照,深度诊断需线下陪跑。'}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,17 +49,19 @@ export default function SelfCheck() {
|
||||
<section className="section">
|
||||
<div className="container" style={{ maxWidth: 720 }}>
|
||||
<div className="card">
|
||||
<h3>轻量对照(4 题)</h3>
|
||||
<h3>轻量对照({items.length} 题)</h3>
|
||||
<p style={{ fontSize: 13, color: '#64748b' }}>每题选择最贴近现状的选项,结果仅作自我对照参考,不生成诊断报告。</p>
|
||||
<div style={{ marginTop: 16, display: 'grid', gap: 16 }}>
|
||||
{CHECK_ITEMS.map(item => (
|
||||
{items.map(item => (
|
||||
<div key={item.id} style={{ border: '1px solid #e2e8f0', borderRadius: 10, padding: 16 }}>
|
||||
<div style={{ fontSize: 12, color: '#0f6b5b', fontWeight: 800 }}>{item.dimension}</div>
|
||||
<div style={{ fontWeight: 700, marginTop: 4 }}>{item.question}</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
|
||||
{['A 较清晰/顺畅', 'M 部分卡点', 'B 待构建'].map(opt => (
|
||||
{(item.options || ['A 较清晰/顺畅', 'M 部分卡点', 'B 待构建']).map((opt: any) => {
|
||||
const label = typeof opt === 'string' ? opt : opt.label
|
||||
return (
|
||||
<label
|
||||
key={opt}
|
||||
key={label}
|
||||
style={{
|
||||
border: answers[item.id] === opt ? '1px solid #163E70' : '1px solid #e2e8f0',
|
||||
background: answers[item.id] === opt ? '#eff6ff' : '#fff',
|
||||
@@ -59,10 +71,11 @@ export default function SelfCheck() {
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input type="radio" name={item.id} value={opt} checked={answers[item.id] === opt} onChange={() => set(item.id, opt)} style={{ marginRight: 6 }} />
|
||||
{opt}
|
||||
<input type="radio" name={item.id} value={label} checked={answers[item.id] === label} onChange={() => set(item.id, label)} style={{ marginRight: 6 }} />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -71,21 +84,21 @@ export default function SelfCheck() {
|
||||
<button className="btn btn-primary" style={{ width: '100%', marginTop: 20 }} onClick={() => setShowResult(true)}>
|
||||
查看轻量对照结果
|
||||
</button>
|
||||
<p style={{ fontSize: 11, color: '#94a3b8', marginTop: 8, textAlign: 'center' }}>一期不做自动诊断报告,结果仅本地展示,不存储。</p>
|
||||
<p style={{ fontSize: 11, color: '#94a3b8', marginTop: 8, textAlign: 'center' }}>{texts['selfcheck.storage_note'] || '结果仅本地展示,不存储。'}</p>
|
||||
</div>
|
||||
|
||||
{showResult && (
|
||||
<div className="card" style={{ marginTop: 16, background: '#f8fafc' }}>
|
||||
<h3>对照结果(占位示意)</h3>
|
||||
<h3>{texts['selfcheck.result_title'] || '对照结果'}</h3>
|
||||
<p style={{ fontSize: 13, color: '#475569' }}>
|
||||
基于你的选择,系统提示关注维度:
|
||||
{cmsCheck?.logic_text || '基于你的选择,系统提示关注维度:'}
|
||||
<br />
|
||||
{Object.entries(answers)
|
||||
.map(([k, v]) => `${CHECK_ITEMS.find(i => i.id === k)?.dimension}:${v}`)
|
||||
.map(([k, v]) => `${items.find(i => i.id === k)?.dimension}:${v}`)
|
||||
.join(';') || '(请选择后再查看)'}
|
||||
</p>
|
||||
<div className="placeholder" style={{ marginTop: 10 }}>
|
||||
【占位|一期不做复杂评分】此处仅为轻量对照提示,不生成诊断报告。深度诊断请预约陪跑。
|
||||
{texts['selfcheck.result_note'] || cmsCheck?.next_guide || '此处仅为轻量对照提示,不生成诊断报告。深度诊断请预约陪跑。'}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, marginTop: 12, flexWrap: 'wrap' }}>
|
||||
<Link to="/contact" className="btn btn-primary">
|
||||
|
||||
@@ -56,9 +56,6 @@ export default function ServiceDetail() {
|
||||
深度定制服务不展示价格,请 <Link to="/contact">咨询/获取方案</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位|待业务确认】交付物、周期、适合对象、服务流程等待灌入。禁止购物车/库存逻辑。
|
||||
</div>
|
||||
<Link to="/contact" className="btn btn-primary" style={{ marginTop: 16, width: '100%' }}>
|
||||
{s.ctaText} →
|
||||
</Link>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { SERVICES as FALLBACK_SERVICES } from '@/data/mock'
|
||||
import { useCMS, getCMSServices } from '@/services/cms'
|
||||
import { useCMS, useCMSTexts, getCMSServices } from '@/services/cms'
|
||||
import GlobalCTA from '@/components/Layout/GlobalCTA'
|
||||
|
||||
export default function Services() {
|
||||
const { data: SERVICES } = useCMS(getCMSServices, FALLBACK_SERVICES)
|
||||
const { data: texts } = useCMSTexts()
|
||||
return (
|
||||
<div>
|
||||
<div className="page-header">
|
||||
@@ -13,15 +14,10 @@ export default function Services() {
|
||||
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
首页
|
||||
</Link>{' '}
|
||||
/ 产品服务
|
||||
</div>
|
||||
<h1>产品服务</h1>
|
||||
<p>
|
||||
禁止电商商城逻辑;重心是“下一步如何行动”,而非货品陈列。标准产品预留价格字段,深度定制展示「咨询/获取方案」。
|
||||
</p>
|
||||
<div className="placeholder" style={{ marginTop: 12 }}>
|
||||
【占位|待业务确认】服务定价、交付清单、周期等需业务确认后灌入。
|
||||
/ {texts['services.title'] || '产品服务'}
|
||||
</div>
|
||||
<h1>{texts['services.title'] || '产品服务'}</h1>
|
||||
<p>{texts['services.header_description'] || '围绕下一步行动展示标准产品与定制陪跑服务。'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section">
|
||||
@@ -30,7 +26,7 @@ export default function Services() {
|
||||
{SERVICES.map(s => (
|
||||
<div key={s.id} className="card" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ fontSize: 11, letterSpacing: '0.08em', color: s.serviceType === 'standard' ? '#163E70' : '#0f6b5b', fontWeight: 800 }}>
|
||||
{s.serviceType === 'standard' ? '标准产品' : '深度定制'}
|
||||
{s.serviceType === 'standard' ? (texts['services.standard_label'] || '标准产品') : (texts['services.custom_label'] || '深度定制')}
|
||||
</div>
|
||||
<h3 style={{ marginTop: 6 }}>{s.title}</h3>
|
||||
<p>{s.summary}</p>
|
||||
@@ -47,7 +43,7 @@ export default function Services() {
|
||||
¥{(s.price / 1000).toFixed(2)}k <span style={{ fontSize: 12, fontWeight: 400 }}>{s.priceUnit}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: 13, color: '#0f6b5b', fontWeight: 700 }}>咨询/获取方案(无价格)</div>
|
||||
<div style={{ fontSize: 13, color: '#0f6b5b', fontWeight: 700 }}>{texts['services.custom_cta'] || '咨询/获取方案(无价格)'}</div>
|
||||
)}
|
||||
{s.priceNote && <div style={{ fontSize: 11, color: '#94a3b8' }}>{s.priceNote}</div>}
|
||||
<Link to={`/services/${s.slug}`} className="btn btn-primary" style={{ width: '100%', marginTop: 10 }}>
|
||||
|
||||
+11
-1
@@ -19,7 +19,9 @@ export interface CMSPublicData {
|
||||
home_sections: any[]
|
||||
ameba: any
|
||||
evolution_thought: any
|
||||
evolution_process: any
|
||||
self_check: any
|
||||
texts: Record<string, string>
|
||||
}
|
||||
|
||||
let cache: CMSPublicData | null = null
|
||||
@@ -28,7 +30,7 @@ let pending: Promise<CMSPublicData | null> | null = null
|
||||
export async function fetchCMSPublic(): Promise<CMSPublicData | null> {
|
||||
if (cache) return cache
|
||||
if (pending) return pending
|
||||
pending = fetch(CMS_PUBLIC).then(async r=>{
|
||||
pending = fetch(`${CMS_PUBLIC}${CMS_PUBLIC.includes('?') ? '&' : '?'}t=${Date.now()}`, { cache: 'no-store' }).then(async r=>{
|
||||
if(!r.ok) throw new Error('cms fetch '+r.status)
|
||||
const j=await r.json()
|
||||
if(j.code!==0) throw new Error(j.msg)
|
||||
@@ -171,6 +173,10 @@ export async function getCMSMethods(): Promise<MethodCapability[]>{ const d=awai
|
||||
export async function getCMSAmb(): Promise<Record<string, AmbStatusMeta>>{ const d=await fetchCMSPublic(); return normalizeAmb(d?.amb_states as any) }
|
||||
export async function getCMSConfig(){ const d=await fetchCMSPublic(); return d?.config || null }
|
||||
export async function getCMSAmeba(){ const d=await fetchCMSPublic(); return d?.ameba || null }
|
||||
export async function getCMSTexts(){ const d=await fetchCMSPublic(); return d?.texts || {} }
|
||||
export async function getCMSEvolutionThought(){ const d=await fetchCMSPublic(); return d?.evolution_thought || null }
|
||||
export async function getCMSEvolutionProcess(){ const d=await fetchCMSPublic(); return d?.evolution_process || null }
|
||||
export async function getCMSSelfCheck(){ const d=await fetchCMSPublic(); return d?.self_check || null }
|
||||
|
||||
// Hook for React
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -184,3 +190,7 @@ export function useCMS<T>(loader: ()=>Promise<T>, fallback: T){
|
||||
},[])
|
||||
return {data, loading}
|
||||
}
|
||||
|
||||
export function useCMSTexts(){
|
||||
return useCMS(getCMSTexts, {} as Record<string, string>)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user