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};
}