@@ -2,6 +2,7 @@ const state = {
22 current : null ,
33 lastInput : "" ,
44 asking : false ,
5+ offerSubmitting : false ,
56 study : { mode : null , cardIndex : 0 , cardRevealed : false , quizAnswer : null }
67} ;
78
@@ -23,6 +24,15 @@ function showToast(message) {
2324 showToast . timer = setTimeout ( ( ) => toast . classList . remove ( "show" ) , 1900 ) ;
2425}
2526
27+ function recordCommercialEvent ( event ) {
28+ fetch ( "/api/commercial-event" , {
29+ method : "POST" ,
30+ headers : { "Content-Type" : "application/json" } ,
31+ body : JSON . stringify ( { event } ) ,
32+ keepalive : true
33+ } ) . catch ( ( ) => { } ) ;
34+ }
35+
2636function showView ( name ) {
2737 $ ( "#startView" ) . hidden = name !== "start" ;
2838 $ ( "#loadingView" ) . hidden = name !== "loading" ;
@@ -35,6 +45,7 @@ function showView(name) {
3545function reset ( ) {
3646 state . current = null ;
3747 state . lastInput = "" ;
48+ state . offerSubmitting = false ;
3849 resetStudy ( ) ;
3950 $ ( "#mainInput" ) . value = "" ;
4051 $ ( "#conversation" ) . innerHTML = "" ;
@@ -109,9 +120,18 @@ function renderSummary(result) {
109120 ${ quickQuestions }
110121 </div>
111122 ${ studyActions ? `<div class="study-actions">${ studyActions } </div>` : "" }
123+ <section class="paid-offer" aria-label="定制学习包">
124+ <div class="paid-offer-copy">
125+ <span>首批定制</span>
126+ <strong>把这个主题做成可执行学习包</strong>
127+ <small>学习路线 · 10 张闪卡 · 5 道测验 · 1 个实战任务</small>
128+ </div>
129+ <button data-offer-open aria-label="定制学习包,29 元每份"><span>定制学习包</span><strong>¥29</strong><i data-lucide="arrow-up-right"></i></button>
130+ </section>
112131 </article>` ;
113132 showView ( "conversation" ) ;
114133 icons ( ) ;
134+ recordCommercialEvent ( "offer_view" ) ;
115135}
116136
117137function resetStudy ( ) {
@@ -150,6 +170,7 @@ function showStudy(mode) {
150170}
151171
152172function renderStudyPanel ( ) {
173+ $ ( "#offerPanel" ) ?. remove ( ) ;
153174 $ ( "#studyPanel" ) ?. remove ( ) ;
154175 if ( ! state . study . mode ) return ;
155176
@@ -208,6 +229,76 @@ function renderStudyPanel() {
208229 scrollToBottom ( ) ;
209230}
210231
232+ function showOffer ( ) {
233+ if ( ! state . current ) return ;
234+ recordCommercialEvent ( "offer_open" ) ;
235+ resetStudy ( ) ;
236+ $ ( "#studyPanel" ) ?. remove ( ) ;
237+ $ ( "#offerPanel" ) ?. remove ( ) ;
238+ $ ( "#conversation" ) . insertAdjacentHTML ( "beforeend" , `
239+ <section class="offer-panel" id="offerPanel">
240+ <header class="study-panel-head">
241+ <div><i data-lucide="package-check"></i><strong>定制学习包</strong><small>¥29 / 份</small></div>
242+ <button class="study-icon-button" data-offer-close aria-label="关闭定制学习包" title="关闭"><i data-lucide="x"></i></button>
243+ </header>
244+ <div class="offer-details">
245+ <strong>24 小时内人工确认并交付</strong>
246+ <ul><li>聚焦目标的学习路线</li><li>10 张闪卡与 5 道带解释的测验</li><li>1 个能真正上手的实战任务</li></ul>
247+ </div>
248+ <form class="offer-form" id="offerForm">
249+ <label>学习主题<input name="topic" value="${ escapeHtml ( state . current . title ) } " maxlength="160" required /></label>
250+ <label>想达到什么目标?<textarea name="goal" minlength="5" maxlength="600" required placeholder="例如:两天内理解 MCP,并独立做出一个只读工具"></textarea></label>
251+ <label>联系方式<input name="contact" maxlength="120" required autocomplete="email" placeholder="微信号、邮箱或手机号" /></label>
252+ <label class="offer-honeypot" aria-hidden="true">网站<input name="website" tabindex="-1" autocomplete="off" /></label>
253+ <label class="offer-consent"><input name="consent" type="checkbox" required /><span>同意仅用此联系方式确认本次需求和付款方式,不用于无关营销</span></label>
254+ <button class="offer-submit" type="submit"><span>登记购买意向</span><strong>¥29</strong></button>
255+ <p>提交只登记意向,不会自动扣款;确认需求后再决定是否付款。</p>
256+ </form>
257+ </section>` ) ;
258+ icons ( ) ;
259+ scrollToBottom ( ) ;
260+ }
261+
262+ async function submitOffer ( form ) {
263+ if ( state . offerSubmitting ) return ;
264+ state . offerSubmitting = true ;
265+ const submit = form . querySelector ( "button[type='submit']" ) ;
266+ submit . disabled = true ;
267+ submit . querySelector ( "span" ) . textContent = "正在登记" ;
268+ const data = new FormData ( form ) ;
269+ try {
270+ const response = await fetch ( "/api/paid-pack-interest" , {
271+ method : "POST" ,
272+ headers : { "Content-Type" : "application/json" } ,
273+ body : JSON . stringify ( {
274+ topic : data . get ( "topic" ) ,
275+ goal : data . get ( "goal" ) ,
276+ contact : data . get ( "contact" ) ,
277+ website : data . get ( "website" ) ,
278+ consent : data . get ( "consent" ) === "on"
279+ } )
280+ } ) ;
281+ const result = await response . json ( ) ;
282+ if ( ! response . ok ) throw new Error ( result . error || "暂时无法登记" ) ;
283+ $ ( "#offerPanel" ) . innerHTML = `
284+ <div class="offer-success" role="status">
285+ <span><i data-lucide="check"></i></span>
286+ <strong>购买意向已登记</strong>
287+ <p>编号 ${ escapeHtml ( result . reference ) } 。我们会通过你留下的联系方式确认范围和付款方式。</p>
288+ <small>本次没有自动扣款。</small>
289+ <button data-offer-close>返回学习</button>
290+ </div>` ;
291+ icons ( ) ;
292+ } catch ( error ) {
293+ showToast ( error . message ) ;
294+ submit . disabled = false ;
295+ submit . querySelector ( "span" ) . textContent = "登记购买意向" ;
296+ } finally {
297+ state . offerSubmitting = false ;
298+ scrollToBottom ( ) ;
299+ }
300+ }
301+
211302async function ask ( question ) {
212303 const value = question . trim ( ) ;
213304 if ( ! value || ! state . current || state . asking ) return ;
@@ -256,6 +347,12 @@ $("#followUpForm").addEventListener("submit", (event) => {
256347 ask ( $ ( "#followUpInput" ) . value ) ;
257348} ) ;
258349
350+ document . addEventListener ( "submit" , ( event ) => {
351+ if ( ! event . target . matches ( "#offerForm" ) ) return ;
352+ event . preventDefault ( ) ;
353+ submitOffer ( event . target ) ;
354+ } ) ;
355+
259356document . addEventListener ( "click" , ( event ) => {
260357 const example = event . target . closest ( "[data-example]" ) ;
261358 const clarify = event . target . closest ( "[data-clarify]" ) ;
@@ -266,6 +363,8 @@ document.addEventListener("click", (event) => {
266363 const cardNav = event . target . closest ( "[data-card-nav]" ) ;
267364 const quizOption = event . target . closest ( "[data-quiz-option]" ) ;
268365 const quizReset = event . target . closest ( "[data-quiz-reset]" ) ;
366+ const offerOpen = event . target . closest ( "[data-offer-open]" ) ;
367+ const offerClose = event . target . closest ( "[data-offer-close]" ) ;
269368 if ( example ) analyze ( example . dataset . example ) ;
270369 if ( clarify ) analyze ( clarify . dataset . clarify ) ;
271370 if ( question ) ask ( question . dataset . question ) ;
@@ -291,6 +390,8 @@ document.addEventListener("click", (event) => {
291390 state . study . quizAnswer = null ;
292391 renderStudyPanel ( ) ;
293392 }
393+ if ( offerOpen ) showOffer ( ) ;
394+ if ( offerClose ) $ ( "#offerPanel" ) ?. remove ( ) ;
294395} ) ;
295396
296397$ ( "#newButton" ) . addEventListener ( "click" , reset ) ;
0 commit comments