| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725 |
- <template>
- <div class="diagnosis-report-page" v-loading="loading">
- <div class="diagnosis-header">
- <div class="diagnosis-header__title-row">
- <span class="diagnosis-header__title">种植报告</span>
- <img class="diagnosis-header__icon" src="@/assets/img/report/star.png" alt="" />
- </div>
- <div class="diagnosis-header__desc">{{ headerDesc }}</div>
- <div class="diagnosis-header__base">{{ headerBase }}</div>
- </div>
- <div class="diagnosis-content">
- <div class="variety-bar">
- <div class="variety-bar__tags">
- <div v-for="item in varietyList" :key="item.id" class="variety-bar__tag"
- :class="{ active: item.id === activeVarietyId }" @click="activeVarietyId = item.id">
- {{ item.name }}
- </div>
- </div>
- <div class="variety-bar__add">
- <el-icon>
- <Plus />
- </el-icon>
- <span>新增品种</span>
- </div>
- </div>
- <div class="base-data">
- <div class="base-data__title">
- <img class="base-data__icon" src="@/assets/img/report/base-icon.png" alt="" />
- <span>基本信息</span>
- </div>
- <div class="base-data__body">
- <div v-if="!loading && !baseInfoList.length" class="base-data__empty">暂无基本信息</div>
- <div v-for="item in baseInfoList" :key="item.key" class="base-info-item">
- <div class="base-info-item__row">
- <span class="base-info-item__label">{{ item.label }}</span>
- <span class="base-info-item__value">{{ item.value }}</span>
- <span v-if="item.actionInline" class="base-info-action"
- @click="handleBaseAction(item.actionInline.key)">
- {{ item.actionInline.text }}
- <el-icon><Edit /></el-icon>
- </span>
- </div>
- <div v-if="item.actionBlock" class="base-info-action base-info-action--block"
- @click="handleBaseAction(item.actionBlock.key)">
- {{ item.actionBlock.text }}
- <el-icon><Edit /></el-icon>
- </div>
- </div>
- </div>
- </div>
- <div class="report-module">
- <div class="report-tabs">
- <div
- v-for="tab in reportTabs"
- :key="tab.key"
- class="report-tabs__item"
- :class="{ active: tab.key === activeReportTab }"
- @click="activeReportTab = tab.key"
- >
- <img class="report-tabs__icon" :src="tab.icon" alt="" />
- <span class="report-tabs__label">{{ tab.label }}</span>
- </div>
- </div>
- <div class="report-panel">
- <div class="report-panel__title">{{ activeReportTabMeta.label }}</div>
- <div
- v-if="activeReportHtml"
- class="report-panel__body"
- v-html="activeReportHtml"
- ></div>
- <div v-else class="report-panel__empty">暂无报告内容</div>
- </div>
- </div>
- </div>
- <adjustPhenologyPopup ref="adjustPhenologyPopupRef" @confirm="handlePhenologyConfirm" />
- <restingDatePopup
- ref="restingDatePopupRef"
- @confirm="handleRestingDateConfirm"
- @switchGrowing="handleSwitchGrowing"
- />
- <editPlantPointPopup ref="editPlantPointPopupRef" @confirm="handlePlantPointConfirm" />
- <complete-farm-info-popup
- v-model:show="showCompleteFarmPopup"
- :image="completeFarmPopupImage"
- highlight-text="种植全析报告"
- @confirm="goCompleteFarmInfo"
- />
- <restore-planting-popup />
- </div>
- </template>
- <script setup>
- import { computed, onActivated, ref } from "vue";
- import { useRouter } from "vue-router";
- import { useStore } from "vuex";
- import { ElMessage } from "element-plus";
- import { Plus, Edit } from "@element-plus/icons-vue";
- import adjustPhenologyPopup from "../components/adjustPhenologyPopup.vue";
- import restingDatePopup from "../components/restingDatePopup.vue";
- import editPlantPointPopup from "../components/editPlantPointPopup.vue";
- import completeFarmInfoPopup from "@/components/popup/completeFarmInfoPopup.vue";
- import tab1Icon from "@/assets/img/report/tab-1.png";
- import tab2Icon from "@/assets/img/report/tab-2.png";
- import tab3Icon from "@/assets/img/report/tab-3.png";
- import tab4Icon from "@/assets/img/report/tab-4.png";
- import tab5Icon from "@/assets/img/report/tab-5.png";
- import completeFarmPopupImage from "@/assets/img/common/empty-bg-3.png";
- import restorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
- // ---------------------------------------------------------------------------
- // Props / Emits — 无
- // ---------------------------------------------------------------------------
- const router = useRouter();
- const store = useStore();
- // ---------------------------------------------------------------------------
- // 常量
- // ---------------------------------------------------------------------------
- /** module1_basic 字段标签 → 基本信息展示项(labels 含接口原文别名) */
- const BASE_FIELD_META = [
- { key: "crop", labels: ["作物品种", "种植作物"], label: "种植作物:", actionInline: { key: "changeVariety", text: "更改品种" } },
- { key: "trait", labels: ["品种特质", "品种优势"], label: "品种特质:" },
- { key: "phenology", labels: ["物候信息", "物候期"], label: "物候信息:", actionInline: { key: "adjustPhenology", text: "调整物候" } },
- { key: "soil", labels: ["土壤条件"], label: "土壤条件:", actionBlock: { key: "soilSense", text: "土壤感知" } },
- { key: "terrain", labels: ["地形条件"], label: "地形条件:", actionInline: { key: "editLocation", text: "修改定位" } },
- ];
- /** Tab key → report/check 返回字段 */
- const REPORT_TAB_FIELD = {
- weather: "module2_weather",
- pest: "module3_pest",
- quality: "module4_quality",
- input: "module5_input",
- manage: "module6_management",
- };
- const reportTabs = [
- { key: "weather", label: "气象风险", icon: tab1Icon },
- { key: "pest", label: "病虫风险", icon: tab2Icon },
- { key: "quality", label: "品质潜力", icon: tab3Icon },
- { key: "input", label: "必要投入", icon: tab4Icon },
- { key: "manage", label: "农事管理", icon: tab5Icon },
- ];
- // ---------------------------------------------------------------------------
- // 页面状态
- // ---------------------------------------------------------------------------
- const loading = ref(false);
- const varietyList = ref([]);
- const activeVarietyId = ref(null);
- const headerDesc = ref("");
- const headerBase = ref("");
- const baseInfoList = ref([]);
- /** 各 Tab 对应的 markdown 原文 */
- const moduleMarkdownMap = ref({});
- const activeReportTab = ref("weather");
- const adjustPhenologyPopupRef = ref(null);
- const restingDatePopupRef = ref(null);
- const editPlantPointPopupRef = ref(null);
- const showCompleteFarmPopup = ref(false); // 无农场时展示「完善农场信息」
- // ---------------------------------------------------------------------------
- // 计算属性
- // ---------------------------------------------------------------------------
- const activeReportTabMeta = computed(
- () => reportTabs.find((tab) => tab.key === activeReportTab.value) || reportTabs[0]
- );
- const activeReportHtml = computed(() => {
- const field = REPORT_TAB_FIELD[activeReportTab.value];
- const md = moduleMarkdownMap.value[field] || "";
- return md ? markdownToHtml(md) : "";
- });
- // ---------------------------------------------------------------------------
- // 工具函数
- // ---------------------------------------------------------------------------
- function getMiniUserId() {
- return store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID");
- }
- function getSelectedFarm() {
- try {
- return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
- } catch {
- return {};
- }
- }
- /** 解析 module1_basic:兼容 `**标签**:` 与纯文本 `标签:`(内容可跨行) */
- function parseBasicSections(markdown = "") {
- const text = String(markdown).replace(/\r\n/g, "\n").trim();
- if (!text) return {};
- const collect = (re) => {
- const matches = [...text.matchAll(re)];
- if (!matches.length) return null;
- const sections = {};
- matches.forEach((match, index) => {
- const label = String(match[1] || "").trim();
- const start = match.index + match[0].length;
- const end = index + 1 < matches.length ? matches[index + 1].index : text.length;
- if (label) sections[label] = text.slice(start, end).trim();
- });
- return sections;
- };
- // 1) markdown:**作物品种**:xxx
- const mdSections = collect(/\*\*([^*]+)\*\*[::]\s*/g);
- if (mdSections) return mdSections;
- // 2) 纯文本:作物品种:xxx(按已知标签切分,避免正文里的冒号误伤)
- const knownLabels = [...new Set(BASE_FIELD_META.flatMap((meta) => meta.labels))];
- const escaped = knownLabels
- .map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
- .join("|");
- if (!escaped) return {};
- return collect(new RegExp(`(?:^|\\n)(${escaped})[::]\\s*`, "g")) || {};
- }
- function findSectionValue(sections, labels) {
- for (const label of labels) {
- if (sections[label]) return sections[label];
- }
- return "";
- }
- function buildBaseInfoList(sections, report = {}) {
- return BASE_FIELD_META.map((meta) => {
- let value = findSectionValue(sections, meta.labels);
- // 种植作物兜底:接口顶层 crop / variety
- if (!value && meta.key === "crop") {
- const crop = report.crop || "";
- const variety = report.variety || "";
- value = [crop, variety].filter(Boolean).join("-");
- }
- return {
- key: meta.key,
- label: meta.label,
- value: value || "—",
- actionInline: meta.actionInline,
- actionBlock: meta.actionBlock,
- };
- });
- }
- /** 轻量 markdown → HTML(标题 / 粗体 / 列表) */
- function markdownToHtml(markdown = "") {
- const escapeHtml = (str) =>
- String(str)
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">");
- const lines = String(markdown).replace(/\r\n/g, "\n").split("\n");
- const html = [];
- let inList = false;
- const flushList = () => {
- if (!inList) return;
- html.push("</ul>");
- inList = false;
- };
- const inline = (str) =>
- escapeHtml(str).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
- lines.forEach((raw) => {
- const line = raw.trimEnd();
- const trimmed = line.trim();
- if (!trimmed) {
- flushList();
- return;
- }
- if (/^###\s+/.test(trimmed)) {
- flushList();
- html.push(`<h4>${inline(trimmed.replace(/^###\s+/, ""))}</h4>`);
- return;
- }
- if (/^##\s+/.test(trimmed)) {
- flushList();
- html.push(`<h3>${inline(trimmed.replace(/^##\s+/, ""))}</h3>`);
- return;
- }
- if (/^#\s+/.test(trimmed)) {
- flushList();
- html.push(`<h2>${inline(trimmed.replace(/^#\s+/, ""))}</h2>`);
- return;
- }
- if (/^[-*]\s+/.test(trimmed)) {
- if (!inList) {
- html.push("<ul>");
- inList = true;
- }
- html.push(`<li>${inline(trimmed.replace(/^[-*]\s+/, ""))}</li>`);
- return;
- }
- flushList();
- html.push(`<p>${inline(trimmed)}</p>`);
- });
- flushList();
- return html.join("");
- }
- function applyReportData(report, farm) {
- const region = report.region || farm.rawAddress || farm.farm_address || farm.city || "";
- const farmName = farm.farm_name || farm.name || "";
- headerDesc.value = region ? `${region}的飞鸟智慧农业` : "飞鸟智慧农业";
- headerBase.value = farmName || "我的基地";
- const cropName = report.variety || report.crop || farm.variety_name || farm.categoryLabel || "作物";
- varietyList.value = [{ id: report.id || 1, name: cropName }];
- activeVarietyId.value = varietyList.value[0].id;
- const sections = parseBasicSections(report.module1_basic || "");
- baseInfoList.value = buildBaseInfoList(sections, report);
- moduleMarkdownMap.value = {
- module2_weather: report.module2_weather || "",
- module3_pest: report.module3_pest || "",
- module4_quality: report.module4_quality || "",
- module5_input: report.module5_input || "",
- module6_management: report.module6_management || "",
- };
- }
- function resetReportView(farm = {}) {
- headerDesc.value = farm.rawAddress || farm.farm_address || farm.city
- ? `${farm.rawAddress || farm.farm_address || farm.city}的飞鸟智慧农业`
- : "飞鸟智慧农业";
- headerBase.value = farm.farm_name || farm.name || "我的基地";
- varietyList.value = [];
- activeVarietyId.value = null;
- baseInfoList.value = [];
- moduleMarkdownMap.value = {};
- }
- // ---------------------------------------------------------------------------
- // 业务逻辑:无农场弹窗 / 加载报告
- // ---------------------------------------------------------------------------
- /** 无果园信息时展示「完善农场信息」弹窗 */
- async function checkCompleteFarmPopup() {
- const id = getMiniUserId();
- if (!id) {
- showCompleteFarmPopup.value = false;
- return;
- }
- try {
- const res = await VE_API.questionnaire.getFarms({ id });
- const list = Array.isArray(res?.data) ? res.data : [];
- showCompleteFarmPopup.value = list.length === 0;
- } catch (e) {
- console.warn("[diagnosisReport] getFarms failed", e);
- showCompleteFarmPopup.value = false;
- }
- }
- async function fetchDiagnosisReport() {
- const farm = getSelectedFarm();
- const farmId = farm.farm_id ?? farm.id;
- const zoneId = farm.zone_id ?? farm.zoneId;
- if (farmId == null || farmId === "" || zoneId == null || zoneId === "") {
- resetReportView(farm);
- // ElMessage.warning("请先选择农场与分区");
- return;
- }
- loading.value = true;
- try {
- const raw = await VE_API.questionnaire.checkReportGenerated({
- farm_id: farmId,
- zone_id: zoneId,
- });
- const res = Array.isArray(raw) ? raw[0] : raw;
- const report = res?.data?.report;
- if (res?.code === 200 && report) {
- applyReportData(report, farm);
- return;
- }
- resetReportView(farm);
- ElMessage.warning(res?.msg || res?.message || "报告正在生成中...");
- } catch (error) {
- console.error("fetchDiagnosisReport failed", error);
- resetReportView(farm);
- ElMessage.error("报告加载失败");
- } finally {
- loading.value = false;
- }
- }
- // ---------------------------------------------------------------------------
- // 事件处理 / 生命周期
- // ---------------------------------------------------------------------------
- const handleBaseAction = (key) => {
- if (key === "adjustPhenology") {
- adjustPhenologyPopupRef.value?.open();
- return;
- }
- if (key === "editLocation") {
- editPlantPointPopupRef.value?.open();
- return;
- }
- if (key === "soilSense") {
- router.push("/soil_sense");
- return;
- }
- console.log("base action:", key);
- };
- const handlePhenologyConfirm = (payload) => {
- restingDatePopupRef.value?.open(payload);
- };
- const handleSwitchGrowing = () => {
- adjustPhenologyPopupRef.value?.open();
- };
- const handleRestingDateConfirm = (payload) => {
- console.log("resting date confirm:", payload);
- };
- const handlePlantPointConfirm = (payload) => {
- console.log("plant point confirm:", payload);
- };
- const goCompleteFarmInfo = () => {
- router.push("/entry_information");
- };
- // tabbar 切换时组件被 keep-alive 缓存,需在激活时重新拉报告 / 校验农场
- onActivated(() => {
- checkCompleteFarmPopup();
- fetchDiagnosisReport();
- });
- </script>
- <style lang="scss" scoped>
- .diagnosis-report-page {
- width: 100%;
- height: calc(100vh - 50px);
- overflow: auto;
- -webkit-overflow-scrolling: touch;
- background: #f5f6fe;
- box-sizing: border-box;
- padding-bottom: 20px;
- .diagnosis-header {
- height: 148px;
- padding: 26px 0 0 14px;
- box-sizing: border-box;
- background: url("@/assets/img/report/title-bg.png") no-repeat center center / 100% 100%;
- color: #fff;
- &__title-row {
- display: flex;
- align-items: center;
- gap: 6px;
- }
- &__title {
- font-size: 35px;
- font-family: "pangmenzhengdao";
- }
- &__icon {
- width: 24px;
- height: 24px;
- }
- }
- .diagnosis-content {
- padding: 0 10px;
- .variety-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 6px;
- background: #fff;
- border-radius: 8px;
- margin-top: -20px;
- &__tags {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 6px;
- }
- &__tag {
- padding: 5px 16px;
- border-radius: 4px;
- background: #f2f3f5;
- color: #4e5969;
- &.active {
- background: #2199f8;
- color: #fff;
- }
- }
- &__add {
- display: flex;
- align-items: center;
- gap: 2px;
- padding: 5px 10px;
- border-radius: 4px;
- border: 1px solid #2199f8;
- background: rgba(33, 153, 248, 0.1);
- color: #2199f8;
- }
- }
- .base-data {
- margin: 10px 0;
- padding: 5px;
- // min-height: 382px;
- min-height: auto;
- background: url("@/assets/img/report/base-bg.png") no-repeat center top / 100% 100%;
- box-sizing: border-box;
- &__title {
- display: flex;
- align-items: center;
- gap: 5px;
- font-size: 17px;
- color: #fff;
- margin: -3px 0 0 -3px;
- }
- &__icon {
- width: 44px;
- height: 44px;
- }
- &__title>span {
- margin-top: -4px;
- }
- &__body {
- margin-top: -8px;
- padding: 10px;
- background: #fff;
- border-radius: 8px;
- .base-data__empty {
- padding: 20px 0;
- text-align: center;
- color: #999;
- font-size: 13px;
- }
- .base-info-item {
- margin-bottom: 8px;
- &__row {
- color: #333;
- }
- &__label {
- color: #999;
- }
- &__value {
- color: #333;
- }
- }
- .base-info-action {
- display: inline-flex;
- align-items: center;
- gap: 2px;
- margin-left: 6px;
- padding: 2px 8px;
- border-radius: 20px;
- background: rgba(33, 153, 248, 0.1);
- color: #2199f8;
- font-size: 12px;
- .el-icon {
- font-size: 12px;
- }
- &--block {
- display: inline-flex;
- margin: 8px 0 0;
- }
- }
- }
- }
- .report-module {
- margin-top: 4px;
- }
- .report-tabs {
- display: flex;
- align-items: stretch;
- gap: 8px;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- scrollbar-width: none;
- &::-webkit-scrollbar {
- display: none;
- }
- &__item {
- flex: 0 0 calc((100% - 24px) / 4);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 5px 5px 14px;
- box-sizing: border-box;
- color: #ADADAD;
- background: url("@/assets/img/report/tab-bg.png") no-repeat center center / 100% 100%;
- &.active {
- background: url("@/assets/img/report/tab-act-bg.png") no-repeat center center / 100% 100%;
- &::after {
- opacity: 1;
- }
- .report-tabs__icon {
- filter: none;
- opacity: 1;
- }
- }
- }
- &__icon {
- width: 22px;
- height: 22px;
- filter: grayscale(1);
- opacity: 0.45;
- }
- }
- .report-panel {
- margin-top: 10px;
- padding: 16px 14px 18px;
- background: #fff;
- border-radius: 10px;
- &__title {
- position: relative;
- display: inline-block;
- font-size: 18px;
- font-weight: 600;
- color: #1d2129;
- line-height: 1.3;
- z-index: 1;
- &::after {
- content: "";
- position: absolute;
- left: 0;
- bottom: 2px;
- width: 36px;
- height: 8px;
- border-radius: 8px;
- background: linear-gradient(90deg, #8ed0ff 0%, rgba(142, 208, 255, 0.35) 100%);
- z-index: -1;
- }
- }
- &__empty {
- margin-top: 14px;
- padding: 24px 0;
- text-align: center;
- color: #999;
- font-size: 13px;
- }
- &__body {
- margin-top: 12px;
- color: #333;
- font-size: 14px;
- line-height: 1.7;
- word-break: break-word;
- :deep(h2) {
- margin: 0 0 10px;
- font-size: 16px;
- font-weight: 600;
- color: #1d2129;
- }
- :deep(h3) {
- margin: 14px 0 8px;
- font-size: 15px;
- font-weight: 600;
- color: #1d2129;
- }
- :deep(h4) {
- margin: 12px 0 6px;
- font-size: 14px;
- font-weight: 600;
- color: #4e5969;
- }
- :deep(p) {
- margin: 0 0 8px;
- }
- :deep(ul) {
- margin: 0 0 10px;
- padding-left: 18px;
- }
- :deep(li) {
- margin-bottom: 4px;
- }
- :deep(strong) {
- font-weight: 600;
- color: #1d2129;
- }
- }
- }
- }
- }
- </style>
|