|
@@ -40,47 +40,129 @@
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="mapwrap" v-show="!hideMapChrome">
|
|
|
|
|
|
|
+ <div class="mapwrap" v-show="!hideMapChrome && showMapArea">
|
|
|
<div ref="farmRef" class="mapcanvas" id="farm"></div>
|
|
<div ref="farmRef" class="mapcanvas" id="farm"></div>
|
|
|
|
|
|
|
|
- <aside class="glass bcard mleft-card" v-if="overview">
|
|
|
|
|
- <div class="bc-name">{{ overview.name }}</div>
|
|
|
|
|
- <div class="bc-meta">
|
|
|
|
|
- <span>{{ overview.id }}</span>
|
|
|
|
|
- <span>·</span>
|
|
|
|
|
- <span>{{ overview.crop }}</span>
|
|
|
|
|
- <span>·</span>
|
|
|
|
|
- <span class="num">{{ overview.mu }}</span> 亩
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="bc-loc">{{ overview.prov }}{{ overview.city }}</div>
|
|
|
|
|
- <div class="bc-stage" v-if="overview.stage">
|
|
|
|
|
- <b>{{ overview.stage.name }}</b>
|
|
|
|
|
- <span>{{ overview.stage.level }} 级 · 第 {{ overview.stage.dayIndex }} 天 / {{ overview.stage.dayTotal }} 天</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="bc-stats" v-if="overview.survey">
|
|
|
|
|
- <div><em>{{ overview.survey.zoneCount }}</em><span>分区</span></div>
|
|
|
|
|
- <div><em>{{ overview.survey.samplePoints }}</em><span>样点</span></div>
|
|
|
|
|
- <div><em>{{ overview.survey.plants }}</em><span>单株</span></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="bc-mission" v-if="overview.mission">
|
|
|
|
|
- <div>飞巡 {{ overview.mission.lastFlightDate }} · 第 {{ overview.mission.flightIndex }} 次</div>
|
|
|
|
|
- <div>
|
|
|
|
|
- 完成率 <b>{{ overview.mission.completionRate }}%</b>
|
|
|
|
|
- · 损伤率 {{ overview.mission.damageRate }}%
|
|
|
|
|
|
|
+ <!-- 左侧浮层:随 Tab 切换(表型/农事=基地卡;数据分析=基地卡+试验区列表) -->
|
|
|
|
|
+ <div class="mleft" v-if="overview && showMapLeft">
|
|
|
|
|
+ <aside class="glass bcard" aria-label="基地详情卡片">
|
|
|
|
|
+ <div class="bc-h">
|
|
|
|
|
+ <h4 :title="overview.title || overview.name">
|
|
|
|
|
+ {{ overview.title || `${overview.prov}·${overview.name}` }}
|
|
|
|
|
+ </h4>
|
|
|
|
|
+ <span class="id">{{ overview.id }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div>
|
|
|
|
|
- 有效点 {{ overview.mission.validPoints.ok }}/{{ overview.mission.validPoints.total }}
|
|
|
|
|
- · 异常 {{ overview.mission.outliers.ok }}/{{ overview.mission.outliers.total }}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bc-grid">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span>作物</span>
|
|
|
|
|
+ <b>{{ overview.crop }} <em>{{ overview.mu }}亩</em></b>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span>地理位置</span>
|
|
|
|
|
+ <b>{{ overview.prov }}{{ overview.city }}</b>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="overview.stage">
|
|
|
|
|
+ <span>当下物候期</span>
|
|
|
|
|
+ <b>{{ overview.stage.name }} <em>第{{ overview.stage.dayIndex }}天</em></b>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span>气象胁迫</span>
|
|
|
|
|
+ <b v-if="overview.stress?.has">
|
|
|
|
|
+ <i class="wd" :style="{ '--wc': overview.stress.color || 'var(--w-red)' }"></i>
|
|
|
|
|
+ {{ overview.stress.type || '' }}{{ overview.stress.level }} 级
|
|
|
|
|
+ <em>持续 {{ overview.stress.days }}天</em>
|
|
|
|
|
+ </b>
|
|
|
|
|
+ <b v-else>
|
|
|
|
|
+ <i class="wd" style="--wc: var(--w-ok)"></i>无胁迫
|
|
|
|
|
+ </b>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- <ul class="bc-events" v-if="overview.recentEvents?.length">
|
|
|
|
|
- <li v-for="e in overview.recentEvents" :key="e.date + e.title">
|
|
|
|
|
- <span>{{ e.date.slice(5) }}</span>{{ e.title }}
|
|
|
|
|
- </li>
|
|
|
|
|
- </ul>
|
|
|
|
|
- </aside>
|
|
|
|
|
|
|
|
|
|
- <aside class="glass point-card" v-if="pointDetail">
|
|
|
|
|
|
|
+ <div class="bc-line" v-if="overview.survey">
|
|
|
|
|
+ <span>实测地块</span>
|
|
|
|
|
+ <b>{{ overview.survey.zoneCount }} 分区 · {{ overview.survey.samplePoints }} 采样点</b>
|
|
|
|
|
+ <em>单株 {{ overview.survey.plants }}</em>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bc-line" v-if="overview.mission">
|
|
|
|
|
+ <span>最近飞巡</span>
|
|
|
|
|
+ <b>{{ overview.mission.lastFlightLabel || overview.mission.lastFlightDate?.slice(5) }}</b>
|
|
|
|
|
+ <em>第{{ overview.mission.flightIndex }}次 · {{ overview.mission.device || 'M3M' }}</em>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bc-ind" v-if="overview.indicators?.length">
|
|
|
|
|
+ <div v-for="ind in overview.indicators.slice(0, 2)" :key="ind.name" :title="ind.name">
|
|
|
|
|
+ <span>{{ ind.name }}</span>
|
|
|
|
|
+ <b>{{ ind.value }}<small>{{ ind.unit }}</small></b>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bc-prog" v-if="overview.progress">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 飞巡完成度
|
|
|
|
|
+ <b>{{ overview.progress.flightDone }}/{{ overview.progress.flightTotal }}</b>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="bar"
|
|
|
|
|
+ :style="{ '--bc': progColor(overview.progress.flightDone, overview.progress.flightTotal) }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i :style="{ width: progPct(overview.progress.flightDone, overview.progress.flightTotal) + '%' }"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 指标完善度
|
|
|
|
|
+ <b>{{ overview.progress.indicatorDone }}/{{ overview.progress.indicatorTotal }}</b>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="bar"
|
|
|
|
|
+ :style="{ '--bc': progColor(overview.progress.indicatorDone, overview.progress.indicatorTotal) }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i :style="{ width: progPct(overview.progress.indicatorDone, overview.progress.indicatorTotal) + '%' }"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bc-plan" v-if="overview.futureFlights?.length">
|
|
|
|
|
+ <div class="t">未来飞巡规划</div>
|
|
|
|
|
+ <div v-for="f in overview.futureFlights" :key="f.date">
|
|
|
|
|
+ <b>{{ f.dateLabel }}</b>
|
|
|
|
|
+ <em>{{ f.afterDays === 0 ? '今日' : f.afterDays + '天后' }}</em>
|
|
|
|
|
+ <span>{{ f.indicators }}</span>
|
|
|
|
|
+ <i>{{ f.mode }}</i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 仅数据分析:试验区列表 -->
|
|
|
|
|
+ <aside class="glass zlist" v-if="tab === 'ana'" aria-label="试验区列表">
|
|
|
|
|
+ <div class="zl-h">
|
|
|
|
|
+ <b>试验区列表</b>
|
|
|
|
|
+ <em>{{ (overview.experimentGroups || []).length }} 个</em>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="zl-list">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="g in overview.experimentGroups || []"
|
|
|
|
|
+ :key="g.id"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="zl-item"
|
|
|
|
|
+ :class="{ on: anaZone === g.id }"
|
|
|
|
|
+ @click="onSelectAnaZone(g)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="zl-n">{{ g.name }}</span>
|
|
|
|
|
+ <span class="zl-m">实验 {{ g.exp.join('、') }} · 对照 {{ g.ctl.join('、') }}</span>
|
|
|
|
|
+ <span class="zl-f">
|
|
|
|
|
+ <i v-for="f in g.focus" :key="f">{{ f }}</i>
|
|
|
|
|
+ <em>{{ g.start.slice(5) }}~{{ g.end.slice(5) }}</em>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <aside class="glass point-card" v-if="pointDetail && tab === 'pheno'">
|
|
|
<div class="pc-h">
|
|
<div class="pc-h">
|
|
|
<b>{{ pointDetail.pointCode }}</b>
|
|
<b>{{ pointDetail.pointCode }}</b>
|
|
|
<span>{{ pointDetail.zoneName }}</span>
|
|
<span>{{ pointDetail.zoneName }}</span>
|
|
@@ -97,6 +179,7 @@
|
|
|
:key="d"
|
|
:key="d"
|
|
|
type="button"
|
|
type="button"
|
|
|
:class="{ on: d === pointDetail.activeDate }"
|
|
:class="{ on: d === pointDetail.activeDate }"
|
|
|
|
|
+ @click="onPointDateClick(d)"
|
|
|
>
|
|
>
|
|
|
{{ d.slice(5) }}
|
|
{{ d.slice(5) }}
|
|
|
</button>
|
|
</button>
|
|
@@ -113,10 +196,10 @@
|
|
|
</aside>
|
|
</aside>
|
|
|
|
|
|
|
|
<div class="glass maplegend">
|
|
<div class="glass maplegend">
|
|
|
- <span><i class="exp"></i>实验组</span>
|
|
|
|
|
- <span><i class="ctl"></i>对照组</span>
|
|
|
|
|
- <span v-if="mapLayers">● 样点 {{ mapLayers.legend?.samplePoints }}</span>
|
|
|
|
|
- <span v-if="mapLayers">网格 {{ mapLayers.legend?.grids }}</span>
|
|
|
|
|
|
|
+ <span><i class="exp"></i>实验组地块</span>
|
|
|
|
|
+ <span><i class="ctl"></i>对照组地块</span>
|
|
|
|
|
+ <span><i class="pt-dot"></i>采样点</span>
|
|
|
|
|
+ <span v-if="mapLayers">样点 {{ mapLayers.legend?.samplePoints }}</span>
|
|
|
<span v-if="mapLayers">单株 {{ mapLayers.legend?.plants }}</span>
|
|
<span v-if="mapLayers">单株 {{ mapLayers.legend?.plants }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -124,10 +207,14 @@
|
|
|
<div v-show="tab === 'pheno'" class="detail-body">
|
|
<div v-show="tab === 'pheno'" class="detail-body">
|
|
|
<div class="row73">
|
|
<div class="row73">
|
|
|
<PatrolTimeline
|
|
<PatrolTimeline
|
|
|
|
|
+ ref="tlRef"
|
|
|
v-model="tlZone"
|
|
v-model="tlZone"
|
|
|
:data="timeline"
|
|
:data="timeline"
|
|
|
:active-indicator-id="activeIndicatorId"
|
|
:active-indicator-id="activeIndicatorId"
|
|
|
|
|
+ :marker-date="syncDate"
|
|
|
|
|
+ @change="onTlChange"
|
|
|
@select-indicator="onSelectIndicator"
|
|
@select-indicator="onSelectIndicator"
|
|
|
|
|
+ @select-task="onSelectTask"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
<section class="panel curve-panel">
|
|
<section class="panel curve-panel">
|
|
@@ -252,7 +339,12 @@
|
|
|
<PhotoViewer v-model:open="viewerOpen" :list="viewerList" :start-index="viewerIndex" />
|
|
<PhotoViewer v-model:open="viewerOpen" :list="viewerList" :start-index="viewerIndex" />
|
|
|
|
|
|
|
|
<div v-show="tab === 'ana'" class="detail-body">
|
|
<div v-show="tab === 'ana'" class="detail-body">
|
|
|
- <DataAnalysis v-if="phenoId" :base-id="phenoId" :active="tab === 'ana'" />
|
|
|
|
|
|
|
+ <DataAnalysis
|
|
|
|
|
+ v-if="phenoId"
|
|
|
|
|
+ :base-id="phenoId"
|
|
|
|
|
+ :active="tab === 'ana'"
|
|
|
|
|
+ :zone-pair="anaZone"
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div v-show="tab === 'farm'" class="detail-body">
|
|
<div v-show="tab === 'farm'" class="detail-body">
|
|
@@ -314,7 +406,6 @@ import {
|
|
|
getComparePhotos,
|
|
getComparePhotos,
|
|
|
getLatestPhotos,
|
|
getLatestPhotos,
|
|
|
getPointDetail,
|
|
getPointDetail,
|
|
|
- isDongguanBase,
|
|
|
|
|
} from '@/api/phenotype'
|
|
} from '@/api/phenotype'
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
const route = useRoute()
|
|
@@ -330,8 +421,10 @@ const tabs = [
|
|
|
const tab = ref('pheno')
|
|
const tab = ref('pheno')
|
|
|
const farmRef = ref(null)
|
|
const farmRef = ref(null)
|
|
|
const curveRef = ref(null)
|
|
const curveRef = ref(null)
|
|
|
|
|
+const tlRef = ref(null)
|
|
|
let farmMap = null
|
|
let farmMap = null
|
|
|
let chart = null
|
|
let chart = null
|
|
|
|
|
+let syncingZone = false
|
|
|
|
|
|
|
|
const overview = ref(null)
|
|
const overview = ref(null)
|
|
|
const mapLayers = ref(null)
|
|
const mapLayers = ref(null)
|
|
@@ -343,19 +436,60 @@ const pointDetail = ref(null)
|
|
|
const tlZone = ref('P3')
|
|
const tlZone = ref('P3')
|
|
|
const curveZone = ref('1B')
|
|
const curveZone = ref('1B')
|
|
|
const photoZone = ref('P3')
|
|
const photoZone = ref('P3')
|
|
|
|
|
+/** 数据分析左侧试验区选中(1A / 1B) */
|
|
|
|
|
+const anaZone = ref('1A')
|
|
|
const phenoId = ref('')
|
|
const phenoId = ref('')
|
|
|
-const activeIndicatorId = ref('I2')
|
|
|
|
|
|
|
+const showMapArea = computed(() => tab.value === 'pheno' || tab.value === 'ana' || tab.value === 'farm')
|
|
|
|
|
+const showMapLeft = computed(() => showMapArea.value)
|
|
|
|
|
+const activeIndicatorId = ref('')
|
|
|
const activeIndicatorName = ref('新梢整齐度')
|
|
const activeIndicatorName = ref('新梢整齐度')
|
|
|
|
|
+/** 曲线 / 样点 / 时间轴共用的同步日期 */
|
|
|
|
|
+const syncDate = ref('')
|
|
|
const viewerOpen = ref(false)
|
|
const viewerOpen = ref(false)
|
|
|
const viewerList = ref([])
|
|
const viewerList = ref([])
|
|
|
const viewerIndex = ref(0)
|
|
const viewerIndex = ref(0)
|
|
|
const farmDetail = ref(null)
|
|
const farmDetail = ref(null)
|
|
|
const hideMapChrome = computed(() => !!farmDetail.value)
|
|
const hideMapChrome = computed(() => !!farmDetail.value)
|
|
|
|
|
|
|
|
|
|
+function zoneToPair(zoneId) {
|
|
|
|
|
+ if (!zoneId || zoneId === 'all') return null
|
|
|
|
|
+ if (zoneId === '1A' || zoneId === 'P1' || zoneId === 'P2') return '1A'
|
|
|
|
|
+ if (zoneId === '1B' || zoneId === 'P3' || zoneId === 'P4') return '1B'
|
|
|
|
|
+ return null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function pairToDefaultZone(pair) {
|
|
|
|
|
+ return pair === '1A' ? 'P1' : 'P3'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function onFarmDetailChange(payload) {
|
|
function onFarmDetailChange(payload) {
|
|
|
farmDetail.value = payload
|
|
farmDetail.value = payload
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function progPct(done, total) {
|
|
|
|
|
+ if (!total) return 0
|
|
|
|
|
+ return Math.round((done / total) * 100)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function progColor(done, total) {
|
|
|
|
|
+ const v = progPct(done, total)
|
|
|
|
|
+ if (v >= 85) return 'var(--w-ok)'
|
|
|
|
|
+ if (v >= 65) return 'var(--amber)'
|
|
|
|
|
+ return 'var(--w-orange)'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onSelectAnaZone(g) {
|
|
|
|
|
+ if (!g?.id) return
|
|
|
|
|
+ anaZone.value = g.id
|
|
|
|
|
+ curveZone.value = g.id
|
|
|
|
|
+ const expId = g.exp?.[0]
|
|
|
|
|
+ if (expId) {
|
|
|
|
|
+ tlZone.value = expId
|
|
|
|
|
+ photoZone.value = expId
|
|
|
|
|
+ farmMap?.setHighlightZone(expId)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const base = computed(() => basesStore.byId(route.params.id))
|
|
const base = computed(() => basesStore.byId(route.params.id))
|
|
|
const filteredLatest = computed(() => {
|
|
const filteredLatest = computed(() => {
|
|
|
const list = latest.value?.list || []
|
|
const list = latest.value?.list || []
|
|
@@ -433,13 +567,23 @@ function renderFarm() {
|
|
|
lat: overview.value?.lat ?? base.value.lat,
|
|
lat: overview.value?.lat ?? base.value.lat,
|
|
|
}
|
|
}
|
|
|
farmMap.render(b, plotsFromLayers(mapLayers.value))
|
|
farmMap.render(b, plotsFromLayers(mapLayers.value))
|
|
|
|
|
+ if (pointDetail.value?.pointCode) farmMap.setSelectedPoint(pointDetail.value.pointCode)
|
|
|
|
|
+ farmMap.setHighlightZone(tlZone.value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderCurve() {
|
|
function renderCurve() {
|
|
|
if (!curveRef.value || !curve.value) return
|
|
if (!curveRef.value || !curve.value) return
|
|
|
- if (!chart) chart = echarts.init(curveRef.value)
|
|
|
|
|
|
|
+ if (!chart) {
|
|
|
|
|
+ chart = echarts.init(curveRef.value)
|
|
|
|
|
+ chart.on('click', (params) => {
|
|
|
|
|
+ if (params?.componentType !== 'series') return
|
|
|
|
|
+ const full = curve.value?.series?.[0]?.points?.[params.dataIndex]?.date
|
|
|
|
|
+ if (full) onCurveDateClick(full)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
const series = curve.value.series || []
|
|
const series = curve.value.series || []
|
|
|
const xs = series[0]?.points?.map((p) => p.date.slice(5)) || []
|
|
const xs = series[0]?.points?.map((p) => p.date.slice(5)) || []
|
|
|
|
|
+ const mark = syncDate.value
|
|
|
chart.setOption({
|
|
chart.setOption({
|
|
|
backgroundColor: 'transparent',
|
|
backgroundColor: 'transparent',
|
|
|
grid: { left: 42, right: 14, top: 16, bottom: 28 },
|
|
grid: { left: 42, right: 14, top: 16, bottom: 28 },
|
|
@@ -468,7 +612,19 @@ function renderCurve() {
|
|
|
width: 2,
|
|
width: 2,
|
|
|
},
|
|
},
|
|
|
itemStyle: { color: s.role === 'ctl' ? '#5ab7ec' : '#ffd489' },
|
|
itemStyle: { color: s.role === 'ctl' ? '#5ab7ec' : '#ffd489' },
|
|
|
- symbolSize: 6,
|
|
|
|
|
|
|
+ symbolSize: (val, params) => {
|
|
|
|
|
+ const d = s.points[params.dataIndex]?.date
|
|
|
|
|
+ return d && d === mark ? 10 : 6
|
|
|
|
|
+ },
|
|
|
|
|
+ markLine: mark
|
|
|
|
|
+ ? {
|
|
|
|
|
+ silent: true,
|
|
|
|
|
+ symbol: 'none',
|
|
|
|
|
+ lineStyle: { color: '#ffd489', type: 'dashed', width: 1, opacity: 0.7 },
|
|
|
|
|
+ data: [{ xAxis: mark.slice(5) }],
|
|
|
|
|
+ label: { show: false },
|
|
|
|
|
+ }
|
|
|
|
|
+ : undefined,
|
|
|
})),
|
|
})),
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -485,21 +641,134 @@ async function reloadCurve() {
|
|
|
renderCurve()
|
|
renderCurve()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function reloadPoint(code, date) {
|
|
|
|
|
+ if (!phenoId.value || !code) return
|
|
|
|
|
+ pointDetail.value = unwrap(
|
|
|
|
|
+ await getPointDetail(phenoId.value, code, date ? { date } : {}),
|
|
|
|
|
+ )
|
|
|
|
|
+ farmMap?.setSelectedPoint(code)
|
|
|
|
|
+ if (pointDetail.value?.activeDate) syncDate.value = pointDetail.value.activeDate
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onMapSelect(hit) {
|
|
|
|
|
+ if (!phenoId.value || !hit) return
|
|
|
|
|
+ if (hit.kind === 'point' && hit.code) {
|
|
|
|
|
+ await reloadPoint(hit.code)
|
|
|
|
|
+ if (hit.zoneId && hit.zoneId !== tlZone.value) {
|
|
|
|
|
+ syncingZone = true
|
|
|
|
|
+ tlZone.value = hit.zoneId
|
|
|
|
|
+ photoZone.value = hit.zoneId
|
|
|
|
|
+ const pair = zoneToPair(hit.zoneId)
|
|
|
|
|
+ if (pair) curveZone.value = pair
|
|
|
|
|
+ farmMap?.setHighlightZone(hit.zoneId)
|
|
|
|
|
+ timeline.value = unwrap(await getTimeline(phenoId.value, { zoneId: hit.zoneId }))
|
|
|
|
|
+ latest.value = unwrap(await getLatestPhotos(phenoId.value, { zoneId: hit.zoneId }))
|
|
|
|
|
+ await reloadCurve()
|
|
|
|
|
+ syncingZone = false
|
|
|
|
|
+ }
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hit.kind === 'plot' && hit.zoneId) {
|
|
|
|
|
+ const pt =
|
|
|
|
|
+ mapLayers.value?.points?.find((p) => p.zoneId === hit.zoneId) ||
|
|
|
|
|
+ null
|
|
|
|
|
+ syncingZone = true
|
|
|
|
|
+ tlZone.value = hit.zoneId
|
|
|
|
|
+ photoZone.value = hit.zoneId
|
|
|
|
|
+ const pair = zoneToPair(hit.zoneId)
|
|
|
|
|
+ if (pair) curveZone.value = pair
|
|
|
|
|
+ farmMap?.setHighlightZone(hit.zoneId)
|
|
|
|
|
+ timeline.value = unwrap(await getTimeline(phenoId.value, { zoneId: hit.zoneId }))
|
|
|
|
|
+ latest.value = unwrap(await getLatestPhotos(phenoId.value, { zoneId: hit.zoneId }))
|
|
|
|
|
+ await reloadCurve()
|
|
|
|
|
+ if (pt) await reloadPoint(pt.code)
|
|
|
|
|
+ syncingZone = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onPointDateClick(date) {
|
|
|
|
|
+ const code = pointDetail.value?.pointCode
|
|
|
|
|
+ if (!code) return
|
|
|
|
|
+ syncDate.value = date
|
|
|
|
|
+ await reloadPoint(code, date)
|
|
|
|
|
+ tlRef.value?.scrollToDate(date)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onTlChange({ zoneId }) {
|
|
|
|
|
+ if (syncingZone || !phenoId.value) return
|
|
|
|
|
+ const pair = zoneToPair(zoneId)
|
|
|
|
|
+ syncingZone = true
|
|
|
|
|
+ if (pair && curveZone.value !== pair) curveZone.value = pair
|
|
|
|
|
+ photoZone.value = zoneId === 'all' ? 'all' : zoneId
|
|
|
|
|
+ farmMap?.setHighlightZone(zoneId)
|
|
|
|
|
+ timeline.value = unwrap(await getTimeline(phenoId.value, { zoneId }))
|
|
|
|
|
+ latest.value = unwrap(await getLatestPhotos(phenoId.value, { zoneId: photoZone.value }))
|
|
|
|
|
+ await reloadCurve()
|
|
|
|
|
+ if (zoneId && zoneId !== 'all') {
|
|
|
|
|
+ const pt = mapLayers.value?.points?.find((p) => p.zoneId === zoneId)
|
|
|
|
|
+ if (pt) await reloadPoint(pt.code, syncDate.value || undefined)
|
|
|
|
|
+ }
|
|
|
|
|
+ syncingZone = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function onSelectIndicator(ind) {
|
|
function onSelectIndicator(ind) {
|
|
|
activeIndicatorId.value = ind.id
|
|
activeIndicatorId.value = ind.id
|
|
|
activeIndicatorName.value = ind.name
|
|
activeIndicatorName.value = ind.name
|
|
|
|
|
+ if (ind.date) {
|
|
|
|
|
+ syncDate.value = ind.date
|
|
|
|
|
+ tlRef.value?.scrollToDate(ind.date)
|
|
|
|
|
+ }
|
|
|
reloadCurve()
|
|
reloadCurve()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function onSelectTask(task) {
|
|
|
|
|
+ if (!phenoId.value || !task) return
|
|
|
|
|
+ syncDate.value = task.date
|
|
|
|
|
+ tlRef.value?.scrollToDate(task.date)
|
|
|
|
|
+ const zoneId = photoZone.value !== 'all' ? photoZone.value : tlZone.value
|
|
|
|
|
+ const photos = unwrap(
|
|
|
|
|
+ await getLatestPhotos(phenoId.value, {
|
|
|
|
|
+ zoneId: zoneId === 'all' ? undefined : zoneId,
|
|
|
|
|
+ date: task.date,
|
|
|
|
|
+ taskId: task.id,
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ const list = photos?.list || []
|
|
|
|
|
+ if (list.length) {
|
|
|
|
|
+ openViewer(list, 0)
|
|
|
|
|
+ } else if (pointDetail.value?.pointCode) {
|
|
|
|
|
+ await reloadPoint(pointDetail.value.pointCode, task.date)
|
|
|
|
|
+ openPointViewer()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onCurveDateClick(date) {
|
|
|
|
|
+ syncDate.value = date
|
|
|
|
|
+ tlRef.value?.scrollToDate(date)
|
|
|
|
|
+ const code = pointDetail.value?.pointCode
|
|
|
|
|
+ if (code) {
|
|
|
|
|
+ // 样点卡日期轴可能不覆盖曲线全区间:尽量贴近
|
|
|
|
|
+ const dates = pointDetail.value?.dates || []
|
|
|
|
|
+ const hit = dates.includes(date)
|
|
|
|
|
+ ? date
|
|
|
|
|
+ : dates.reduce((best, d) => {
|
|
|
|
|
+ if (!best) return d
|
|
|
|
|
+ return Math.abs(+new Date(d) - +new Date(date)) < Math.abs(+new Date(best) - +new Date(date))
|
|
|
|
|
+ ? d
|
|
|
|
|
+ : best
|
|
|
|
|
+ }, null)
|
|
|
|
|
+ if (hit) await reloadPoint(code, hit)
|
|
|
|
|
+ }
|
|
|
|
|
+ renderCurve()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function loadPhenotype() {
|
|
async function loadPhenotype() {
|
|
|
const b = base.value
|
|
const b = base.value
|
|
|
if (!b) return
|
|
if (!b) return
|
|
|
const id = resolvePhenotypeBaseId(b)
|
|
const id = resolvePhenotypeBaseId(b)
|
|
|
phenoId.value = id
|
|
phenoId.value = id
|
|
|
|
|
|
|
|
- // 非东莞基地:仍可走 mock 东莞包演示;真实对接后按 id 请求
|
|
|
|
|
- const useDemo = isDongguanBase(b) || import.meta.env.VITE_USE_MOCK !== 'false'
|
|
|
|
|
- const bid = useDemo && isDongguanBase(b) ? id : id
|
|
|
|
|
|
|
+ const bid = id
|
|
|
|
|
|
|
|
const [ov, layers, tl, cv, cmp, lt] = await Promise.all([
|
|
const [ov, layers, tl, cv, cmp, lt] = await Promise.all([
|
|
|
getBaseOverview(bid).then(unwrap),
|
|
getBaseOverview(bid).then(unwrap),
|
|
@@ -516,9 +785,24 @@ async function loadPhenotype() {
|
|
|
compare.value = cmp
|
|
compare.value = cmp
|
|
|
latest.value = lt
|
|
latest.value = lt
|
|
|
|
|
|
|
|
- const firstPt = layers?.points?.find((p) => p.zoneId === 'P3') || layers?.points?.[0]
|
|
|
|
|
|
|
+ // 默认选中首个 PHENO_CONFIG 指标
|
|
|
|
|
+ const firstInd = tl?.indicators?.[0]
|
|
|
|
|
+ if (firstInd) {
|
|
|
|
|
+ activeIndicatorId.value = firstInd.id
|
|
|
|
|
+ activeIndicatorName.value = firstInd.name
|
|
|
|
|
+ if (!cv || cv.indicator !== firstInd.name) {
|
|
|
|
|
+ curve.value = unwrap(
|
|
|
|
|
+ await getIndicatorCurve(bid, { indicator: firstInd.name, zonePair: curveZone.value }),
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const preferZone = tlZone.value
|
|
|
|
|
+ const firstPt =
|
|
|
|
|
+ layers?.points?.find((p) => p.zoneId === preferZone) || layers?.points?.[0]
|
|
|
if (firstPt) {
|
|
if (firstPt) {
|
|
|
pointDetail.value = unwrap(await getPointDetail(bid, firstPt.code))
|
|
pointDetail.value = unwrap(await getPointDetail(bid, firstPt.code))
|
|
|
|
|
+ syncDate.value = pointDetail.value?.activeDate || ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
await nextTick()
|
|
await nextTick()
|
|
@@ -532,21 +816,49 @@ watch(base, async () => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
watch(photoZone, async () => {
|
|
watch(photoZone, async () => {
|
|
|
- if (!phenoId.value) return
|
|
|
|
|
|
|
+ if (!phenoId.value || syncingZone) return
|
|
|
latest.value = unwrap(await getLatestPhotos(phenoId.value, { zoneId: photoZone.value }))
|
|
latest.value = unwrap(await getLatestPhotos(phenoId.value, { zoneId: photoZone.value }))
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-watch(curveZone, () => {
|
|
|
|
|
|
|
+watch(curveZone, async (pair) => {
|
|
|
|
|
+ if (syncingZone) {
|
|
|
|
|
+ reloadCurve()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // 4A:曲线区 → 时间轴默认试验区
|
|
|
|
|
+ const z = pairToDefaultZone(pair)
|
|
|
|
|
+ if (z && tlZone.value !== z && tlZone.value !== 'all') {
|
|
|
|
|
+ syncingZone = true
|
|
|
|
|
+ tlZone.value = z
|
|
|
|
|
+ photoZone.value = z
|
|
|
|
|
+ farmMap?.setHighlightZone(z)
|
|
|
|
|
+ if (phenoId.value) {
|
|
|
|
|
+ timeline.value = unwrap(await getTimeline(phenoId.value, { zoneId: z }))
|
|
|
|
|
+ latest.value = unwrap(await getLatestPhotos(phenoId.value, { zoneId: z }))
|
|
|
|
|
+ }
|
|
|
|
|
+ syncingZone = false
|
|
|
|
|
+ }
|
|
|
reloadCurve()
|
|
reloadCurve()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-watch(hideMapChrome, async (hidden) => {
|
|
|
|
|
- if (hidden) return
|
|
|
|
|
|
|
+watch([hideMapChrome, showMapArea], async ([hidden, show]) => {
|
|
|
|
|
+ if (hidden || !show) return
|
|
|
await nextTick()
|
|
await nextTick()
|
|
|
farmMap?.resize()
|
|
farmMap?.resize()
|
|
|
chart?.resize()
|
|
chart?.resize()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+watch(tab, async (t) => {
|
|
|
|
|
+ if (t === 'ana' && overview.value?.experimentGroups?.length) {
|
|
|
|
|
+ const g = overview.value.experimentGroups.find((x) => x.id === anaZone.value)
|
|
|
|
|
+ if (g) onSelectAnaZone(g)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (t === 'pheno' || t === 'ana' || t === 'farm') {
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ farmMap?.resize()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
await basesStore.init()
|
|
await basesStore.init()
|
|
|
await nextTick()
|
|
await nextTick()
|
|
@@ -555,7 +867,9 @@ onMounted(async () => {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
farmMap = new FarmMap()
|
|
farmMap = new FarmMap()
|
|
|
- await farmMap.init(farmRef.value, [base.value.lng, base.value.lat])
|
|
|
|
|
|
|
+ await farmMap.init(farmRef.value, [base.value.lng, base.value.lat], {
|
|
|
|
|
+ onSelect: onMapSelect,
|
|
|
|
|
+ })
|
|
|
await loadPhenotype()
|
|
await loadPhenotype()
|
|
|
window.addEventListener('resize', onResize)
|
|
window.addEventListener('resize', onResize)
|
|
|
})
|
|
})
|
|
@@ -659,65 +973,18 @@ onUnmounted(() => {
|
|
|
min-height: 500px;
|
|
min-height: 500px;
|
|
|
}
|
|
}
|
|
|
.mapcanvas { position: absolute; inset: 0;height: 500px; }
|
|
.mapcanvas { position: absolute; inset: 0;height: 500px; }
|
|
|
-.mleft-card {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 12px;
|
|
|
|
|
- left: 12px;
|
|
|
|
|
- width: 268px;
|
|
|
|
|
- z-index: 3;
|
|
|
|
|
- padding: 12px 14px;
|
|
|
|
|
- max-height: calc(100% - 24px);
|
|
|
|
|
- overflow: auto;
|
|
|
|
|
-}
|
|
|
|
|
-.bc-name { font-size: 16px; font-weight: 800; margin-bottom: 4px; color: var(--amber); }
|
|
|
|
|
-.bc-meta, .bc-loc { color: var(--muted); font-size: 12px; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
|
|
|
-.bc-loc { margin-top: 2px; }
|
|
|
|
|
-.bc-stage {
|
|
|
|
|
- margin-top: 10px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 2px;
|
|
|
|
|
- font-size: 12.5px;
|
|
|
|
|
- color: var(--text);
|
|
|
|
|
-}
|
|
|
|
|
-.bc-stage b { color: var(--amber); font-size: 14px; }
|
|
|
|
|
-.bc-stats {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- margin-top: 10px;
|
|
|
|
|
-}
|
|
|
|
|
-.bc-stats div {
|
|
|
|
|
- background: var(--panel-2);
|
|
|
|
|
- border: 1px solid var(--line);
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- padding: 6px 4px;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
-}
|
|
|
|
|
-.bc-stats em {
|
|
|
|
|
- display: block;
|
|
|
|
|
- font-style: normal;
|
|
|
|
|
- font-weight: 800;
|
|
|
|
|
- color: var(--amber);
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
|
|
+/* 左侧浮层用全局 .mleft / .bcard / .zlist;此处只微调地图内高度 */
|
|
|
|
|
+.mapwrap > .mleft {
|
|
|
|
|
+ width: min(240px, calc(100% - 20px));
|
|
|
}
|
|
}
|
|
|
-.bc-stats span { font-size: 11px; color: var(--faint); }
|
|
|
|
|
-.bc-mission {
|
|
|
|
|
- margin-top: 10px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: var(--muted);
|
|
|
|
|
- line-height: 1.55;
|
|
|
|
|
|
|
+.mapwrap > .mleft .bcard {
|
|
|
|
|
+ /* flex: 1 1 auto; */
|
|
|
|
|
+ min-height: 0;
|
|
|
}
|
|
}
|
|
|
-.bc-mission b { color: var(--amber); }
|
|
|
|
|
-.bc-events {
|
|
|
|
|
- margin: 10px 0 0;
|
|
|
|
|
- padding: 0;
|
|
|
|
|
- list-style: none;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: var(--muted);
|
|
|
|
|
|
|
+.mapwrap > .mleft .zlist {
|
|
|
|
|
+ flex: 0 1 auto;
|
|
|
|
|
+ max-height: 42%;
|
|
|
}
|
|
}
|
|
|
-.bc-events li { display: flex; gap: 8px; padding: 3px 0; }
|
|
|
|
|
-.bc-events span { color: var(--amber); min-width: 42px; }
|
|
|
|
|
|
|
|
|
|
.point-card {
|
|
.point-card {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
@@ -767,7 +1034,8 @@ onUnmounted(() => {
|
|
|
|
|
|
|
|
.maplegend {
|
|
.maplegend {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
- left: 12px;
|
|
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
bottom: 12px;
|
|
bottom: 12px;
|
|
|
z-index: 3;
|
|
z-index: 3;
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -777,17 +1045,31 @@ onUnmounted(() => {
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
|
color: var(--muted);
|
|
color: var(--muted);
|
|
|
max-width: calc(100% - 250px);
|
|
max-width: calc(100% - 250px);
|
|
|
- transform: none;
|
|
|
|
|
}
|
|
}
|
|
|
.maplegend i {
|
|
.maplegend i {
|
|
|
display: inline-block;
|
|
display: inline-block;
|
|
|
- width: 10px;
|
|
|
|
|
|
|
+ width: 14px;
|
|
|
height: 10px;
|
|
height: 10px;
|
|
|
border-radius: 2px;
|
|
border-radius: 2px;
|
|
|
margin-right: 4px;
|
|
margin-right: 4px;
|
|
|
|
|
+ vertical-align: -1px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+.maplegend .exp {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: 2px solid #ffd489;
|
|
|
|
|
+}
|
|
|
|
|
+.maplegend .ctl {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: 2px dashed #5ab7ec;
|
|
|
|
|
+}
|
|
|
|
|
+.maplegend .pt-dot {
|
|
|
|
|
+ width: 10px;
|
|
|
|
|
+ height: 10px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 2px solid #ffd489;
|
|
|
}
|
|
}
|
|
|
-.maplegend .exp { background: rgba(255, 212, 137, 0.85); }
|
|
|
|
|
-.maplegend .ctl { background: rgba(90, 183, 236, 0.85); }
|
|
|
|
|
|
|
|
|
|
.detail-body { display: flex; flex-direction: column; gap: 12px; }
|
|
.detail-body { display: flex; flex-direction: column; gap: 12px; }
|
|
|
.row73 {
|
|
.row73 {
|