| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- /**
- * 东莞壹号农业 — 表型详情 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 }
|