diagnosisReport.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. <template>
  2. <div class="diagnosis-report-page" v-loading="loading">
  3. <div class="diagnosis-header">
  4. <div class="diagnosis-header__title-row">
  5. <span class="diagnosis-header__title">种植报告</span>
  6. <img class="diagnosis-header__icon" src="@/assets/img/report/star.png" alt="" />
  7. </div>
  8. <div class="diagnosis-header__desc">{{ headerDesc }}</div>
  9. <div class="diagnosis-header__base">{{ headerBase }}</div>
  10. </div>
  11. <div class="diagnosis-content">
  12. <div class="variety-bar">
  13. <div class="variety-bar__tags">
  14. <div v-for="item in varietyList" :key="item.id" class="variety-bar__tag"
  15. :class="{ active: item.id === activeVarietyId }" @click="activeVarietyId = item.id">
  16. {{ item.name }}
  17. </div>
  18. </div>
  19. <div class="variety-bar__add">
  20. <el-icon>
  21. <Plus />
  22. </el-icon>
  23. <span>新增品种</span>
  24. </div>
  25. </div>
  26. <div class="base-data">
  27. <div class="base-data__title">
  28. <img class="base-data__icon" src="@/assets/img/report/base-icon.png" alt="" />
  29. <span>基本信息</span>
  30. </div>
  31. <div class="base-data__body">
  32. <div v-if="!loading && !baseInfoList.length" class="base-data__empty">暂无基本信息</div>
  33. <div v-for="item in baseInfoList" :key="item.key" class="base-info-item">
  34. <div class="base-info-item__row">
  35. <span class="base-info-item__label">{{ item.label }}</span>
  36. <span class="base-info-item__value">{{ item.value }}</span>
  37. <span v-if="item.actionInline" class="base-info-action"
  38. @click="handleBaseAction(item.actionInline.key)">
  39. {{ item.actionInline.text }}
  40. <el-icon><Edit /></el-icon>
  41. </span>
  42. </div>
  43. <div v-if="item.actionBlock" class="base-info-action base-info-action--block"
  44. @click="handleBaseAction(item.actionBlock.key)">
  45. {{ item.actionBlock.text }}
  46. <el-icon><Edit /></el-icon>
  47. </div>
  48. </div>
  49. </div>
  50. </div>
  51. <div class="report-module">
  52. <div class="report-tabs">
  53. <div
  54. v-for="tab in reportTabs"
  55. :key="tab.key"
  56. class="report-tabs__item"
  57. :class="{ active: tab.key === activeReportTab }"
  58. @click="activeReportTab = tab.key"
  59. >
  60. <img class="report-tabs__icon" :src="tab.icon" alt="" />
  61. <span class="report-tabs__label">{{ tab.label }}</span>
  62. </div>
  63. </div>
  64. <div class="report-panel">
  65. <div class="report-panel__title">{{ activeReportTabMeta.label }}</div>
  66. <div
  67. v-if="activeReportHtml"
  68. class="report-panel__body"
  69. v-html="activeReportHtml"
  70. ></div>
  71. <div v-else class="report-panel__empty">暂无报告内容</div>
  72. </div>
  73. </div>
  74. </div>
  75. <adjustPhenologyPopup ref="adjustPhenologyPopupRef" @confirm="handlePhenologyConfirm" />
  76. <restingDatePopup
  77. ref="restingDatePopupRef"
  78. @confirm="handleRestingDateConfirm"
  79. @switchGrowing="handleSwitchGrowing"
  80. />
  81. <editPlantPointPopup ref="editPlantPointPopupRef" @confirm="handlePlantPointConfirm" />
  82. <complete-farm-info-popup
  83. v-model:show="showCompleteFarmPopup"
  84. :image="completeFarmPopupImage"
  85. highlight-text="种植全析报告"
  86. @confirm="goCompleteFarmInfo"
  87. />
  88. <restore-planting-popup />
  89. </div>
  90. </template>
  91. <script setup>
  92. import { computed, onActivated, ref } from "vue";
  93. import { useRouter } from "vue-router";
  94. import { useStore } from "vuex";
  95. import { ElMessage } from "element-plus";
  96. import { Plus, Edit } from "@element-plus/icons-vue";
  97. import adjustPhenologyPopup from "../components/adjustPhenologyPopup.vue";
  98. import restingDatePopup from "../components/restingDatePopup.vue";
  99. import editPlantPointPopup from "../components/editPlantPointPopup.vue";
  100. import completeFarmInfoPopup from "@/components/popup/completeFarmInfoPopup.vue";
  101. import tab1Icon from "@/assets/img/report/tab-1.png";
  102. import tab2Icon from "@/assets/img/report/tab-2.png";
  103. import tab3Icon from "@/assets/img/report/tab-3.png";
  104. import tab4Icon from "@/assets/img/report/tab-4.png";
  105. import tab5Icon from "@/assets/img/report/tab-5.png";
  106. import completeFarmPopupImage from "@/assets/img/common/empty-bg-3.png";
  107. import restorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
  108. // ---------------------------------------------------------------------------
  109. // Props / Emits — 无
  110. // ---------------------------------------------------------------------------
  111. const router = useRouter();
  112. const store = useStore();
  113. // ---------------------------------------------------------------------------
  114. // 常量
  115. // ---------------------------------------------------------------------------
  116. /** module1_basic 字段标签 → 基本信息展示项(labels 含接口原文别名) */
  117. const BASE_FIELD_META = [
  118. { key: "crop", labels: ["作物品种", "种植作物"], label: "种植作物:", actionInline: { key: "changeVariety", text: "更改品种" } },
  119. { key: "trait", labels: ["品种特质", "品种优势"], label: "品种特质:" },
  120. { key: "phenology", labels: ["物候信息", "物候期"], label: "物候信息:", actionInline: { key: "adjustPhenology", text: "调整物候" } },
  121. { key: "soil", labels: ["土壤条件"], label: "土壤条件:", actionBlock: { key: "soilSense", text: "土壤感知" } },
  122. { key: "terrain", labels: ["地形条件"], label: "地形条件:", actionInline: { key: "editLocation", text: "修改定位" } },
  123. ];
  124. /** Tab key → report/check 返回字段 */
  125. const REPORT_TAB_FIELD = {
  126. weather: "module2_weather",
  127. pest: "module3_pest",
  128. quality: "module4_quality",
  129. input: "module5_input",
  130. manage: "module6_management",
  131. };
  132. const reportTabs = [
  133. { key: "weather", label: "气象风险", icon: tab1Icon },
  134. { key: "pest", label: "病虫风险", icon: tab2Icon },
  135. { key: "quality", label: "品质潜力", icon: tab3Icon },
  136. { key: "input", label: "必要投入", icon: tab4Icon },
  137. { key: "manage", label: "农事管理", icon: tab5Icon },
  138. ];
  139. // ---------------------------------------------------------------------------
  140. // 页面状态
  141. // ---------------------------------------------------------------------------
  142. const loading = ref(false);
  143. const varietyList = ref([]);
  144. const activeVarietyId = ref(null);
  145. const headerDesc = ref("");
  146. const headerBase = ref("");
  147. const baseInfoList = ref([]);
  148. /** 各 Tab 对应的 markdown 原文 */
  149. const moduleMarkdownMap = ref({});
  150. const activeReportTab = ref("weather");
  151. const adjustPhenologyPopupRef = ref(null);
  152. const restingDatePopupRef = ref(null);
  153. const editPlantPointPopupRef = ref(null);
  154. const showCompleteFarmPopup = ref(false); // 无农场时展示「完善农场信息」
  155. // ---------------------------------------------------------------------------
  156. // 计算属性
  157. // ---------------------------------------------------------------------------
  158. const activeReportTabMeta = computed(
  159. () => reportTabs.find((tab) => tab.key === activeReportTab.value) || reportTabs[0]
  160. );
  161. const activeReportHtml = computed(() => {
  162. const field = REPORT_TAB_FIELD[activeReportTab.value];
  163. const md = moduleMarkdownMap.value[field] || "";
  164. return md ? markdownToHtml(md) : "";
  165. });
  166. // ---------------------------------------------------------------------------
  167. // 工具函数
  168. // ---------------------------------------------------------------------------
  169. function getMiniUserId() {
  170. return store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID");
  171. }
  172. function getSelectedFarm() {
  173. try {
  174. return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
  175. } catch {
  176. return {};
  177. }
  178. }
  179. /** 解析 module1_basic:兼容 `**标签**:` 与纯文本 `标签:`(内容可跨行) */
  180. function parseBasicSections(markdown = "") {
  181. const text = String(markdown).replace(/\r\n/g, "\n").trim();
  182. if (!text) return {};
  183. const collect = (re) => {
  184. const matches = [...text.matchAll(re)];
  185. if (!matches.length) return null;
  186. const sections = {};
  187. matches.forEach((match, index) => {
  188. const label = String(match[1] || "").trim();
  189. const start = match.index + match[0].length;
  190. const end = index + 1 < matches.length ? matches[index + 1].index : text.length;
  191. if (label) sections[label] = text.slice(start, end).trim();
  192. });
  193. return sections;
  194. };
  195. // 1) markdown:**作物品种**:xxx
  196. const mdSections = collect(/\*\*([^*]+)\*\*[::]\s*/g);
  197. if (mdSections) return mdSections;
  198. // 2) 纯文本:作物品种:xxx(按已知标签切分,避免正文里的冒号误伤)
  199. const knownLabels = [...new Set(BASE_FIELD_META.flatMap((meta) => meta.labels))];
  200. const escaped = knownLabels
  201. .map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
  202. .join("|");
  203. if (!escaped) return {};
  204. return collect(new RegExp(`(?:^|\\n)(${escaped})[::]\\s*`, "g")) || {};
  205. }
  206. function findSectionValue(sections, labels) {
  207. for (const label of labels) {
  208. if (sections[label]) return sections[label];
  209. }
  210. return "";
  211. }
  212. function buildBaseInfoList(sections, report = {}) {
  213. return BASE_FIELD_META.map((meta) => {
  214. let value = findSectionValue(sections, meta.labels);
  215. // 种植作物兜底:接口顶层 crop / variety
  216. if (!value && meta.key === "crop") {
  217. const crop = report.crop || "";
  218. const variety = report.variety || "";
  219. value = [crop, variety].filter(Boolean).join("-");
  220. }
  221. return {
  222. key: meta.key,
  223. label: meta.label,
  224. value: value || "—",
  225. actionInline: meta.actionInline,
  226. actionBlock: meta.actionBlock,
  227. };
  228. });
  229. }
  230. /** 轻量 markdown → HTML(标题 / 粗体 / 列表) */
  231. function markdownToHtml(markdown = "") {
  232. const escapeHtml = (str) =>
  233. String(str)
  234. .replace(/&/g, "&amp;")
  235. .replace(/</g, "&lt;")
  236. .replace(/>/g, "&gt;");
  237. const lines = String(markdown).replace(/\r\n/g, "\n").split("\n");
  238. const html = [];
  239. let inList = false;
  240. const flushList = () => {
  241. if (!inList) return;
  242. html.push("</ul>");
  243. inList = false;
  244. };
  245. const inline = (str) =>
  246. escapeHtml(str).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
  247. lines.forEach((raw) => {
  248. const line = raw.trimEnd();
  249. const trimmed = line.trim();
  250. if (!trimmed) {
  251. flushList();
  252. return;
  253. }
  254. if (/^###\s+/.test(trimmed)) {
  255. flushList();
  256. html.push(`<h4>${inline(trimmed.replace(/^###\s+/, ""))}</h4>`);
  257. return;
  258. }
  259. if (/^##\s+/.test(trimmed)) {
  260. flushList();
  261. html.push(`<h3>${inline(trimmed.replace(/^##\s+/, ""))}</h3>`);
  262. return;
  263. }
  264. if (/^#\s+/.test(trimmed)) {
  265. flushList();
  266. html.push(`<h2>${inline(trimmed.replace(/^#\s+/, ""))}</h2>`);
  267. return;
  268. }
  269. if (/^[-*]\s+/.test(trimmed)) {
  270. if (!inList) {
  271. html.push("<ul>");
  272. inList = true;
  273. }
  274. html.push(`<li>${inline(trimmed.replace(/^[-*]\s+/, ""))}</li>`);
  275. return;
  276. }
  277. flushList();
  278. html.push(`<p>${inline(trimmed)}</p>`);
  279. });
  280. flushList();
  281. return html.join("");
  282. }
  283. function applyReportData(report, farm) {
  284. const region = report.region || farm.rawAddress || farm.farm_address || farm.city || "";
  285. const farmName = farm.farm_name || farm.name || "";
  286. headerDesc.value = region ? `${region}的飞鸟智慧农业` : "飞鸟智慧农业";
  287. headerBase.value = farmName || "我的基地";
  288. const cropName = report.variety || report.crop || farm.variety_name || farm.categoryLabel || "作物";
  289. varietyList.value = [{ id: report.id || 1, name: cropName }];
  290. activeVarietyId.value = varietyList.value[0].id;
  291. const sections = parseBasicSections(report.module1_basic || "");
  292. baseInfoList.value = buildBaseInfoList(sections, report);
  293. moduleMarkdownMap.value = {
  294. module2_weather: report.module2_weather || "",
  295. module3_pest: report.module3_pest || "",
  296. module4_quality: report.module4_quality || "",
  297. module5_input: report.module5_input || "",
  298. module6_management: report.module6_management || "",
  299. };
  300. }
  301. function resetReportView(farm = {}) {
  302. headerDesc.value = farm.rawAddress || farm.farm_address || farm.city
  303. ? `${farm.rawAddress || farm.farm_address || farm.city}的飞鸟智慧农业`
  304. : "飞鸟智慧农业";
  305. headerBase.value = farm.farm_name || farm.name || "我的基地";
  306. varietyList.value = [];
  307. activeVarietyId.value = null;
  308. baseInfoList.value = [];
  309. moduleMarkdownMap.value = {};
  310. }
  311. // ---------------------------------------------------------------------------
  312. // 业务逻辑:无农场弹窗 / 加载报告
  313. // ---------------------------------------------------------------------------
  314. /** 无果园信息时展示「完善农场信息」弹窗 */
  315. async function checkCompleteFarmPopup() {
  316. const id = getMiniUserId();
  317. if (!id) {
  318. showCompleteFarmPopup.value = false;
  319. return;
  320. }
  321. try {
  322. const res = await VE_API.questionnaire.getFarms({ id });
  323. const list = Array.isArray(res?.data) ? res.data : [];
  324. showCompleteFarmPopup.value = list.length === 0;
  325. } catch (e) {
  326. console.warn("[diagnosisReport] getFarms failed", e);
  327. showCompleteFarmPopup.value = false;
  328. }
  329. }
  330. async function fetchDiagnosisReport() {
  331. const farm = getSelectedFarm();
  332. const farmId = farm.farm_id ?? farm.id;
  333. const zoneId = farm.zone_id ?? farm.zoneId;
  334. if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
  335. resetReportView(farm);
  336. // ElMessage.warning("请先选择农场与分区");
  337. return;
  338. }
  339. loading.value = true;
  340. try {
  341. const raw = await VE_API.questionnaire.checkReportGenerated({
  342. farm_id: farmId,
  343. zone_id: zoneId,
  344. });
  345. const res = Array.isArray(raw) ? raw[0] : raw;
  346. const report = res?.data?.report;
  347. if (res?.code === 200 && report) {
  348. applyReportData(report, farm);
  349. return;
  350. }
  351. resetReportView(farm);
  352. ElMessage.warning(res?.msg || res?.message || "报告正在生成中...");
  353. } catch (error) {
  354. console.error("fetchDiagnosisReport failed", error);
  355. resetReportView(farm);
  356. ElMessage.error("报告加载失败");
  357. } finally {
  358. loading.value = false;
  359. }
  360. }
  361. // ---------------------------------------------------------------------------
  362. // 事件处理 / 生命周期
  363. // ---------------------------------------------------------------------------
  364. const handleBaseAction = (key) => {
  365. if (key === "adjustPhenology") {
  366. adjustPhenologyPopupRef.value?.open();
  367. return;
  368. }
  369. if (key === "editLocation") {
  370. editPlantPointPopupRef.value?.open();
  371. return;
  372. }
  373. if (key === "soilSense") {
  374. router.push("/soil_sense");
  375. return;
  376. }
  377. console.log("base action:", key);
  378. };
  379. const handlePhenologyConfirm = (payload) => {
  380. restingDatePopupRef.value?.open(payload);
  381. };
  382. const handleSwitchGrowing = () => {
  383. adjustPhenologyPopupRef.value?.open();
  384. };
  385. const handleRestingDateConfirm = (payload) => {
  386. console.log("resting date confirm:", payload);
  387. };
  388. const handlePlantPointConfirm = (payload) => {
  389. console.log("plant point confirm:", payload);
  390. };
  391. const goCompleteFarmInfo = () => {
  392. router.push("/entry_information");
  393. };
  394. // tabbar 切换时组件被 keep-alive 缓存,需在激活时重新拉报告 / 校验农场
  395. onActivated(() => {
  396. checkCompleteFarmPopup();
  397. fetchDiagnosisReport();
  398. });
  399. </script>
  400. <style lang="scss" scoped>
  401. .diagnosis-report-page {
  402. width: 100%;
  403. height: calc(100vh - 50px);
  404. overflow: auto;
  405. -webkit-overflow-scrolling: touch;
  406. background: #f5f6fe;
  407. box-sizing: border-box;
  408. padding-bottom: 20px;
  409. .diagnosis-header {
  410. height: 148px;
  411. padding: 26px 0 0 14px;
  412. box-sizing: border-box;
  413. background: url("@/assets/img/report/title-bg.png") no-repeat center center / 100% 100%;
  414. color: #fff;
  415. &__title-row {
  416. display: flex;
  417. align-items: center;
  418. gap: 6px;
  419. }
  420. &__title {
  421. font-size: 35px;
  422. font-family: "pangmenzhengdao";
  423. }
  424. &__icon {
  425. width: 24px;
  426. height: 24px;
  427. }
  428. }
  429. .diagnosis-content {
  430. padding: 0 10px;
  431. .variety-bar {
  432. display: flex;
  433. align-items: center;
  434. justify-content: space-between;
  435. padding: 10px 6px;
  436. background: #fff;
  437. border-radius: 8px;
  438. margin-top: -20px;
  439. &__tags {
  440. display: flex;
  441. align-items: center;
  442. flex-wrap: wrap;
  443. gap: 6px;
  444. }
  445. &__tag {
  446. padding: 5px 16px;
  447. border-radius: 4px;
  448. background: #f2f3f5;
  449. color: #4e5969;
  450. &.active {
  451. background: #2199f8;
  452. color: #fff;
  453. }
  454. }
  455. &__add {
  456. display: flex;
  457. align-items: center;
  458. gap: 2px;
  459. padding: 5px 10px;
  460. border-radius: 4px;
  461. border: 1px solid #2199f8;
  462. background: rgba(33, 153, 248, 0.1);
  463. color: #2199f8;
  464. }
  465. }
  466. .base-data {
  467. margin: 10px 0;
  468. padding: 5px;
  469. // min-height: 382px;
  470. min-height: auto;
  471. background: url("@/assets/img/report/base-bg.png") no-repeat center top / 100% 100%;
  472. box-sizing: border-box;
  473. &__title {
  474. display: flex;
  475. align-items: center;
  476. gap: 5px;
  477. font-size: 17px;
  478. color: #fff;
  479. margin: -3px 0 0 -3px;
  480. }
  481. &__icon {
  482. width: 44px;
  483. height: 44px;
  484. }
  485. &__title>span {
  486. margin-top: -4px;
  487. }
  488. &__body {
  489. margin-top: -8px;
  490. padding: 10px;
  491. background: #fff;
  492. border-radius: 8px;
  493. .base-data__empty {
  494. padding: 20px 0;
  495. text-align: center;
  496. color: #999;
  497. font-size: 13px;
  498. }
  499. .base-info-item {
  500. margin-bottom: 8px;
  501. &__row {
  502. color: #333;
  503. }
  504. &__label {
  505. color: #999;
  506. }
  507. &__value {
  508. color: #333;
  509. }
  510. }
  511. .base-info-action {
  512. display: inline-flex;
  513. align-items: center;
  514. gap: 2px;
  515. margin-left: 6px;
  516. padding: 2px 8px;
  517. border-radius: 20px;
  518. background: rgba(33, 153, 248, 0.1);
  519. color: #2199f8;
  520. font-size: 12px;
  521. .el-icon {
  522. font-size: 12px;
  523. }
  524. &--block {
  525. display: inline-flex;
  526. margin: 8px 0 0;
  527. }
  528. }
  529. }
  530. }
  531. .report-module {
  532. margin-top: 4px;
  533. }
  534. .report-tabs {
  535. display: flex;
  536. align-items: stretch;
  537. gap: 8px;
  538. overflow-x: auto;
  539. -webkit-overflow-scrolling: touch;
  540. scrollbar-width: none;
  541. &::-webkit-scrollbar {
  542. display: none;
  543. }
  544. &__item {
  545. flex: 0 0 calc((100% - 24px) / 4);
  546. display: flex;
  547. flex-direction: column;
  548. align-items: center;
  549. justify-content: center;
  550. padding: 5px 5px 14px;
  551. box-sizing: border-box;
  552. color: #ADADAD;
  553. background: url("@/assets/img/report/tab-bg.png") no-repeat center center / 100% 100%;
  554. &.active {
  555. background: url("@/assets/img/report/tab-act-bg.png") no-repeat center center / 100% 100%;
  556. &::after {
  557. opacity: 1;
  558. }
  559. .report-tabs__icon {
  560. filter: none;
  561. opacity: 1;
  562. }
  563. }
  564. }
  565. &__icon {
  566. width: 22px;
  567. height: 22px;
  568. filter: grayscale(1);
  569. opacity: 0.45;
  570. }
  571. }
  572. .report-panel {
  573. margin-top: 10px;
  574. padding: 16px 14px 18px;
  575. background: #fff;
  576. border-radius: 10px;
  577. &__title {
  578. position: relative;
  579. display: inline-block;
  580. font-size: 18px;
  581. font-weight: 600;
  582. color: #1d2129;
  583. line-height: 1.3;
  584. z-index: 1;
  585. &::after {
  586. content: "";
  587. position: absolute;
  588. left: 0;
  589. bottom: 2px;
  590. width: 36px;
  591. height: 8px;
  592. border-radius: 8px;
  593. background: linear-gradient(90deg, #8ed0ff 0%, rgba(142, 208, 255, 0.35) 100%);
  594. z-index: -1;
  595. }
  596. }
  597. &__empty {
  598. margin-top: 14px;
  599. padding: 24px 0;
  600. text-align: center;
  601. color: #999;
  602. font-size: 13px;
  603. }
  604. &__body {
  605. margin-top: 12px;
  606. color: #333;
  607. font-size: 14px;
  608. line-height: 1.7;
  609. word-break: break-word;
  610. :deep(h2) {
  611. margin: 0 0 10px;
  612. font-size: 16px;
  613. font-weight: 600;
  614. color: #1d2129;
  615. }
  616. :deep(h3) {
  617. margin: 14px 0 8px;
  618. font-size: 15px;
  619. font-weight: 600;
  620. color: #1d2129;
  621. }
  622. :deep(h4) {
  623. margin: 12px 0 6px;
  624. font-size: 14px;
  625. font-weight: 600;
  626. color: #4e5969;
  627. }
  628. :deep(p) {
  629. margin: 0 0 8px;
  630. }
  631. :deep(ul) {
  632. margin: 0 0 10px;
  633. padding-left: 18px;
  634. }
  635. :deep(li) {
  636. margin-bottom: 4px;
  637. }
  638. :deep(strong) {
  639. font-weight: 600;
  640. color: #1d2129;
  641. }
  642. }
  643. }
  644. }
  645. }
  646. </style>