ai生成文章,流式傳輸(uniapp,微信小程序)

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(/&nbsp;/g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&amp;/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};
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/921931.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/921931.shtml
英文地址,請注明出處:http://en.pswp.cn/news/921931.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux設備內存不足如何處理

[rootlocalhost ~]# free -mtotal used free shared buff/cache available Mem: 31208 14317 1280 1551 15610 14657 Swap: 15927 2781 13146 [rootlocalhost ~]#從 free -m 輸出來看&…

中間件八股

文章目錄RedisRedis為什么快&#xff1f;Redis Redis為什么快&#xff1f; 首先它是內存數據庫&#xff0c;所有數據直接操作內存而非磁盤&#xff0c;避免了 I/O 瓶頸&#xff1b;其次采用單線程模型&#xff0c;消除了多線程切換的開銷&#xff0c;同時通過非阻塞 I/O 多路…

【參數詳解與使用指南】PyTorch MNIST數據集加載

# 加載MNIST數據集 train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) # 下載訓練集 test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # 下載測試集在深度學習入門過程中&#xff0c;MNIST手…

閉包面試題

閉包&#xff08;Closure&#xff09; 是指一個函數能夠記住并訪問其詞法作用域&#xff08;定義時的作用域&#xff09;&#xff0c;即使該函數在其詞法作用域之外執行。一、通俗理解&#xff08;面試可這樣開頭&#xff09;&#xff1a;> 閉包就是一個函數“記住”了它出生…

WebSocket 雙向通信實戰:SCADA 移動端實時操控響應優化

引言&#xff1a;SCADA 移動端的 “延遲煩惱” 與破局之道在電力調度、水廠監控、智能制造等場景中&#xff0c;SCADA 系統&#xff08;數據采集與監視控制系統&#xff09;是當之無愧的 “工業指揮官”—— 它能實時采集設備運行數據&#xff08;如電網負荷、水泵壓力、機床轉…

SafeEar:浙大和清華聯合推出的AI音頻偽造檢測框架,錯誤率低至2.02%

本文轉載自&#xff1a;https://www.hello123.com/safeear ** 一、&#x1f512; SafeEar&#xff1a;你的聲音 “防火墻”&#xff0c;讓 AI 偽造音頻無所遁形 擔心自己的聲音被 AI 模仿甚至偽造&#xff1f;SafeEar就是來幫你解決這個難題的&#xff01;它是由浙江大學和清…

uni-app iOS 日志與崩潰分析全流程 多工具協作的實戰指南

在 uni-app 跨平臺開發中&#xff0c;iOS 應用的日志與崩潰分析往往是開發者最頭疼的問題。 日志分散&#xff1a;uni-app 的 JS 日志、原生插件日志、系統日志分布在不同位置&#xff1b;崩潰難復現&#xff1a;用戶反饋的崩潰往往無法在開發機還原&#xff1b;符號化復雜&…

CSS定義網格的列模板grid-template-columns什么意思,為什么要用這么復雜的單詞

這個詞確實看起來復雜&#xff0c;但其實很好理解。讓我來拆解一下&#xff1a;單詞分解grid-template-columns grid - 網格template - 模板columns - 列連起來就是&#xff1a;網格模板列 → 定義網格的列模板為什么要用這么長的單詞&#xff1f;語義明確&#xff1a;長單詞能…

Umi-OCR:Windows7和Linux上可免費離線使用的OCR應用!

工具介紹 Umi-OCR 是一款免費、開源的離線OCR軟件&#xff0c;主要由作者 hiroi-sora 用業余時間在開發和維護。 Umi-OCR 內置多國語言庫&#xff0c;支持截屏/批量導入圖片&#xff0c;PDF文檔識別&#xff0c;排除水印/頁眉頁腳以及二維碼的掃描/生成。 適用平臺&#xff1…

30 分鐘讓 AI 開口查訂單:React-Native + Coze 全鏈路語音對話落地指南

一、前言&#xff1a;為什么你需要“可說話、能查庫”的 AI&#xff1f; 聊天機器人在 2025 已不新鮮&#xff0c;但**“張嘴就能查詢私有業務數據”**的端到端方案依然踩坑無數&#xff1a; ASR/TTS 選型多、SDK 難對齊大模型與內部 API 安全打通RN 端流式渲染 音頻播放并發…

