@@ -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 >
}