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
+64
View File
@@ -0,0 +1,64 @@
import { Link } from 'react-router-dom'
import GlobalCTA from '@/components/Layout/GlobalCTA'
import { useCMS, getCMSAmeba } from '@/services/cms'
export default function Ameba() {
const { data } = useCMS(getCMSAmeba, null)
const content = data || {}
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<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 }}>
</div>
</div>
</div>
<section className="section">
<div className="container">
<div className="grid-2">
<div className="card">
<h3></h3>
<p>{content.philosophy || '阿米巴哲学本土化实践,强调人人经营、数据透明、持续改善。'}</p>
<div className="placeholder" style={{ marginTop: 12 }}>
</div>
</div>
<div className="card">
<h3></h3>
<p>{content.practice || '教练式陪跑:顾问入企,手把手带教,体系化交付 SOP 与工具包。'}</p>
<div className="placeholder" style={{ marginTop: 12 }}>
</div>
</div>
</div>
</div>
</section>
<section className="section section-muted">
<div className="container">
<h2 className="h2"> · </h2>
<div className="card">
<p className="desc">
</p>
<div className="placeholder" style={{ marginTop: 12 }}>
{content.mission || '【占位|待业务确认】公司简介、团队、资质证书、合作客户 LOGO 墙均待业务方提供定稿素材。'}
</div>
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+91
View File
@@ -0,0 +1,91 @@
import { useParams, Link } from 'react-router-dom'
import { CASES as FALLBACK_CASES } from '@/data/mock'
import { useCMS, getCMSCases } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
const LABELS: Record<string, string> = {
originalState: '企业原状',
goal: '企业目标',
gap: '现存差距',
rootCause: '根因',
amebaIntervention: '阿美巴介入',
evolutionPath: '进化路径',
actions: '落地动作',
phasedResult: '阶段结果',
continuousIteration: '持续迭代',
}
export default function CaseDetail() {
const { data: CASES } = useCMS(getCMSCases, FALLBACK_CASES)
const { slug } = useParams()
const c = CASES.find(x => x.slug === slug)
if (!c)
return (
<div className="container section">
<div className="placeholder">{slug}</div>
<Link to="/cases" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/{' '}
<Link to="/cases" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/
</div>
<h1>{c.title}</h1>
<p>{c.summary}</p>
</div>
</div>
<section className="section">
<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 }}>
{Object.entries(c.narrative).map(([key, val]) => (
<div key={key} className="card">
<div style={{ fontSize: 11, letterSpacing: '0.08em', color: '#0f6b5b', fontWeight: 800 }}>{LABELS[key]}</div>
<p style={{ marginTop: 6, fontSize: 14, color: '#334155' }}>{val as string}</p>
</div>
))}
</div>
<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' }}>
<Link to="/cases" className="btn btn-ghost">
</Link>
<Link to="/contact" className="btn btn-primary">
</Link>
<Link to="/services" className="btn btn-ghost">
</Link>
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+49
View File
@@ -0,0 +1,49 @@
import { Link } from 'react-router-dom'
import { CASES as FALLBACK_CASES } from '@/data/mock'
import { useCMS, getCMSCases } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
export default function Cases() {
const { data: CASES } = useCMS(getCMSCases, FALLBACK_CASES)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/
</div>
<h1></h1>
<p> 9 CMS </p>
</div>
</div>
<section className="section">
<div className="container">
<div className="grid-2">
{CASES.map(c => (
<Link key={c.id} to={`/cases/${c.slug}`} className="card" style={{ display: 'block' }}>
<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>
</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 />
</div>
)
}
+132
View File
@@ -0,0 +1,132 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
export default function Contact() {
const [form, setForm] = useState({ name: '', company: '', phone: '', need: '' })
const [submitted, setSubmitted] = useState(false)
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
const submit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setSaving(true)
try {
const response = await fetch('/api/contact/leads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
const result = await response.json()
if (!response.ok || result.code !== 0) throw new Error(result.msg || '提交失败')
setSubmitted(true)
setForm({ name: '', company: '', phone: '', need: '' })
} catch (e) {
setError(e instanceof Error ? e.message : '提交失败,请稍后再试')
} finally {
setSaving(false)
}
}
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/
</div>
<h1></h1>
<p></p>
</div>
</div>
<section className="section">
<div className="container" style={{ maxWidth: 720 }}>
<div className="grid-2">
<div className="card">
<h3></h3>
<p style={{ fontSize: 13, color: '#64748b' }}>线</p>
<form onSubmit={submit} style={{ marginTop: 16, display: 'grid', gap: 12 }}>
<label style={{ fontSize: 13 }}>
<input
required
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
placeholder="请输入姓名"
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
/>
</label>
<label style={{ fontSize: 13 }}>
<input
value={form.company}
onChange={e => setForm({ ...form, company: e.target.value })}
placeholder="【占位】选填"
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
/>
</label>
<label style={{ fontSize: 13 }}>
<input
required
value={form.phone}
onChange={e => setForm({ ...form, phone: e.target.value })}
placeholder="请输入手机号"
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
/>
</label>
<label style={{ fontSize: 13 }}>
<textarea
value={form.need}
onChange={e => setForm({ ...form, need: e.target.value })}
placeholder="请简述你的企业现状与期望(占位)"
rows={3}
style={{ width: '100%', marginTop: 6, padding: '10px 12px', border: '1px solid #e2e8f0', borderRadius: 8 }}
/>
</label>
<button type="submit" className="btn btn-primary" style={{ width: '100%' }} disabled={saving}>
{saving ? '提交中…' : '提交咨询'}
</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>
</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>
<p style={{ fontSize: 13, color: '#475569' }}> </p>
<p style={{ fontSize: 13, color: '#475569' }}> /A/M/B </p>
<Link to="/self-check" className="btn btn-ghost" style={{ width: '100%', marginTop: 10 }}>
</Link>
</div>
</div>
</div>
</div>
</section>
<section className="cta-band">
<div className="container">
<div>
<strong></strong>
<p> CTA </p>
</div>
<Link to="/self-check" className="btn" style={{ background: '#fff', color: '#163E70' }}>
</Link>
</div>
</section>
</div>
)
}
+164
View File
@@ -0,0 +1,164 @@
import { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
function Radar({scores}:{scores:Record<string,number>}){
const keys=['value1','value2','value3','value4']
const labels:{[k:string]:string}={value1:'价值创造',value2:'价值传递',value3:'价值交付',value4:'价值支持'}
const vals = keys.map(k=> scores[k]||0)
const max=5
const size=200, cx=100, cy=100, r=80
const points = vals.map((v,i)=>{
const angle = -90 + (360/keys.length)*i
const rad = angle*Math.PI/180
const rr = (v/max)*r
return `${cx + rr*Math.cos(rad)},${cy + rr*Math.sin(rad)}`
}).join(' ')
const grid = [0.25,0.5,0.75,1].map(s=>{
const pts = keys.map((_,i)=>{
const angle=-90+(360/keys.length)*i
const rad=angle*Math.PI/180
const rr=r*s
return `${cx+rr*Math.cos(rad)},${cy+rr*Math.sin(rad)}`
}).join(' ')
return <polygon key={s} points={pts} fill="none" stroke="#e2e8f0" strokeWidth={1} />
})
return (
<div style={{textAlign:'center'}}>
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
{grid}
{keys.map((k,i)=>{
const angle=-90+(360/keys.length)*i
const rad=angle*Math.PI/180
return <line key={k} x1={cx} y1={cy} x2={cx+r*Math.cos(rad)} y2={cy+r*Math.sin(rad)} stroke="#e2e8f0" />
})}
<polygon points={points} fill="rgba(22,62,112,0.2)" stroke="#163E70" strokeWidth={2} />
{vals.map((v,i)=>{
const angle=-90+(360/keys.length)*i
const rad=angle*Math.PI/180
const rr=(v/max)*r
return <circle key={i} cx={cx+rr*Math.cos(rad)} cy={cy+rr*Math.sin(rad)} r={4} fill="#163E70" />
})}
</svg>
<div style={{display:'flex', justifyContent:'space-around', fontSize:11, color:'#475569'}}>
{keys.map(k=> <span key={k}>{labels[k]}:{scores[k]}</span>)}
</div>
</div>
)
}
export default function Report(){
const {token}=useParams()
const [data,setData]=useState<any>(null)
const [loading,setLoading]=useState(true)
const [err,setErr]=useState('')
const [lead,setLead]=useState({name:'',company:'',phone:''})
const [leadDone,setLeadDone]=useState(false)
const [showFull,setShowFull]=useState(false)
useEffect(()=>{
fetch(`/api/diagnosis/reports/${token}`).then(r=>r.json()).then(j=>{
if(j.code!==0) throw new Error(j.msg)
setData(j.data)
}).catch(e=> setErr(e.message)).finally(()=> setLoading(false))
},[token])
if(loading) return <div className="container section">... &lt;5s</div>
if(err) return <div className="container section"><div className="placeholder">{err}</div><Link to="/diagnosis" className="btn btn-primary"></Link></div>
if(!data) return null
const scores=data.scores, amb=data.amb, gap=data.gap, rc=data.root_cause, ps=data.path_suggestion
const shareUrl=`${location.origin}/diagnosis/share/${data.share_token}`
const submitLead=async()=>{
if(lead.phone && !/^1[3-9]\d{9}$/.test(lead.phone)) return alert('手机号格式错误')
const r=await fetch(`/api/diagnosis/reports/${token}/lead`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(lead)}).then(r=>r.json())
if(r.code===0){ setLeadDone(true); setShowFull(true) }
else alert(r.msg)
}
return (
<div>
<div className="page-header">
<div className="container">
<h1></h1>
<p></p>
<p style={{fontSize:12, color:'rgba(255,255,255,0.7)'}}>ID {data.token} · {new Date(data.expires_at).toLocaleDateString()}</p>
</div>
</div>
<section className="section">
<div className="container" style={{maxWidth:800}}>
<div className="card">
<h3>1. </h3>
<Radar scores={scores} />
<div style={{marginTop:12, display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, fontSize:13}}>
{Object.keys(amb).map(k=> <div key={k} style={{background:'#f8fafc', padding:8, borderRadius:6}}>{k}:{amb[k]}·{scores[k]}</div>)}
</div>
</div>
<div className="card">
<h3>2. </h3>
<table className="table"><thead><tr><th></th><th></th><th></th><th>(5)</th></tr></thead>
<tbody>{Object.keys(scores).map(k=> <tr key={k}><td>{k}</td><td>{scores[k]}</td><td>{amb[k]}</td><td>{gap[k]}</td></tr>)}</tbody>
</table>
</div>
<div className="card">
<h3>3. </h3>
<p style={{fontSize:14}}>{rc?.label}{rc?.reason}</p>
<p style={{fontSize:12,color:'#64748b'}}></p>
</div>
<div className="card">
<h3>4. </h3>
<p><strong>{ps?.stage}</strong> - {ps?.recommendation} {ps?.overall}</p>
<p style={{fontSize:12,color:'#475569'}}></p>
</div>
{!showFull && (
<div className="card" style={{border:'2px solid #163E70'}}>
<h3>5. </h3>
<p style={{fontSize:13, color:'#64748b'}}> + </p>
<div className="form-row" style={{marginTop:12}}>
<div className="field"><label></label><input value={lead.name} onChange={e=> setLead({...lead,name:e.target.value})} /></div>
<div className="field"><label></label><input value={lead.company} onChange={e=> setLead({...lead,company:e.target.value})} /></div>
</div>
<div className="field"><label></label><input value={lead.phone} onChange={e=> setLead({...lead,phone:e.target.value})} placeholder="11位手机号" /></div>
<button className="btn btn-primary" onClick={submitLead}></button>
<button className="btn btn-ghost" style={{marginLeft:8}} onClick={()=> setShowFull(true)}></button>
</div>
)}
{showFull && (
<>
<div className="card">
<h3>5. </h3>
<p style={{fontSize:13}}>{ps?.stage==='A态入门'?'推荐:A态入门方案 · 单点突破':'M态进阶'===ps?.stage?'推荐:M态进阶方案 · 机制完善':'推荐:B态全案 · 系统化构建'}</p>
<Link to="/services" className="btn btn-secondary"></Link>
</div>
<div className="card">
<h3>6. </h3>
<p style={{fontSize:13}}> / / 400线</p>
<div style={{display:'flex',gap:10}}>
<Link to="/contact" className="btn btn-primary"></Link>
<a href={`tel:400`} className="btn btn-ghost">400线</a>
</div>
</div>
</>
)}
<div className="card" style={{background:'#fff7e6'}}>
<h3>7. </h3>
<p style={{fontSize:12,color:'#92400e'}}></p>
</div>
<div style={{display:'flex', gap:10, flexWrap:'wrap', marginTop:16}}>
<a href={`/api/diagnosis/reports/${token}/pdf`} className="btn btn-primary">PDF</a>
<button className="btn btn-ghost" onClick={()=> navigator.clipboard.writeText(shareUrl).then(()=> alert('分享链接已复制:'+shareUrl))}></button>
<Link to="/diagnosis" className="btn btn-ghost"></Link>
</div>
<div style={{fontSize:11,color:'#94a3b8', marginTop:8}}>{shareUrl} · </div>
</div>
</section>
</div>
)
}
+24
View File
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
export default function Share(){
const {shareToken}=useParams()
const [data,setData]=useState<any>(null)
const [err,setErr]=useState('')
useEffect(()=>{
fetch(`/api/diagnosis/share/${shareToken}`).then(r=>r.json()).then(j=>{
if(j.code!==0) throw new Error(j.msg)
setData(j.data)
}).catch(e=> setErr(e.message))
},[shareToken])
if(err) return <div className="container section"><div className="placeholder">{err}</div></div>
if(!data) return <div className="container section">...</div>
return (
<div className="container section">
<h2></h2>
<p style={{fontSize:12,color:'#64748b'}}></p>
<div className="card"><pre style={{whiteSpace:'pre-wrap',fontSize:12}}>{JSON.stringify({scores:data.scores, amb:data.amb, gap:data.gap}, null, 2)}</pre></div>
<a href={`/api/diagnosis/reports/${data.token}/pdf`} className="btn btn-primary">PDF</a>
</div>
)
}
+107
View File
@@ -0,0 +1,107 @@
import { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
type Q = { id:string, group_key:string, group_name:string, type:string, title:string, options:any[], sort_order:number }
const STORAGE_KEY='diagnosis_progress'
export default function Diagnosis(){
const [questions,setQuestions]=useState<Q[]>([])
const [answers,setAnswers]=useState<Record<string,any>>({})
const [idx,setIdx]=useState(0)
const nav=useNavigate()
const [loading,setLoading]=useState(true)
useEffect(()=>{
fetch('/api/diagnosis/questions').then(r=>r.json()).then(j=>{
const list = (j.data||[]).sort((a:Q,b:Q)=> b.sort_order - a.sort_order)
setQuestions(list)
// 断点续填
const saved = localStorage.getItem(STORAGE_KEY)
if(saved){
try{ const p=JSON.parse(saved); if(p.answers) setAnswers(p.answers); if(typeof p.idx==='number') setIdx(p.idx)}catch{}
}
setLoading(false)
})
},[])
useEffect(()=>{
localStorage.setItem(STORAGE_KEY, JSON.stringify({answers, idx}))
},[answers, idx])
const q = questions[idx]
const progress = questions.length? Math.round(((idx)/questions.length)*100):0
const answeredCount = Object.keys(answers).length
const handleAnswer=(val:any)=>{
const next={...answers, [q.id]:val}
setAnswers(next)
}
const nextQ=()=>{
if(idx<questions.length-1) setIdx(idx+1)
else submit()
}
const prevQ=()=> setIdx(Math.max(0, idx-1))
const submit=async()=>{
// 创建会话并提交
const sess = await fetch('/api/diagnosis/sessions',{method:'POST'}).then(r=>r.json())
const token = sess.data.token
await fetch(`/api/diagnosis/sessions/${token}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({answers})})
const rep = await fetch(`/api/diagnosis/sessions/${token}/submit`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({answers})}).then(r=>r.json())
localStorage.removeItem(STORAGE_KEY)
nav(`/diagnosis/report/${rep.data.token}`)
}
if(loading) return <div className="container section">... &lt;2s</div>
if(!q) return <div className="container section">CMS后台配置</div>
const isAnswered = answers[q.id]!==undefined
return (
<div>
<div className="page-header">
<div className="container">
<h1></h1>
<p>15-20 · · · </p>
<div style={{marginTop:12, background:'rgba(255,255,255,0.2)', height:6, borderRadius:6}}>
<div style={{width:`${progress}%`, height:'100%', background:'#fff', borderRadius:6}} />
</div>
<div style={{fontSize:12, color:'rgba(255,255,255,0.8)', marginTop:4}}>{idx+1}/{questions.length} · {answeredCount} · </div>
</div>
</div>
<section className="section">
<div className="container" style={{maxWidth:720}}>
<div className="card">
<div style={{fontSize:12, color:'#0f6b5b', fontWeight:800}}>{q.group_name} · {q.type==='scale'?'量表 1-5':q.type==='multiple'?'多选':'单选'}</div>
<h3 style={{marginTop:6}}>{idx+1}. {q.title}</h3>
<div style={{marginTop:12, display:'grid', gap:8}}>
{q.options.map((opt:any)=> {
const selected = q.type==='multiple' ? (Array.isArray(answers[q.id]) && answers[q.id].includes(opt.label)) : answers[q.id]===opt.label
return (
<label key={opt.label} style={{border: selected?'2px solid #163E70':'1px solid #e2e8f0', background: selected?'#eff6ff':'#fff', padding:'10px 12px', borderRadius:8, cursor:'pointer', display:'flex', justifyContent:'space-between'}}>
<span><input type={q.type==='multiple'?'checkbox':'radio'} checked={!!selected} onChange={()=>{
if(q.type==='multiple'){
const arr=Array.isArray(answers[q.id])? [...answers[q.id]]:[]
if(selected) handleAnswer(arr.filter(v=>v!==opt.label))
else handleAnswer([...arr, opt.label])
} else handleAnswer(opt.label)
}} style={{marginRight:8}} />{opt.label}</span>
<span style={{fontSize:11,color:'#64748b'}}>{opt.score}</span>
</label>
)
})}
</div>
<div style={{display:'flex', gap:10, marginTop:16}}>
<button className="btn btn-ghost" onClick={prevQ} disabled={idx===0}></button>
<button className="btn btn-primary" style={{flex:1}} onClick={nextQ} disabled={!isAnswered}>{idx===questions.length-1?'提交生成报告':'下一步'}</button>
</div>
<div style={{fontSize:11, color:'#94a3b8', marginTop:8}}> · · </div>
</div>
<div style={{marginTop:12, display:'flex', gap:10}}>
<Link to="/self-check" className="btn btn-ghost"></Link>
<button className="btn btn-ghost" onClick={()=>{localStorage.removeItem(STORAGE_KEY); setAnswers({}); setIdx(0)}}></button>
</div>
</div>
</section>
</div>
)
}
+67
View File
@@ -0,0 +1,67 @@
import { useParams, Link } from 'react-router-dom'
import { AMB_STATUS_META } from '@/data/mock'
import { useCMS, 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 meta = ambMeta[status as string]
if (!meta) {
return (
<div className="container section">
<div className="placeholder">{status}</div>
<Link to="/evolution" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
)
}
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/{' '}
<Link to="/evolution" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/ {meta.label}
</div>
<h1>{meta.label}</h1>
<p>{meta.description}</p>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}> · </p>
</div>
</div>
<section className="section">
<div className="container">
<div className="card">
<h3></h3>
<ul style={{ paddingLeft: 18, lineHeight: 2, fontSize: 14 }}>
{meta.traits.map(t => (
<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">
</Link>
<Link to="/contact" className="btn btn-primary">
</Link>
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+71
View File
@@ -0,0 +1,71 @@
import { useParams, Link } from 'react-router-dom'
import { FOUR_VALUES as FALLBACK_FOUR } from '@/data/mock'
import { useCMS, getCMSFourValues } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
export default function ValuesDetail() {
const { data: FOUR_VALUES } = useCMS(getCMSFourValues, FALLBACK_FOUR)
const { key } = useParams()
const value = FOUR_VALUES.find(v => v.key === key)
if (!value) {
return (
<div className="container section">
<div className="placeholder">{key}</div>
<Link to="/evolution" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
)
}
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/{' '}
<Link to="/evolution" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/ {value.name}
</div>
<h1>{value.name}</h1>
<p>{value.summary}</p>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}> · · CMS预留字段已就绪</p>
</div>
</div>
<section className="section">
<div className="container">
<div className="card">
<h3></h3>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 8 }}>
{value.observationDimensions.map(d => (
<span key={d} style={{ background: '#f1f5f9', padding: '6px 12px', borderRadius: 999, fontSize: 12 }}>
{d}
</span>
))}
</div>
<div className="placeholder" style={{ marginTop: 16 }}>
{value.description}
<br />
</div>
</div>
<div style={{ marginTop: 16, display: 'flex', gap: 10 }}>
<Link to="/evolution" className="btn btn-ghost">
</Link>
<Link to="/self-check" className="btn btn-primary">
</Link>
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+176
View File
@@ -0,0 +1,176 @@
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 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 [foldValues, setFoldValues] = useState(false)
const [foldAmb, setFoldAmb] = useState(false)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/
</div>
<h1></h1>
<p>
<strong> A/M/B </strong>
</p>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}>
V1.1 FINAL
</p>
</div>
</div>
{/* 1. 企业进化思想 */}
<section className="section" id="idea">
<div className="container">
<p className="eyebrow">01 </p>
<h2 className="h2"></h2>
<p className="desc">
</p>
<div className="card" style={{ marginTop: 16 }}>
<h3></h3>
<ul style={{ paddingLeft: 18, fontSize: 14, color: '#475569', lineHeight: 2 }}>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
<div className="placeholder" style={{ marginTop: 12 }}>
</div>
</div>
</div>
</section>
{/* 2. 四大价值坐标 */}
<section className="section section-muted" id="values">
<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>
</div>
<button className="foldable-trigger" onClick={() => setFoldValues(!foldValues)} style={{ width: 'auto' }}>
{foldValues ? '收起' : '展开'} <span>{foldValues ? '' : '+'}</span>
</button>
</div>
<div className={`foldable-content ${foldValues ? 'expanded' : 'collapsed'}`} style={{ display: foldValues ? 'block' : undefined }}>
<div className="grid-2" style={{ marginTop: 20 }}>
{FOUR_VALUES.map(v => (
<div key={v.id} className="card">
<h3>{v.name}</h3>
<p style={{ fontSize: 13, color: '#64748b' }}>{v.summary}</p>
<div style={{ marginTop: 10, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{v.observationDimensions.map(d => (
<span key={d} style={{ fontSize: 11, background: '#f1f5f9', padding: '4px 8px', borderRadius: 999 }}>
{d}
</span>
))}
</div>
<div className="placeholder" style={{ marginTop: 12, fontSize: 12 }}>
{v.description}
</div>
<Link to={`/evolution/values/${v.key}`} className="btn btn-ghost" style={{ marginTop: 12, width: '100%' }}>
</Link>
</div>
))}
</div>
{/* 始终显示在桌面端 */}
<div className="grid-2" style={{ marginTop: 20 }} data-desktop-only>
{/* 兼容:桌面端始终展开,移动端由折叠控制 */}
</div>
</div>
{/* 桌面端强制展开 */}
<div className="grid-2" style={{ marginTop: 20 }}>
{FOUR_VALUES.map(v => (
<div key={v.id} className="card" data-desktop-card>
<h3>{v.name}</h3>
<p style={{ fontSize: 13, color: '#64748b' }}>{v.summary}</p>
<div style={{ marginTop: 10, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{v.observationDimensions.map(d => (
<span key={d} style={{ fontSize: 11, background: '#f1f5f9', padding: '4px 8px', borderRadius: 999 }}>
{d}
</span>
))}
</div>
<Link to={`/evolution/values/${v.key}`} className="btn btn-ghost" style={{ marginTop: 12, width: '100%' }}>
</Link>
</div>
))}
</div>
<style>{`@media(max-width:768px){ [data-desktop-card]{display:none} } @media(min-width:769px){ .foldable-trigger{display:none} .foldable-content{display:none!important} }`}</style>
</div>
</section>
{/* 3. A/M/B 状态坐标 */}
<section className="section" id="amb">
<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>
</div>
<button className="foldable-trigger" onClick={() => setFoldAmb(!foldAmb)} style={{ width: 'auto' }}>
{foldAmb ? '收起' : '展开'} <span>{foldAmb ? '' : '+'}</span>
</button>
</div>
<div className="grid-3" style={{ marginTop: 20 }}>
{Object.values(AMB_META).map(m => (
<div key={m.status} className="card">
<h3>{m.label}</h3>
<p style={{ fontSize: 13 }}>{m.description}</p>
<ul style={{ fontSize: 13, color: '#475569', paddingLeft: 18, marginTop: 8 }}>
{m.traits.map(t => (
<li key={t}>{t}</li>
))}
</ul>
<Link to={`/evolution/amb/${m.status}`} className="btn btn-secondary" style={{ marginTop: 12, width: '100%' }}>
{m.status}
</Link>
</div>
))}
</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>
</div>
<div style={{ marginTop: 16, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<Link to="/self-check" className="btn btn-primary">
</Link>
<Link to="/contact" className="btn btn-ghost">
</Link>
</div>
</div>
</section>
<GlobalCTA title="看清坐标,再谈行动" desc="先完成轻量对照,定位你的价值×状态矩阵,再选择陪跑路径。" />
</div>
)
}
+23
View File
@@ -0,0 +1,23 @@
.hero-home {
background: linear-gradient(135deg, #0f172a 0%, #163E70 55%, #0f6b5b 100%);
color: #fff;
padding: 64px 0 56px;
}
.hero-home-inner { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 32px; align-items: center; }
.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-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; }
.visual-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.visual-grid div { background: #f8fafc; border-radius: 10px; padding: 14px; }
.visual-grid strong { font-size: 13px; color: var(--primary); }
.visual-grid p { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
.section-heading { margin-bottom: 20px; }
@media (max-width: 900px) {
.hero-home-inner { grid-template-columns: 1fr; }
.hero-copy h1 { font-size: 28px; }
}
+229
View File
@@ -0,0 +1,229 @@
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 './home.css'
export default function Home() {
const { data: fourValues } = useCMS(getCMSFourValues, FALLBACK_FOUR)
const { data: services } = useCMS(getCMSServices, FALLBACK_SERVICES)
const { data: cases } = useCMS(getCMSCases, FALLBACK_CASES)
const { data: insights } = useCMS(getCMSInsights, FALLBACK_INSIGHTS)
return (
<div className="home">
{/* 首屏:3秒识别品牌 */}
<section className="hero-home">
<div className="container hero-home-inner">
<div className="hero-copy">
<p className="eyebrow"> · </p>
<h1>
<br />
</h1>
<p className="hero-desc">
线<strong></strong><strong> A/M/B </strong>
<br />
<span style={{ color: '#f59e0b' }}></span>
</p>
<div className="hero-actions">
<Link to="/diagnosis" className="btn btn-primary">
</Link>
<Link to="/contact" className="btn btn-secondary">
</Link>
<Link to="/self-check" className="btn btn-ghost">
</Link>
</div>
<div className="hero-trust">
<span>30 </span>
</div>
</div>
<div className="hero-visual">
<div className="visual-card">
<div className="visual-title"></div>
<div className="visual-grid">
<div>
<strong></strong>
<p></p>
</div>
<div>
<strong></strong>
<p> × A/M/B </p>
</div>
<div>
<strong></strong>
<p> </p>
</div>
<div>
<strong></strong>
<p> / </p>
</div>
</div>
<Link to="/evolution" className="btn btn-ghost" style={{ width: '100%', marginTop: 16 }}>
</Link>
</div>
</div>
</div>
</section>
{/* 摘要板块:阿美巴 */}
<section className="section section-muted">
<div className="container">
<div className="section-heading">
<p className="eyebrow">Ameba</p>
<h2 className="h2"></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 }}>
<br />
<span className="placeholder" style={{ display: 'inline-block', marginTop: 8 }}>
使
</span>
</p>
<Link to="/ameba" className="btn btn-secondary">
</Link>
</div>
</div>
</section>
{/* 摘要:企业进化(顺序预告) */}
<section className="section">
<div className="container">
<div className="section-heading">
<p className="eyebrow">Enterprise Evolution</p>
<h2 className="h2"> · </h2>
<p className="desc"> A/M/B </p>
</div>
<div className="grid-3">
{fourValues.slice(0, 3).map(v => (
<div key={v.id} className="card">
<h3>{v.name}</h3>
<p>{v.summary}</p>
<Link to={`/evolution/values/${v.key}`} className="btn btn-ghost" style={{ marginTop: 12, width: '100%' }}>
</Link>
</div>
))}
</div>
<div style={{ marginTop: 16, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<Link to="/evolution" className="btn btn-primary">
</Link>
<Link to="/evolution/amb/A" className="btn btn-ghost">
A/M/B
</Link>
</div>
</div>
</section>
{/* 摘要:方法与能力 */}
<section className="section section-muted">
<div className="container">
<div className="section-heading">
<p className="eyebrow">Methods & Capabilities</p>
<h2 className="h2"></h2>
<p className="desc"> × × </p>
</div>
<div className="grid-2">
<div className="card">
<h3></h3>
<p>访 + + </p>
</div>
<div className="card">
<h3></h3>
<p></p>
</div>
</div>
<Link to="/methods" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
</section>
{/* 摘要:实践案例 */}
<section className="section">
<div className="container">
<div className="section-heading">
<p className="eyebrow">Cases</p>
<h2 className="h2"></h2>
<p className="desc"> </p>
</div>
<div className="grid-2">
{cases.map(c => (
<Link key={c.id} to={`/cases/${c.slug}`} className="card" style={{ display: 'block' }}>
<h3>{c.title}</h3>
<p>{c.summary}</p>
<span style={{ fontSize: 12, color: '#0f6b5b', fontWeight: 700 }}> </span>
</Link>
))}
</div>
<Link to="/cases" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
</section>
{/* 摘要:产品服务 */}
<section className="section section-muted">
<div className="container">
<div className="section-heading">
<p className="eyebrow">Services</p>
<h2 className="h2"> · </h2>
<p className="desc">/</p>
</div>
<div className="grid-3">
{services.map(s => (
<div key={s.id} className="card">
<h3>{s.title}</h3>
<p>{s.summary}</p>
{s.serviceType === 'standard' && s.price ? (
<div style={{ marginTop: 10, fontWeight: 800, color: 'var(--primary)' }}>
¥{s.price} <span style={{ fontWeight: 400, fontSize: 12 }}>{s.priceUnit}</span>
</div>
) : (
<div style={{ marginTop: 10, fontSize: 12, color: '#0f6b5b', fontWeight: 700 }}>/</div>
)}
<Link to={`/services/${s.slug}`} className="btn btn-primary" style={{ marginTop: 12, width: '100%' }}>
{s.ctaText}
</Link>
</div>
))}
</div>
</div>
</section>
{/* 摘要:认知观察 */}
<section className="section">
<div className="container">
<div className="section-heading">
<p className="eyebrow">Insights</p>
<h2 className="h2"></h2>
<p className="desc">A/M/B </p>
</div>
<div className="grid-2">
{insights.map(i => (
<Link key={i.id} to={`/insights/${i.slug}`} className="card" style={{ display: 'block' }}>
<h3>{i.title}</h3>
<p>{i.summary}</p>
<span style={{ fontSize: 12, color: '#64748b' }}>{i.readingMinutes} · {i.author}</span>
</Link>
))}
</div>
<Link to="/insights" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
</section>
{/* 底部行动层 */}
<GlobalCTA />
</div>
)
}
+74
View File
@@ -0,0 +1,74 @@
import { useParams, Link } from 'react-router-dom'
import { INSIGHTS as FALLBACK_INSIGHTS } from '@/data/mock'
import { useCMS, getCMSInsights } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
export default function InsightDetail() {
const { data: INSIGHTS } = useCMS(getCMSInsights, FALLBACK_INSIGHTS)
const { slug } = useParams()
const item = INSIGHTS.find(i => i.slug === slug)
if (!item)
return (
<div className="container section">
<div className="placeholder">{slug}</div>
<Link to="/insights" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/{' '}
<Link to="/insights" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/
</div>
<h1>{item.title}</h1>
<p>{item.summary}</p>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 8 }}>
{item.readingMinutes} · {item.author} · {item.category}
</p>
</div>
</div>
<section className="section">
<div className="container">
<article className="card" style={{ maxWidth: 760 }}>
{item.contentBlocks.map(b => (
<div key={b.id} style={{ marginBottom: 16 }}>
{b.type === 'heading' && <h3>{b.text}</h3>}
{b.type === 'paragraph' && <p style={{ fontSize: 15, color: '#334155', lineHeight: 1.8 }}>{b.text}</p>}
{b.type === 'list' && (
<ul style={{ paddingLeft: 18, fontSize: 14, color: '#334155' }}>
{b.items?.map(x => (
<li key={x}>{x}</li>
))}
</ul>
)}
{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">
</Link>
<Link to="/contact" className="btn btn-primary">
</Link>
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+44
View File
@@ -0,0 +1,44 @@
import { Link } from 'react-router-dom'
import { INSIGHTS as FALLBACK_INSIGHTS } from '@/data/mock'
import { useCMS, getCMSInsights } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
export default function Insights() {
const { data: INSIGHTS } = useCMS(getCMSInsights, FALLBACK_INSIGHTS)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<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 }}>
稿
</div>
</div>
</div>
<section className="section">
<div className="container">
<div className="grid-2">
{INSIGHTS.map(i => (
<Link key={i.id} to={`/insights/${i.slug}`} className="card" style={{ display: 'block' }}>
<div style={{ fontSize: 11, color: '#0f6b5b', fontWeight: 800, letterSpacing: '0.08em' }}>{i.category.toUpperCase()}</div>
<h3 style={{ marginTop: 6 }}>{i.title}</h3>
<p>{i.summary}</p>
<div style={{ fontSize: 12, color: '#94a3b8', marginTop: 8 }}>
{i.readingMinutes} · {i.author} · {i.sortWeight}
</div>
</Link>
))}
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+63
View File
@@ -0,0 +1,63 @@
import { useParams, Link } from 'react-router-dom'
import { METHODS as FALLBACK_METHODS } from '@/data/mock'
import { useCMS, getCMSMethods } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
export default function MethodDetail() {
const { data: METHODS } = useCMS(getCMSMethods, FALLBACK_METHODS)
const { slug } = useParams()
const item = METHODS.find(m => m.slug === slug)
if (!item)
return (
<div className="container section">
<div className="placeholder">{slug}</div>
<Link to="/methods" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/{' '}
<Link to="/methods" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/ {item.title}
</div>
<h1>{item.title}</h1>
<p>{item.summary}</p>
</div>
</div>
<section className="section">
<div className="container">
<div className="card">
<h3></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">
</Link>
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+49
View File
@@ -0,0 +1,49 @@
import { Link } from 'react-router-dom'
import { METHODS as FALLBACK_METHODS } from '@/data/mock'
import { useCMS, getCMSMethods } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
export default function Methods() {
const { data: METHODS } = useCMS(getCMSMethods, FALLBACK_METHODS)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/
</div>
<h1></h1>
<p></p>
<div className="placeholder" style={{ marginTop: 12 }}>
</div>
</div>
</div>
<section className="section">
<div className="container">
<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>
<h3>{m.title}</h3>
<p>{m.summary}</p>
{m.steps && (
<ol style={{ fontSize: 13, color: '#475569', paddingLeft: 18, marginTop: 8 }}>
{m.steps.map(s => (
<li key={s}>{s}</li>
))}
</ol>
)}
<span style={{ fontSize: 12, color: '#0f6b5b', fontWeight: 700, marginTop: 10, display: 'inline-block' }}> </span>
</Link>
))}
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+18
View File
@@ -0,0 +1,18 @@
import { Link } from 'react-router-dom'
export default function NotFound() {
return (
<div className="container section" style={{ textAlign: 'center', maxWidth: 640 }}>
<h1 style={{ fontSize: 48, color: '#163E70' }}>404</h1>
<p style={{ color: '#64748b' }}> URL 301 </p>
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', marginTop: 16, flexWrap: 'wrap' }}>
<Link to="/" className="btn btn-primary">
</Link>
<Link to="/contact" className="btn btn-ghost">
</Link>
</div>
</div>
)
}
+110
View File
@@ -0,0 +1,110 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import GlobalCTA from '@/components/Layout/GlobalCTA'
type CheckItem = { id: string; question: string; dimension: string }
const CHECK_ITEMS: CheckItem[] = [
{ id: 'q1', question: '战略方向在团队中是否清晰一致?', dimension: '战略价值' },
{ id: 'q2', question: '组织协同是否顺畅、职责是否清晰?', dimension: '组织价值' },
{ id: 'q3', question: '经营机制(目标/复盘/数据)是否闭环?', dimension: '经营价值' },
{ id: 'q4', question: '团队是否具备经营意识与成长机制?', dimension: '人的价值' },
]
export default function SelfCheck() {
const [answers, setAnswers] = useState<Record<string, string>>({})
const [showResult, setShowResult] = useState(false)
const set = (id: string, val: string) => setAnswers(prev => ({ ...prev, [id]: val }))
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/
</div>
<h1></h1>
<p>
× A/M/B
<br />
<strong style={{ color: '#fde68a' }}></strong>线
</p>
</div>
</div>
<section className="section">
<div className="container" style={{ maxWidth: 720 }}>
<div className="card">
<h3>4 </h3>
<p style={{ fontSize: 13, color: '#64748b' }}></p>
<div style={{ marginTop: 16, display: 'grid', gap: 16 }}>
{CHECK_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 => (
<label
key={opt}
style={{
border: answers[item.id] === opt ? '1px solid #163E70' : '1px solid #e2e8f0',
background: answers[item.id] === opt ? '#eff6ff' : '#fff',
padding: '8px 12px',
borderRadius: 999,
fontSize: 13,
cursor: 'pointer',
}}
>
<input type="radio" name={item.id} value={opt} checked={answers[item.id] === opt} onChange={() => set(item.id, opt)} style={{ marginRight: 6 }} />
{opt}
</label>
))}
</div>
</div>
))}
</div>
<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>
</div>
{showResult && (
<div className="card" style={{ marginTop: 16, background: '#f8fafc' }}>
<h3></h3>
<p style={{ fontSize: 13, color: '#475569' }}>
<br />
{Object.entries(answers)
.map(([k, v]) => `${CHECK_ITEMS.find(i => i.id === k)?.dimension}${v}`)
.join('') || '(请选择后再查看)'}
</p>
<div className="placeholder" style={{ marginTop: 10 }}>
</div>
<div style={{ display: 'flex', gap: 10, marginTop: 12, flexWrap: 'wrap' }}>
<Link to="/contact" className="btn btn-primary">
</Link>
<Link to="/evolution" className="btn btn-ghost">
</Link>
</div>
</div>
)}
<div className="placeholder" style={{ marginTop: 16 }}>
/ A/M/B
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+79
View File
@@ -0,0 +1,79 @@
import { useParams, Link } from 'react-router-dom'
import { SERVICES as FALLBACK_SERVICES } from '@/data/mock'
import { useCMS, getCMSServices } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
export default function ServiceDetail() {
const { data: SERVICES } = useCMS(getCMSServices, FALLBACK_SERVICES)
const { slug } = useParams()
const s = SERVICES.find(x => x.slug === slug)
if (!s)
return (
<div className="container section">
<div className="placeholder">{slug}</div>
<Link to="/services" className="btn btn-secondary" style={{ marginTop: 16 }}>
</Link>
</div>
)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/{' '}
<Link to="/services" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/ {s.title}
</div>
<h1>{s.title}</h1>
<p>{s.summary}</p>
</div>
</div>
<section className="section">
<div className="container">
<div className="card">
<h3></h3>
<ul style={{ paddingLeft: 18, lineHeight: 2, fontSize: 14 }}>
{s.highlights?.map(h => (
<li key={h}>{h}</li>
))}
</ul>
{s.serviceType === 'standard' ? (
<div style={{ marginTop: 16, background: '#f1f5f9', padding: 16, borderRadius: 10 }}>
<strong></strong>
<div style={{ fontSize: 22, fontWeight: 900, color: 'var(--primary)' }}>
¥{s.price} <span style={{ fontSize: 13, fontWeight: 400 }}>{s.priceUnit}</span>
</div>
<p style={{ fontSize: 12, color: '#64748b' }}>{s.priceNote}</p>
</div>
) : (
<div style={{ marginTop: 16, background: '#ecfdf5', padding: 16, borderRadius: 10, color: '#065f46' }}>
<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>
</div>
<div style={{ marginTop: 16, display: 'flex', gap: 10 }}>
<Link to="/services" className="btn btn-ghost">
</Link>
<Link to="/cases" className="btn btn-ghost">
</Link>
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}
+65
View File
@@ -0,0 +1,65 @@
import { Link } from 'react-router-dom'
import { SERVICES as FALLBACK_SERVICES } from '@/data/mock'
import { useCMS, getCMSServices } from '@/services/cms'
import GlobalCTA from '@/components/Layout/GlobalCTA'
export default function Services() {
const { data: SERVICES } = useCMS(getCMSServices, FALLBACK_SERVICES)
return (
<div>
<div className="page-header">
<div className="container">
<div className="breadcrumb">
<Link to="/" style={{ color: 'rgba(255,255,255,0.8)' }}>
</Link>{' '}
/
</div>
<h1></h1>
<p>
/
</p>
<div className="placeholder" style={{ marginTop: 12 }}>
</div>
</div>
</div>
<section className="section">
<div className="container">
<div className="grid-3">
{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' ? '标准产品' : '深度定制'}
</div>
<h3 style={{ marginTop: 6 }}>{s.title}</h3>
<p>{s.summary}</p>
{s.highlights && (
<ul style={{ fontSize: 13, color: '#475569', paddingLeft: 18, marginTop: 8 }}>
{s.highlights.map(h => (
<li key={h}>{h}</li>
))}
</ul>
)}
<div style={{ marginTop: 'auto', paddingTop: 16 }}>
{s.serviceType === 'standard' && s.price ? (
<div style={{ fontWeight: 900, color: 'var(--primary)', fontSize: 18 }}>
¥{(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>
)}
{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 }}>
{s.ctaText}
</Link>
</div>
</div>
))}
</div>
</div>
</section>
<GlobalCTA />
</div>
)
}