1.環境
? ? ? ??nutui-uniapp+vue3+ts+unocss
2.功能源碼
? ? ? ? 包含ai生成邏輯,內容生成實時打字機功能,ai數據處理等
<script setup lang="ts">
import {queryAIParams,
} from '@/api/pagesA'
import { submitFn } from '@/api/ai'import Navbar from '@/components/navBar/index.vue'
import { useAuthStore } from '@/store'
import { imgQuality40 } from '@/utils/imageUrl'
import { isFastClick } from '@/utils/shared'
import { loadingProp, warningProp, successProp } from '@/utils/tostProps'
import { useToast } from 'nutui-uniapp/composables'
import {transformToInlineStyleFragment,parseHealthContentByAngleBrackets,extractMainTitle,parseContent,
} from '@/utils/html'import TextDecoder from '@/utils/TextDecoder'const authStore = useAuthStore()
const toast = useToast()// 狀態管理
const textContent = ref(''// `認識慢性疾病:管理、預防與生活方式 日常生活中,許多人都會聽到身邊有人提到高血壓、糖尿病或者慢性疼痛這樣的問題。管理、預防與生活方式 日常生活中,許多人都會聽到身邊有人提到高血壓、糖尿病或者慢性疼痛這樣的問題。管理、預防與生活方式 日常生活中,許多人都會聽到身邊有人提到高血壓、糖尿病或者慢性疼痛這樣的問題。有些人或許家中也有長輩需要每天按醫囑吃藥、控制飲食,也有人疑惑:慢性病到底是怎么發生的?自己有沒有風險?其實,慢性疾病離我們并不遙遠,但也沒必要談虎色變。把握一些基本知識,調整生活習慣,大多數慢性病其實都能管得住,不必為未知的小擔心而焦慮。 01 簡單聊一聊:慢性疾病到底是什么? 慢性疾病,這個詞其實并不復雜。簡單來說,就是那些拖得比較久、病程長、進展慢的健康問題,比如高血壓、糖尿病、慢阻肺,甚至有的人常年腰腿痛、關節不舒服,也算在內。這類毛病不像感冒發燒那樣來得快去得也快,反而像個"鄰居",你把它管好了,也能平安無事。每個人都有可能在某一時段遇到慢性病的困擾,但大可不必被嚇到,一方面這些疾病的早期征兆往不明顯,另一方面通過科學管理,生活質量一樣可以很好。 根據世界衛生組織的數據,全球約70%的死亡和慢性疾病有關(WHO, Noncommunicable diseases, 2021)。在中國,慢性病和相關并發癥更是影響了上億人。但不用太擔心,這意味著:如果我們能早理解,日常多注意,很多慢性病其實可以很好地被控制住。 02 常見信號:身體在提醒你些什么? 總是覺得累: 不是工作太拼,休息夠了還是無精打采?慢性疾病常以持續疲勞開場,特別是糖尿病、高血壓患者,容易覺得乏力。 疼痛持續而不明原因: 比如膝蓋、腰背、肩頸等關節疼痛,時間一長,總覺得這就是“老化”,其實很可能是慢性炎癥在作祟。 體重變化奇怪: 沒有刻意減肥,但體重慢減少,或反復無明顯理由地增加,這也是信號之一。 情緒波動與睡眠變差: 睡不安穩、容易焦慮,常被忽視。慢性病容易讓人體力和情緒一起受影響。 特殊病例一例: 比如有位男士,因為意外導致頭部受傷后反復頭暈、惡心嘔吐、視力異常,經檢查初步診斷為頭部外傷。這種情況下,慢性軀體不適信號和急性癥狀不同,需要及時檢查(病例參考見下文)。 這些信號像是身體的小鬧鐘,及時注意有助于早干預。不過,光憑這些癥狀還難以判斷是哪種慢性病,最好能跟專業醫生溝通一下。 ?? 03 慢性病為什么會找上門? 說起來,慢性疾病出現,并不是哪一天突然冒出來的“大麻煩”,而是多種小因素積累的結果。具體來說,有以下幾類主要原因: 遺傳因素:某些慢性疾病(如高血壓、糖尿病)遺傳傾向明顯。如果家里長輩有類似病史,個人風險會更高。 年齡相關變化:年齡的增長,意味著身體各個系統都在緩慢變化,比如代謝變慢、血管彈性下降,慢病隨年齡見多不怪。 生活習慣:飲食結構單一、運動太少、長期壓力大、熬夜,這些看似“習以為常”的習慣,其實就像慢磨損的零件。比如研究發現,長期缺乏規律鍛煉增加心腦血管病風險(Booth et al., Waging war on modern chronic diseases, JAMA, 2012)。 環境因素:長期接觸空氣污染、某些職業暴露,也會提升患慢性肺病等風險。從中可以看出,致病機理往是綜合的,不是單一原因能解釋全部。不過,生活習慣調整依然是相對好控制的一環。從中可以看出,致病機理往是綜合的,不是單一原因能解釋全部。不過,生活習慣調整依然是相對好控制的一環。從中可以看出,致病機理往是綜合的,不是單一原因能解釋全部。不過,生活習慣調整依然是相對好控制的一環。從中可以看出,致病機理往是綜合的,不是單一原因能解釋全部。不過,生活習慣調整依然是相對好控制的一環。 從中可以看出,致病機理往是綜合的,不是單一原因能解釋全部。不過,生活習慣調整依然是相對好控制的一環。`
)
const generatedText = ref('')
// 內容示例格式:
// `<div class="material-guidance-wrapper">
// <h1 id="material_title" class="material-custom-title">
// 認識慢性疾病:管理、預防與生活方式
// <span class="material-book-emoji">📚</span>
// </h1>// <!-- 開頭部分 -->
// <div class="material-intro-block">
// 日常生活中,許多人都會聽到身邊有人提到高血壓、糖尿病或者慢性疼痛這樣的問題。有些人或許家中也有長輩需要每天按醫囑吃藥、控制飲食,也有人疑惑:慢性病到底是怎么發生的?自己有沒有風險?其實,慢性疾病離我們并不遙遠,但也沒必要談虎色變。把握一些基本知識,調整生活習慣,大多數慢性病其實都能管得住,不必為未知的小擔心而焦慮。
// </div>// <!-- 01 什么是慢性疾病 -->
// <section class="material-section" style="background: linear-gradient(90deg, #f4f7fa 0%, #e6f2ff 100%);">
// <h2 class="material-section-title material-icon-with-bg">
// <span class="material-section-emoji">🌱</span>
// 01 簡單聊一聊:慢性疾病到底是什么?
// </h2>
// <div class="material-section-content">
// <p>
// 慢性疾病,這個詞其實并不復雜。簡單來說,就是那些拖得比較久、病程長、進展慢的健康問題,比如高血壓、糖尿病、慢阻肺,甚至有的人常年腰腿痛、關節不舒服,也算在內。這類毛病不像感冒發燒那樣來得快去得也快,反而像個"鄰居",你把它管好了,也能平安無事。每個人都有可能在某一時段遇到慢性病的困擾,但大可不必被嚇到,一方面這些疾病的早期征兆往不明顯,另一方面通過科學管理,生活質量一樣可以很好。
// </p>
// <p>
// 根據世界衛生組織的數據,全球約70%的死亡和慢性疾病有關(WHO, Noncommunicable diseases, 2021)。在中國,慢性病和相關并發癥更是影響了上億人。但不用太擔心,這意味著:如果我們能早理解,日常多注意,很多慢性病其實可以很好地被控制住。
// </p>
// </div>
// </section>// <!-- 02 慢性疾病的常見癥狀是什么? -->
// <section class="material-section" style="background: linear-gradient(90deg, #e8f6ef 0%, #daf8e3 100%);">
// <h2 class="material-section-title material-icon-with-bg">
// <span class="material-section-emoji">🔍</span>
// 02 常見信號:身體在提醒你些什么?
// </h2>
// <div class="material-section-content">
// <ul class="material-ul-points">
// <li>
// <strong>總是覺得累:</strong>
// 不是工作太拼,休息夠了還是無精打采?慢性疾病常以持續疲勞開場,特別是糖尿病、高血壓患者,容易覺得乏力。
// </li>
// <li>
// <strong>疼痛持續而不明原因:</strong>
// 比如膝蓋、腰背、肩頸等關節疼痛,時間一長,總覺得這就是“老化”,其實很可能是慢性炎癥在作祟。
// </li>
// <li>
// <strong>體重變化奇怪:</strong>
// 沒有刻意減肥,但體重慢減少,或反復無明顯理由地增加,這也是信號之一。
// </li>
// <li>
// <strong>情緒波動與睡眠變差:</strong>
// 睡不安穩、容易焦慮,常被忽視。慢性病容易讓人體力和情緒一起受影響。
// </li>
// <li>
// <strong>特殊病例一例:</strong>
// 比如有位男士,因為意外導致頭部受傷后反復頭暈、惡心嘔吐、視力異常,經檢查初步診斷為頭部外傷。這種情況下,慢性軀體不適信號和急性癥狀不同,需要及時檢查(病例參考見下文)。
// </li>
// </ul>
// <p>
// 這些信號像是身體的小鬧鐘,及時注意有助于早干預。不過,光憑這些癥狀還難以判斷是哪種慢性病,最好能跟專業醫生溝通一下。
// </p>
// </div>
// </section>// <!-- 03 慢性疾病的主要致病機理是什么? -->
// <section class="material-section" style="background: linear-gradient(90deg, #faf6e8 0%, #f9ebda 100%);">
// <h2 class="material-section-title material-icon-with-bg">
// <span class="material-section-emoji">??</span>
// 03 慢性病為什么會找上門?
// </h2>
// <div class="material-section-content">
// <p>
// 說起來,慢性疾病出現,并不是哪一天突然冒出來的“大麻煩”,而是多種小因素積累的結果。具體來說,有以下幾類主要原因:
// </p>
// <ul class="material-ul-points">
// <li>
// <strong>遺傳因素:</strong>某些慢性疾病(如高血壓、糖尿病)遺傳傾向明顯。如果家里長輩有類似病史,個人風險會更高。
// </li>
// <li>
// <strong>年齡相關變化:</strong>年齡的增長,意味著身體各個系統都在緩慢變化,比如代謝變慢、血管彈性下降,慢病隨年齡見多不怪。
// </li>
// <li>
// <strong>生活習慣:</strong>飲食結構單一、運動太少、長期壓力大、熬夜,這些看似“習以為常”的習慣,其實就像慢磨損的零件。比如研究發現,長期缺乏規律鍛煉增加心腦血管病風險(Booth et al., Waging war on modern chronic diseases, JAMA, 2012)。
// </li>
// <li>
// <strong>環境因素:</strong>長期接觸空氣污染、某些職業暴露,也會提升患慢性肺病等風險。
// </li>
// </ul>
// <p>
// 從中可以看出,致病機理往是綜合的,不是單一原因能解釋全部。不過,生活習慣調整依然是相對好控制的一環。<br/>
// <span class="material-cite-block">
// <em>參考文獻:</em> Booth, F. W., Roberts, C. K., & Laye, M. J. (2012). Lack of exercise is a major cause of chronic diseases. The Journal of Physiology, 590(3), 703-731.
// </span>
// </p>
// </div>
// </section>// <!-- 04 如何進行慢性疾病的診斷? -->
// <section class="material-section" style="background: linear-gradient(90deg, #f9f7fc 0%, #e9e3fa 100%);">
// <h2 class="material-section-title material-icon-with-bg">
// <span class="material-section-emoji">🧪</span>
// 04 慢性病怎么查出來?——診斷流程一覽
// </h2>
// <div class="material-section-content">
// <ol class="material-ol-points">
// <li>
// <strong>醫生問診:</strong>
// 先和醫生詳細聊:不舒服多久了?有沒有類似家族史?生活習慣怎么樣?
// </li>
// <li>
// <strong>體格檢查:</strong>
// 醫生會做一些基礎檢查,比如量血壓、聽心肺,有時會摸肚子、看看關節活動度。
// </li>
// <li>
// <strong>實驗室檢查:</strong>
// 常見的有血常規、生化全套(如血糖、血脂、肝腎功能等)。對有疑似糖尿病、高血壓、肝病等患者尤其重要。
// </li>
// <li>
// <strong>影像學檢查:</strong>
// 具體包括X光、CT、超聲等。以案例為例,有男性患者頭部外傷后反復頭暈,經顱腦平掃、DR鼻骨側位等影像學手段確認病因,這些工具同樣適用于判斷慢性息肉、關節退變等疾病。
// </li>
// <li>
// <strong>專科診斷:</strong>
// 必要時,醫生還會安排專項檢查,如心電圖、心臟彩超等,判斷器官功能。
// </li>
// </ol>
// <p>
// 檢查流程其實并不復雜,很多慢性疾病都是通過這些環節逐步排查、最后鎖定的。遇到不明原因的不適,拖拉更容易耽誤治療。<br/>
// <span class="material-cite-block">
// <em>資料參考:</em> Mayo Clinic, “Head injury: First aid”, 2022.
// </span>
// </p>
// </div>
// </section>// <!-- 05 慢性疾病的治療方法和預期效果有哪些? -->
// <section class="material-section" style="background: linear-gradient(90deg, #e4f3fd 0%, #c6e2f7 100%);">
// <h2 class="material-section-title material-icon-with-bg">
// <span class="material-section-emoji">💡</span>
// 05 管理慢性病:有哪些靠譜的辦法?
// </h2>
// <div class="material-section-content">
// <p>
// 既然慢性疾病"難纏",那治療和管理有哪些常見方法?其實,主要有以下三類:
// </p>
// <ul class="material-ul-points">
// <li>
// <strong>藥物干預:</strong>如降壓藥、降糖藥、抗炎藥,根據具體病種組合使用。需要注意的是,藥物調整需在醫生指導下進行,切勿擅自加減。
// </li>
// <li>
// <strong>生活方式調整:</strong>規律作息、勞逸結合、適當運動,可根據個人情況選擇散步、游泳、慢跑等。比如規律運動有助于改善胰島素敏感性,輔助控制糖尿病(Colberg et al., Exercise and Type 2 Diabetes, Diabetes Care, 2016)。
// </li>
// <li>
// <strong>心理支持與健康教育:</strong>面對慢病,情緒波動正常,焦慮時家人和醫生多一些溝通,也可以參與病友支持小組,獲得更多力量和經驗分享。
// </li>
// </ul>
// <p>
// 治療效果因人而異,但多數慢性病通過上述方法能獲得很大改善。比如高血壓患者有規律監測和全程管理,患心腦血管意外的概率會大減少。
// </p>
// <p class="material-cite-block">
// <em>參考文獻:</em> Colberg, S. R., et al. (2016). Exercise and Type 2 Diabetes. Diabetes Care, 39(11), 2065-2079.
// </p>
// </div>
// </section>// <!-- 06 日常管理與預防慢性疾病的方法有哪些? -->
// <section class="material-section" style="background: linear-gradient(90deg, #fffbe6 0%, #fff2cc 100%);">
// <h2 class="material-section-title material-icon-with-bg">
// <span class="material-section-emoji">🥗</span>
// 06 生活小貼士:健康管理怎么做更靠譜?
// </h2>
// <div class="material-section-content">
// <ul class="material-ul-points">
// <li>
// <strong>每日動一動:</strong>
// 散步、快走、騎自行車、打太極都可以,運動有助于心血管和代謝健康,最好的辦法是每天累計30分鐘以上。
// </li>
// <li>
// <strong>飲食有講究:</strong>
// 多吃蔬菜水果(促進腸道健康),適當攝入全谷類(有助穩定血糖),優質蛋白(比如豆制品、瘦肉、魚類)有助于保持身體機能。
// </li>
// <li>
// <strong>定時體檢:</strong>
// 建議40歲開始,每1-2年做一次基礎健康體檢,關注血壓、血糖、血脂等指標。
// </li>
// <li>
// <strong>調節情緒:</strong>
// 保持樂觀,遇到壓力時,可以和朋友聊、聽音樂或練深呼吸。長期壓力過大會影響免疫力和慢病管理。
// </li>
// <li>
// <strong>充足睡眠:</strong>
// 每天7-8小時較為理想,睡眠質量好有助于修復機體,降低多種慢病風險。
// </li>
// <li>
// <strong>出現異常及時就診:</strong>
// 有不明原因體重變化、持續乏力、食欲減退、疼痛等新癥狀時,最好能預約專業醫生,避免“小問題拖成大麻煩”。
// </li>
// </ul>
// <p>
// 小結一下,健康生活并不復雜,養成這些好習慣,慢性病風險自然會減少,不僅是長壽,更重要的是生活質量好,能享受喜歡的事。
// </p>
// </div>
// </section>// <!-- 結束語 -->
// <div class="material-end-block">
// <span class="material-heart-emoji">💚</span>
// 慢性疾病雖然常見,也不必因此焦慮。核心在于多關注一點身體變化,養成良好的作息和飲食習慣。有了基礎知識和方法做支撐,生活也會多一份踏實和底氣。快把這份指南推薦給身邊的朋友和家人,讓健康多一份主動權!
// </div>// <!-- 文獻引用 -->
// <div class="material-reference-block">
// <h3 class="material-ref-title">參考文獻</h3>
// <ol class="material-ref-list">
// <li>
// World Health Organization (2021). Noncommunicable diseases. Retrieved from <a href="https://www.who.int/news-room/fact-sheets/detail/noncommunicable-diseases" target="_blank">https://www.who.int/news-room/fact-sheets/detail/noncommunicable-diseases</a>
// </li>
// <li>
// Booth, F. W., Roberts, C. K., & Laye, M. J. (2012). Lack of exercise is a major cause of chronic diseases. The Journal of Physiology, 590(3), 703-731. <a href="https://pubmed.ncbi.nlm.nih.gov/22289907/" target="_blank">PubMed</a>
// </li>
// <li>
// Colberg, S. R., et al. (2016). Exercise and Type 2 Diabetes. Diabetes Care, 39(11), 2065-2079. <a href="https://pubmed.ncbi.nlm.nih.gov/27926890/" target="_blank">PubMed</a>
// </li>
// <li>
// Mayo Clinic Staff. (2022). Head injury: First aid. Mayo Clinic. <a href="https://www.mayoclinic.org/first-aid/first-aid-head-trauma/basics/art-20056626" target="_blank">Link</a>
// </li>
// </ol>
// </div>
// </div>// <style>
// .material-guidance-wrapper {
// max-width: 790px;
// margin: 18px auto 30px auto;
// font-family: 'PingFang SC', 'Microsoft YaHei', Arial, Helvetica, sans-serif;
// color: #232729;
// background-color: #fb;
// border-radius: 17px;
// overflow: hidden;
// box-shadow: 0 4px 36px 0 rgba(186,199,214,0.13);
// padding-bottom: 40px;
// }// .material-custom-title {
// font-size: 2.2em;
// text-align: center;
// padding-top: 35px;
// color: #205180;
// letter-spacing: 2px;
// margin-bottom: 18px;
// background: linear-gradient(90deg, #f8fafc 60%, #e7f2ff 100%);
// border-bottom: 2px solid #aad3fa;
// border-radius: 0 0 13px 13px;
// box-shadow: 0 2px 12px 0 rgba(140,170,220,0.07);
// }// .material-book-emoji {
// font-size: 1.1em;
// margin-left: 0.2em;
// vertical-align: middle;
// }// .material-intro-block {
// padding: 27px 34px 7px 36px;
// font-size: 1.18em;
// background: linear-gradient(90deg, #f8fbff 0%, #fefefe 100%);
// border-left: 5px solid #b6d3ee;
// margin-bottom: 3px;
// line-height: 1.7;
// border-radius: 0 20px 20px 0;
// }// .material-section {
// margin: 26px 34px 25px 34px;
// padding: 34px 36px 6px 36px;
// border-radius: 19px;
// box-shadow: 0 2px 18px 0 rgba(215,225,239,.07);
// transition: box-shadow 0.3s;
// }// .material-section-title {
// font-size: 1.51em;
// color: #316399;
// margin-bottom: 14px;
// display: flex;
// align-items: center;
// font-weight: 600;
// letter-spacing: .5px;
// }// .material-section-emoji {
// font-size: 1.32em;
// margin-right: 0.45em;
// background: rgba(210, 230, 245, 0.52);
// padding: 2.5px 8px;
// border-radius: 8px;
// }// .material-section-content {
// font-size: 1.12em;
// line-height: 1.82;
// color: #212c35;
// margin-top: -10px;
// }// .material-ul-points {
// margin-left: 0;
// margin-bottom: 12px;
// padding-left: 21px;
// list-style-type: disc;
// }// .material-ul-points li {
// margin-bottom: 13px;
// padding-left: 1px;
// }// .material-ol-points {
// margin-left: 0;
// margin-bottom: 18px;
// padding-left: 23px;
// list-style-type: decimal;
// }// .material-ol-points li {
// margin-bottom: 12px;
// }// .material-cite-block {
// display: block;
// color: #7297d1;
// font-size: .96em;
// font-style: italic;
// margin-top: 9px;
// }// .material-end-block {
// margin: 38px 36px 9px 36px;
// padding: 24px 24px 21px 24px;
// border-radius: 16px;
// background: linear-gradient(90deg,#eefdff 40%,#f7f6f2 100%);
// font-size: 1.13em;
// color: #297a49;
// box-shadow: 0 1px 8px 0 rgba(170,220,180,0.17);
// text-align: center;
// }// .material-heart-emoji {
// font-size: 1.17em;
// margin-right: 0.2em;
// }// .material-reference-block {
// padding: 28px 36px 15px 36px;
// margin: 32px 34px 0 34px;
// border-radius: 13px;
// background: linear-gradient(90deg,#e8f4ff 10%,#fbf9ff 90%);
// color: #295799;
// }// .material-ref-title {
// margin: 0;
// font-size: 1.12em;
// font-weight: 700;
// color: #3970b5;
// margin-bottom: 14px;
// letter-spacing: 0.7px;
// }// .material-ref-list {
// margin: 0;
// padding-left: 18px;
// font-size: 0.97em;
// line-height: 1.8;
// }// .material-ref-list a {
// color: #2173a7;
// text-decoration: none;
// border-bottom: 1px dotted #2173a7;
// margin-left: 2px;
// }// .material-ref-list a:hover {
// text-decoration: underline;
// }
// </style>
// `const contentTitle = ref('')const newGeneratedText = ref(``) // 處理為行內樣式的數據// const TDK = ref<any>(null);
const TDK = ref<any>({})
// {
// title: "認識慢性疾病:管理、預防與生活方式",
// description:
// "了解慢性疾病的成因、癥狀及有效管理方法。掌握這些知識能幫助你或家人預防高血壓、糖尿病等常見慢性病,提升生活質量。",
// keywords: "慢性疾病, 高血壓, 糖尿病, 管理方法, 生活方式",
// cover:
// "https://ystcdn.venuertc.com/venue/AI/25f122b2-6f4d-4bb6-a646-7c34ec415e7f.jpg",
// seo_analysis: {
// core_keywords: ["慢性疾病", "高血壓", "糖尿病"],
// long_tail_keywords: [
// "慢性疾病癥狀",
// "如何管理糖尿病",
// "高血壓的治療方法",
// "慢性病預防措施",
// ],
// target_audience: "關注健康的成年人,特別是中老年群體及其家屬。",
// search_intent:
// "用戶希望獲取有關慢性病的預防和管理信息,以及相關癥狀的認知。",
// },
// }const scrollTarget = ref('scroll-to-bottom')
const scrollTop = ref(0)
const showOverlayFlag = ref(false) // 生成中
const showTipPropFlag = ref(false) // 提示彈窗
const tipType = ref(1) // 提示類型 1文本字數提示 2錄制前提示
const scrollViewHeight = ref(0)
const isGenerating = ref(false)
const controller = ref<any>(null)
const isStreamEnded = ref(false)// 打字機邏輯
const pendingText = ref('')
const isTyping = ref(false)
let typingTimer: any = nullconst lastScrollTime = ref(0)
const lastChunk = ref('')
const lastContentLength = ref(0)// 解析參數
const pageType = ref(1) // 1生成文章 2生成腳本(視頻) 3待錄制
const operationType = ref('add') // add添加新 edit修改 look查看
const aiParams = ref<any>({}) // ai參數
const promptId = ref(0) // 指令id
const sprId = ref(0) // sprIdonLoad(async () => {// 獲取默認滾動區域高度updateScrollViewHeight()// 獲取 AI 配置await fetchPromptId()// 自動生成文章startStreamRequest()
})// 獲取 AI 配置
async function fetchPromptId() {try {const res: any = await queryAIParams({sprId: sprId.value,promptId: Number(promptId.value),}).queryFn()if (res.code === 401) returnif (res.code !== 200) return toast.warning(res.msg || 'AI配置獲取失敗', warningProp)else if (res.data) return (aiParams.value = { ...res.data })} catch (err) {toast.warning('網絡錯誤', warningProp)}
}// ========== 更新滾動區域高度 ==========
function updateScrollViewHeight() {calcScrollViewHeight()
}// ? 計算 scroll-view 高度
function calcScrollViewHeight() {const query = uni.createSelectorQuery()// console.log("query-----", query);// 直接通過 class 或 id 查詢query.select('.nav-bar').boundingClientRect()query.select('.bto-box').boundingClientRect()query.selectViewport().boundingClientRect() // 獲取窗口大小query.exec((res) => {if (!res || res?.length < 3) return// console.log("res-----", res);const viewport = res[2] // selectViewportconst header = res[0]const footer = res[1]const windowHeight = viewport.heightconst headerHeight = header ? header.height : 0const footerHeight = footer ? footer.height : ((158 * 2) / 750) * uni.upx2px(750) // 兜底 158rpx 轉 px// 計算 scroll-view 高度(單位 px)const heightInPx = windowHeight - headerHeight - footerHeight// 轉回 rpx 顯示(可選),或直接用 pxscrollViewHeight.value = heightInPx // scroll-view 支持 px// console.log("scrollViewHeight.value----", scrollViewHeight.value);})
}// ========== 重構的 SSE 流處理邏輯 ==========
function createDifyStream(payload: any, cb: any) {let buffer = ''let streamEnded = false// const decoder = new TextDecoder("utf-8");// key 生成內容調用接口的參數,自行取舍const key = 'app-key'const req: any = uni.request({url: 'api', // 接口地址method: 'POST',timeout: 300000, // 設置為 5 分鐘(默認 60 秒,最大可設 300000 = 5 分鐘)enableChunked: true,header: {Authorization: `Bearer ${key}`,'Content-Type': 'application/json',Accept: 'text/event-stream',},data: payload,success: () => {processBuffer()// 兜底:如果還沒結束,強制觸發 onFinishif (!streamEnded) {streamEnded = truecb.onFinish?.()}},fail: (err) => {cb.onError?.(new Error(err.errMsg))if (err.errMsg.includes('timeout')) {wx.showToast({title: '網絡較慢,請稍后重試',icon: 'none',})}},})if (req?.onChunkReceived) {req.onChunkReceived((res: any) => {const arrayBuffer = new Uint8Array(res.data)const chunk = new TextDecoder().decode(arrayBuffer)buffer += chunkprocessBuffer()})}function processBuffer() {const lines = buffer.split('\n')buffer = lines.pop() || '' // 保留未完成行for (const line of lines) {// console.log("SSE Line:", line); // 調試if (!line.startsWith('data:')) continueconst dataStr = line.slice(5).trim()// [DONE] 表示流結束if (dataStr === '[DONE]') {if (!streamEnded) {streamEnded = true// console.log("Received [DONE] -> onFinish triggered");cb.onFinish?.()}continue}if (!dataStr) continuetry {const json = JSON.parse(dataStr)// console.log("Parsed JSON:", json);// 多種結束信號兼容if (json.is_finished === true ||json.event === 'message_end' ||json.status === 'completed' ||json.final_answer !== undefined) {if (!streamEnded) {streamEnded = true// 延遲1秒后處理setTimeout(() => {// 結束前處理數據// 純文本const str = parseHealthContentByAngleBrackets(generatedText.value)textContent.value = str// console.log("🎬 純文本------", textContent.value);// html+TDKconst data = parseContent(generatedText.value)generatedText.value = data.htmlTDK.value = data.tdk// console.log("🎬 html+TDK------", TDK.value);// 轉為行內const div = transformToInlineStyleFragment(generatedText.value)newGeneratedText.value = div// console.log("🎬 轉為行內------", newGeneratedText.value);// 標題提取if (!TDK.value?.title) {contentTitle.value = extractMainTitle(generatedText.value) || ''// console.log("🎬 標題提取------", newGeneratedText.value);}// 關閉 清除cb.onFinish?.()}, 1000)}}// 提取文本內容(兼容不同字段)const text = json.answer || json.text || json.contentif (text && !streamEnded) {cb.onPartialAnswer?.(text)}} catch (err) {console.error('Parse error:', err, 'Data:', dataStr)}}}return {abort() {req?.abort?.()},isEnded() {return streamEnded},}
}// ========== 優化的滾動到底部邏輯 ==========
function scrollIfNeeded() {const now = Date.now()if (now - lastScrollTime.value < 50) returnlastScrollTime.value = nownextTick(() => {const query = uni.createSelectorQuery()query.select('.scroll-content').boundingClientRect()query.exec((res) => {// console.log("scroll-content--res-----", res[0].height);scrollTop.value = res[0].height})})
}// ========== 開始輸入 ==========
function startTyping() {if (isTyping.value) returnif (!pendingText.value) returnisTyping.value = truetypingTimer = setInterval(() => {if (!pendingText.value) {stopTyping()return}// 每次取 10~20 字符,減少 DOM 更新次數const chunk = pendingText.value.slice(0, 20)generatedText.value += chunkpendingText.value = pendingText.value.slice(20)scrollIfNeeded()}, 30)
}// ========== 停止輸入 ==========
function stopTyping() {// console.log("stopTyping-------");if (typingTimer) clearInterval(typingTimer)typingTimer = nullif (pendingText.value) {generatedText.value += pendingText.valuependingText.value = ''}isTyping.value = falsescrollIfNeeded()// console.log("stopTyping----end---");
}// ========== 開始請求 ==========
async function startStreamRequest() {if (isGenerating.value) returnisGenerating.value = trueshowOverlayFlag.value = truenewGeneratedText.value = ''generatedText.value = ''pendingText.value = ''stopTyping()lastChunk.value = ''lastContentLength.value = 0textContent.value = ''// 構建請求負載const payload = {inputs: {doctor: aiParams.value.userName || '',workunit: aiParams.value.hospital || '',dept: aiParams.value.department,role: aiParams.value.roleType,MedicalFieldDescription: aiParams.value.medicalFieldDescription,contentType: aiParams.value.contentType,isCover: aiParams.value.isCover ? 1 : 0,isSearch: aiParams.value.isSearch ? 1 : 0,},promptId: promptId.value.toString(),// message: "內容生成",// query: JSON.stringify(da || aiParams.value.sprContent),query: JSON.stringify(aiParams.value.sprContent),response_mode: 'streaming',stream: true,user: `miAPP-${authStore.userInfo.userId}-${authStore.userInfo.userName}`,}controller.value = createDifyStream(payload, {onPartialAnswer: (frag: string) => {// console.log("onPartialAnswer-------");if (!frag || frag === lastChunk.value) returnlastChunk.value = fragconst cleanFrag = frag.replace(/[\x00-\x08\v\f\x0E-\x1F\x7F-\x9F]/g, '')if (!cleanFrag) returnpendingText.value += cleanFragconst newLen = generatedText.value.length + pendingText.value.lengthif (newLen <= lastContentLength.value) returnlastContentLength.value = newLenif (!isTyping.value) startTyping()},onFinish: () => {// console.log("onFinish-------");stopTyping()isGenerating.value = falseisStreamEnded.value = trueshowOverlayFlag.value = false},onError: (err: Error) => {// console.log("onError-------");// console.error(err);stopTyping()isGenerating.value = falseshowOverlayFlag.value = false},})
}// ========== 提交 ==========
async function submit() {if (isFastClick(500)) return console.warn('?? 防止快速點擊,跳過提交')toast.loading('加載中', loadingProp)// 文章流程需要限制字數超過1500 視頻根據生成內容來if (pageType.value === 1 && textContent.value.length < 1500) {toast.hide()tipType.value = 1return (showTipPropFlag.value = true)}const data: any = {name: TDK.value.title || contentTitle.value,content: generatedText.value,sourceId: sprId.value,promptId: Number(promptId.value),}if (TDK.value) {data.name = TDK.value.titledata.title = TDK.value.titledata.seoTitle = TDK.value.titledata.seoKeywords = TDK.value.keywordsdata.seoDescription = TDK.value.descriptiondata.imageUrl = TDK.value.coverdata.seoAnalysis = TDK.value.seo_analysis}// console.log("📝 提交數據:", JSON.stringify(data, null, 2));// 調用就提交接口處理await submitFn(data).queryFn()
}// ========== 組件銷毀 ==========
onBeforeUnmount(() => {console.log('🧹 組件銷毀開始')if (controller.value) {console.log('🛑 中止 SSE 流請求')controller.value.abort()controller.value = null}stopTyping() // 確保清理打字機console.log('🧹 打字機清理完成')console.log('? AI生成組件已銷毀')
})
</script><template><div class="page h-screen flex flex-col justify-between bg-white"><Navbar title="ai生成" fixed :placeholder="true" backgroundColor="#FFFFFF" /><div class="flex-1 overflow-hidden bg-[#FEFEFE]"><scroll-view:scroll-top="scrollTop":scroll-y="true":scroll-into-view="scrollTarget":scroll-with-animation="true":show-scrollbar="false":enhanced="true":style="{ height: `${scrollViewHeight}px` }"class="border border-[#eee] rounded-[16rpx] bg-[#FCFCFC] p-[30rpx]"><div class="scroll-content"><div v-if="newGeneratedText" v-html="newGeneratedText"></div><div v-else-if="generatedText" v-html="generatedText"></div><div v-else class="text-[32rpx] text-[#888888]">AI 正在思考中...</div><divv-if="textContent.length"class="w-full min-h-[50rpx] text-[#85BFFB] text-[28rpx] mt-[20rpx]">共計<span>{{ textContent.length }}</span>字</div><div class="w-full h-[100rpx]"></div></div></scroll-view></div><div class="h-[158rpx] w-full"><divclass="fixed bottom-0 z-10 h-[158rpx] w-full flex items-center justify-between bg-white px-[38rpx]"><div class="flex flex-1 items-center justify-around"><div class="flex items-center" @click="startStreamRequest"><img:src="`https://ystcdn.venuertc.com/venue/app/static/2025-08-22/a054f4cb-e841-4989-af50-e3c14704eb91.png${imgQuality40}`"class="h-[36rpx] w-[36rpx]"/><span class="ml-[8rpx] text-[32rpx] text-[#38393C] font-500">重寫</span></div><!-- <div class="flex items-center" @click="editGenerateContent"><img:src="`https://ystcdn.venuertc.com/venue/app/static/2025-08-13/85e1b66b-4f0b-421a-9478-b4bc2e60d1a2.png${imgQuality40}`"class="w-[36rpx] h-[36rpx]"/><span class="text-[#38393C] text-[32rpx] font-500 ml-[8rpx]">編輯</span></div> --></div><divv-if="times"class="h-[96rpx] w-[392rpx] rounded-full bg-[#FFFFFF] text-center text-[#1089FF] font-600 leading-[96rpx] border-solid border-[2rpx] border-[#1089FF]">閱讀中{{ displayTime }}</div><divv-elseclass="h-[96rpx] w-[392rpx] rounded-full bg-[#1089FF] text-center text-white font-600 leading-[96rpx]"@click="submit"><span v-if="pageType === 1">保存提交</span><spanv-else-if="pageType === 1 && ['editArticle', 'editTask', 'reassignTask'].includes(operationType)">保存提交</span><span v-else-if="pageType === 2">保存提交</span><spanv-else-if="pageType === 2 && ['editVideo', 'editTask', 'reassignTask'].includes(operationType)">重新錄制</span></div></div></div><nut-overlay v-model:visible="showOverlayFlag" :z-index="2000" :close-on-click-overlay="false"><div class="h-full w-full flex items-center justify-center"><div class="flex flex-col items-center text-white"><img:src="`https://ystcdn.venuertc.com/venue/app/static/2025-08-14/35f2b461-940f-4f33-a97c-0d0d372512b3.gif${imgQuality40}`"class="h-[206rpx] w-[206rpx]"/><div>生成中,請耐心等待…</div><div class="mt-[46rpx] w-[514rpx] text-center">溫馨提示:醫學知識具有專業性,當前科普內容由人工智能輔助生成,需經醫療從業者二次核驗,建議結合專業診療意見綜合參考。</div></div></div></nut-overlay></div>
</template><style lang="scss" scoped>
/* 解決小程序和app滾動條的問題 *//* #ifdef MP-WEIXIN || APP-PLUS */
::-webkit-scrollbar {display: none;width: 0 !important;height: 0 !important;color: transparent;appearance: none;background: transparent;
}/* #endif *//* 解決H5 的問題 *//* #ifdef H5 */
uni-scroll-view .uni-scroll-view::-webkit-scrollbar {display: none;width: 0 !important;height: 0 !important;color: transparent;appearance: none;background: transparent;
}/* #endif *//* 修復內容高度不足時底部空白問題 */
.scroll-view {display: flex;flex-direction: column;min-height: 100%;& > div {flex: 1 0 auto;}/* 確保內容區域可伸縮 */.content-container {flex: 1;min-height: 100%;}
}
</style>
3.相關數據處理函數
/*** 將包含 <style> 的 HTML 字符串轉換為行內樣式* 嚴格保留原有標簽結構,不增不刪任何屬性,僅合并 style*/
export function transformToInlineStyleFragment(htmlContent: string): string {if (!htmlContent || typeof htmlContent !== "string") return "";// 1. 提取并解析 <style> 中的 CSS 規則const styleMap: Record<string, Record<string, string>> = {};const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;let styleMatch;while ((styleMatch = styleRegex.exec(htmlContent)) !== null) {const cssText = styleMatch[1];const cleanCss = cssText.replace(/\/\*[\s\S]*?\*\//g, ""); // 移除注釋const ruleRegex = /([^{]+)\{([^}]*)\}/g;let rule;while ((rule = ruleRegex.exec(cleanCss)) !== null) {const selectorStr = rule[1].trim();const declaration = rule[2].trim();const selectors = selectorStr.split(",").map((s) => s.trim());const styleObj: Record<string, string> = {};declaration.split(";").map((p) => p.trim()).filter((p) => p).forEach((prop) => {const [k, v] = prop.split(":").map((s) => s?.trim()).filter(Boolean) as [string, string];if (k && v) {styleObj[k] = v;}});selectors.forEach((sel) => {if (sel && sel !== "") {styleMap[sel] = { ...styleMap[sel], ...styleObj };}});}}// 2. 移除所有 <style> 標簽let tempHtml = htmlContent.replace(styleRegex, "");// 3. 匹配每一個 HTML 標簽(支持自閉合、屬性順序保留)const tagRegex = /<([a-zA-Z][a-zA-Z0-9:]*)([^>]*)>/g;tempHtml = tempHtml.replace(tagRegex,(fullMatch, tagName: string, attrs = "") => {const attrsTrimmed = attrs.trim();// 解析 id, class, styleconst idMatch = /id\s*=\s*"([^"]*)"/.exec(attrs);const classMatch = /class\s*=\s*"([^"]*)"/.exec(attrs);const styleMatch = /style\s*=\s*"([^"]*)"/.exec(attrs);const id = idMatch ? `#${idMatch[1]}` : null;const classes = classMatch? classMatch[1].split(/\s+/).filter(Boolean).map((c) => `.${c}`): [];const existingStyleText = styleMatch ? styleMatch[1] : "";// 構建最終 style 對象,按優先級合并const finalStyle: Record<string, string> = {};// 1. 通配符 *if (styleMap["*"]) Object.assign(finalStyle, styleMap["*"]);// 2. 標簽選擇器const lowerTagName = tagName.toLowerCase();if (styleMap[lowerTagName])Object.assign(finalStyle, styleMap[lowerTagName]);// 3. 類選擇器(按順序)classes.forEach((cls) => {if (styleMap[cls]) Object.assign(finalStyle, styleMap[cls]);});// 4. ID 選擇器(最高優先級之一)if (id && styleMap[id]) Object.assign(finalStyle, styleMap[id]);// 5. 原有行內樣式(最高優先級,覆蓋前面所有)if (existingStyleText) {existingStyleText.split(";").forEach((pair) => {const [k, v] = pair.split(":").map((s) => s.trim()).filter(Boolean) as [string, string];if (k && v) {finalStyle[k] = v;}});}// 生成新的 style 字符串(保留原始格式風格:k: v)const newStyleStr = Object.entries(finalStyle).map(([k, v]) => `${k}: ${v}`).join("; ").replace(/\s*;\s*/g, "; "); // 標準化空格// 重新構建屬性字符串(保留原始屬性順序)let newAttrs = attrsTrimmed;if (newStyleStr) {if (styleMatch) {// 替換原有 style 屬性(精確匹配)const styleAttrRegex = /style\s*=\s*"([^"]*)"/;newAttrs = newAttrs.replace(styleAttrRegex, `style="${newStyleStr}"`);} else {// 添加 style 屬性(放在最后)newAttrs = newAttrs + ` style="${newStyleStr}"`;}}// 返回完整標簽return `<${tagName} ${newAttrs.trim()}>`;});return tempHtml;
}/*** 純文本提取(支持可選的“開始輸出正文”標記)* - 若存在“開始輸出正文”,則從此處開始* - 自動去除 <style> 標簽及其后內容、參考文獻及之后內容* - 清理 HTML 標簽、純 emoji 行、多余空白*/
export function parseHealthContentByAngleBrackets(rawText: string): string {if (!rawText || typeof rawText !== "string") {return "";}let text = rawText;// 1. 統一并清理換行符和特殊符號text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/?/g, "\n").replace(/\n+/g, "\n");// 2. 可選:從“開始輸出正文”之后開始(若存在)const startMarker = "開始輸出正文";const startIndex = text.indexOf(startMarker);if (startIndex !== -1) {text = text.slice(startIndex + startMarker.length);} else {console.warn("未找到 '開始輸出正文' 標記,將處理全文",rawText.substring(0, 100) + "...");}// 3. 截斷:遇到 <style 或 </style> 就停止(不區分大小寫)const styleRegex = /<\s*(?:\/\s*)?style\b/i;const styleMatch = styleRegex.exec(text);if (styleMatch) {text = text.slice(0, styleMatch.index);}// 4. 去除“參考文獻”及之后的內容const refIndex = text.indexOf("參考文獻");if (refIndex !== -1) {text = text.slice(0, refIndex);}// 5. 去除所有 HTML 標簽:<xxx>、</xxx>、<xxx/> 等let plainText = text.replace(/<[^>]+>/g, "");// 6. 去除僅包含 emoji 的行plainText = plainText.split("\n").map((line) => line.trim()).filter((line) => {if (!line) return false;const noSpaces = line.replace(/\s/g, "");if (!noSpaces) return false;// 判斷是否全為 emoji(含組合符)const isOnlyEmoji = /^[\p{Extended_Pictographic}\u{200D}]+$/u.test(noSpaces);return !isOnlyEmoji;}).join(" "); // 合并為單行,用空格連接// 7. 清理多余空白const result = plainText.replace(/\s+/g, " ").trim();return result;
}/*** 從 HTML 內容中智能提取最可能的主標題文本(安全版,防卡死)** @param {string} html - HTML 字符串* @returns {string|null} 提取出的標題文本,未找到則返回 null*/
export function extractMainTitle(html: string): string | null {if (typeof html !== "string" || !html.trim()) {console.warn("Invalid HTML content");return null;}// ? 安全限制:截斷過長 HTML(防攻擊或性能問題)const MAX_LENGTH = 50_000;const truncatedHtml = html.length > MAX_LENGTH ? html.slice(0, MAX_LENGTH) : html;// ? 使用非貪婪但安全的正則,限制匹配范圍const tagPattern = /<(h1|h2|h3|h4|title|div|p|span)[^>]*?(?:class\s*=\s*["'][^"']*?(?:title|headline|heading|header|top)[^"']*?["'])?[^>]*>([^<]{1,200}?)<\/\1>/gi;const candidates = [];let match;let index = 0;// ? 防止無限循環:限制最大匹配次數const MAX_MATCHES = 50;while ((match = tagPattern.exec(truncatedHtml)) !== null && index++ < MAX_MATCHES) {const [, tag, text] = match;const classAttr = /class\s*=\s*["'][^"']*?(?:title|headline|heading|header|top)[^"']*?["']/i.test(match[0]);candidates.push({tag,text: text.trim(),hasTitleClass: classAttr,index: match.index,});}if (candidates.length === 0) {// 回退:取前 200 字的純文本(去標簽)return extractPlainTextFallback(truncatedHtml);}// 評分排序const scored = candidates.map((item, i) => {let score = 0;if (item.tag === 'h1') score += 40;else if (item.tag === 'h2') score += 30;else if (['h3', 'h4'].includes(item.tag)) score += 10;if (item.hasTitleClass) score += 20;const len = item.text.length;if (len >= 5 && len <= 100) score += 10;else if (len === 0) score -= 50;// 越靠前越好score += Math.max(0, 20 - i * 2);return { ...item, score };});scored.sort((a, b) => b.score - a.score);const best = scored[0];return best.score > 0 ? cleanText(best.text) : null;
}/*** 純文本回退策略:去除 HTML 標簽,取開頭有意義文本*/
function extractPlainTextFallback(html: string): string | null {// 去除標簽(安全方式)const plain = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();if (!plain) return null;// 取前幾句const firstSentence = plain.split(/[,,。.;]/)[0];if (firstSentence.length >= 5 && firstSentence.length <= 100) {return cleanText(firstSentence);}return cleanText(plain.substring(0, 50));
}/*** 清理文本:去空格、轉義字符等*/
function cleanText(text: string): string {return text.replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
}/*** 解析 HTML 并提取疑似標題的候選元素*/
function findTitleCandidates(html: string) {const candidates = [];const tagMatchRegex =/<([a-zA-Z]+)([^>]*)>([^<]*<[^>]+>[^<]*)*[^<]*<\/\1>|<([a-zA-Z]+)([^>]*)\s*\/>/g;const selfClosingTags = new Set(["br", "hr", "img", "input", "meta", "link"]);let match;while ((match = tagMatchRegex.exec(html)) !== null) {const full = match[0];const tag = match[1] || match[4]; // 匹配開始標簽名const attrs = match[2] || match[5] || "";const innerHTML = match[3] || "";// 跳過自閉合標簽if (selfClosingTags.has(tag.toLowerCase())) continue;// 提取 class 屬性const classMatch = attrs.match(/class\s*=\s*["']([^"']*)["']/i);const className = classMatch ? classMatch[1] : "";const text = extractTextContent(innerHTML).trim();// 只保留可能為標題的標簽或含關鍵詞 classconst isHeadingTag = /^h[1-6]$/i.test(tag);const hasTitleClass = /\b(title|headline|heading|header)\b/i.test(className);if (isHeadingTag || hasTitleClass) {candidates.push({tag: tag.toLowerCase(),class: className,text,html: full,});}}return candidates;
}/*** 從 HTML 片段中提取純文本(去標簽)*/
function extractTextContent(html: string): string {return html.replace(/<[^>]+>/g, "").trim();
}/*** 回退方案:提取 HTML 中前幾個有意義的文本塊(用于無明確標題時)*/
function extractTopTextualContent(html: string): string | null {// 移除 script/styleconst plain = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");// 匹配塊級標簽中的文本const blockTags = ["p", "div", "h1", "h2", "h3", "section", "article", "li"];const parts: any = [];blockTags.forEach((tag) => {const regex = new RegExp(`<${tag}[^>]*>([^<]+)<\\/${tag}>`, "gi");let m;while ((m = regex.exec(plain)) !== null) {const text = cleanText(m[1]);if (text.length > 5 && text.length < 150) {parts.push({ text, index: m.index });}}});// 按出現位置排序,取最前面的parts.sort((a: any, b: any) => a.index - b.index);return parts.length > 0 ? parts[0].text : null;
}/*** 解析數據 保留ai文章/腳本的標簽+樣式內容*/
export function parseContent(content: any) {// 1. 統一換行符:將 ? \r\n 替換為 \nconst normalized = content.replace(/?/g, "\n").replace(/\r\n/g, "\n");// 2. 定位 "TDK信息開始輸出" 標記const tdkMarker = "TDK信息開始輸出";const tdkIndex = normalized.indexOf(tdkMarker);if (tdkIndex === -1) {console.log("未匹配到'TDK信息開始輸出'------");return {tdk: null,html: content,};}// 3. 提取 TDK 之后的內容,用于解析 JSONconst afterTdk = normalized.slice(tdkIndex + tdkMarker.length).trim();// 4. 找到第一個 '{' 開始提取 JSONconst jsonStartIdx = afterTdk.indexOf("{");if (jsonStartIdx === -1) {console.log("未找到 JSON 起始符 {------");return {tdk: null,html: content,};}let jsonString = "";let braceCount = 0;const chars = afterTdk.substring(jsonStartIdx);for (let i = 0; i < chars.length; i++) {const char = chars[i];jsonString += char;if (char === "{") braceCount++;if (char === "}") braceCount--;if (braceCount === 0) break; // 完整閉合}if (braceCount !== 0) {console.log("JSON 括號未閉合-----");return {tdk: null,html: content,};}// 清理并解析 JSONconst cleanedJson = jsonString.replace(/\n/g, " ").replace(/\s+/g, " ").replace(/,\s*\}/g, "}").replace(/,\s*\]/g, "]").trim();let tdkData;try {tdkData = JSON.parse(cleanedJson);} catch (e: any) {console.error("JSON 解析失敗:", e.message);console.error("待解析字符串:", cleanedJson);console.log(`JSON 格式錯誤:${e.message}----`);return {tdk: null,html: content,};}// 5. 提取 TDK 之前的內容const beforeTdk = normalized.slice(0, tdkIndex);// 6. 找到第一個 '<' 的位置,只保留從這里開始的 HTML(包含 style 標簽)const firstLessThan = beforeTdk.indexOf("<");if (firstLessThan === -1) {console.log("未找到 HTML 起始標簽 <-----");return {tdk: null,html: content,};}const htmlWithStyle = beforeTdk.slice(firstLessThan).trim(); // 包含完整的 HTML 和 <style>...</style>// 7. 返回結果:tdk + 合并后的完整 HTML(含 style 標簽)return {tdk: tdkData,html: htmlWithStyle, // ? 包含 <style> 和 </style> 的完整 HTML};
}