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

发布于:2025-09-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

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 = null

const 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) // sprId

onLoad(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) return
    if (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] // selectViewport
    const header = res[0]
    const footer = res[1]

    const windowHeight = viewport.height
    const headerHeight = header ? header.height : 0
    const footerHeight = footer ? footer.height : ((158 * 2) / 750) * uni.upx2px(750) // 兜底 158rpx 转 px

    // 计算 scroll-view 高度(单位 px)
    const heightInPx = windowHeight - headerHeight - footerHeight

    // 转回 rpx 显示(可选),或直接用 px
    scrollViewHeight.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()
      // 兜底:如果还没结束,强制触发 onFinish
      if (!streamEnded) {
        streamEnded = true
        cb.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 += chunk
      processBuffer()
    })
  }

  function processBuffer() {
    const lines = buffer.split('\n')
    buffer = lines.pop() || '' // 保留未完成行

    for (const line of lines) {
      // console.log("SSE Line:", line); // 调试

      if (!line.startsWith('data:')) continue

      const dataStr = line.slice(5).trim()

      // [DONE] 表示流结束
      if (dataStr === '[DONE]') {
        if (!streamEnded) {
          streamEnded = true
          // console.log("Received [DONE] -> onFinish triggered");
          cb.onFinish?.()
        }
        continue
      }

      if (!dataStr) continue

      try {
        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+TDK
              const data = parseContent(generatedText.value)
              generatedText.value = data.html
              TDK.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.content
        if (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) return
  lastScrollTime.value = now
  nextTick(() => {
    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) return
  if (!pendingText.value) return

  isTyping.value = true
  typingTimer = setInterval(() => {
    if (!pendingText.value) {
      stopTyping()
      return
    }
    // 每次取 10~20 字符,减少 DOM 更新次数
    const chunk = pendingText.value.slice(0, 20)
    generatedText.value += chunk
    pendingText.value = pendingText.value.slice(20)
    scrollIfNeeded()
  }, 30)
}

// ========== 停止输入 ==========
function stopTyping() {
  // console.log("stopTyping-------");
  if (typingTimer) clearInterval(typingTimer)
  typingTimer = null
  if (pendingText.value) {
    generatedText.value += pendingText.value
    pendingText.value = ''
  }
  isTyping.value = false
  scrollIfNeeded()
  // console.log("stopTyping----end---");
}

// ========== 开始请求 ==========
async function startStreamRequest() {
  if (isGenerating.value) return
  isGenerating.value = true
  showOverlayFlag.value = true
  newGeneratedText.value = ''
  generatedText.value = ''
  pendingText.value = ''
  stopTyping()
  lastChunk.value = ''
  lastContentLength.value = 0
  textContent.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) return
      lastChunk.value = frag

      const cleanFrag = frag.replace(/[\x00-\x08\v\f\x0E-\x1F\x7F-\x9F]/g, '')
      if (!cleanFrag) return

      pendingText.value += cleanFrag
      const newLen = generatedText.value.length + pendingText.value.length
      if (newLen <= lastContentLength.value) return
      lastContentLength.value = newLen

      if (!isTyping.value) startTyping()
    },
    onFinish: () => {
      // console.log("onFinish-------");

      stopTyping()
      isGenerating.value = false
      isStreamEnded.value = true
      showOverlayFlag.value = false
    },
    onError: (err: Error) => {
      // console.log("onError-------");

      // console.error(err);
      stopTyping()
      isGenerating.value = false
      showOverlayFlag.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 = 1
    return (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.title
    data.title = TDK.value.title
    data.seoTitle = TDK.value.title
    data.seoKeywords = TDK.value.keywords
    data.seoDescription = TDK.value.description
    data.imageUrl = TDK.value.cover
    data.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>

          <div
            v-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">
      <div
        class="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>
        <div
          v-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>
        <div
          v-else
          class="h-[96rpx] w-[392rpx] rounded-full bg-[#1089FF] text-center text-white font-600 leading-[96rpx]"
          @click="submit"
        >
          <span v-if="pageType === 1">保存提交</span>
          <span
            v-else-if="
              pageType === 1 && ['editArticle', 'editTask', 'reassignTask'].includes(operationType)
            "
            >保存提交</span
          >

          <span v-else-if="pageType === 2">保存提交</span>
          <span
            v-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, style
      const 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();

    // 只保留可能为标题的标签或含关键词 class
    const 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/style
  const 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 替换为 \n
  const 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 之后的内容,用于解析 JSON
  const afterTdk = normalized.slice(tdkIndex + tdkMarker.length).trim();

  // 4. 找到第一个 '{' 开始提取 JSON
  const 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,
    };
  }

  // 清理并解析 JSON
  const 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
  };
}


网站公告

今日签到

点亮在社区的每一天
去签到