dongguan.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. /**
  2. * 东莞壹号农业 — 表型详情 mock 数据
  3. * 地图分区/样点来自 realBases GIS;其余指标/任务/照片为演示数据,字段对齐接口契约。
  4. */
  5. import mapPack from './dongguan-map.json'
  6. import { getPhenologyIndicatorPlan } from '@/data/phenoConfig'
  7. const BASE_ID = mapPack.id // FN-155
  8. function ok(data) {
  9. return { success: true, code: 0, msg: 'ok', data }
  10. }
  11. function delay(ms = 120) {
  12. return new Promise((r) => setTimeout(r, ms))
  13. }
  14. /** 演示用 DJI 近景照片(循环复用) */
  15. const DEMO_PHOTOS = [
  16. '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',
  17. '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',
  18. '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',
  19. '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',
  20. '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',
  21. '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',
  22. ]
  23. function photoUrl(seed = 0) {
  24. return DEMO_PHOTOS[((seed % DEMO_PHOTOS.length) + DEMO_PHOTOS.length) % DEMO_PHOTOS.length]
  25. }
  26. const ZONE_META = {
  27. P1: { name: '分区1A 抗旱性试验区', role: '实验组', lng: 114.1233, lat: 22.9185 },
  28. P2: { name: '分区1A 抗旱性试验区', role: '对照组', lng: 114.1247, lat: 22.9187 },
  29. P3: { name: '分区1B 实验区', role: '实验组', lng: 114.1256, lat: 22.9194 },
  30. P4: { name: '分区1B 对照区', role: '对照组', lng: 114.1259, lat: 22.9192 },
  31. }
  32. /** 补齐弹窗所需元数据 */
  33. function withViewerMeta(photo, seed = 0) {
  34. const z = ZONE_META[photo.zoneId] || ZONE_META.P3
  35. const n = Number(String(photo.pointCode).replace(/\D/g, '')) || seed + 1
  36. const flight = 80 + (n % 10)
  37. const url = photo.thumbUrl || photoUrl(seed)
  38. const fileTail = String(1000 + (n % 90)).padStart(4, '0')
  39. return {
  40. ...photo,
  41. url,
  42. thumbUrl: url,
  43. filename: `DJI_2026101110${String(n).padStart(2, '0')}32_${fileTail}.JPG`,
  44. captureDate: photo.captureDate || '2026-10-11',
  45. captureTime: photo.captureTime || '2026-10-11 10:12',
  46. captureLabel: photo.captureLabel || '10-11',
  47. captureMethod: '样点高清',
  48. plotLabel: `${photo.zoneId} ${z.name} · ${z.role} · 采样点 ${photo.pointCode}`,
  49. taskLabel: `#F-${flight}-${photo.zoneId} · 计划巡检 · 秋梢老熟期 · 第 ${flight} 次飞巡`,
  50. altitudeGsd: '25 m / 1 cm · Mavic 3E 可见光',
  51. position: `${z.lat.toFixed(6)}, ${z.lng.toFixed(6)}`,
  52. metricTitle: '秋梢老熟期指标',
  53. metrics: [
  54. { name: '新梢整齐度', value: `${(photo.value ?? 73.3 - (n % 5) * 1.2).toFixed?.(1) ?? 73.3}%` },
  55. { name: '新梢表型状态', value: `${(0.7 + (n % 7) * 0.02).toFixed(2)}指数` },
  56. { name: '末次梢老熟进度', value: `${(0.75 + (n % 5) * 0.02).toFixed(2)}指数` },
  57. ],
  58. }
  59. }
  60. function datesBetween(start, end, stepDays = 3) {
  61. const out = []
  62. const cur = new Date(start)
  63. const last = new Date(end)
  64. while (cur <= last) {
  65. out.push(cur.toISOString().slice(0, 10))
  66. cur.setDate(cur.getDate() + stepDays)
  67. }
  68. return out
  69. }
  70. const CURVE_DATES = datesBetween('2024-10-15', '2024-11-07', 2)
  71. 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]
  72. 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]
  73. const SHOOT_DATES = datesBetween('2024-08-22', '2024-10-11', 5)
  74. const SHOOT_EXP = [62.4, 68.0, 74.2, 81.5, 88.6, 93.0, 95.4, 96.2, 97.0, 97.1]
  75. const SHOOT_CTL = [58.1, 61.5, 65.2, 68.8, 72.0, 74.5, 76.2, 77.4, 78.0, 78.2]
  76. const zoneP3 = mapPack.zones.find((z) => z.id === 'P3')
  77. const zoneP4 = mapPack.zones.find((z) => z.id === 'P4')
  78. function buildComparePhotos() {
  79. const exp = Array.from({ length: 8 }, (_, i) =>
  80. withViewerMeta(
  81. {
  82. pointCode: `P3-${String(i + 1).padStart(2, '0')}`,
  83. zoneId: 'P3',
  84. role: 'exp',
  85. thumbUrl: photoUrl(i),
  86. indicator: '秋梢老熟度',
  87. value: +(94.9 - i * 0.4 - (i % 3) * 0.3).toFixed(1),
  88. unit: '%',
  89. captureDate: '2026-10-11',
  90. captureTime: '2026-10-11 10:12',
  91. captureLabel: '10-11',
  92. },
  93. i,
  94. ),
  95. )
  96. const ctl = Array.from({ length: 8 }, (_, i) =>
  97. withViewerMeta(
  98. {
  99. pointCode: `P4-${String(i + 1).padStart(2, '0')}`,
  100. zoneId: 'P4',
  101. role: 'ctl',
  102. thumbUrl: photoUrl(i + 3),
  103. indicator: '秋梢老熟度',
  104. value: +(78.4 - i * 0.35 + (i % 2) * 0.5).toFixed(1),
  105. unit: '%',
  106. captureDate: '2026-10-11',
  107. captureTime: '2026-10-11 10:18',
  108. captureLabel: '10-11',
  109. },
  110. i + 8,
  111. ),
  112. )
  113. const mean = (arr) => +(arr.reduce((s, x) => s + x.value, 0) / arr.length).toFixed(1)
  114. return {
  115. indicator: '秋梢老熟度',
  116. date: '2026-10-11',
  117. experimental: { zoneId: 'P3', zoneName: zoneP3?.name || '分区1B 实验区', mean: mean(exp), photos: exp },
  118. control: { zoneId: 'P4', zoneName: zoneP4?.name || '分区1B 对照区', mean: mean(ctl), photos: ctl },
  119. }
  120. }
  121. function buildLatestPhotos() {
  122. const list = []
  123. ;['P3', 'P4', 'P1', 'P2'].forEach((zid, zi) => {
  124. for (let i = 1; i <= 6; i++) {
  125. list.push(
  126. withViewerMeta(
  127. {
  128. id: `PH-${zid}-${i}`,
  129. pointCode: `${zid}-${String(i).padStart(2, '0')}`,
  130. zoneId: zid,
  131. taskId: 'T-97',
  132. taskName: '第97次飞巡',
  133. source: '近景无人机',
  134. thumbUrl: photoUrl(zi * 2 + i),
  135. captureDate: '2026-10-11',
  136. captureTime: '2026-10-11 10:12',
  137. captureLabel: '10-11',
  138. value: +(88 - zi * 3 - i * 0.6).toFixed(1),
  139. },
  140. zi * 6 + i,
  141. ),
  142. )
  143. }
  144. })
  145. return { total: 5335, list }
  146. }
  147. function buildTimeline() {
  148. // 对齐原型「当前周期」:7 月~11 月
  149. const tasks = [
  150. { id: 'T-70', date: '2024-07-12', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' },
  151. { id: 'T-71', date: '2024-07-26', type: 'manual', status: 'done', label: '人工普查', zoneId: 'P3' },
  152. { id: 'T-72', date: '2024-08-08', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P1' },
  153. { id: 'T-73', date: '2024-08-22', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' },
  154. { id: 'T-74', date: '2024-09-05', type: 'manual', status: 'done', label: '人工普查', zoneId: 'P4' },
  155. { id: 'T-75', date: '2024-09-18', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' },
  156. { id: 'T-76', date: '2024-10-02', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P2' },
  157. { id: 'T-77', date: '2024-10-11', type: 'manual', status: 'done', label: '人工普查', zoneId: 'P3' },
  158. { id: 'T-78', date: '2024-10-22', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' },
  159. { id: 'T-79', date: '2024-11-02', type: 'uav', status: 'done', label: '飞巡', zoneId: 'P3' },
  160. { id: 'T-80', date: '2024-11-08', type: 'manual', status: 'pending', label: '人工普查', zoneId: 'P4' },
  161. { id: 'T-81', date: '2024-11-18', type: 'uav', status: 'todo', label: '飞巡', zoneId: 'P3' },
  162. { id: 'T-82', date: '2024-11-25', type: 'uav', status: 'overdue', label: '飞巡', zoneId: 'P1' },
  163. ]
  164. // 物候期对齐荔枝模板关键展示名
  165. const phenology = [
  166. { id: 'PH1', name: '果实成熟采收期', start: '2024-07-01', end: '2024-08-05', key: true },
  167. { id: 'PH2', name: '秋梢萌发期', start: '2024-08-06', end: '2024-09-10', key: false },
  168. { id: 'PH3', name: '秋梢老熟期', start: '2024-09-11', end: '2024-10-20', key: false },
  169. { id: 'PH4', name: '控梢期(休眠期)', start: '2024-10-21', end: '2024-11-30', key: false },
  170. ]
  171. // 识别指标来自 PHENO_CONFIG(荔枝模板),挂到各物候期中点
  172. const plan = getPhenologyIndicatorPlan('荔枝')
  173. const indicators = []
  174. let idx = 1
  175. phenology.forEach((ph) => {
  176. const row = plan.find((p) => p.stage === ph.name || p.stage.includes(ph.name.replace(/[((].*$/, '')) || ph.name.includes(p.stage.replace(/[((].*$/, '')))
  177. const names = row?.indicators?.length ? row.indicators : ['新梢整齐度']
  178. const mid = new Date((+new Date(ph.start) + +new Date(ph.end)) / 2)
  179. const date = mid.toISOString().slice(0, 10)
  180. names.slice(0, 2).forEach((name, i) => {
  181. indicators.push({
  182. id: `I${idx++}`,
  183. name,
  184. date,
  185. value: +(70 + idx * 1.7 + i).toFixed(1),
  186. unit: '%',
  187. source: i % 2 === 0 ? 'uav' : 'manual',
  188. stage: ph.name,
  189. })
  190. })
  191. })
  192. const stresses = [
  193. { id: 'S1', start: '2024-07-18', end: '2024-07-24', type: 'high_dry', label: '高温干旱' },
  194. { id: 'S2', start: '2024-08-14', end: '2024-08-19', type: 'high_humid', label: '高温高湿' },
  195. { id: 'S3', start: '2024-09-03', end: '2024-09-07', type: 'heavy_rain', label: '强降水' },
  196. { id: 'S4', start: '2024-10-08', end: '2024-10-12', type: 'drought', label: '干旱' },
  197. { id: 'S5', start: '2024-11-01', end: '2024-11-04', type: 'low_temp', label: '低温' },
  198. ]
  199. const events = [
  200. { date: '2024-11-11', title: '秋梢老熟普查' },
  201. { date: '2024-11-17', title: '植株长势调控' },
  202. { date: '2024-11-24', title: '控梢药剂评估' },
  203. ]
  204. return {
  205. range: { start: '2024-07-01', end: '2024-11-30' },
  206. today: '2024-11-03',
  207. zoneOptions: [
  208. { id: 'P3', name: 'P3 分区 1B 实验区', role: 'exp' },
  209. { id: 'P4', name: 'P4 分区 1B 对照区', role: 'ctl' },
  210. { id: 'P1', name: 'P1 分区 1A 实验区', role: 'exp' },
  211. { id: 'P2', name: 'P2 分区 1A 对照区', role: 'ctl' },
  212. ],
  213. curveZoneOptions: [
  214. { id: '1B', name: '分区 1B · 抗寒性试验区 (P3 / P4)', exp: 'P3', ctl: 'P4' },
  215. { id: '1A', name: '分区 1A · 抗旱性试验区 (P1 / P2)', exp: 'P1', ctl: 'P2' },
  216. ],
  217. tasks,
  218. indicators,
  219. phenology,
  220. stresses,
  221. events,
  222. }
  223. }
  224. export function isDongguanBase(baseOrId) {
  225. if (!baseOrId) return false
  226. if (typeof baseOrId === 'string') {
  227. return baseOrId === BASE_ID || baseOrId === mapPack.key || baseOrId === mapPack.name
  228. }
  229. return (
  230. baseOrId.id === BASE_ID ||
  231. baseOrId.name === mapPack.name ||
  232. baseOrId.real?.key === mapPack.key ||
  233. baseOrId.real?.name === mapPack.name
  234. )
  235. }
  236. export async function mockGetOverview(baseId) {
  237. await delay()
  238. return ok({
  239. id: BASE_ID,
  240. name: mapPack.name,
  241. title: `${mapPack.prov}·${mapPack.name}`,
  242. prov: mapPack.prov,
  243. city: mapPack.city,
  244. crop: mapPack.crop,
  245. mu: mapPack.mu,
  246. lng: mapPack.lon0,
  247. lat: mapPack.lat0,
  248. stage: {
  249. name: '控梢期(休眠期)',
  250. level: 'I',
  251. dayIndex: 20,
  252. dayTotal: 40,
  253. },
  254. stress: {
  255. level: 'I',
  256. type: '',
  257. days: 12,
  258. color: 'var(--w-red)',
  259. has: true,
  260. },
  261. survey: {
  262. zoneCount: 1,
  263. samplePoints: 219,
  264. plants: mapPack.plantCount || 1039,
  265. grids: mapPack.gridCount,
  266. },
  267. mission: {
  268. lastFlightDate: '2024-11-02',
  269. lastFlightLabel: '11-02',
  270. flightIndex: 97,
  271. device: 'M3M',
  272. completionRate: 88.5,
  273. damageRate: 0.36,
  274. validPoints: { ok: 97, total: 97 },
  275. outliers: { ok: 24, total: 24 },
  276. },
  277. /** 最近飞巡关键指标(左侧卡 KPI) */
  278. indicators: [
  279. { name: '秋梢老熟度', value: 88.5, unit: '%' },
  280. { name: '抽梢率', value: 0.36, unit: '%' },
  281. ],
  282. progress: {
  283. flightDone: 97,
  284. flightTotal: 97,
  285. indicatorDone: 24,
  286. indicatorTotal: 24,
  287. },
  288. futureFlights: [
  289. {
  290. date: '2024-11-11',
  291. dateLabel: '11-11',
  292. afterDays: 8,
  293. indicators: '秋梢老熟度、抽梢率',
  294. mode: '多光谱',
  295. },
  296. {
  297. date: '2024-11-17',
  298. dateLabel: '11-17',
  299. afterDays: 14,
  300. indicators: '秋梢老熟度、抽梢率',
  301. mode: '多光谱',
  302. },
  303. ],
  304. experimentGroups: [
  305. {
  306. id: '1A',
  307. name: '分区1A · 抗旱性试验区',
  308. exp: ['P1'],
  309. ctl: ['P2'],
  310. focus: ['抗旱性', '长势'],
  311. start: '2024-11-15',
  312. end: '2024-11-30',
  313. },
  314. {
  315. id: '1B',
  316. name: '分区1B · 抗寒性试验区',
  317. exp: ['P3'],
  318. ctl: ['P4'],
  319. focus: ['抗寒性', '长势'],
  320. start: '2024-11-15',
  321. end: '2024-11-30',
  322. },
  323. ],
  324. recentEvents: buildTimeline().events,
  325. focus: ['秋梢老熟度', '控梢效果', '花芽分化率'],
  326. })
  327. }
  328. export async function mockGetMapLayers(baseId) {
  329. await delay()
  330. const points = mapPack.zones.flatMap((z) =>
  331. z.points.map((pt) => ({
  332. ...pt,
  333. zoneId: z.id,
  334. role: z.role,
  335. group: z.group,
  336. })),
  337. )
  338. return ok({
  339. baseId: BASE_ID,
  340. center: [mapPack.lon0, mapPack.lat0],
  341. zoom: 17,
  342. legend: {
  343. experimental: true,
  344. control: true,
  345. samplePoints: mapPack.samplePointCount,
  346. grids: mapPack.gridCount,
  347. plants: mapPack.plantCount,
  348. },
  349. zones: mapPack.zones.map((z) => ({
  350. id: z.id,
  351. name: z.name,
  352. part: z.part,
  353. sub: z.sub,
  354. role: z.role,
  355. group: z.group,
  356. mu: z.mu,
  357. ring: z.ring,
  358. pointTotal: z.pointTotal,
  359. })),
  360. points,
  361. })
  362. }
  363. export async function mockGetTimeline(baseId, params = {}) {
  364. await delay()
  365. const pack = buildTimeline()
  366. const zoneId = params.zoneId
  367. if (zoneId && zoneId !== 'all') {
  368. pack.tasks = pack.tasks.filter((t) => !t.zoneId || t.zoneId === zoneId)
  369. }
  370. return ok(pack)
  371. }
  372. export async function mockGetCurve(baseId, params = {}) {
  373. await delay()
  374. const indicator = params.indicator || '新梢整齐度'
  375. const pair = params.zonePair === '1A' ? { exp: 'P1', ctl: 'P2' } : { exp: 'P3', ctl: 'P4' }
  376. const useShoot = indicator.includes('新梢') || indicator.includes('整齐')
  377. const dates = useShoot ? SHOOT_DATES : CURVE_DATES
  378. const expVals = useShoot ? SHOOT_EXP : EXP_CURVE
  379. const ctlVals = useShoot ? SHOOT_CTL : CTL_CURVE
  380. return ok({
  381. indicator,
  382. unit: '%',
  383. updatedAt: dates.at(-1),
  384. summary: {
  385. date: dates.at(-1),
  386. experimental: expVals.at(-1),
  387. control: ctlVals.at(-1),
  388. },
  389. series: [
  390. {
  391. key: 'exp',
  392. name: `实验组 ${pair.exp}`,
  393. role: 'exp',
  394. zoneId: pair.exp,
  395. style: 'solid',
  396. points: dates.map((date, i) => ({ date, value: expVals[i] ?? null })),
  397. },
  398. {
  399. key: 'ctl',
  400. name: `对照组 ${pair.ctl}`,
  401. role: 'ctl',
  402. zoneId: pair.ctl,
  403. style: 'dashed',
  404. points: dates.map((date, i) => ({ date, value: ctlVals[i] ?? null })),
  405. },
  406. ],
  407. })
  408. }
  409. export async function mockGetComparePhotos(baseId, _params = {}) {
  410. await delay()
  411. return ok(buildComparePhotos())
  412. }
  413. export async function mockGetLatestPhotos(baseId, params = {}) {
  414. await delay()
  415. const pack = buildLatestPhotos()
  416. let list = pack.list
  417. if (params.zoneId && params.zoneId !== 'all') {
  418. list = list.filter((x) => x.zoneId === params.zoneId)
  419. }
  420. if (params.date) {
  421. // mock 照片 captureDate 多为 2026;按月日模糊匹配任务日
  422. const md = String(params.date).slice(5)
  423. list = list.filter((x) => (x.captureDate || '').includes(md) || (x.captureLabel || '') === md)
  424. if (!list.length) {
  425. // 无精确匹配时,返回该分区前几张并改写日期,便于任务点击演示
  426. list = (pack.list || [])
  427. .filter((x) => !params.zoneId || params.zoneId === 'all' || x.zoneId === params.zoneId)
  428. .slice(0, 6)
  429. .map((x, i) =>
  430. withViewerMeta(
  431. {
  432. ...x,
  433. captureDate: params.date,
  434. captureLabel: md,
  435. captureTime: `${params.date} 10:${String(12 + i).padStart(2, '0')}`,
  436. },
  437. i,
  438. ),
  439. )
  440. }
  441. }
  442. if (params.taskId && params.taskId !== 'all') {
  443. list = list.map((x) => ({ ...x, taskId: params.taskId }))
  444. }
  445. return ok({
  446. total: params.date || params.zoneId ? list.length : pack.total,
  447. filters: {
  448. zones: buildTimeline().zoneOptions,
  449. dates: [{ id: 'all', name: '全部日期' }, { id: '2024-11-02', name: '2024-11-02' }],
  450. tasks: [
  451. { id: 'all', name: '全部飞巡任务 (97)' },
  452. { id: 'T-97', name: '第97次飞巡 · 11-02' },
  453. ],
  454. sources: [
  455. { id: 'all', name: '全部近景无人机照片' },
  456. { id: 'close', name: '近景无人机' },
  457. ],
  458. },
  459. list,
  460. })
  461. }
  462. export async function mockGetPointDetail(baseId, pointCode, params = {}) {
  463. await delay()
  464. const pt = mapPack.zones.flatMap((z) => z.points.map((p) => ({ ...p, zone: z }))).find((p) => p.code === pointCode)
  465. const zone = pt?.zone || zoneP3
  466. const role = zone?.role || 'exp'
  467. const dates = datesBetween('2024-10-22', '2024-11-02', 2)
  468. const activeDate = params.date && dates.includes(params.date) ? params.date : dates.at(-1)
  469. const di = Math.max(0, dates.indexOf(activeDate))
  470. const seed = (Number(String(pointCode).replace(/\D/g, '')) || 1) + di
  471. const baseVal = role === 'exp' ? 94.0 : 78.4
  472. const val = +(baseVal - di * 1.6 + (seed % 3) * 0.3).toFixed(1)
  473. return ok({
  474. pointCode,
  475. zoneId: zone?.id || 'P3',
  476. zoneName: zone?.name || '分区1B',
  477. role,
  478. thumbUrl: photoUrl(seed),
  479. dates,
  480. activeDate,
  481. metrics: [
  482. {
  483. name: '新梢整齐度',
  484. value: val,
  485. unit: '%',
  486. delta: role === 'exp' ? +( -1.2 - di * 0.4).toFixed(1) : +( -0.6 - di * 0.2).toFixed(1),
  487. deltaUnit: '%',
  488. },
  489. {
  490. name: '末次梢老熟进度',
  491. value: +(0.72 + di * 0.04 + (role === 'exp' ? 0.08 : 0)).toFixed(2),
  492. unit: '',
  493. delta: +(0.02 + di * 0.01).toFixed(2),
  494. deltaUnit: '',
  495. },
  496. ],
  497. lng: pt?.lng ?? mapPack.lon0,
  498. lat: pt?.lat ?? mapPack.lat0,
  499. })
  500. }
  501. export { BASE_ID, mapPack }