/** * 东莞壹号农业 — 表型详情 mock 数据 * 地图分区/样点来自 realBases GIS;其余指标/任务/照片为演示数据,字段对齐接口契约。 */ import mapPack from './dongguan-map.json' import { getPhenologyIndicatorPlan } from '@/data/phenoConfig' const BASE_ID = mapPack.id // FN-155 function ok(data) { return { success: true, code: 0, msg: 'ok', data } } function delay(ms = 120) { return new Promise((r) => setTimeout(r, ms)) } /** 演示用 DJI 近景照片(循环复用) */ const DEMO_PHOTOS = [ 'https://birdseye-img-ali-cdn.sysuimars.com/sikong/fh_sync/71892baf-42b7-4d35-a763-2a420014d901/3c0dd577-f4dc-44fd-b963-489a58a36eed/media/fb87a44d-7b1a-4b9c-bc17-f76c571718c4/DJI_20260715084144_0071_V_code-ws14kupff8tm.jpeg', 'https://birdseye-img-ali-cdn.sysuimars.com/sikong/fh_sync/71892baf-42b7-4d35-a763-2a420014d901/3c0dd577-f4dc-44fd-b963-489a58a36eed/media/fb87a44d-7b1a-4b9c-bc17-f76c571718c4/DJI_20260715084301_0082_V_code-ws14mh05ts08.jpeg', 'https://birdseye-img-ali-cdn.sysuimars.com/sikong/fh_sync/71892baf-42b7-4d35-a763-2a420014d901/3c0dd577-f4dc-44fd-b963-489a58a36eed/media/fb87a44d-7b1a-4b9c-bc17-f76c571718c4/DJI_20260715084232_0080_V_code-ws14mh055bs2.jpeg', 'https://birdseye-img-ali-cdn.sysuimars.com/sikong/fh_sync/71892baf-42b7-4d35-a763-2a420014d901/3c0dd577-f4dc-44fd-b963-489a58a36eed/media/fb87a44d-7b1a-4b9c-bc17-f76c571718c4/DJI_20260715084229_0079_V_code-ws14mh055bs2.jpeg', 'https://birdseye-img-ali-cdn.sysuimars.com/sikong/fh_sync/71892baf-42b7-4d35-a763-2a420014d901/3c0dd577-f4dc-44fd-b963-489a58a36eed/media/fb87a44d-7b1a-4b9c-bc17-f76c571718c4/DJI_20260715084221_0078_V_code-ws14mh04e1dt.jpeg', 'https://birdseye-img-ali-cdn.sysuimars.com/sikong/fh_sync/71892baf-42b7-4d35-a763-2a420014d901/3c0dd577-f4dc-44fd-b963-489a58a36eed/media/fb87a44d-7b1a-4b9c-bc17-f76c571718c4/DJI_20260715084218_0077_V_code-ws14mh04e1dt.jpeg', ] function photoUrl(seed = 0) { return DEMO_PHOTOS[((seed % DEMO_PHOTOS.length) + DEMO_PHOTOS.length) % DEMO_PHOTOS.length] } const ZONE_META = { P1: { name: '分区1A 抗旱性试验区', role: '实验组', lng: 114.1233, lat: 22.9185 }, P2: { name: '分区1A 抗旱性试验区', role: '对照组', lng: 114.1247, lat: 22.9187 }, P3: { name: '分区1B 实验区', role: '实验组', lng: 114.1256, lat: 22.9194 }, P4: { name: '分区1B 对照区', role: '对照组', lng: 114.1259, lat: 22.9192 }, } /** 补齐弹窗所需元数据 */ function withViewerMeta(photo, seed = 0) { const z = ZONE_META[photo.zoneId] || ZONE_META.P3 const n = Number(String(photo.pointCode).replace(/\D/g, '')) || seed + 1 const flight = 80 + (n % 10) const url = photo.thumbUrl || photoUrl(seed) const fileTail = String(1000 + (n % 90)).padStart(4, '0') return { ...photo, url, thumbUrl: url, filename: `DJI_2026101110${String(n).padStart(2, '0')}32_${fileTail}.JPG`, captureDate: photo.captureDate || '2026-10-11', captureTime: photo.captureTime || '2026-10-11 10:12', captureLabel: photo.captureLabel || '10-11', captureMethod: '样点高清', plotLabel: `${photo.zoneId} ${z.name} · ${z.role} · 采样点 ${photo.pointCode}`, taskLabel: `#F-${flight}-${photo.zoneId} · 计划巡检 · 秋梢老熟期 · 第 ${flight} 次飞巡`, altitudeGsd: '25 m / 1 cm · Mavic 3E 可见光', position: `${z.lat.toFixed(6)}, ${z.lng.toFixed(6)}`, metricTitle: '秋梢老熟期指标', metrics: [ { name: '新梢整齐度', value: `${(photo.value ?? 73.3 - (n % 5) * 1.2).toFixed?.(1) ?? 73.3}%` }, { name: '新梢表型状态', value: `${(0.7 + (n % 7) * 0.02).toFixed(2)}指数` }, { name: '末次梢老熟进度', value: `${(0.75 + (n % 5) * 0.02).toFixed(2)}指数` }, ], } } function datesBetween(start, end, stepDays = 3) { const out = [] const cur = new Date(start) const last = new Date(end) while (cur <= last) { out.push(cur.toISOString().slice(0, 10)) cur.setDate(cur.getDate() + stepDays) } return out } const CURVE_DATES = datesBetween('2024-10-15', '2024-11-07', 2) const EXP_CURVE = [73.1, 75.2, 78.4, 81.0, 83.6, 85.1, 86.8, 88.2, 89.0, 90.3, 88.5, 86.0] const CTL_CURVE = [74.0, 74.8, 75.5, 76.2, 76.9, 77.4, 78.0, 78.6, 79.1, 79.5, 79.2, 79.0] const SHOOT_DATES = datesBetween('2024-08-22', '2024-10-11', 5) const SHOOT_EXP = [62.4, 68.0, 74.2, 81.5, 88.6, 93.0, 95.4, 96.2, 97.0, 97.1] const SHOOT_CTL = [58.1, 61.5, 65.2, 68.8, 72.0, 74.5, 76.2, 77.4, 78.0, 78.2] const zoneP3 = mapPack.zones.find((z) => z.id === 'P3') const zoneP4 = mapPack.zones.find((z) => z.id === 'P4') function buildComparePhotos() { const exp = Array.from({ length: 8 }, (_, i) => withViewerMeta( { pointCode: `P3-${String(i + 1).padStart(2, '0')}`, zoneId: 'P3', role: 'exp', thumbUrl: photoUrl(i), indicator: '秋梢老熟度', value: +(94.9 - i * 0.4 - (i % 3) * 0.3).toFixed(1), unit: '%', captureDate: '2026-10-11', captureTime: '2026-10-11 10:12', captureLabel: '10-11', }, i, ), ) const ctl = Array.from({ length: 8 }, (_, i) => withViewerMeta( { pointCode: `P4-${String(i + 1).padStart(2, '0')}`, zoneId: 'P4', role: 'ctl', thumbUrl: photoUrl(i + 3), indicator: '秋梢老熟度', value: +(78.4 - i * 0.35 + (i % 2) * 0.5).toFixed(1), unit: '%', captureDate: '2026-10-11', captureTime: '2026-10-11 10:18', captureLabel: '10-11', }, i + 8, ), ) const mean = (arr) => +(arr.reduce((s, x) => s + x.value, 0) / arr.length).toFixed(1) return { indicator: '秋梢老熟度', date: '2026-10-11', experimental: { zoneId: 'P3', zoneName: zoneP3?.name || '分区1B 实验区', mean: mean(exp), photos: exp }, control: { zoneId: 'P4', zoneName: zoneP4?.name || '分区1B 对照区', mean: mean(ctl), photos: ctl }, } } function buildLatestPhotos() { const list = [] ;['P3', 'P4', 'P1', 'P2'].forEach((zid, zi) => { for (let i = 1; i <= 6; i++) { list.push( withViewerMeta( { id: `PH-${zid}-${i}`, pointCode: `${zid}-${String(i).padStart(2, '0')}`, zoneId: zid, taskId: 'T-97', taskName: '第97次飞巡', source: '近景无人机', thumbUrl: photoUrl(zi * 2 + i), captureDate: '2026-10-11', captureTime: '2026-10-11 10:12', captureLabel: '10-11', value: +(88 - zi * 3 - i * 0.6).toFixed(1), }, zi * 6 + i, ), ) } }) return { total: 5335, list } } function buildTimeline() { // 对齐原型「当前周期」:7 月~11 月 const tasks = [ { id: 'T-70', date: '2024-07-12', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' }, { id: 'T-71', date: '2024-07-26', type: 'manual', status: 'done', label: '人工普查', zoneId: 'P3' }, { id: 'T-72', date: '2024-08-08', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P1' }, { id: 'T-73', date: '2024-08-22', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' }, { id: 'T-74', date: '2024-09-05', type: 'manual', status: 'done', label: '人工普查', zoneId: 'P4' }, { id: 'T-75', date: '2024-09-18', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' }, { id: 'T-76', date: '2024-10-02', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P2' }, { id: 'T-77', date: '2024-10-11', type: 'manual', status: 'done', label: '人工普查', zoneId: 'P3' }, { id: 'T-78', date: '2024-10-22', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' }, { id: 'T-79', date: '2024-11-02', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' }, { id: 'T-80', date: '2024-11-08', type: 'manual', status: 'pending', label: '人工普查', zoneId: 'P4' }, { id: 'T-81', date: '2024-11-18', type: 'uav', status: 'todo', label: '飞巡', zoneId: 'P3' }, { id: 'T-82', date: '2024-11-25', type: 'uav', status: 'overdue', label: '飞巡', zoneId: 'P1' }, ] // 物候期对齐荔枝模板关键展示名 const phenology = [ { id: 'PH1', name: '果实成熟采收期', start: '2024-07-01', end: '2024-08-05', key: true }, { id: 'PH2', name: '秋梢萌发期', start: '2024-08-06', end: '2024-09-10', key: false }, { id: 'PH3', name: '秋梢老熟期', start: '2024-09-11', end: '2024-10-20', key: false }, { id: 'PH4', name: '控梢期(休眠期)', start: '2024-10-21', end: '2024-11-30', key: false }, ] // 识别指标来自 PHENO_CONFIG(荔枝模板),挂到各物候期中点 const plan = getPhenologyIndicatorPlan('荔枝') const indicators = [] let idx = 1 phenology.forEach((ph) => { const row = plan.find((p) => p.stage === ph.name || p.stage.includes(ph.name.replace(/[((].*$/, '')) || ph.name.includes(p.stage.replace(/[((].*$/, ''))) const names = row?.indicators?.length ? row.indicators : ['新梢整齐度'] const mid = new Date((+new Date(ph.start) + +new Date(ph.end)) / 2) const date = mid.toISOString().slice(0, 10) names.slice(0, 2).forEach((name, i) => { indicators.push({ id: `I${idx++}`, name, date, value: +(70 + idx * 1.7 + i).toFixed(1), unit: '%', source: i % 2 === 0 ? 'uav' : 'manual', stage: ph.name, }) }) }) const stresses = [ { id: 'S1', start: '2024-07-18', end: '2024-07-24', type: 'high_dry', label: '高温干旱' }, { id: 'S2', start: '2024-08-14', end: '2024-08-19', type: 'high_humid', label: '高温高湿' }, { id: 'S3', start: '2024-09-03', end: '2024-09-07', type: 'heavy_rain', label: '强降水' }, { id: 'S4', start: '2024-10-08', end: '2024-10-12', type: 'drought', label: '干旱' }, { id: 'S5', start: '2024-11-01', end: '2024-11-04', type: 'low_temp', label: '低温' }, ] const events = [ { date: '2024-11-11', title: '秋梢老熟普查' }, { date: '2024-11-17', title: '植株长势调控' }, { date: '2024-11-24', title: '控梢药剂评估' }, ] return { range: { start: '2024-07-01', end: '2024-11-30' }, today: '2024-11-03', zoneOptions: [ { id: 'P3', name: 'P3 分区 1B 实验区', role: 'exp' }, { id: 'P4', name: 'P4 分区 1B 对照区', role: 'ctl' }, { id: 'P1', name: 'P1 分区 1A 实验区', role: 'exp' }, { id: 'P2', name: 'P2 分区 1A 对照区', role: 'ctl' }, ], curveZoneOptions: [ { id: '1B', name: '分区 1B · 抗寒性试验区 (P3 / P4)', exp: 'P3', ctl: 'P4' }, { id: '1A', name: '分区 1A · 抗旱性试验区 (P1 / P2)', exp: 'P1', ctl: 'P2' }, ], tasks, indicators, phenology, stresses, events, } } export function isDongguanBase(baseOrId) { if (!baseOrId) return false if (typeof baseOrId === 'string') { return baseOrId === BASE_ID || baseOrId === mapPack.key || baseOrId === mapPack.name } return ( baseOrId.id === BASE_ID || baseOrId.name === mapPack.name || baseOrId.real?.key === mapPack.key || baseOrId.real?.name === mapPack.name ) } export async function mockGetOverview(baseId) { await delay() return ok({ id: BASE_ID, name: mapPack.name, title: `${mapPack.prov}·${mapPack.name}`, prov: mapPack.prov, city: mapPack.city, crop: mapPack.crop, mu: mapPack.mu, lng: mapPack.lon0, lat: mapPack.lat0, stage: { name: '控梢期(休眠期)', level: 'I', dayIndex: 20, dayTotal: 40, }, stress: { level: 'I', type: '', days: 12, color: 'var(--w-red)', has: true, }, survey: { zoneCount: 1, samplePoints: 219, plants: mapPack.plantCount || 1039, grids: mapPack.gridCount, }, mission: { lastFlightDate: '2024-11-02', lastFlightLabel: '11-02', flightIndex: 97, device: 'M3M', completionRate: 88.5, damageRate: 0.36, validPoints: { ok: 97, total: 97 }, outliers: { ok: 24, total: 24 }, }, /** 最近飞巡关键指标(左侧卡 KPI) */ indicators: [ { name: '秋梢老熟度', value: 88.5, unit: '%' }, { name: '抽梢率', value: 0.36, unit: '%' }, ], progress: { flightDone: 97, flightTotal: 97, indicatorDone: 24, indicatorTotal: 24, }, futureFlights: [ { date: '2024-11-11', dateLabel: '11-11', afterDays: 8, indicators: '秋梢老熟度、抽梢率', mode: '多光谱', }, { date: '2024-11-17', dateLabel: '11-17', afterDays: 14, indicators: '秋梢老熟度、抽梢率', mode: '多光谱', }, ], experimentGroups: [ { id: '1A', name: '分区1A · 抗旱性试验区', exp: ['P1'], ctl: ['P2'], focus: ['抗旱性', '长势'], start: '2024-11-15', end: '2024-11-30', }, { id: '1B', name: '分区1B · 抗寒性试验区', exp: ['P3'], ctl: ['P4'], focus: ['抗寒性', '长势'], start: '2024-11-15', end: '2024-11-30', }, ], recentEvents: buildTimeline().events, focus: ['秋梢老熟度', '控梢效果', '花芽分化率'], }) } export async function mockGetMapLayers(baseId) { await delay() const points = mapPack.zones.flatMap((z) => z.points.map((pt) => ({ ...pt, zoneId: z.id, role: z.role, group: z.group, })), ) return ok({ baseId: BASE_ID, center: [mapPack.lon0, mapPack.lat0], zoom: 17, legend: { experimental: true, control: true, samplePoints: mapPack.samplePointCount, grids: mapPack.gridCount, plants: mapPack.plantCount, }, zones: mapPack.zones.map((z) => ({ id: z.id, name: z.name, part: z.part, sub: z.sub, role: z.role, group: z.group, mu: z.mu, ring: z.ring, pointTotal: z.pointTotal, })), points, }) } export async function mockGetTimeline(baseId, params = {}) { await delay() const pack = buildTimeline() const zoneId = params.zoneId if (zoneId && zoneId !== 'all') { pack.tasks = pack.tasks.filter((t) => !t.zoneId || t.zoneId === zoneId) } return ok(pack) } export async function mockGetCurve(baseId, params = {}) { await delay() const indicator = params.indicator || '新梢整齐度' const pair = params.zonePair === '1A' ? { exp: 'P1', ctl: 'P2' } : { exp: 'P3', ctl: 'P4' } const useShoot = indicator.includes('新梢') || indicator.includes('整齐') const dates = useShoot ? SHOOT_DATES : CURVE_DATES const expVals = useShoot ? SHOOT_EXP : EXP_CURVE const ctlVals = useShoot ? SHOOT_CTL : CTL_CURVE return ok({ indicator, unit: '%', updatedAt: dates.at(-1), summary: { date: dates.at(-1), experimental: expVals.at(-1), control: ctlVals.at(-1), }, series: [ { key: 'exp', name: `实验组 ${pair.exp}`, role: 'exp', zoneId: pair.exp, style: 'solid', points: dates.map((date, i) => ({ date, value: expVals[i] ?? null })), }, { key: 'ctl', name: `对照组 ${pair.ctl}`, role: 'ctl', zoneId: pair.ctl, style: 'dashed', points: dates.map((date, i) => ({ date, value: ctlVals[i] ?? null })), }, ], }) } export async function mockGetComparePhotos(baseId, _params = {}) { await delay() return ok(buildComparePhotos()) } export async function mockGetLatestPhotos(baseId, params = {}) { await delay() const pack = buildLatestPhotos() let list = pack.list if (params.zoneId && params.zoneId !== 'all') { list = list.filter((x) => x.zoneId === params.zoneId) } if (params.date) { // mock 照片 captureDate 多为 2026;按月日模糊匹配任务日 const md = String(params.date).slice(5) list = list.filter((x) => (x.captureDate || '').includes(md) || (x.captureLabel || '') === md) if (!list.length) { // 无精确匹配时,返回该分区前几张并改写日期,便于任务点击演示 list = (pack.list || []) .filter((x) => !params.zoneId || params.zoneId === 'all' || x.zoneId === params.zoneId) .slice(0, 6) .map((x, i) => withViewerMeta( { ...x, captureDate: params.date, captureLabel: md, captureTime: `${params.date} 10:${String(12 + i).padStart(2, '0')}`, }, i, ), ) } } if (params.taskId && params.taskId !== 'all') { list = list.map((x) => ({ ...x, taskId: params.taskId })) } return ok({ total: params.date || params.zoneId ? list.length : pack.total, filters: { zones: buildTimeline().zoneOptions, dates: [{ id: 'all', name: '全部日期' }, { id: '2024-11-02', name: '2024-11-02' }], tasks: [ { id: 'all', name: '全部飞巡任务 (97)' }, { id: 'T-97', name: '第97次飞巡 · 11-02' }, ], sources: [ { id: 'all', name: '全部近景无人机照片' }, { id: 'close', name: '近景无人机' }, ], }, list, }) } export async function mockGetPointDetail(baseId, pointCode, params = {}) { await delay() const pt = mapPack.zones.flatMap((z) => z.points.map((p) => ({ ...p, zone: z }))).find((p) => p.code === pointCode) const zone = pt?.zone || zoneP3 const role = zone?.role || 'exp' const dates = datesBetween('2024-10-22', '2024-11-02', 2) const activeDate = params.date && dates.includes(params.date) ? params.date : dates.at(-1) const di = Math.max(0, dates.indexOf(activeDate)) const seed = (Number(String(pointCode).replace(/\D/g, '')) || 1) + di const baseVal = role === 'exp' ? 94.0 : 78.4 const val = +(baseVal - di * 1.6 + (seed % 3) * 0.3).toFixed(1) return ok({ pointCode, zoneId: zone?.id || 'P3', zoneName: zone?.name || '分区1B', role, thumbUrl: photoUrl(seed), dates, activeDate, metrics: [ { name: '新梢整齐度', value: val, unit: '%', delta: role === 'exp' ? +( -1.2 - di * 0.4).toFixed(1) : +( -0.6 - di * 0.2).toFixed(1), deltaUnit: '%', }, { name: '末次梢老熟进度', value: +(0.72 + di * 0.04 + (role === 'exp' ? 0.08 : 0)).toFixed(2), unit: '', delta: +(0.02 + di * 0.01).toFixed(2), deltaUnit: '', }, ], lng: pt?.lng ?? mapPack.lon0, lat: pt?.lat ?? mapPack.lat0, }) } export { BASE_ID, mapPack }