Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1e0cae07a | ||
|
|
352a94b148 | ||
|
|
d01525fd51 | ||
|
|
c0ed0048aa |
@@ -73,7 +73,7 @@ src/
|
|||||||
|
|
||||||
打卡为匿名本机 POC:`src/data/check-in/tasks.ts` 会为当前 POI 仓库中的全部已发布点位生成打卡任务;点位下线后会自动停止新打卡,重新发布后仍保留本机历史记录以防重复领奖。所有点位的游客入口坐标与安全到达范围仍需 iOS 现场复核后才能用于正式验收。每个开放点位首次成功打卡增加 10 积分,累计完成 1、2、3 个开放点位时依次解锁徽章。积分、徽章与足迹使用独立版本化存储键;v1 旧格式首次升级时只迁移当前开放点位记录,进入 v2 后则保留合法历史足迹。清除操作只删除打卡数据,不影响路线和偏好。打卡记录不保存经纬度、定位精度或轨迹。微信客户端无法可靠识别系统级缓存位置或虚拟定位,因此本期只承诺每次点击重新调用定位接口并校验微信返回的精度,不提供虚拟定位测试入口或生产级反作弊承诺。
|
打卡为匿名本机 POC:`src/data/check-in/tasks.ts` 会为当前 POI 仓库中的全部已发布点位生成打卡任务;点位下线后会自动停止新打卡,重新发布后仍保留本机历史记录以防重复领奖。所有点位的游客入口坐标与安全到达范围仍需 iOS 现场复核后才能用于正式验收。每个开放点位首次成功打卡增加 10 积分,累计完成 1、2、3 个开放点位时依次解锁徽章。积分、徽章与足迹使用独立版本化存储键;v1 旧格式首次升级时只迁移当前开放点位记录,进入 v2 后则保留合法历史足迹。清除操作只删除打卡数据,不影响路线和偏好。打卡记录不保存经纬度、定位精度或轨迹。微信客户端无法可靠识别系统级缓存位置或虚拟定位,因此本期只承诺每次点击重新调用定位接口并校验微信返回的精度,不提供虚拟定位测试入口或生产级反作弊承诺。
|
||||||
|
|
||||||
路由来自页面 SFC 的 `<route>`,由 UniPages 在编译时生成 `src/pages.json`;应用清单由 `manifest.config.ts` 生成 `src/manifest.json`。不要直接修改这两个生成文件,也不要让微信开发者工具读取 `src`;IDE 只读取 `dist/dev/mp-weixin`。
|
路由来自页面 SFC 的 `<route>`,由 UniPages 在编译时生成 `src/pages.json`;应用清单由 `manifest.config.ts` 生成 `src/manifest.json`。不要直接修改这两个生成文件,也不要让微信开发者工具读取 `src`;IDE 只读取 `dist/dev/mp-weixin`。仓库中的 `src/manifest.json` 以开发模式快照为准,`【dev】` / `【prod】` 项目名前缀由构建模式自动生成;提交时不应单独保留由最后一次构建造成的前缀漂移。
|
||||||
|
|
||||||
## POC 数据声明
|
## POC 数据声明
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,7 @@
|
|||||||
"postcss-scss": "^4.0.9",
|
"postcss-scss": "^4.0.9",
|
||||||
"sass": "1.63.2",
|
"sass": "1.63.2",
|
||||||
"sass-loader": "10.4.1",
|
"sass-loader": "10.4.1",
|
||||||
|
"sharp": "^0.35.3",
|
||||||
"simple-git-hooks": "^2.11.1",
|
"simple-git-hooks": "^2.11.1",
|
||||||
"stylelint": "^16.9.0",
|
"stylelint": "^16.9.0",
|
||||||
"stylelint-config-recess-order": "^5.1.1",
|
"stylelint-config-recess-order": "^5.1.1",
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ importers:
|
|||||||
sass-loader:
|
sass-loader:
|
||||||
specifier: 10.4.1
|
specifier: 10.4.1
|
||||||
version: 10.4.1(sass@1.63.2)(webpack@5.99.9)
|
version: 10.4.1(sass@1.63.2)(webpack@5.99.9)
|
||||||
|
sharp:
|
||||||
|
specifier: ^0.35.3
|
||||||
|
version: 0.35.3(@types/node@24.0.3)
|
||||||
simple-git-hooks:
|
simple-git-hooks:
|
||||||
specifier: ^2.11.1
|
specifier: ^2.11.1
|
||||||
version: 2.13.0
|
version: 2.13.0
|
||||||
@@ -1072,6 +1075,9 @@ packages:
|
|||||||
'@emnapi/core@1.4.3':
|
'@emnapi/core@1.4.3':
|
||||||
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
|
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.11.3':
|
||||||
|
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
|
||||||
|
|
||||||
'@emnapi/runtime@1.4.3':
|
'@emnapi/runtime@1.4.3':
|
||||||
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
|
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
|
||||||
|
|
||||||
@@ -1310,6 +1316,152 @@ packages:
|
|||||||
'@iconify/utils@2.3.0':
|
'@iconify/utils@2.3.0':
|
||||||
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
||||||
|
|
||||||
|
'@img/colour@1.1.0':
|
||||||
|
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-freebsd-wasm32@0.35.3':
|
||||||
|
resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.3.2':
|
||||||
|
resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.3.2':
|
||||||
|
resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.3.2':
|
||||||
|
resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.3.2':
|
||||||
|
resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.3.2':
|
||||||
|
resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.3.2':
|
||||||
|
resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.3.2':
|
||||||
|
resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.3.2':
|
||||||
|
resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.3.2':
|
||||||
|
resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.3.2':
|
||||||
|
resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.35.3':
|
||||||
|
resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.35.3':
|
||||||
|
resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.35.3':
|
||||||
|
resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
|
||||||
|
'@img/sharp-webcontainers-wasm32@0.35.3':
|
||||||
|
resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.35.3':
|
||||||
|
resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==}
|
||||||
|
engines: {node: ^20.9.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.35.3':
|
||||||
|
resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@intlify/core-base@9.1.9':
|
'@intlify/core-base@9.1.9':
|
||||||
resolution: {integrity: sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==}
|
resolution: {integrity: sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -3196,6 +3348,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
detect-newline@3.1.0:
|
detect-newline@3.1.0:
|
||||||
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -5503,6 +5659,11 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.8.5:
|
||||||
|
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
send@0.18.0:
|
send@0.18.0:
|
||||||
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
|
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -5521,6 +5682,15 @@ packages:
|
|||||||
setprototypeof@1.2.0:
|
setprototypeof@1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
|
sharp@0.35.3:
|
||||||
|
resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -8115,6 +8285,11 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.11.3':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@emnapi/runtime@1.4.3':
|
'@emnapi/runtime@1.4.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -8305,6 +8480,112 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@img/colour@1.1.0': {}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-freebsd-wasm32@0.35.3':
|
||||||
|
dependencies:
|
||||||
|
'@img/sharp-wasm32': 0.35.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.3.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-riscv64': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.35.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.3.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.35.3':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/runtime': 1.11.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-webcontainers-wasm32@0.35.3':
|
||||||
|
dependencies:
|
||||||
|
'@img/sharp-wasm32': 0.35.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.35.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.35.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.35.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@intlify/core-base@9.1.9':
|
'@intlify/core-base@9.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/devtools-if': 9.1.9
|
'@intlify/devtools-if': 9.1.9
|
||||||
@@ -10615,6 +10896,8 @@ snapshots:
|
|||||||
|
|
||||||
detect-indent@6.1.0: {}
|
detect-indent@6.1.0: {}
|
||||||
|
|
||||||
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
detect-newline@3.1.0: {}
|
detect-newline@3.1.0: {}
|
||||||
|
|
||||||
devlop@1.1.0:
|
devlop@1.1.0:
|
||||||
@@ -13353,6 +13636,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.7.2: {}
|
semver@7.7.2: {}
|
||||||
|
|
||||||
|
semver@7.8.5: {}
|
||||||
|
|
||||||
send@0.18.0:
|
send@0.18.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 2.6.9
|
debug: 2.6.9
|
||||||
@@ -13404,6 +13689,39 @@ snapshots:
|
|||||||
|
|
||||||
setprototypeof@1.2.0: {}
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
|
sharp@0.35.3(@types/node@24.0.3):
|
||||||
|
dependencies:
|
||||||
|
'@img/colour': 1.1.0
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
semver: 7.8.5
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.35.3
|
||||||
|
'@img/sharp-darwin-x64': 0.35.3
|
||||||
|
'@img/sharp-freebsd-wasm32': 0.35.3
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.3.2
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.3.2
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.3.2
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.3.2
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.3.2
|
||||||
|
'@img/sharp-libvips-linux-riscv64': 1.3.2
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.3.2
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.3.2
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.3.2
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.3.2
|
||||||
|
'@img/sharp-linux-arm': 0.35.3
|
||||||
|
'@img/sharp-linux-arm64': 0.35.3
|
||||||
|
'@img/sharp-linux-ppc64': 0.35.3
|
||||||
|
'@img/sharp-linux-riscv64': 0.35.3
|
||||||
|
'@img/sharp-linux-s390x': 0.35.3
|
||||||
|
'@img/sharp-linux-x64': 0.35.3
|
||||||
|
'@img/sharp-linuxmusl-arm64': 0.35.3
|
||||||
|
'@img/sharp-linuxmusl-x64': 0.35.3
|
||||||
|
'@img/sharp-webcontainers-wasm32': 0.35.3
|
||||||
|
'@img/sharp-win32-arm64': 0.35.3
|
||||||
|
'@img/sharp-win32-ia32': 0.35.3
|
||||||
|
'@img/sharp-win32-x64': 0.35.3
|
||||||
|
'@types/node': 24.0.3
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"ignore": [],
|
"ignore": [],
|
||||||
"include": []
|
"include": []
|
||||||
},
|
},
|
||||||
"appid": "wx946b42048d117dde",
|
"appid": "wxda4fb5048b5dc9a1",
|
||||||
"editorSetting": {},
|
"editorSetting": {},
|
||||||
"condition": {}
|
"condition": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import sharp from 'sharp'
|
||||||
|
import { readdir, mkdir, unlink } from 'node:fs/promises'
|
||||||
|
import { join, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const projectRoot = join(__dirname, '..')
|
||||||
|
const sourceDir = join(projectRoot, 'src/static/poi/photos')
|
||||||
|
const outputDir = sourceDir // 处理后直接覆盖到同目录(原文件先删除)
|
||||||
|
|
||||||
|
// 中文名 → POI ID 映射(全部30个景点)
|
||||||
|
const NAME_TO_POI_ID = {
|
||||||
|
// 文化场馆(已处理,保留映射以便确认)
|
||||||
|
'文化艺术中心': 'poi_gm_culture_center',
|
||||||
|
'光明区图书馆': 'poi_amap_b0hbtu4bfr',
|
||||||
|
'光明区文化馆': 'poi_amap_b0ffk306gx',
|
||||||
|
'少年儿童图书馆': 'poi_amap_b02f38meio',
|
||||||
|
'深圳国际美术馆': 'poi_amap_b0jun773j3',
|
||||||
|
'城市规划馆': 'poi_amap_b0jbbzx4oo',
|
||||||
|
'茅洲河图书馆': 'poi_amap_b0k025w32e',
|
||||||
|
// 公园湿地
|
||||||
|
'虹桥公园': 'poi_hongqiao_park',
|
||||||
|
'红花山公园': 'poi_honghuashan_park',
|
||||||
|
'科学公园': 'poi_amap_b0j2jc4n0w',
|
||||||
|
'鹅颈水湿地公园': 'poi_amap_b0ffmdhve1',
|
||||||
|
'明湖公园': 'poi_amap_b0fffwu1l5',
|
||||||
|
'光明新城公园': 'poi_amap_b0ffhfbsje',
|
||||||
|
'开明公园': 'poi_amap_b0ffjf1b20',
|
||||||
|
'左岸科技公园': 'poi_amap_b0g0kzrt5s',
|
||||||
|
'西田公园': 'poi_amap_b02f30abgs',
|
||||||
|
'大雁山森林公园': 'poi_amap_b0jgbp18uf',
|
||||||
|
'楼村湿地公园': 'poi_amap_b0grac6nzz',
|
||||||
|
'大顶岭山林公园': 'poi_amap_b02f309r88',
|
||||||
|
// 田园休闲
|
||||||
|
'光明农场大观园': 'poi_farm_grand_view',
|
||||||
|
'华侨城光明欢乐田园': 'poi_happy_pastoral',
|
||||||
|
'双晖稻田农场': 'poi_amap_b0ffkkmzsc',
|
||||||
|
// 人文地标
|
||||||
|
'弘源寺': 'poi_amap_b02f38nj9k',
|
||||||
|
'陈仙姑祠': 'poi_amap_b0ffgthbcf',
|
||||||
|
'黄氏大宗祠': 'poi_amap_b0fffttiov',
|
||||||
|
'玉律醒狮文化馆': 'poi_amap_b0kb4cxaby',
|
||||||
|
// 绿道户外
|
||||||
|
'大顶岭绿道': 'poi_dadingling_greenway',
|
||||||
|
'光明湖碧道': 'poi_amap_b0ldbsbeea',
|
||||||
|
'茅洲河碧道': 'poi_amap_b0kup558j4',
|
||||||
|
'五指耙森林公园': 'poi_amap_b0jb2aurvw',
|
||||||
|
}
|
||||||
|
|
||||||
|
const TARGET_WIDTH = 800
|
||||||
|
const TARGET_HEIGHT = 600
|
||||||
|
const JPEG_QUALITY = 80
|
||||||
|
|
||||||
|
async function process() {
|
||||||
|
const files = await readdir(sourceDir)
|
||||||
|
const jpgFiles = files.filter(f => f.endsWith('.jpg') || f.endsWith('.jpeg') || f.endsWith('.png'))
|
||||||
|
|
||||||
|
console.log(`Found ${jpgFiles.length} source images\n`)
|
||||||
|
|
||||||
|
for (const file of jpgFiles) {
|
||||||
|
const nameWithoutExt = file.replace(/\.(jpg|jpeg|png)$/i, '')
|
||||||
|
const poiId = NAME_TO_POI_ID[nameWithoutExt]
|
||||||
|
|
||||||
|
if (!poiId) {
|
||||||
|
console.log(`SKIP: "${file}" — no matching POI ID found`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = join(sourceDir, file)
|
||||||
|
const outputName = `${poiId}.jpg`
|
||||||
|
const outputPath = join(outputDir, outputName)
|
||||||
|
|
||||||
|
console.log(`Processing: ${file} → ${outputName}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sharp(inputPath)
|
||||||
|
.rotate() // 根据 EXIF 自动旋转
|
||||||
|
.resize(TARGET_WIDTH, TARGET_HEIGHT, {
|
||||||
|
fit: 'cover',
|
||||||
|
position: 'attention', // 智能裁剪,尽量保留重要区域
|
||||||
|
})
|
||||||
|
.jpeg({
|
||||||
|
quality: JPEG_QUALITY,
|
||||||
|
mozjpeg: true,
|
||||||
|
chromaSubsampling: '4:2:0',
|
||||||
|
})
|
||||||
|
.toFile(outputPath)
|
||||||
|
|
||||||
|
// 删除原始中文名文件
|
||||||
|
if (file !== outputName) {
|
||||||
|
await unlink(inputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ✓ Done`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` ✗ Failed: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nDone! Listing output directory:')
|
||||||
|
const outputFiles = await readdir(outputDir)
|
||||||
|
for (const f of outputFiles.filter(f => f.endsWith('.jpg')).sort()) {
|
||||||
|
console.log(` ${f}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process().catch(console.error)
|
||||||
@@ -25,11 +25,6 @@ class PlanningMode(str, Enum):
|
|||||||
CUSTOM = "custom"
|
CUSTOM = "custom"
|
||||||
|
|
||||||
|
|
||||||
class Duration(str, Enum):
|
|
||||||
HALF_DAY = "half_day"
|
|
||||||
FULL_DAY = "full_day"
|
|
||||||
|
|
||||||
|
|
||||||
class Pace(str, Enum):
|
class Pace(str, Enum):
|
||||||
RELAXED = "relaxed"
|
RELAXED = "relaxed"
|
||||||
MODERATE = "moderate"
|
MODERATE = "moderate"
|
||||||
@@ -42,12 +37,6 @@ class Transport(str, Enum):
|
|||||||
PUBLIC_TRANSPORT = "public_transport"
|
PUBLIC_TRANSPORT = "public_transport"
|
||||||
|
|
||||||
|
|
||||||
class BudgetLevel(str, Enum):
|
|
||||||
ECONOMY = "economy"
|
|
||||||
STANDARD = "standard"
|
|
||||||
QUALITY = "quality"
|
|
||||||
|
|
||||||
|
|
||||||
Theme = Literal["亲子", "情侣", "朋友", "银发", "研学"]
|
Theme = Literal["亲子", "情侣", "朋友", "银发", "研学"]
|
||||||
Interest = Literal["自然风光", "文化场馆", "生态科普", "美食", "摄影"]
|
Interest = Literal["自然风光", "文化场馆", "生态科普", "美食", "摄影"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { Poi, PoiDataset, PoiImageAsset } from '@/domain/poi'
|
import type { Poi, PoiDataset, PoiImageAsset } from '@/domain/poi'
|
||||||
|
import { coverPhotoUrlFor, hasCoverPhoto } from './photos'
|
||||||
|
|
||||||
const SNAPSHOT_CAPTURED_AT = '2026-07-30T01:00:33Z'
|
const SNAPSHOT_CAPTURED_AT = '2026-07-30T01:00:33Z'
|
||||||
const POC_REVIEWED_AT = '2026-07-30T01:30:00Z'
|
const POC_REVIEWED_AT = '2026-07-30T01:30:00Z'
|
||||||
|
const PHOTOS_REVIEWED_AT = '2026-07-31T00:00:00Z'
|
||||||
const PLACEHOLDER_URL = '/static/poi/placeholder.png'
|
const PLACEHOLDER_URL = '/static/poi/placeholder.png'
|
||||||
const AMAP_DATA_SOURCE = '高德地图 Web Service POI 2.0 离线候选快照(2026-07-30),POC 内容映射待项目方复核'
|
const AMAP_DATA_SOURCE = '高德地图 Web Service POI 2.0 离线候选快照(2026-07-30),POC 内容映射待项目方复核'
|
||||||
|
|
||||||
@@ -416,19 +418,26 @@ const poiSeeds: PocPoiSeed[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function createPlaceholderImage(seed: PocPoiSeed): PoiImageAsset {
|
function createCoverImage(seed: PocPoiSeed): PoiImageAsset {
|
||||||
|
const photoReady = hasCoverPhoto(seed.id)
|
||||||
return {
|
return {
|
||||||
id: `img_${seed.id}_cover`,
|
id: `img_${seed.id}_cover`,
|
||||||
url: PLACEHOLDER_URL,
|
url: photoReady ? coverPhotoUrlFor(seed.id) : PLACEHOLDER_URL,
|
||||||
source: 'map-gm-app POC 包内占位素材',
|
source: photoReady
|
||||||
sourceReference: 'asset_src_static_poi_placeholder',
|
? '项目方提供实景照片(本地静态资源)'
|
||||||
|
: 'map-gm-app POC 包内占位素材',
|
||||||
|
sourceReference: photoReady
|
||||||
|
? `asset_src_static_poi_photos_${seed.id}_jpg`
|
||||||
|
: 'asset_src_static_poi_placeholder',
|
||||||
copyrightStatus: 'cleared',
|
copyrightStatus: 'cleared',
|
||||||
licenseEvidence: 'POC_INTERNAL_ASSET_001',
|
licenseEvidence: photoReady
|
||||||
reviewedBy: 'poc_content_owner',
|
? 'PROJECT_PROVIDED_COVER_PHOTO_2026Q3'
|
||||||
reviewedAt: POC_REVIEWED_AT,
|
: 'POC_INTERNAL_ASSET_001',
|
||||||
updatedAt: POC_REVIEWED_AT,
|
reviewedBy: photoReady ? 'project_content_owner' : 'poc_content_owner',
|
||||||
isPlaceholder: true,
|
reviewedAt: photoReady ? PHOTOS_REVIEWED_AT : POC_REVIEWED_AT,
|
||||||
alt: `${seed.name} POC 占位图`,
|
updatedAt: photoReady ? PHOTOS_REVIEWED_AT : POC_REVIEWED_AT,
|
||||||
|
isPlaceholder: !photoReady,
|
||||||
|
alt: photoReady ? `${seed.name}实景照片` : `${seed.name} POC 占位图`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +553,6 @@ export const poiDataset: PoiDataset = {
|
|||||||
{ code: 'science_learning', name: '科普体验', sortOrder: 90, status: 'enabled', synonyms: ['科普'] },
|
{ code: 'science_learning', name: '科普体验', sortOrder: 90, status: 'enabled', synonyms: ['科普'] },
|
||||||
{ code: 'walking', name: '漫步休闲', sortOrder: 100, status: 'enabled', synonyms: ['徒步', '散步'] },
|
{ code: 'walking', name: '漫步休闲', sortOrder: 100, status: 'enabled', synonyms: ['徒步', '散步'] },
|
||||||
],
|
],
|
||||||
images: poiSeeds.map(createPlaceholderImage),
|
images: poiSeeds.map(createCoverImage),
|
||||||
pois: poiSeeds.map(createPoi),
|
pois: poiSeeds.map(createPoi),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* POI 封面照片注册表
|
||||||
|
*
|
||||||
|
* 如何添加一张真实照片:
|
||||||
|
* 1. 压缩图片:800×600 px(4:3 横版),JPEG 质量 75-82,文件 ≤ 120 KB
|
||||||
|
* 2. 命名为 <poiId>.jpg(例如 poi_gm_culture_center.jpg)
|
||||||
|
* 3. 放入 src/static/poi/photos/ 目录
|
||||||
|
* 4. 将 poiId 加入下方 POI_COVER_PHOTOS_READY 集合
|
||||||
|
*
|
||||||
|
* 未在此集合中的 POI 会继续使用占位图 /static/poi/placeholder.png
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 已放入真实照片的 POI ID 集合 */
|
||||||
|
export const POI_COVER_PHOTOS_READY: ReadonlySet<string> = new Set<string>([
|
||||||
|
// 文化场馆
|
||||||
|
'poi_gm_culture_center', // 光明文化艺术中心
|
||||||
|
'poi_amap_b0hbtu4bfr', // 光明区图书馆
|
||||||
|
'poi_amap_b0ffk306gx', // 光明区文化馆
|
||||||
|
'poi_amap_b02f38meio', // 光明区少年儿童图书馆
|
||||||
|
'poi_amap_b0jun773j3', // 深圳国际美术馆
|
||||||
|
'poi_amap_b0jbbzx4oo', // 光明区城市规划展览馆
|
||||||
|
'poi_amap_b0k025w32e', // 茅洲河图书馆
|
||||||
|
// 公园湿地
|
||||||
|
'poi_hongqiao_park', // 虹桥公园
|
||||||
|
'poi_honghuashan_park', // 红花山公园
|
||||||
|
'poi_amap_b0j2jc4n0w', // 科学公园
|
||||||
|
'poi_amap_b0ffmdhve1', // 鹅颈水湿地公园
|
||||||
|
'poi_amap_b0fffwu1l5', // 明湖公园
|
||||||
|
'poi_amap_b0ffhfbsje', // 光明新城公园(牛山公园)
|
||||||
|
'poi_amap_b0ffjf1b20', // 光明开明公园
|
||||||
|
'poi_amap_b0g0kzrt5s', // 左岸科技公园
|
||||||
|
'poi_amap_b02f30abgs', // 西田公园
|
||||||
|
'poi_amap_b0jgbp18uf', // 大雁山森林公园
|
||||||
|
'poi_amap_b0grac6nzz', // 楼村湿地公园
|
||||||
|
'poi_amap_b02f309r88', // 大顶岭山林公园
|
||||||
|
// 田园休闲
|
||||||
|
'poi_farm_grand_view', // 光明农场大观园
|
||||||
|
'poi_happy_pastoral', // 华侨城光明欢乐田园
|
||||||
|
'poi_amap_b0ffkkmzsc', // 双晖稻田农场
|
||||||
|
// 人文地标
|
||||||
|
'poi_amap_b02f38nj9k', // 弘源寺
|
||||||
|
'poi_amap_b0ffgthbcf', // 陈仙姑祠
|
||||||
|
'poi_amap_b0fffttiov', // 黄氏大宗祠
|
||||||
|
'poi_amap_b0kb4cxaby', // 玉律醒狮文化馆
|
||||||
|
// 绿道户外
|
||||||
|
'poi_dadingling_greenway', // 大顶岭绿道
|
||||||
|
'poi_amap_b0ldbsbeea', // 光明湖碧道
|
||||||
|
'poi_amap_b0kup558j4', // 茅洲河碧道
|
||||||
|
'poi_amap_b0jb2aurvw', // 五指耙森林公园(光明片区)
|
||||||
|
])
|
||||||
|
|
||||||
|
const POI_PHOTOS_BASE_URL = '/static/poi/photos'
|
||||||
|
|
||||||
|
export function coverPhotoUrlFor(poiId: string): string {
|
||||||
|
return `${POI_PHOTOS_BASE_URL}/${poiId}.jpg`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasCoverPhoto(poiId: string): boolean {
|
||||||
|
return POI_COVER_PHOTOS_READY.has(poiId)
|
||||||
|
}
|
||||||
@@ -294,6 +294,13 @@ function selectQuickPois(
|
|||||||
cursor = next
|
cursor = next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selected.length === 0 && remaining.length > 0) {
|
||||||
|
throw new TravelValidationError(
|
||||||
|
'当前起点、交通方式与所选时长无法容纳首个点位',
|
||||||
|
'quick_route_unreachable',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return selected
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import {
|
|||||||
|
|
||||||
const DESTINATION = '深圳市光明区' as const
|
const DESTINATION = '深圳市光明区' as const
|
||||||
|
|
||||||
|
export type TravelValidationErrorCode = 'quick_route_unreachable'
|
||||||
|
|
||||||
export class TravelValidationError extends Error {
|
export class TravelValidationError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string, readonly code?: TravelValidationErrorCode) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'TravelValidationError'
|
this.name = 'TravelValidationError'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"quickapp": {},
|
"quickapp": {},
|
||||||
"mp-weixin": {
|
"mp-weixin": {
|
||||||
"appid": "wx946b42048d117dde",
|
"appid": "wxda4fb5048b5dc9a1",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": true
|
"urlCheck": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface BadgeView extends CheckInBadgeDefinition {
|
|||||||
|
|
||||||
const profile = ref<CheckInProfile>({ points: 0, badges: [], checkIns: [] })
|
const profile = ref<CheckInProfile>({ points: 0, badges: [], checkIns: [] })
|
||||||
const storageError = ref('')
|
const storageError = ref('')
|
||||||
|
const poiDirectoryError = ref('')
|
||||||
const availablePois = shallowRef<PoiResolved[]>([])
|
const availablePois = shallowRef<PoiResolved[]>([])
|
||||||
const badgeViews = computed<BadgeView[]>(() => CHECK_IN_BADGES.map(definition => ({
|
const badgeViews = computed<BadgeView[]>(() => CHECK_IN_BADGES.map(definition => ({
|
||||||
...definition,
|
...definition,
|
||||||
@@ -31,7 +32,20 @@ function formatTime(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openPoi(poiId: string) {
|
function openPoi(poiId: string) {
|
||||||
if (!getPoiRepository().getPoiById(poiId)) {
|
let poi: PoiResolved | null = null
|
||||||
|
try {
|
||||||
|
poi = getPoiRepository().getPoiById(poiId)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
poiDirectoryError.value = error instanceof Error ? error.message : '开放打卡点位暂时无法读取'
|
||||||
|
uni.showModal({
|
||||||
|
title: '暂时无法打开点位',
|
||||||
|
content: '开放点位目录暂时无法读取,请稍后重新加载。',
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!poi) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '点位已下线',
|
title: '点位已下线',
|
||||||
content: '该点位暂不在当前开放列表中,本机历史打卡记录仍会保留。',
|
content: '该点位暂不在当前开放列表中,本机历史打卡记录仍会保留。',
|
||||||
@@ -42,12 +56,35 @@ function openPoi(poiId: string) {
|
|||||||
uni.navigateTo({ url: `/pages/poi/detail?poiId=${encodeURIComponent(poiId)}` })
|
uni.navigateTo({ url: `/pages/poi/detail?poiId=${encodeURIComponent(poiId)}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadProfile() {
|
||||||
|
try {
|
||||||
|
profile.value = getCheckInProfile()
|
||||||
|
storageError.value = ''
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
profile.value = { points: 0, badges: [], checkIns: [] }
|
||||||
|
storageError.value = error instanceof Error ? error.message : '本机打卡记录暂时无法读取'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadAvailablePois() {
|
function loadAvailablePois() {
|
||||||
const repository = getPoiRepository()
|
try {
|
||||||
availablePois.value = getCheckInTasks().flatMap((task) => {
|
const repository = getPoiRepository()
|
||||||
const poi = repository.getPoiById(task.poiId)
|
availablePois.value = getCheckInTasks().flatMap((task) => {
|
||||||
return poi ? [poi] : []
|
const poi = repository.getPoiById(task.poiId)
|
||||||
})
|
return poi ? [poi] : []
|
||||||
|
})
|
||||||
|
poiDirectoryError.value = ''
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
availablePois.value = []
|
||||||
|
poiDirectoryError.value = error instanceof Error ? error.message : '开放打卡点位暂时无法读取'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRecords() {
|
||||||
|
loadProfile()
|
||||||
|
loadAvailablePois()
|
||||||
}
|
}
|
||||||
|
|
||||||
function poiCheckedIn(poiId: string): boolean {
|
function poiCheckedIn(poiId: string): boolean {
|
||||||
@@ -80,16 +117,7 @@ function clearLocalRecords() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow(() => {
|
onShow(loadRecords)
|
||||||
loadAvailablePois()
|
|
||||||
try {
|
|
||||||
profile.value = getCheckInProfile()
|
|
||||||
storageError.value = ''
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
storageError.value = error instanceof Error ? error.message : '本机打卡记录暂时无法读取'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -98,9 +126,9 @@ onShow(() => {
|
|||||||
<text class="records-hero__eyebrow">LOCAL JOURNEY</text>
|
<text class="records-hero__eyebrow">LOCAL JOURNEY</text>
|
||||||
<text class="records-hero__title">我的光明足迹</text>
|
<text class="records-hero__title">我的光明足迹</text>
|
||||||
<view class="records-stats">
|
<view class="records-stats">
|
||||||
<view><text>{{ profile.points }}</text><text>本机积分</text></view>
|
<view><text>{{ storageError ? '--' : profile.points }}</text><text>本机积分</text></view>
|
||||||
<view><text>{{ profile.checkIns.length }}</text><text>打卡点位</text></view>
|
<view><text>{{ storageError ? '--' : profile.checkIns.length }}</text><text>打卡点位</text></view>
|
||||||
<view><text>{{ profile.badges.length }}</text><text>已获徽章</text></view>
|
<view><text>{{ storageError ? '--' : profile.badges.length }}</text><text>已获徽章</text></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -111,7 +139,13 @@ onShow(() => {
|
|||||||
<button @click="clearLocalRecords">清除异常打卡数据</button>
|
<button @click="clearLocalRecords">清除异常打卡数据</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="records-section">
|
<view v-if="poiDirectoryError" class="records-error">
|
||||||
|
<text class="records-error__title">开放点位暂时无法读取</text>
|
||||||
|
<text>{{ poiDirectoryError }}</text>
|
||||||
|
<button @click="loadAvailablePois">重新加载开放点位</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!storageError" class="records-section">
|
||||||
<view class="records-section__heading">
|
<view class="records-section__heading">
|
||||||
<view><text class="records-section__kicker">BADGES</text><text class="records-section__title">光明徽章</text></view>
|
<view><text class="records-section__kicker">BADGES</text><text class="records-section__title">光明徽章</text></view>
|
||||||
<text>{{ profile.badges.length }} / {{ badgeViews.length }}</text>
|
<text>{{ profile.badges.length }} / {{ badgeViews.length }}</text>
|
||||||
@@ -131,7 +165,7 @@ onShow(() => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="records-section">
|
<view v-if="!poiDirectoryError && !storageError" class="records-section">
|
||||||
<view class="records-section__heading">
|
<view class="records-section__heading">
|
||||||
<view><text class="records-section__kicker">PLACES</text><text class="records-section__title">开放打卡点位</text></view>
|
<view><text class="records-section__kicker">PLACES</text><text class="records-section__title">开放打卡点位</text></view>
|
||||||
<text>{{ availableCheckInCount }} / {{ availablePois.length }}</text>
|
<text>{{ availableCheckInCount }} / {{ availablePois.length }}</text>
|
||||||
@@ -150,7 +184,7 @@ onShow(() => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="records-section">
|
<view v-if="!storageError" class="records-section">
|
||||||
<view class="records-section__heading">
|
<view class="records-section__heading">
|
||||||
<view><text class="records-section__kicker">HISTORY</text><text class="records-section__title">打卡记录</text></view>
|
<view><text class="records-section__kicker">HISTORY</text><text class="records-section__title">打卡记录</text></view>
|
||||||
<text>{{ profile.checkIns.length }} 条</text>
|
<text>{{ profile.checkIns.length }} 条</text>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import type { PoiSummary } from '@/domain/poi'
|
import type { PoiSummary } from '@/domain/poi'
|
||||||
import type { BudgetLevel, Interest, Pace, PlanningRequest, Theme, Transport, TravelPreferences } from '@/domain/travel'
|
import type { BudgetLevel, Interest, Pace, PlanningRequest, Theme, Transport, TravelPreferences } from '@/domain/travel'
|
||||||
import { getPoiRepository } from '@/data/poi'
|
import { getPoiRepository } from '@/data/poi'
|
||||||
|
import { TravelValidationError } from '@/domain/travel'
|
||||||
|
import { isLocationSnapshotFresh } from '@/services/location'
|
||||||
import { createPlan, loadPlanningRequest, loadPreferences, savePlan, savePreferences } from '@/services/travel-assistant'
|
import { createPlan, loadPlanningRequest, loadPreferences, savePlan, savePreferences } from '@/services/travel-assistant'
|
||||||
import { useLocationStore } from '@/stores'
|
import { useLocationStore } from '@/stores'
|
||||||
|
|
||||||
@@ -56,14 +58,18 @@ const selectedPoiIds = ref<string[]>([])
|
|||||||
const customStartsFromCurrentLocation = ref(false)
|
const customStartsFromCurrentLocation = ref(false)
|
||||||
const locationFreshnessCheckedAt = ref(Date.now())
|
const locationFreshnessCheckedAt = ref(Date.now())
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
let locationFreshnessTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let pageVisible = false
|
||||||
const locationStore = useLocationStore()
|
const locationStore = useLocationStore()
|
||||||
const locationAvailable = computed(() => {
|
const locationAvailable = computed(() => {
|
||||||
const checkedAt = locationFreshnessCheckedAt.value
|
|
||||||
const snapshot = locationStore.snapshot
|
const snapshot = locationStore.snapshot
|
||||||
return locationStore.status === 'ready'
|
return locationStore.status === 'ready'
|
||||||
&& Boolean(snapshot)
|
&& Boolean(snapshot)
|
||||||
&& Number.isFinite(Date.parse(snapshot?.capturedAt ?? ''))
|
&& isLocationSnapshotFresh(
|
||||||
&& checkedAt - Date.parse(snapshot?.capturedAt ?? '') <= LOCATION_MAX_AGE_MS
|
snapshot?.capturedAt ?? '',
|
||||||
|
locationFreshnessCheckedAt.value,
|
||||||
|
LOCATION_MAX_AGE_MS,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
const poiSummaries = shallowRef<PoiSummary[]>(getPoiRepository().getPoiSummaries())
|
const poiSummaries = shallowRef<PoiSummary[]>(getPoiRepository().getPoiSummaries())
|
||||||
const poiGroups = computed(() => {
|
const poiGroups = computed(() => {
|
||||||
@@ -217,6 +223,32 @@ function isPermissionDenied(error: unknown): boolean {
|
|||||||
return /auth deny|auth denied|authorize:fail|permission/i.test(message)
|
return /auth deny|auth denied|authorize:fail|permission/i.test(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearLocationFreshnessTimer() {
|
||||||
|
if (locationFreshnessTimer === null)
|
||||||
|
return
|
||||||
|
clearTimeout(locationFreshnessTimer)
|
||||||
|
locationFreshnessTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLocationFreshness() {
|
||||||
|
clearLocationFreshnessTimer()
|
||||||
|
const checkedAt = Date.now()
|
||||||
|
locationFreshnessCheckedAt.value = checkedAt
|
||||||
|
const snapshot = locationStore.snapshot
|
||||||
|
if (locationStore.status !== 'ready' || !snapshot)
|
||||||
|
return
|
||||||
|
if (!pageVisible)
|
||||||
|
return
|
||||||
|
|
||||||
|
const capturedAt = Date.parse(snapshot.capturedAt)
|
||||||
|
if (!Number.isFinite(capturedAt) || capturedAt > checkedAt)
|
||||||
|
return
|
||||||
|
const expiresInMs = capturedAt + LOCATION_MAX_AGE_MS - checkedAt
|
||||||
|
if (expiresInMs < 0)
|
||||||
|
return
|
||||||
|
locationFreshnessTimer = setTimeout(refreshLocationFreshness, expiresInMs + 1)
|
||||||
|
}
|
||||||
|
|
||||||
function requestCurrentLocation() {
|
function requestCurrentLocation() {
|
||||||
if (locationStore.status === 'locating')
|
if (locationStore.status === 'locating')
|
||||||
return
|
return
|
||||||
@@ -234,6 +266,7 @@ function requestCurrentLocation() {
|
|||||||
|
|
||||||
function locateFromUserAction() {
|
function locateFromUserAction() {
|
||||||
locationStore.startLocating()
|
locationStore.startLocating()
|
||||||
|
refreshLocationFreshness()
|
||||||
uni.getLocation({
|
uni.getLocation({
|
||||||
type: 'gcj02',
|
type: 'gcj02',
|
||||||
isHighAccuracy: true,
|
isHighAccuracy: true,
|
||||||
@@ -249,7 +282,7 @@ function locateFromUserAction() {
|
|||||||
uni.showToast({ title: '定位结果无效', icon: 'none' })
|
uni.showToast({ title: '定位结果无效', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
locationFreshnessCheckedAt.value = Date.now()
|
refreshLocationFreshness()
|
||||||
if (planningMode.value === 'custom')
|
if (planningMode.value === 'custom')
|
||||||
customStartsFromCurrentLocation.value = true
|
customStartsFromCurrentLocation.value = true
|
||||||
uni.showToast({ title: '已更新当前位置', icon: 'success' })
|
uni.showToast({ title: '已更新当前位置', icon: 'success' })
|
||||||
@@ -286,7 +319,7 @@ function showDurationFitNotice(status: 'overflow' | 'underfilled'): Promise<void
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
locationFreshnessCheckedAt.value = Date.now()
|
refreshLocationFreshness()
|
||||||
if (!form.themes.length && !form.interests.length) {
|
if (!form.themes.length && !form.interests.length) {
|
||||||
uni.showToast({ title: '请至少选择一项主题或偏好', icon: 'none' })
|
uni.showToast({ title: '请至少选择一项主题或偏好', icon: 'none' })
|
||||||
return
|
return
|
||||||
@@ -343,9 +376,13 @@ async function submit() {
|
|||||||
uni.navigateTo({ url: '/pages/itinerary/index' })
|
uni.navigateTo({ url: '/pages/itinerary/index' })
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
const quickRouteUnreachable = error instanceof TravelValidationError
|
||||||
|
&& error.code === 'quick_route_unreachable'
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '暂时无法生成路线',
|
title: quickRouteUnreachable ? '当前路线无法完成' : '暂时无法生成路线',
|
||||||
content: error instanceof Error ? error.message : '请检查选择后重试',
|
content: quickRouteUnreachable
|
||||||
|
? '当前起点、交通方式与所选时长无法容纳首个点位。请靠近光明区、延长游玩时长,或改用自选点位规划。'
|
||||||
|
: error instanceof Error ? error.message : '请检查选择后重试',
|
||||||
showCancel: false,
|
showCancel: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -355,7 +392,8 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
locationFreshnessCheckedAt.value = Date.now()
|
pageVisible = true
|
||||||
|
refreshLocationFreshness()
|
||||||
const storedRequest = loadPlanningRequest()
|
const storedRequest = loadPlanningRequest()
|
||||||
const storedPreferences = storedRequest?.preferences ?? loadPreferences()
|
const storedPreferences = storedRequest?.preferences ?? loadPreferences()
|
||||||
if (storedPreferences)
|
if (storedPreferences)
|
||||||
@@ -371,6 +409,14 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
applyPreset()
|
applyPreset()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function deactivatePlannerPage() {
|
||||||
|
pageVisible = false
|
||||||
|
clearLocationFreshnessTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide(deactivatePlannerPage)
|
||||||
|
onUnload(deactivatePlannerPage)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ interface CurrentLocationOptions {
|
|||||||
timeoutMs?: number
|
timeoutMs?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLocationSnapshotFresh(
|
||||||
|
capturedAt: string,
|
||||||
|
checkedAt = Date.now(),
|
||||||
|
maxAgeMs = 5 * 60 * 1000,
|
||||||
|
): boolean {
|
||||||
|
const capturedTimestamp = Date.parse(capturedAt)
|
||||||
|
if (!Number.isFinite(capturedTimestamp)
|
||||||
|
|| !Number.isFinite(checkedAt)
|
||||||
|
|| !Number.isFinite(maxAgeMs)
|
||||||
|
|| maxAgeMs < 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const ageMs = checkedAt - capturedTimestamp
|
||||||
|
return ageMs >= 0 && ageMs <= maxAgeMs
|
||||||
|
}
|
||||||
|
|
||||||
function locationOptionsSupported(): boolean {
|
function locationOptionsSupported(): boolean {
|
||||||
try {
|
try {
|
||||||
return typeof uni.canIUse !== 'function'
|
return typeof uni.canIUse !== 'function'
|
||||||
|
|||||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 129 KiB |
@@ -1,5 +1,9 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { getCurrentGcj02Location, isLocationPermissionDenied } from '../src/services/location'
|
import {
|
||||||
|
getCurrentGcj02Location,
|
||||||
|
isLocationPermissionDenied,
|
||||||
|
isLocationSnapshotFresh,
|
||||||
|
} from '../src/services/location'
|
||||||
|
|
||||||
describe('current location adapter', () => {
|
describe('current location adapter', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -73,4 +77,16 @@ describe('current location adapter', () => {
|
|||||||
expect(isLocationPermissionDenied({ errMsg: 'getLocation:fail privacy permission is not authorized' })).toBe(false)
|
expect(isLocationPermissionDenied({ errMsg: 'getLocation:fail privacy permission is not authorized' })).toBe(false)
|
||||||
expect(isLocationPermissionDenied({ errMsg: 'getLocation:fail system error' })).toBe(false)
|
expect(isLocationPermissionDenied({ errMsg: 'getLocation:fail system error' })).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('evaluates location freshness at exact boundaries and rejects invalid clocks', () => {
|
||||||
|
const capturedAt = '2026-07-31T02:00:00.000Z'
|
||||||
|
const capturedTimestamp = Date.parse(capturedAt)
|
||||||
|
const maxAgeMs = 5 * 60 * 1000
|
||||||
|
|
||||||
|
expect(isLocationSnapshotFresh(capturedAt, capturedTimestamp, maxAgeMs)).toBe(true)
|
||||||
|
expect(isLocationSnapshotFresh(capturedAt, capturedTimestamp + maxAgeMs, maxAgeMs)).toBe(true)
|
||||||
|
expect(isLocationSnapshotFresh(capturedAt, capturedTimestamp + maxAgeMs + 1, maxAgeMs)).toBe(false)
|
||||||
|
expect(isLocationSnapshotFresh(capturedAt, capturedTimestamp - 1, maxAgeMs)).toBe(false)
|
||||||
|
expect(isLocationSnapshotFresh('invalid', capturedTimestamp, maxAgeMs)).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ describe('poi POC dataset', () => {
|
|||||||
expect(poiDataset.meta.authoritativeBaselineProvided).toBe(false)
|
expect(poiDataset.meta.authoritativeBaselineProvided).toBe(false)
|
||||||
expect(poiDataset.meta.canClaimFullCoverage).toBe(false)
|
expect(poiDataset.meta.canClaimFullCoverage).toBe(false)
|
||||||
expect(poiDataset.meta.coverageLabel).toContain('非全域权威清单')
|
expect(poiDataset.meta.coverageLabel).toContain('非全域权威清单')
|
||||||
expect(result.issues.filter(issue => issue.code === 'POC_PLACEHOLDER_ASSET')).toHaveLength(poiDataset.images.length)
|
// All 30 POIs have real photos, so no placeholder warnings remain
|
||||||
|
expect(result.issues.filter(issue => issue.code === 'POC_PLACEHOLDER_ASSET')).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('provides the seven requested presentation fields for every sample POI', () => {
|
it('provides the seven requested presentation fields for every sample POI', () => {
|
||||||
@@ -191,9 +192,9 @@ describe('poi selection and repository', () => {
|
|||||||
})).toBe('开放时间待确认')
|
})).toBe('开放时间待确认')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('distinguishes official and unreviewed POC recommendation labels', () => {
|
it('formats recommendation labels consistently for all sources', () => {
|
||||||
expect(formatRecommendationLabel('official_editorial', 5)).toBe('官方推荐指数 5/5')
|
expect(formatRecommendationLabel('official_editorial', 5)).toBe('推荐指数 5/5')
|
||||||
expect(formatRecommendationLabel('poc_default', 4)).toBe('POC 推荐指数(待审核) 4/5')
|
expect(formatRecommendationLabel('poc_default', 4)).toBe('推荐指数 4/5')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns resolved summaries and details from one dataset version', () => {
|
it('returns resolved summaries and details from one dataset version', () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type PlanResponse,
|
type PlanResponse,
|
||||||
resolveCanonicalPoiId,
|
resolveCanonicalPoiId,
|
||||||
type TravelPreferences,
|
type TravelPreferences,
|
||||||
|
TravelValidationError,
|
||||||
} from '../src/domain/travel'
|
} from '../src/domain/travel'
|
||||||
import {
|
import {
|
||||||
createPlan,
|
createPlan,
|
||||||
@@ -222,13 +223,22 @@ describe('deterministic local POC planner', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('rejects a quick plan when even the first stop cannot fit the budget', () => {
|
it('rejects a quick plan when even the first stop cannot fit the budget', () => {
|
||||||
expect(() => createLocalPlan({
|
let thrown: unknown
|
||||||
mode: 'quick',
|
try {
|
||||||
preferences: preferences({ pace: 'relaxed', transport: 'walking' }),
|
createLocalPlan({
|
||||||
durationMinutes: 120,
|
mode: 'quick',
|
||||||
selectedPoiIds: [],
|
preferences: preferences({ pace: 'relaxed', transport: 'walking' }),
|
||||||
origin: { longitude: 114.3, latitude: 22.3, coordinateSystem: 'GCJ02' },
|
durationMinutes: 120,
|
||||||
}, repository)).toThrow('没有可用于规划的地点')
|
selectedPoiIds: [],
|
||||||
|
origin: { longitude: 114.3, latitude: 22.3, coordinateSystem: 'GCJ02' },
|
||||||
|
}, repository)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
thrown = error
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(thrown).toBeInstanceOf(TravelValidationError)
|
||||||
|
expect(thrown).toMatchObject({ code: 'quick_route_unreachable' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the same route and canonical IDs for equivalent input', () => {
|
it('returns the same route and canonical IDs for equivalent input', () => {
|
||||||
|
|||||||