chore: initialize deployable website and CMS

This commit is contained in:
Codex User
2026-09-08 11:57:17 +08:00
commit 3a99ae30c5
57 changed files with 11205 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex, nofollow" />
<title>CMS后台 - 阿美巴传统官网</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1750
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "ameba-cms-admin",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 15174",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0 --port 14174"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.2"
}
}
+310
View File
@@ -0,0 +1,310 @@
import { useState, useEffect } from 'react'
const API_BASE = '' // 同域,相对路径
const SIDEBAR = [
{ key: 'config', label: '1 全局配置' },
{ key: 'home', label: '2 首页模块' },
{ key: 'ameba', label: '3 阿美巴页面' },
{ key: 'evolution', label: '4 企业进化' },
{ key: 'methods', label: '5 方法与能力' },
{ key: 'services', label: '6 产品服务' },
{ key: 'cases', label: '7 案例管理' },
{ key: 'articles', label: '8 认知观察' },
{ key: 'selfcheck', label: '9 自我对照' },
{ key: 'diag_q', label: '10 诊断问卷' },
{ key: 'diag_config', label: '11 诊断配置' },
{ key: 'diag_reports', label: '12 诊断记录' },
{ key: 'diag_leads', label: '13 留资统计' },
{ key: 'contact_leads', label: '14 咨询线索' },
{ key: 'password', label: '⚙ 修改密码' },
]
function useAuth(){
const [token,setToken]=useState(localStorage.getItem('cms_token')||'')
const login=async(u:string,p:string)=>{
const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})})
const j=await r.json()
if(j.code!==0) throw new Error(j.msg)
localStorage.setItem('cms_token', j.data.token)
setToken(j.data.token)
}
const logout=()=>{localStorage.removeItem('cms_token'); setToken('')}
return {token,login,logout}
}
function api(path:string, token:string, opts:RequestInit={}){
const headers:any={...(opts.headers||{})}
if(token) headers['Authorization']='Bearer '+token
if(!(opts.body instanceof FormData)) headers['Content-Type']='application/json'
return fetch(path,{...opts, headers}).then(r=>r.json())
}
export default function App(){
const {token,login,logout}=useAuth()
const [user,setUser]=useState('')
const [pwd,setPwd]=useState('')
const [err,setErr]=useState('')
const [cur,setCur]=useState('config')
if(!token) return (
<div className="login">
<h3>CMS后台登录</h3>
<input placeholder="账号" value={user} onChange={e=>setUser(e.target.value)} />
<input placeholder="密码" type="password" value={pwd} onChange={e=>setPwd(e.target.value)} />
{err && <div style={{color:'#e11d48',fontSize:12}}>{err}</div>}
<button className="btn" style={{width:'100%',marginTop:12}} onClick={async()=>{
try{ await login(user,pwd); setErr('')}catch(e:any){setErr(e.message)}
}}></button>
</div>
)
return (
<div className="layout">
<div className="sidebar">
<h2> CMS后台<br/><small style={{color:'#64748b'}}>V1.1 FINAL </small></h2>
{SIDEBAR.map(s=> <div key={s.key} className={'nav-item'+(cur===s.key?' active':'')} onClick={()=>setCur(s.key)}>{s.label}</div>)}
<div style={{padding:20}}><button className="btn btn-ghost" onClick={logout}>退</button></div>
</div>
<div className="main">
<div className="topbar"><h2>{SIDEBAR.find(s=>s.key===cur)?.label}</h2><span style={{fontSize:12,color:'#94a3b8'}}> · </span></div>
{cur==='config' && <Config token={token} />}
{cur==='home' && <Home token={token} />}
{cur==='ameba' && <Ameba token={token} />}
{cur==='evolution' && <Evolution token={token} />}
{cur==='methods' && <Methods token={token} />}
{cur==='services' && <Services token={token} />}
{cur==='cases' && <Cases token={token} />}
{cur==='articles' && <Articles token={token} />}
{cur==='selfcheck' && <SelfCheck token={token} />}
{cur==='diag_q' && <DiagQ token={token} />}
{cur==='diag_config' && <DiagConfig token={token} />}
{cur==='diag_reports' && <DiagReports token={token} />}
{cur==='diag_leads' && <DiagLeads token={token} />}
{cur==='contact_leads' && <ContactLeads token={token} />}
{cur==='password' && <ChangePassword token={token} />}
</div>
</div>
)
}
function ContactLeads({token}:{token:string}){
const [list,setList]=useState<any[]>([])
useEffect(()=>{api('/api/cms/contact-leads',token).then(j=> setList(j.data||[]))},[])
return <div className="card"><h3>线200</h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{list.map(l=><tr key={l.id}><td>{l.name}</td><td>{l.company}</td><td>{l.phone}</td><td>{l.need}</td><td>{l.created_at}</td></tr>)}</tbody>
</table>
</div>
}
// —— 全局配置
function Config({token}:{token:string}){
const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/config',token).then(j=> setF(j.data||{}))},[])
const save=()=> api('/api/cms/config',token,{method:'PUT',body:JSON.stringify(f)}).then(j=> alert(j.msg||'保存成功'))
return <div className="card">
<h3></h3>
{['site_title','slogan','seo_title','seo_desc','seo_keywords','hotline','contact_email','contact_address','cta_text','cta_link'].map(k=>(
<div key={k} className="field"><label>{k}</label><input value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
))}
<button className="btn" onClick={save}></button>
</div>
}
function Home({token}:{token:string}){
const [list,setList]=useState<any[]>([])
useEffect(()=>{api('/api/cms/home-sections',token).then(j=> setList(j.data||[]))},[])
const save=()=> api('/api/cms/home-sections',token,{method:'PUT',body:JSON.stringify(list)}).then(()=> alert('保存成功'))
return <div className="card">
<h3>visible/sort_order</h3>
<table className="table"><thead><tr><th>key</th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{list.map((it,i)=>(
<tr key={it.id}><td>{it.key}</td><td><input value={it.title} onChange={e=>{const n=[...list]; n[i].title=e.target.value; setList(n)}}/></td><td><input value={it.subtitle} onChange={e=>{const n=[...list]; n[i].subtitle=e.target.value; setList(n)}}/></td><td><input type="checkbox" checked={!!it.visible} onChange={e=>{const n=[...list];n[i].visible=e.target.checked?1:0; setList(n)}}/></td><td><input type="number" value={it.sort_order} onChange={e=>{const n=[...list];n[i].sort_order=Number(e.target.value); setList(n)}} /></td></tr>
))}</tbody></table>
<button className="btn" style={{marginTop:12}} onClick={save}></button>
</div>
}
function Ameba({token}:{token:string}){
const [f,setF]=useState<any>({}); const [ex,setEx]=useState<any[]>([]); const [ne,setNe]=useState<any>({})
useEffect(()=>{api('/api/cms/ameba',token).then(j=> setF(j.data||{})); api('/api/cms/experts',token).then(j=> setEx(j.data||[]))},[])
return <>
<div className="card"><h3></h3>
{['brand_position','mission','philosophy','practice'].map(k=>(
<div key={k} className="field"><label>{k}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
))}
<button className="btn" onClick={()=> api('/api/cms/ameba',token,{method:'PUT',body:JSON.stringify(f)}).then(()=> alert('保存成功'))}></button>
</div>
<div className="card"><h3></h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{ex.map(e=> <tr key={e.id}><td>{e.name}</td><td>{e.title}</td><td>{e.sort_weight}</td><td>{e.status}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/experts/'+e.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table>
<div className="form-row" style={{marginTop:12}}>
<div className="field"><label></label><input value={ne.name||''} onChange={e=> setNe({...ne,name:e.target.value})}/></div>
<div className="field"><label></label><input value={ne.title||''} onChange={e=> setNe({...ne,title:e.target.value})}/></div>
</div>
<div className="field"><label></label><textarea value={ne.bio||''} onChange={e=> setNe({...ne,bio:e.target.value})}/></div>
<div className="form-row"><div className="field"><label></label><input type="number" value={ne.sort_weight||0} onChange={e=> setNe({...ne,sort_weight:Number(e.target.value)})}/></div><div className="field"><label></label><select value={ne.status||'published'} onChange={e=> setNe({...ne,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div></div>
<Upload token={token} onUrl={url=> setNe({...ne,avatar_url:url})} />
{ne.avatar_url && <img src={ne.avatar_url} style={{width:80,height:80,objectFit:'cover',borderRadius:8}}/>}
<button className="btn" onClick={()=> api('/api/cms/experts',token,{method:'POST',body:JSON.stringify(ne)}).then(()=> location.reload())}></button>
</div>
</>
}
function Evolution({token}:{token:string}){
const [thought,setThought]=useState(''); const [fvs,setFvs]=useState<any[]>([]); const [ambs,setAmbs]=useState<any[]>([])
useEffect(()=>{api('/api/cms/evolution/thought',token).then(j=> setThought(j.data?.content||'')); api('/api/cms/four-values',token).then(j=> setFvs(j.data||[])); api('/api/cms/amb-states',token).then(j=> setAmbs(j.data||[]))},[])
return <>
<div className="card"><h3></h3><div className="field"><textarea value={thought} onChange={e=> setThought(e.target.value)} /></div><button className="btn" onClick={()=> api('/api/cms/evolution/thought',token,{method:'PUT',body:JSON.stringify({content:thought})}).then(()=> alert('保存成功'))}></button></div>
<div className="card"><h3></h3><table className="table"><thead><tr><th>key</th><th></th><th></th><th></th><th></th></tr></thead><tbody>{fvs.map(v=> <tr key={v.id}><td>{v.key}</td><td>{v.name}</td><td>{v.sort_order}</td><td>{v.status}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/four-values/'+v.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>A/M/B </h3><table className="table"><thead><tr><th>key</th><th></th><th></th></tr></thead><tbody>{ambs.map(a=> <tr key={a.id}><td>{a.key}</td><td>{a.name}</td><td style={{fontSize:11}}>{a.fixed_note}</td></tr>)}</tbody></table><p style={{fontSize:11,color:'#92400e',background:'#fff7e6',padding:8,borderRadius:6}}>A/M/B不绑定企业规模的说明文案固定不可删</p></div>
</>
}
function Methods({token}:{token:string}){
const [proc,setProc]=useState<any>({}); const [sys,setSys]=useState<any[]>([])
useEffect(()=>{api('/api/cms/evolution-process',token).then(j=> setProc(j.data||{})); api('/api/cms/ten-systems',token).then(j=> setSys(j.data||[]))},[])
return <>
<div className="card"><h3></h3>{['steps','scenarios','description'].map(k=> <div key={k} className="field"><label>{k}</label><textarea value={proc[k]||''} onChange={e=> setProc({...proc,[k]:e.target.value})} /></div>)}<button className="btn" onClick={()=> api('/api/cms/evolution-process',token,{method:'PUT',body:JSON.stringify(proc)}).then(()=> alert('保存成功'))}></button></div>
<div className="card"><h3></h3><table className="table"><thead><tr><th></th><th></th><th></th></tr></thead><tbody>{sys.map(s=> <tr key={s.id}><td>{s.name}</td><td>{s.sort_order}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/ten-systems/'+s.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
</>
}
function Services({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/services',token).then(j=> setList(j.data||[]))},[])
return <>
<div className="card"><h3></h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(s=> <tr key={s.id}><td>{s.name}</td><td>{s.type}</td><td>{s.price||'-'}</td><td>{s.status}</td><td>{s.sort_weight}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/services/'+s.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>/</h3>
<div className="form-row"><div className="field"><label>*</label><input value={f.name||''} onChange={e=> setF({...f,name:e.target.value})}/></div><div className="field"><label>slug</label><input value={f.slug||''} onChange={e=> setF({...f,slug:e.target.value})}/></div></div>
<div className="form-row"><div className="field"><label></label><select value={f.type||'custom'} onChange={e=> setF({...f,type:e.target.value})}><option value="standard"></option><option value="custom">/</option></select></div><div className="field"><label></label><input value={f.price||''} onChange={e=> setF({...f,price:e.target.value})} placeholder="仅标准化需要"/></div></div>
<div className="field"><label></label><textarea value={f.intro||''} onChange={e=> setF({...f,intro:e.target.value})}/></div>
<div className="form-row"><div className="field"><label></label><select value={f.status||'draft'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.sort_weight||0} onChange={e=> setF({...f,sort_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={()=> api('/api/cms/services',token,{method:'POST',body:JSON.stringify(f)}).then(j=> {if(j.code===0) location.reload(); else alert(j.msg)})}></button>
</div>
</>
}
function Cases({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/cases',token).then(j=> setList(j.data||[]))},[])
return <>
<div className="card"><h3>9</h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(c=> <tr key={c.id}><td>{c.title}</td><td>{c.industry}</td><td>{c.status}</td><td>{c.sort_weight}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/cases/'+c.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3>9</h3>
<div className="field"><label>*</label><input value={f.title||''} onChange={e=> setF({...f,title:e.target.value})}/></div>
{['original_state','goal','gap','root_cause','ameba_intervention','evolution_path','actions','phased_result','continuous_iteration'].map(k=>(
<div key={k} className="field"><label>{k}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>
))}
<div className="form-row"><div className="field"><label></label><select value={f.status||'draft'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.sort_weight||0} onChange={e=> setF({...f,sort_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={()=> api('/api/cms/cases',token,{method:'POST',body:JSON.stringify(f)}).then(j=> {if(j.code===0) location.reload(); else alert(j.msg)})}></button>
</div>
</>
}
function Articles({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/articles',token).then(j=> setList(j.data||[]))},[])
return <>
<div className="card"><h3></h3><table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody>{list.map(a=> <tr key={a.id}><td>{a.title}</td><td>{a.category}</td><td>{a.recommend_weight}</td><td>{a.status}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/articles/'+a.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody></table></div>
<div className="card"><h3></h3>
<div className="field"><label>*</label><input value={f.title||''} onChange={e=> setF({...f,title:e.target.value})}/></div>
<div className="field"><label> HTML</label><textarea value={f.content_html||''} onChange={e=> setF({...f,content_html:e.target.value})} /></div>
<Upload token={token} onUrl={url=> setF({...f,cover_url:url})} />
{f.cover_url && <img src={f.cover_url} style={{width:120,height:80,objectFit:'cover'}}/>}
<div className="form-row"><div className="field"><label></label><select value={f.status||'draft'} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div><div className="field"><label></label><input type="number" value={f.recommend_weight||0} onChange={e=> setF({...f,recommend_weight:Number(e.target.value)})}/></div></div>
<button className="btn" onClick={()=> api('/api/cms/articles',token,{method:'POST',body:JSON.stringify(f)}).then(j=> {if(j.code===0) location.reload(); else alert(j.msg)})}></button>
</div>
</>
}
function SelfCheck({token}:{token:string}){
const [f,setF]=useState<any>({})
useEffect(()=>{api('/api/cms/self-check',token).then(j=> setF(j.data||{}))},[])
return <div className="card"><h3></h3>
{['guide','questions','logic_text','next_guide'].map(k=> <div key={k} className="field"><label>{k}</label><textarea value={f[k]||''} onChange={e=> setF({...f,[k]:e.target.value})} /></div>)}
<p style={{fontSize:11,color:'#92400e'}}></p>
<button className="btn" onClick={()=> api('/api/cms/self-check',token,{method:'PUT',body:JSON.stringify(f)}).then(()=> alert('保存成功'))}></button>
</div>
}
function Upload({token,onUrl}:{token:string,onUrl:(u:string)=>void}){
const up=(e:any)=>{
const file=e.target.files[0]; if(!file) return;
const fd=new FormData(); fd.append('file',file);
fetch('/api/cms/upload',{method:'POST',headers:{Authorization:'Bearer '+token},body:fd}).then(r=>r.json()).then(j=>{ if(j.code===0) onUrl(j.data.url); else alert(j.msg)})
}
return <div className="field"><label></label><input type="file" onChange={up} accept="image/*" /></div>
}
function DiagQ({token}:{token:string}){
const [list,setList]=useState<any[]>([]); const [f,setF]=useState<any>({group_key:'value1',type:'single',sort_order:0,status:'published'})
useEffect(()=>{api('/api/cms/diagnosis/questions',token).then(j=> setList(j.data||[]))},[])
return <>
<div className="card"><h3></h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{list.map(q=> <tr key={q.id}><td>{q.group_name}</td><td style={{maxWidth:200,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{q.title}</td><td>{q.type}</td><td>{q.sort_order}</td><td>{q.status}</td><td><button onClick={()=>{if(confirm('删除?')) api('/api/cms/diagnosis/questions/'+q.id,token,{method:'DELETE'}).then(()=> location.reload())}}></button></td></tr>)}</tbody>
</table>
</div>
<div className="card"><h3></h3>
<div className="form-row"><div className="field"><label>key</label><select value={f.group_key} onChange={e=> setF({...f,group_key:e.target.value})}><option value="value1">value1 </option><option value="value2">value2 </option><option value="value3">value3 </option><option value="value4">value4 </option></select></div><div className="field"><label></label><select value={f.type} onChange={e=> setF({...f,type:e.target.value})}><option value="single"></option><option value="multiple"></option><option value="scale">1-5</option></select></div></div>
<div className="field"><label>*</label><input value={f.title||''} onChange={e=> setF({...f,title:e.target.value})} /></div>
<div className="field"><label>JSON数组 [{label,score}]</label><textarea value={f.options_text||''} onChange={e=> setF({...f,options_text:e.target.value})} placeholder='[{"label":"非常清晰","score":5}]' /></div>
<div className="form-row"><div className="field"><label></label><input type="number" value={f.sort_order||0} onChange={e=> setF({...f,sort_order:Number(e.target.value)})}/></div><div className="field"><label></label><select value={f.status} onChange={e=> setF({...f,status:e.target.value})}><option value="published">published</option><option value="draft">draft</option></select></div></div>
<button className="btn" onClick={()=>{
let opts=[]
try{ opts=f.options_text? JSON.parse(f.options_text):[]}catch{ return alert('选项JSON格式错误')}
api('/api/cms/diagnosis/questions',token,{method:'POST',body:JSON.stringify({...f, options:opts})}).then(j=>{ if(j.code===0) location.reload(); else alert(j.msg)})
}}></button>
</div>
</>
}
function DiagConfig({token}:{token:string}){
const [cfg,setCfg]=useState<any>({weights:{},thresholds:{},templates:{},share_expire_days:30})
useEffect(()=>{api('/api/cms/diagnosis/config',token).then(j=> setCfg(j.data||cfg))},[])
return <div className="card"><h3></h3>
<div className="field"><label>JSON</label><textarea value={JSON.stringify(cfg.weights||{},null,2)} onChange={e=>{try{setCfg({...cfg,weights:JSON.parse(e.target.value)})}catch{}}} /></div>
<div className="field"><label>B_max, M_max</label><textarea value={JSON.stringify(cfg.thresholds||{},null,2)} onChange={e=>{try{setCfg({...cfg,thresholds:JSON.parse(e.target.value)})}catch{}}} /></div>
<div className="field"><label></label><textarea value={JSON.stringify(cfg.templates||{},null,2)} onChange={e=>{try{setCfg({...cfg,templates:JSON.parse(e.target.value)})}catch{}}} /></div>
<div className="field"><label></label><input type="number" value={cfg.share_expire_days||30} onChange={e=> setCfg({...cfg,share_expire_days:Number(e.target.value)})}/></div>
<button className="btn" onClick={()=> api('/api/cms/diagnosis/config',token,{method:'PUT',body:JSON.stringify(cfg)}).then(()=> alert('保存成功'))}></button>
</div>
}
function DiagReports({token}:{token:string}){
const [list,setList]=useState<any[]>([])
useEffect(()=>{api('/api/cms/diagnosis/reports',token).then(j=> setList(j.data||[]))},[])
return <div className="card"><h3>100</h3>
<table className="table"><thead><tr><th>token</th><th></th><th></th><th></th></tr></thead>
<tbody>{list.map(r=> <tr key={r.id}><td style={{fontSize:10}}>{r.token.slice(0,8)}...</td><td>{r.created_at}</td><td>{r.expires_at}</td><td>{r.share_token.slice(0,8)}...</td></tr>)}</tbody>
</table>
</div>
}
function DiagLeads({token}:{token:string}){
const [leads,setLeads]=useState<any[]>([]); const [stats,setStats]=useState<any>({})
useEffect(()=>{api('/api/cms/diagnosis/leads',token).then(j=> setLeads(j.data||[])); api('/api/cms/diagnosis/stats',token).then(j=> setStats(j.data||{}))},[])
return <>
<div className="card"><h3></h3><p>{stats.totalSessions} · {stats.totalReports} · {stats.totalLeads}</p></div>
<div className="card"><h3></h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{leads.map(l=> <tr key={l.id}><td>{l.name}</td><td>{l.company}</td><td>{l.phone}</td><td>{l.created_at}</td></tr>)}</tbody>
</table>
<button className="btn btn-ghost" onClick={()=>{
const rows=[['姓名','企业','手机','时间'], ...leads.map(l=>[l.name,l.company,l.phone,l.created_at])]
const csv=rows.map(r=> r.map(v=>`"${v||''}"`).join(',')).join('\n')
const blob=new Blob([csv],{type:'text/csv'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='leads.csv'; a.click()
}}>CSV</button>
</div>
</>
}
function ChangePassword({token}:{token:string}){
const [oldPwd,setOldPwd]=useState('')
const [newPwd,setNewPwd]=useState('')
const [confirmPwd,setConfirmPwd]=useState('')
const [msg,setMsg]=useState('')
const [err,setErr]=useState('')
const submit=async()=>{
setMsg(''); setErr('')
if(!oldPwd || !newPwd || !confirmPwd){ setErr('请填写所有字段'); return }
if(newPwd.length<6){ setErr('新密码长度至少6位'); return }
if(newPwd!==confirmPwd){ setErr('两次密码不一致'); return }
const r=await api('/api/auth/change-password',token,{method:'POST',body:JSON.stringify({oldPassword:oldPwd,newPassword:newPwd})})
if(r.code===0){ setMsg('密码修改成功!'); setOldPwd(''); setNewPwd(''); setConfirmPwd('') }
else setErr(r.msg||'修改失败')
}
return <div className="card">
<h3>修改密码</h3>
<div className="field"><label>旧密码</label><input type="password" value={oldPwd} onChange={e=>setOldPwd(e.target.value)} /></div>
<div className="field"><label>新密码至少6位</label><input type="password" value={newPwd} onChange={e=>setNewPwd(e.target.value)} /></div>
<div className="field"><label>确认新密码</label><input type="password" value={confirmPwd} onChange={e=>setConfirmPwd(e.target.value)} /></div>
{msg && <div style={{color:'#059669',fontSize:13,margin:'8px 0'}}>{msg}</div>}
{err && <div style={{color:'#e11d48',fontSize:13,margin:'8px 0'}}>{err}</div>}
<button className="btn" onClick={submit}>修改密码</button>
</div>
}
+5
View File
@@ -0,0 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './style.css'
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
+25
View File
@@ -0,0 +1,25 @@
*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#f5f7fa;color:#1e293b}
a{color:#163E70;text-decoration:none}
.login{max-width:360px;margin:80px auto;background:#fff;padding:32px;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.08)}
.login input{width:100%;padding:10px 12px;margin:8px 0;border:1px solid #e2e8f0;border-radius:8px}
.btn{padding:8px 16px;border-radius:8px;border:1px solid #163E70;background:#163E70;color:#fff;cursor:pointer}
.btn-ghost{background:#fff;color:#163E70}
.layout{display:flex;min-height:100vh}
.sidebar{width:220px;background:#0f172a;color:#cbd5e1;padding:20px 0;flex-shrink:0}
.sidebar h2{padding:0 20px 16px;font-size:14px;color:#fff;border-bottom:1px solid #1e293b;margin-bottom:12px}
.nav-item{padding:10px 20px;cursor:pointer;display:block;font-size:13px}
.nav-item.active,.nav-item:hover{background:#1e293b;color:#fff}
.main{flex:1;padding:24px;overflow:auto}
.card{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:20px;margin-bottom:16px}
.card h3{font-size:15px;margin-bottom:12px;color:#0f172a}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.form-row.single{grid-template-columns:1fr}
.field{margin-bottom:12px}
.field label{font-size:12px;color:#64748b;display:block;margin-bottom:4px}
.field input,.field textarea,.field select{width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px}
.field textarea{min-height:80px}
.table{width:100%;border-collapse:collapse;font-size:13px}
.table th,.table td{border:1px solid #e2e8f0;padding:8px;text-align:left}
.table th{background:#f8fafc}
.tag{font-size:11px;padding:2px 6px;border-radius:4px;background:#e0f2fe;color:#0369a1}
.topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src"]
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: '/admin/',
server: { host: '0.0.0.0', port: 15174 },
build: { outDir: 'dist' }
})