@@ -3,6 +3,8 @@ const state = {
data : null ,
opFilter : "all" ,
chart : null ,
chart2 : null ,
productPlatform : "all" ,
} ;
const money = ( value ) => ` ${ Number ( value || 0 ) . toLocaleString ( "zh-CN" ) } 万 ` ;
@@ -59,9 +61,8 @@ function switchTab(tab) {
function render ( ) {
if ( ! state . data ) return ;
renderHome ( ) ;
renderSale s ( ) ;
renderProject s ( ) ;
renderProposals ( ) ;
renderOperations ( ) ;
renderProducts ( ) ;
renderFinance ( ) ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
@@ -96,15 +97,26 @@ function renderHome() {
<div class="grid gap-5">
<div class="grid grid-cols-4 gap-3">
${ [
[ "P0 客户数" , m . p0 _customers , "sale s" ] ,
[ "跟进中销售机会" , m . active _sales , "sale s" ] ,
[ "已签约执行项目" , m . execution _projects , "operation s" ] ,
[ "有风险项目" , m . risk _projects , "operation s" ] ,
[ "P0 客户数" , m . p0 _customers , "project s" ] ,
[ "跟进中销售机会" , m . active _sales , "project s" ] ,
[ "已签约执行项目" , m . execution _projects , "project s" ] ,
[ "有风险项目" , m . risk _projects , "project s" ] ,
[ "本月收入" , money ( m . monthly _revenue ) , "finance" ] ,
[ "本月净利" , money ( m . monthly _net _profit ) , "finance" ] ,
[ "即将上线版本" , m . upcoming _products , "products" ] ,
[ "已签约未执行" , money ( m . signed _not _executed ) , "finance" ] ,
] . map ( ( [ label , value , tab ] ) => ` <button class="metric-card" onclick="switchTab(' ${ tab } ')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="gauge"></i> ${ label } </span><strong class="mt-2 block text-2xl"> ${ value } </strong></button> ` ) . join ( "" ) }
</div>
<div class="grid grid-cols-4 gap-3">
${ [
[ "已签约合同总额" , money ( m . signed _amount ) , "projects" ] ,
[ "合同流程中" , money ( m . pipeline _amount ) , "projects" ] ,
[ "年度累计确收" , money ( m . revenue _annual ) , "finance" ] ,
[ "Q2 累计确收" , money ( m . revenue _q2 ) , "finance" ] ,
[ "年度累计毛利" , money ( m . gross _annual ) , "finance" ] ,
[ "Q2 累计毛利" , money ( m . gross _q2 ) , "finance" ] ,
] . map ( ( [ label , value , tab ] ) => ` <button class="metric-card" onclick="switchTab(' ${ tab } ')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="trending-up"></i> ${ label } </span><strong class="mt-2 block text-2xl"> ${ value } </strong></button> ` ) . join ( "" ) }
</div>
<div class="grid grid-cols-2 gap-5">
${ card ( ` <div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">财务趋势</h2> ${ badge ( "YYYY-MM" ) } </div><div style="position:relative;height:140px"><canvas id="financeChart"></canvas></div> ` , "p-4" ) }
${ card ( ` <h2 class="text-lg font-bold">风险提醒</h2><div class="mt-3 grid gap-2"> ${ ( summary . risks . length ? summary . risks : [ { title : "暂无高风险" , content : "当前无明确阻塞,按周更新即可。" } ]).map((r) => ` < div class = "rounded-md border border-amber-200 bg-amber-50 p-3" > < p class = "font-bold text-amber-900" > $ { r . title } </p><p class="mt-1 text-sm text-amber-800 break-words"> ${ r . content } </p></div> ` ) . join ( "" ) } < / d i v > ` , " p - 5 " ) }
@@ -143,10 +155,15 @@ function formHtml(fields, button) {
async function createResource ( event , resource ) {
event . preventDefault ( ) ;
const data = Object . fromEntries ( new FormData ( event . currentTarget ) . entries ( ) ) ;
await api ( ` /api/ ${ resource } ` , { method : "POST" , body : JSON . s tringify ( { data } ) } ) ;
event . currentTarget . reset ( ) ;
await load ( ) ;
const form = event . currentTarget ;
const data = Object . fromEntries ( new FormData ( form ) . en tries ( ) ) ;
try {
await api ( ` /api/ ${ resource } ` , { method : "POST" , body : JSON . stringify ( { data } ) } ) ;
form . reset ( ) ;
await load ( ) ;
} catch ( error ) {
alert ( "创建失败:" + error . message ) ;
}
}
window . createSales = ( event ) => createResource ( event , "sales" ) ;
@@ -156,96 +173,72 @@ window.createProduct = (event) => createResource(event, "products");
window . createFinance = ( event ) => createResource ( event , "finance" ) ;
window . switchTab = switchTab ;
function renderSale s ( ) {
const rows = state . data . sales . map ( ( x ) => [ x . target _customer , badge ( x . priority ) , badge ( x . status ) , text ( x . latest _follow _up _record ) ] ) ;
const salesClick s = state . data . sales . map ( ( x ) => ( { resource : "sales" , id : x . id } ) ) ;
document . querySelector ( "#sales" ) . innerHTML = ` <div class="grid gap-4">
function renderProject s ( ) {
// Merge sales_leads and operation_projects into one table
const salesItem s = state . data . sales . map ( ( x ) => ( {
name : x . target _customer ,
version : "" ,
type : "opportunity" ,
status : x . status ,
amount : 0 ,
stage : "" ,
files : 0 ,
followup : x . latest _follow _up _record ,
resource : "sales" ,
id : x . id ,
} ) ) ;
const opItems = state . data . operations . map ( ( x ) => ( {
name : x . project _name ,
version : x . project _version ,
type : x . project _type ,
status : x . project _status ,
amount : x . expected _contract _amount || 0 ,
stage : x . current _stage || x . sop _stage ,
files : x . files . length ,
followup : x . latest _follow _up _record ,
resource : "operations" ,
id : x . id ,
} ) ) ;
const allItems = [ ... salesItems , ... opItems ] ;
const items = state . opFilter === "all" ? allItems : allItems . filter ( ( x ) => x . type === state . opFilter || ( state . opFilter === "opportunity" && x . type === "opportunity" ) ) ;
const rows = items . map ( ( x ) => [ ` <strong> ${ x . name } </strong> ${ x . version ? ` <p class="text-xs text-slate-500"> ${ x . version } </p> ` : "" } ` , badge ( x . type ) , badge ( x . status ) , x . amount ? money ( x . amount ) : "—" , text ( x . stage ) , text ( x . followup ) ] ) ;
const clicks = items . map ( ( x ) => ( { resource : x . resource , id : x . id } ) ) ;
document . querySelector ( "#projects" ) . innerHTML = ` <div class="grid gap-4">
${ card ( formHtml ( [
{ label : "目标客户 " , input : ` <input name="target_customer" required placeholder="客户名称"> ` } ,
{ label : "业务机会 " , input : ` <input name="target_customer" required placeholder="客户名称"> ` } ,
{ label: "优先级", input: ` < select name = "priority" > < option > P0 < / o p t i o n > < o p t i o n s e l e c t e d > P 1 < / o p t i o n > < o p t i o n > P 2 < / o p t i o n > < o p t i o n > P 3 < / o p t i o n > < / s e l e c t > ` } ,
{ label : "状态" , input : ` <select name="status"><option>待跟进</option><option>跟进中</option><option>方案中</option><option>商务谈判</option><option>已签约</option><option>暂缓</option><option>已丢单</option></select> ` } ,
] , { handler : "createSales" , text : ` <i data-lucide="plus"></i>新增客户 ` } ) , "p-4" ) }
$ { renderTable ( [ "目标客户" , "优先级" , "状态" , "最新跟进记录" ] , rows , salesClicks ) }
< / d i v > ` ;
}
function renderProposals ( ) {
const categories = [ "方案" , "成本" , "SOP" , "财务流程" ] ;
const proposalRows = state . data . proposals . map ( ( p ) => [ p . customer _or _project _name , p . version , badge ( p . status ) , p . files . length + " 个" ] ) ;
const proposalClicks = state . data . proposals . map ( ( p ) => ( { resource : "proposals" , id : p . id } ) ) ;
document . querySelector ( "#proposals" ) . innerHTML = ` <div class="grid gap-4">
${ card ( formHtml ( [
{ label : "客户/项目" , input : ` <input name="customer_or_project_name" required placeholder="如:信达生物"> ` } ,
{ label: "版本号", input: ` < input name = "version" required placeholder = "v1.0" > ` },
{ label: "状态", input: ` < select name = "status" > < option > 草稿 < / o p t i o n > < o p t i o n > 内 部 评 审 < / o p t i o n > < o p t i o n s e l e c t e d > 已 提 交 客 户 < / o p t i o n > < o p t i o n > 客 户 反 馈 中 < / o p t i o n > < o p t i o n > 已 确 认 < / o p t i o n > < o p t i o n > 已 归 档 < / o p t i o n > < / s e l e c t > ` } ,
] , { handler : "createProposal" , text : "新增版本" } ) , "p-4" ) }
$ { renderTable ( [ "客户/项目" , "版本号" , "状态" , "文件数" ] , proposalRows , proposalClicks ) }
< / d i v > ` ;
}
function fileGroup ( module , ownerId , version , category , files ) {
return ` <div class="rounded-md border border-slate-200 px-3 py-2">
<div class="flex items-center justify-between gap-3"><p class="text-[13px] font-semibold text-slate-800"> ${ category } </p><label class="inline-flex cursor-pointer items-center gap-1 rounded-md border border-slate-200 px-2 py-1 text-[12px] font-medium text-slate-600 hover:bg-slate-50"><i data-lucide="upload"></i>上传<input class="hidden" type="file" onchange="uploadFile(event,' ${ module } ', ${ ownerId } ,' ${ version } ',' ${ category } ')"></label></div>
<div class="mt-2 grid gap-1.5"> ${ files . length ? files . map ( fileItem ) . join ( "" ) : ` <p class="text-[12px] text-slate-400">暂无文件</p> ` } </div>
</div> ` ;
}
function fileItem ( file ) {
return ` <div class="flex items-center justify-between gap-2 rounded-md bg-slate-50 px-2 py-1.5 text-[13px]"><div class="min-w-0 flex-1"><p class="truncate font-medium text-slate-800"> ${ file . file _name } </p><div class="mt-0.5 flex gap-3"><a class="file-link inline-flex items-center gap-1" target="_blank" href="/api/files/ ${ file . id } /content?inline=true"><i data-lucide="eye"></i>预览</a><a class="file-link inline-flex items-center gap-1 text-slate-600" href="/api/files/ ${ file . id } /content?inline=false"><i data-lucide="download"></i>下载</a></div></div><button class="btn btn-ghost btn-sm text-red-600" onclick="deleteFile( ${ file . id } )" title="删除"><i data-lucide="trash-2"></i></button></div> ` ;
}
window . deleteFile = async ( fileId ) => {
if ( ! confirm ( "确认删除此文件?" ) ) return ;
await api ( ` /api/files/ ${ fileId } ` , { method : "DELETE" } ) ;
await load ( ) ;
closeDrawer ( ) ;
} ;
window . uploadFile = async ( event , module , ownerId , version , category ) => {
const file = event . target . files [ 0 ] ;
if ( ! file ) return ;
const form = new FormData ( ) ;
form . append ( "module" , module ) ;
form . append ( "owner_id" , ownerId ) ;
form . append ( "owner_version" , version ) ;
form . append ( "file_category" , category ) ;
form . append ( "file" , file ) ;
await api ( "/api/files/upload" , { method : "POST" , body : form } ) ;
await load ( ) ;
} ;
function renderOperations ( ) {
const items = state . opFilter === "all" ? state . data . operations : state . data . operations . filter ( ( x ) => x . project _type === state . opFilter ) ;
const opRows = items . map ( ( x ) => [ ` <strong> ${ x . project _name } </strong><p class="text-xs text-slate-500"> ${ x . project _version } </p> ` , badge ( x . project _type ) , badge ( x . project _status ) , text ( x . current _stage || x . sop _stage ) , ` ${ x . files . length } 个 ` , text ( x . latest _follow _up _record ) ] ) ;
const opClicks = items . map ( ( x ) => ( { resource : "operations" , id : x . id } ) ) ;
document . querySelector ( "#operations" ) . innerHTML = ` <div class="grid gap-4">
] , { handler : "createSales" , text : ` <i data-lucide="plus"></i>新增业务机会 ` } ) , "p-4" ) }
$ { card ( formHtml ( [
{ label : "项目名称" , input : ` <input name="project_name" required> ` } ,
{ label : "项目版本" , input : ` <input name="project_version" value="v1.0"> ` } ,
{ label : "项目类型" , input : ` <select name="project_type"><option value="opportunity">业务机会项目</option><option value="execution">已签约执行项目</option></select> ` } ,
{ label : "状态" , input : ` <input name="project_status" value="线索发现"> ` } ,
] , { handler : "createOperation" , text : "新增项目" } ) , "p-4" ) }
< div class = "flex gap-2" > $ { [ [ "all" , "全部项目 " ] , [ "opportunity" , "业务机会项目 " ] , [ "execution" , "已签约执行项目 " ] ] . map ( ( [ k , v ] ) => ` <button class="btn ${ state . opFilter === k ? "btn-primary" : "btn-ghost" } " onclick="state.opFilter=' ${ k } '; renderOperation s()"> ${ v } </button> ` ) . join ( "" ) } < / d i v >
$ { renderTable ( [ "项目名称 " , "类型" , "状态" , "当前阶段 " , "交付文件 " , "最新跟进" ] , opR ows, opC licks) }
< div class = "flex gap-2" > $ { [ [ "all" , "全部" ] , [ "opportunity" , "业务机会" ] , [ "execution" , "已签约执行" ] ] . map ( ( [ k , v ] ) => ` <button class="btn ${ state . opFilter === k ? "btn-primary" : "btn-ghost" } " onclick="state.opFilter=' ${ k } '; renderProject s()"> ${ v } </button> ` ) . join ( "" ) } < / d i v >
$ { renderTable ( [ "项目/客户 " , "类型" , "状态" , "金额 " , "当前阶段 " , "最新跟进" ] , r ows, c licks) }
< / d i v > ` ;
}
function renderProducts ( ) {
const items = state . productPlatform === "all" ? state . data . products : state . data . products . filter ( ( x ) => ( x . platform || "" ) === state . productPlatform ) ;
document . querySelector ( "#products" ) . innerHTML = ` <div class="grid gap-4">
${ card ( formHtml ( [
{ label : "产品名称" , input : ` <input name="product_name" required> ` } ,
{ label: "版本号", input: ` < input name = "version" required > ` },
{ label: "平台", input: ` < select name = "platform" > < option value = "" > — < / o p t i o n > < o p t i o n > 真 研 平 台 < / o p t i o n > < o p t i o n > 科 普 平 台 < / o p t i o n > < o p t i o n > 关 爱 平 台 < / o p t i o n > < / s e l e c t > ` } ,
{ label : "上线日期" , input : ` <input name="launch_date" placeholder="2026-Q3"> ` } ,
{ label : "状态" , input : ` <select name="status"><option>规划中</option><option>设计中</option><option>开发中</option><option>测试中</option><option>已上线</option><option>已延期</option><option>已取消</option></select> ` } ,
] , { handler : "createProduct" , text : "新增版本" } ) , "p-4" ) }
$ { renderTable ( [ "产品名称 " , "版本号" , "版本目标 " , "核心功能" , "上线日期 " , "状态 " ] , state . data . products . map ( ( p ) => [ p . product _name , p . version , text ( p . version _goal ) , text ( p . feature _list ) , text ( p . launch _date ) , badge ( p . status ) ] ) , state . data . products . map ( ( p ) => ( { resource : "products" , id : p . id } ) ) ) }
< div class = "flex gap-2" > $ { [ [ "all " , "全部" ] , [ "真研平台 " , "真研" ] , [ "科普平台 " , "科普 " ] , [ "关爱平台" , "关爱" ] ] . map ( ( [ k , v ] ) => ` <button class="btn ${ state . productPlatform === k ? "btn-primary" : "btn-ghost" } " onclick="state.productPlatform=' ${ k } '; renderProducts()"> ${ v } </button> ` ) . join ( "" ) } < / d i v >
$ { renderTable ( [ "产品名称" , "版本号" , "版本目标" , "核心功能" , "平台" , "上线日期" , "状态" ] , items . map ( ( p ) => [ p . product _name , p . version , text ( p . version _goal ) , text ( p . feature _list ) , text ( p . platform ) , text ( p . launch _date ) , badge ( p . status ) ] ) , items . map ( ( p ) => ( { resource : "products" , id : p . id } ) ) ) }
< / d i v > ` ;
}
function renderFinance ( ) {
const rows = state . data . finance . map ( ( x ) => [ x . month , badge ( x . record _type === "revenue" ? "收入" : "成本/费用" ) , x . category , money ( x . amount ) , x . occurred _date , text ( x . notes ) ] ) ;
document . querySelector ( "#finance" ) . innerHTML = ` <div class="grid gap-4">
${ card ( ` <h2 class="mb-4 text-lg font-bold">收入、毛利、成本/费用、净利月度曲线</h2><canvas id="financeChart2" height="105" ></canvas> ` , "p-5" ) }
${ card ( ` <h2 class="mb-4 text-lg font-bold">收入、毛利、成本/费用、净利月度曲线</h2><div style="position:relative;height:300px">< canvas id="financeChart2"></canvas></div> ` , "p-5" ) }
${ card ( formHtml ( [
{ label : "月份" , input : ` <input name="month" required placeholder="YYYY-MM" pattern=" \\ d{4}- \\ d{2}"> ` } ,
{ label: "类型", input: ` < select name = "record_type" > < option value = "revenue" > 收入 < / o p t i o n > < o p t i o n v a l u e = " c o s t _ e x p e n s e " > 成 本 / 费 用 < / o p t i o n > < / s e l e c t > ` } ,
@@ -261,7 +254,8 @@ function renderFinance() {
function renderChartOn ( id , data ) {
const canvas = document . querySelector ( ` # ${ id } ` ) ;
if ( ! canvas || ! window . Chart ) return ;
new C hart( canvas , {
if ( state . c hart2 ) state . chart2 . destroy ( ) ;
state . chart2 = new Chart ( canvas , {
type : "line" ,
data : {
labels : data . map ( ( x ) => x . month ) ,
@@ -292,19 +286,19 @@ function openDrawer(resource, id) {
const item = list . find ( ( x ) => x . id === id ) ;
const drawer = document . querySelector ( "#drawer" ) ;
const fields = resource === "sales"
? [ [ "target_customer" , "目标客户 " ] , [ "priority" , "优先级" ] , [ "status" , "状态" ] ]
? [ [ "target_customer" , "业务机会 " ] , [ "priority" , "优先级" ] , [ "status" , "状态" ] ]
: resource === "operations"
? [ [ "project_name" , "项目名称" ] , [ "project_version" , "项目版本" ] , [ "project_status" , "项目状态" ] , [ "current_stage" , "当前阶段" ] , [ "target_customer" , "目标客户 " ] , [ "customer_need" , "客户需求" ] , [ "expected_contract_amount" , "预计签约金额" ] , [ "expected_sign_date" , "预计签约时间" ] , [ "sign_probability" , "签约概率" ] , [ "sop_stage" , "SOP 阶段" ] , [ "execution_progress" , "执行进度" ] , [ "current_deliverable" , "当前交付物" ] , [ "risks" , "风险与阻塞" ] , [ "next_action" , "下一步动作" ] ]
? [ [ "project_name" , "项目名称" ] , [ "project_version" , "项目版本" ] , [ "project_status" , "项目状态" ] , [ "current_stage" , "当前阶段" ] , [ "target_customer" , "业务机会 " ] , [ "customer_need" , "客户需求" ] , [ "expected_contract_amount" , "预计签约金额" ] , [ "expected_sign_date" , "预计签约时间" ] , [ "sign_probability" , "签约概率" ] , [ "sop_stage" , "SOP 阶段" ] , [ "execution_progress" , "执行进度" ] , [ "current_deliverable" , "当前交付物" ] , [ "risks" , "风险与阻塞" ] , [ "next_action" , "下一步动作" ] ]
: resource === "proposals"
? [ [ "customer_or_project_name" , "客户/项目" ] , [ "version" , "版本号" ] , [ "description" , "版本说明" ] , [ "created_date" , "创建日期" ] , [ "status" , "状态" ] ]
: [ [ "product_name" , "产品名称" ] , [ "version" , "版本号" ] , [ "version_goal" , "版本目标" ] , [ "feature_list" , "核心功能清单" ] , [ "launch_date" , "上线日期" ] , [ "status" , "当前状态" ] , [ "notes" , "备注" ] ] ;
: [ [ "product_name" , "产品名称" ] , [ "version" , "版本号" ] , [ "version_goal" , "版本目标" ] , [ "feature_list" , "核心功能清单" ] , [ "platform" , "平台" ] , [ " launch_date" , "上线日期" ] , [ "status" , "当前状态" ] , [ "notes" , "备注" ] ] ;
const fieldIcons = {
target _customer : "user" , priority : "flag" , status : "circle-dot" ,
project _name : "briefcase-business" , project _version : "git-branch" , project _status : "circle-dot" , current _stage : "map-pin" ,
customer _need : "file-text" , expected _contract _amount : "banknote" , expected _sign _date : "calendar" ,
sign _probability : "percent" , sop _stage : "list-checks" , execution _progress : "activity" ,
current _deliverable : "package" , risks : "alert-triangle" , next _action : "arrow-right" ,
product _name : "box" , version : "tag" , version _goal : "target" , feature _list : "list" ,
product _name : "box" , version : "tag" , version _goal : "target" , feature _list : "list" , platform : "layers" ,
launch _date : "calendar" , notes : "sticky-note"
} ;
const multilineFields = [ "customer_need" , "current_deliverable" , "risks" , "next_action" , "version_goal" , "feature_list" , "notes" ] ;
@@ -316,6 +310,20 @@ function openDrawer(resource, id) {
<form id="drawerForm" class="drawer-fields"> ${ fields . map ( ( [ key , label ] ) => drawerField ( fieldIcons [ key ] || "circle" , label , key , item [ key ] , multilineFields . includes ( key ) ) ) . join ( "" ) } </form>
</section>
${ resource === "proposals" ? ` <section><h3 class="drawer-section-title">方案文件</h3><div class="grid gap-2"> ${ [ "方案" , "成本" , "SOP" , "财务流程" ] . map ( ( cat ) => fileGroup ( "proposal" , item . id , item . version , cat , item . files . filter ( ( f ) => f . file _category === cat ) ) ) . join ( "" ) } </div></section> ` : "" }
${ resource === "operations" ? ( ( ) => {
const tasks = ( state . data . tasks || [ ] ) . filter ( ( t ) => t . project _id === id ) ;
if ( ! tasks . length ) return "" ;
const phases = [ ... new Set ( tasks . map ( ( t ) => t . phase ) ) ] ;
return ` <section><h3 class="drawer-section-title">项目任务</h3><div class="grid gap-3"> ${ phases . map ( ( phase ) => {
const pt = tasks . filter ( ( t ) => t . phase === phase ) ;
return ` <div class="rounded-md border border-slate-200 p-3"><p class="text-[13px] font-semibold text-slate-700 mb-2"><i data-lucide="layers" style="width:14px;height:14px;display:inline;vertical-align:-2px;margin-right:4px"></i> ${ phase } </p><div class="grid gap-1.5"> ${ pt . map ( ( t ) => {
const due = t . due _date ? ` <span class="text-[11px] text-slate-400 ml-1">📅 ${ t . due _date } </span> ` : "" ;
const owner = t . owner ? ` <span class="text-[11px] text-slate-500 ml-1">👤 ${ t . owner } </span> ` : "" ;
const blocker = t . blockers ? ` <p class="text-[11px] text-red-600 mt-0.5">⚠ ${ t . blockers } </p> ` : "" ;
return ` <div class="rounded bg-slate-50 px-2.5 py-1.5"><p class="text-[12px] text-slate-800"><strong> ${ t . milestone ? t . milestone + ": " : "" } </strong> ${ t . task } ${ due } ${ owner } </p> ${ blocker } </div> ` ;
} ).join("")}</div></div> ` ;
} ).join("")}</div></section> ` ;
} )() : ""}
${ followupTarget ? ` <section>
<h3 class="drawer-section-title">活动 / 跟进</h3>
<div class="grid gap-2"> ${ ( item . followups || [ ] ) . map ( ( f ) => ` <div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span> ${ f . follower } · ${ f . follow _up _method } </span><span> ${ f . followed _at } </span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html=" ${ encodeURIComponent ( f . content || '' ) } "></div> ${ f . next _action ? ` <p class="mt-1 leading-5 text-blue-700">下一步: ${ text ( f . next _action ) } </p> ` : "" } </div><button class="activity-delete" onclick="deleteFollowup(event, ${ f . id } , ' ${ resource } ', ${ item . id } )" title="删除评论"><i data-lucide="trash-2"></i></button></div> ` ) . join ( "" ) } </div>