|
@@ -4,22 +4,53 @@
|
|
|
:anchors="anchors">
|
|
:anchors="anchors">
|
|
|
<div class="file-float-content">
|
|
<div class="file-float-content">
|
|
|
<div class="float-tabs">
|
|
<div class="float-tabs">
|
|
|
- <div class="tab-active-bg" :style="activeBgStyle"></div>
|
|
|
|
|
|
|
+ <div class="tab-active-bg" :style="primaryActiveBgStyle"></div>
|
|
|
<div v-for="(item, index) in floatTabLabels" :key="item.value" class="tab-item"
|
|
<div v-for="(item, index) in floatTabLabels" :key="item.value" class="tab-item"
|
|
|
- @click="changeTab(item, index)" :class="{ 'tab-item-active': activeTab === index }">
|
|
|
|
|
|
|
+ @click="changePrimaryTab(index)" :class="{ 'tab-item-active': activeTab === index }">
|
|
|
{{ item.title }}
|
|
{{ item.title }}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
<div class="tab-content-group" v-show="height !== anchors[0]">
|
|
<div class="tab-content-group" v-show="height !== anchors[0]">
|
|
|
- <div class="tab-loading" v-if="loading">{{ t('agriFile.loading') }}</div>
|
|
|
|
|
- <div class="tab-empty" v-else-if="currentList.length === 0">{{ t('agriFile.noRecord') }}</div>
|
|
|
|
|
- <div v-else class="tab-content-item" v-for="item in displayList" :key="item.id">
|
|
|
|
|
- <div class="time-tag">{{ item.time }}</div>
|
|
|
|
|
- <div class="item-info">
|
|
|
|
|
- {{ item.recordText }}
|
|
|
|
|
- <span class="blue-text">{{ item.ratio }}{{ item.showRatio ? '%' : '' }}</span>
|
|
|
|
|
|
|
+ <template v-if="isAgriRecordTab">
|
|
|
|
|
+ <div class="float-sub-tabs">
|
|
|
|
|
+ <div v-for="(item, index) in agriSubTabLabels" :key="item.value" class="sub-tab-item"
|
|
|
|
|
+ :class="{ 'sub-tab-item-active': activeSubTab === index }" @click="changeSubTab(index)">
|
|
|
|
|
+ {{ item.title }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tab-loading" v-if="loading">{{ t('agriFile.loading') }}</div>
|
|
|
|
|
+ <div class="tab-empty" v-else-if="currentList.length === 0">{{ t('agriFile.noData') }}</div>
|
|
|
|
|
+ <div v-else class="tab-content-item" v-for="item in displayList" :key="item.id">
|
|
|
|
|
+ <div class="time-tag">{{ item.time }}</div>
|
|
|
|
|
+ <div class="item-info">
|
|
|
|
|
+ {{ item.recordText }}
|
|
|
|
|
+ <span class="blue-text">{{ item.ratio }}{{ item.showRatio ? '%' : '' }}</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <div v-else-if="isRemoteSensingTab" class="remote-sensing-chart">
|
|
|
|
|
+ <div class="remote-sensing-chart__header">
|
|
|
|
|
+ <span class="remote-sensing-chart__title">{{ t('agriFile.remoteSensingChartTitle') }}</span>
|
|
|
|
|
+ <div class="remote-sensing-chart__legend">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="item in remoteSensingLegendItems"
|
|
|
|
|
+ :key="item.key"
|
|
|
|
|
+ class="remote-sensing-chart__legend-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="remote-sensing-chart__legend-line"
|
|
|
|
|
+ :style="{ background: item.color }"
|
|
|
|
|
+ ></span>
|
|
|
|
|
+ <span class="remote-sensing-chart__legend-text">{{ item.label }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tab-loading" v-if="loading">{{ t('agriFile.loading') }}</div>
|
|
|
|
|
+ <remote-sensing-chart
|
|
|
|
|
+ v-else-if="hasRemoteSensingData"
|
|
|
|
|
+ :chart-data="remoteSensingChartData"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="tab-empty" v-else>{{ t('agriFile.noData') }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -31,6 +62,7 @@ import { useI18n } from "@/i18n";
|
|
|
import { RECORD_KEY_MAP } from "@/i18n/recordTextMap";
|
|
import { RECORD_KEY_MAP } from "@/i18n/recordTextMap";
|
|
|
import { FloatingPanel } from 'vant';
|
|
import { FloatingPanel } from 'vant';
|
|
|
import { computed, ref, watch } from 'vue';
|
|
import { computed, ref, watch } from 'vue';
|
|
|
|
|
+import remoteSensingChart from './remoteSensingChart.vue';
|
|
|
|
|
|
|
|
const { t } = useI18n();
|
|
const { t } = useI18n();
|
|
|
|
|
|
|
@@ -43,13 +75,21 @@ const props = defineProps({
|
|
|
type: Number,
|
|
type: Number,
|
|
|
default: 0,
|
|
default: 0,
|
|
|
},
|
|
},
|
|
|
|
|
+ activeSubTab: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ default: 0,
|
|
|
|
|
+ },
|
|
|
loading: {
|
|
loading: {
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
default: false,
|
|
default: false,
|
|
|
},
|
|
},
|
|
|
|
|
+ remoteSensingData: {
|
|
|
|
|
+ type: Array,
|
|
|
|
|
+ default: () => [],
|
|
|
|
|
+ },
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-const emit = defineEmits(["update:activeTab"]);
|
|
|
|
|
|
|
+const emit = defineEmits(["update:activeTab", "update:activeSubTab"]);
|
|
|
|
|
|
|
|
const anchors = [
|
|
const anchors = [
|
|
|
130,
|
|
130,
|
|
@@ -58,34 +98,96 @@ const anchors = [
|
|
|
];
|
|
];
|
|
|
const height = ref(anchors[0]);
|
|
const height = ref(anchors[0]);
|
|
|
|
|
|
|
|
|
|
+const AGRI_SUB_TAB_KEYS = ["phenology", "abnormal", "farming"];
|
|
|
|
|
+
|
|
|
const floatTabLabels = computed(() => [
|
|
const floatTabLabels = computed(() => [
|
|
|
|
|
+ { title: t("agriFile.tabAgriRecord"), value: "agriRecord" },
|
|
|
|
|
+ { title: t("agriFile.tabRemoteSensing"), value: "remoteSensing" },
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
|
|
+const agriSubTabLabels = computed(() => [
|
|
|
{ title: t("agriFile.tabPhenology"), value: "phenology" },
|
|
{ title: t("agriFile.tabPhenology"), value: "phenology" },
|
|
|
{ title: t("agriFile.tabAbnormal"), value: "abnormal" },
|
|
{ title: t("agriFile.tabAbnormal"), value: "abnormal" },
|
|
|
{ title: t("agriFile.tabFarming"), value: "farming" },
|
|
{ title: t("agriFile.tabFarming"), value: "farming" },
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
const currentList = ref([]);
|
|
const currentList = ref([]);
|
|
|
-const activeTabValue = computed(() => floatTabLabels.value[props.activeTab]?.value || "phenology");
|
|
|
|
|
|
|
+
|
|
|
|
|
+const isAgriRecordTab = computed(() => floatTabLabels.value[props.activeTab]?.value === "agriRecord");
|
|
|
|
|
+
|
|
|
|
|
+const isRemoteSensingTab = computed(() => floatTabLabels.value[props.activeTab]?.value === "remoteSensing");
|
|
|
|
|
+
|
|
|
|
|
+const REMOTE_SENSING_LEGEND_CONFIG = [
|
|
|
|
|
+ { key: "zone", labelKey: "agriFile.remoteSensingLegendZone", color: "#2199F8" },
|
|
|
|
|
+ { key: "standard", labelKey: "agriFile.remoteSensingLegendStandard", color: "#9FD1FA" },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const remoteSensingLegendItems = computed(() =>
|
|
|
|
|
+ REMOTE_SENSING_LEGEND_CONFIG.map(({ key, labelKey, color }) => ({
|
|
|
|
|
+ key,
|
|
|
|
|
+ label: t(labelKey),
|
|
|
|
|
+ color,
|
|
|
|
|
+ }))
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const remoteSensingChartData = computed(() => {
|
|
|
|
|
+ const raw = props.remoteSensingData;
|
|
|
|
|
+ if (!raw) return null;
|
|
|
|
|
+
|
|
|
|
|
+ if (!Array.isArray(raw) && raw.zoneSeries?.length) {
|
|
|
|
|
+ return raw;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!Array.isArray(raw) || raw.length === 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const highlightIndex = raw.findIndex((item) => item.highlight);
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ timeLabels: raw.map((item) => item.time ?? item.label ?? t("agriFile.timeAxisLabel")),
|
|
|
|
|
+ zoneSeries: raw.map((item) => Number(item.zone ?? item.zoneValue ?? item.value)),
|
|
|
|
|
+ standardSeries: raw.map((item) => Number(item.standard ?? item.standardValue ?? item.stdValue)),
|
|
|
|
|
+ highlightIndex: highlightIndex >= 0 ? highlightIndex : undefined,
|
|
|
|
|
+ };
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const hasRemoteSensingData = computed(() => {
|
|
|
|
|
+ const data = remoteSensingChartData.value;
|
|
|
|
|
+ return Boolean(data?.zoneSeries?.length && data?.standardSeries?.length);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const activeSubTabValue = computed(
|
|
|
|
|
+ () => agriSubTabLabels.value[props.activeSubTab]?.value || AGRI_SUB_TAB_KEYS[0]
|
|
|
|
|
+);
|
|
|
|
|
|
|
|
const displayList = computed(() =>
|
|
const displayList = computed(() =>
|
|
|
currentList.value.map((item) => ({
|
|
currentList.value.map((item) => ({
|
|
|
...item,
|
|
...item,
|
|
|
recordText: t(RECORD_KEY_MAP[item.record] || item.record),
|
|
recordText: t(RECORD_KEY_MAP[item.record] || item.record),
|
|
|
- showRatio: activeTabValue.value !== "farming" && String(item.ratio ?? "").length > 0,
|
|
|
|
|
|
|
+ showRatio: activeSubTabValue.value !== "farming" && String(item.ratio ?? "").length > 0,
|
|
|
}))
|
|
}))
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const syncCurrentList = () => {
|
|
const syncCurrentList = () => {
|
|
|
- const tabValue = floatTabLabels.value[props.activeTab]?.value;
|
|
|
|
|
- currentList.value = props.farmRecordData?.[tabValue] || [];
|
|
|
|
|
|
|
+ if (!isAgriRecordTab.value) {
|
|
|
|
|
+ currentList.value = [];
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ currentList.value = props.farmRecordData?.[activeSubTabValue.value] || [];
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const changeTab = (item, index) => {
|
|
|
|
|
|
|
+const changePrimaryTab = (index) => {
|
|
|
emit("update:activeTab", index);
|
|
emit("update:activeTab", index);
|
|
|
- currentList.value = props.farmRecordData?.[item.value] || [];
|
|
|
|
|
|
|
+ syncCurrentList();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const activeBgStyle = computed(() => ({
|
|
|
|
|
|
|
+const changeSubTab = (index) => {
|
|
|
|
|
+ emit("update:activeSubTab", index);
|
|
|
|
|
+ syncCurrentList();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const primaryActiveBgStyle = computed(() => ({
|
|
|
transform: `translateX(${props.activeTab * 100}%)`,
|
|
transform: `translateX(${props.activeTab * 100}%)`,
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
@@ -100,6 +202,10 @@ watch(
|
|
|
watch(() => props.activeTab, () => {
|
|
watch(() => props.activeTab, () => {
|
|
|
syncCurrentList();
|
|
syncCurrentList();
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+watch(() => props.activeSubTab, () => {
|
|
|
|
|
+ syncCurrentList();
|
|
|
|
|
+});
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
@@ -149,7 +255,7 @@ watch(() => props.activeTab, () => {
|
|
|
padding: 3px;
|
|
padding: 3px;
|
|
|
background: #E9E9E9;
|
|
background: #E9E9E9;
|
|
|
display: grid;
|
|
display: grid;
|
|
|
- grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
|
|
@@ -157,7 +263,7 @@ watch(() => props.activeTab, () => {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
top: 3px;
|
|
top: 3px;
|
|
|
left: 3px;
|
|
left: 3px;
|
|
|
- width: calc((100% - 6px) / 3);
|
|
|
|
|
|
|
+ width: calc((100% - 6px) / 2);
|
|
|
height: 26px;
|
|
height: 26px;
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
background: #fff;
|
|
background: #fff;
|
|
@@ -181,6 +287,33 @@ watch(() => props.activeTab, () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .float-sub-tabs {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ .sub-tab-item {
|
|
|
|
|
+ height: 26px;
|
|
|
|
|
+ line-height: 24px;
|
|
|
|
|
+ padding: 0 10px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #767676;
|
|
|
|
|
+ background: #e9e9e9;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &.sub-tab-item-active {
|
|
|
|
|
+ color: #2199f8;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-color: #2199f8;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.tab-content-group {
|
|
.tab-content-group {
|
|
|
padding-top: 12px;
|
|
padding-top: 12px;
|
|
|
|
|
|
|
@@ -221,6 +354,41 @@ watch(() => props.activeTab, () => {
|
|
|
color: #2199f8;
|
|
color: #2199f8;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .remote-sensing-chart {
|
|
|
|
|
+ &__header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__legend {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__legend-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__legend-line {
|
|
|
|
|
+ width: 14px;
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__legend-text {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #666666;
|
|
|
|
|
+ line-height: 18px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|