玄機--應急響應--webshell查殺

靶場連接1.黑客webshell里面的flag flag{xxxxx-xxxx-xxxx-xxxx-xxxx}使用命令查找特殊文件//搜索目錄下適配當前應用的網頁文件&#xff0c;查看內容是否有Webshell特征 find ./ type f -name "*.jsp" -exec grep -l "exec(" {} \; find ./ type f -name &…

Nodejs讀取目錄下面的文件

需求&#xff1a;給定一個目錄&#xff0c;讀取該目錄下面的所有文件&#xff0c;包括該目錄下面文件夾里面的子文件&#xff0c;子子文件......const fs require(fs);const path require(path);// 指定要遍歷的目錄const directoryPath D:\\;//調用函數入口處readDir(direc…

PPTist,一個完全免費的 AI 生成 PPT 在線網站

PPTist&#xff0c;一個完全免費的 AI 生成 PPT 在線網站 PPTist 是一個完全免費的 AI 生成 PPT 在線網站、PPT 在線演示網站、PPT 在線編輯網站。 它完全免費&#xff0c;無需登錄注冊&#xff0c;支持 AI 生成 PPT 功能&#xff0c;可以一句話生成 PPT &#xff0c;支持輸入…

C++中操作重載與類型轉換

文章目錄基本概念調用選擇作為成員還是非成員輸入和輸出運算符算術和關系運算符相等和不等運算符賦值運算符下標運算符遞增和遞減運算符成員訪問運算符函數調用運算符lambda是函數對象標準庫定義的函數對象可調用對象與function重載、類型轉換與運算符類型轉換運算符避免有二義…

Java學習之——“IO流“的進階流之轉換流的學習

在博主的上一篇博文中&#xff0c;詳細的介紹了“IO”流中最基本的一些知識&#xff0c;包括基本的常見的字節流和字符流&#xff0c;以及對應的緩沖流&#xff0c;對于“IO”流基礎知識相對薄弱的同學可以先去看博主的上一篇博文Java學習之——萬字詳解“IO流”中基本的字節流…

PMP考試結構、學習框架與基本術語

一、PMP考試整體結構 考試基本信息 考試形式&#xff1a;紙筆考試&#xff08;中國大陸地區&#xff09;考試時長&#xff1a;230分鐘&#xff08;約4小時&#xff09;題目數量&#xff1a;180道題 170道單選題&#xff08;四選一&#xff09;10道多選題包含5道非計分的試驗題…

淺談前端框架

在 Web 開發的演進過程中&#xff0c;前端框架扮演著越來越重要的角色。從早期的 jQuery 到如今的 React、Vue、Svelte 等&#xff0c;前端開發模式發生了翻天覆地的變化。本文將從前端框架的定義、核心特性、分類以及主流框架的差異等方面&#xff0c;帶你深入理解前端框架。 …

10.3 馬爾可夫矩陣、人口和經濟

本節內容是關于正矩陣&#xff08;postive matrices&#xff09;&#xff1a; 每個元素 aij>0a_{ij}>0aij?>0&#xff0c;它核心的結論是&#xff1a;最大的特征值為正實數&#xff0c;其對應的特征向量也是如此。 在經濟學、生態學、人口動力系統和隨機游走過程中都…

python學習進階之面向對象(二)

文章目錄 1.面向對象編程介紹 2.面向對象基本語法 3.面向對象的三大特征 4.面向對象其他語法 1.面向對象編程介紹 1.1 基本概念 概念:面向對象編程(Object-Oriented Programming, OOP)是一種流行的編程范式,它以"對象"為核心組織代碼和數據 在面向對象的世界里: …

VS+QT的編程開發工作:關于QT VS tools的使用 qt的官方幫助

加粗樣式 最近的工作用到VS2022QT5.9.9/QT5.12.9&#xff0c;在查找相關資料的時候&#xff0c;發現Qt 官方的資料還是很不錯的&#xff0c;特記錄下來&#xff0c;要記得抽時間學習下。 Add Qt versions https://doc.qt.io/qtvstools/qtvstools-how-to-add-qt-versions.html B…