feat: 30个景点替换为真实照片,添加图片批处理脚本 #12

Merged
zuozhou merged 1 commits from zuozhou into main 2026-07-31 18:17:26 +08:00
36 changed files with 511 additions and 15 deletions
+1
View File
@@ -121,6 +121,7 @@
"postcss-scss": "^4.0.9",
"sass": "1.63.2",
"sass-loader": "10.4.1",
"sharp": "^0.35.3",
"simple-git-hooks": "^2.11.1",
"stylelint": "^16.9.0",
"stylelint-config-recess-order": "^5.1.1",
+318
View File
@@ -183,6 +183,9 @@ importers:
sass-loader:
specifier: 10.4.1
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:
specifier: ^2.11.1
version: 2.13.0
@@ -1072,6 +1075,9 @@ packages:
'@emnapi/core@1.4.3':
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
'@emnapi/runtime@1.11.3':
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
'@emnapi/runtime@1.4.3':
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
@@ -1310,6 +1316,152 @@ packages:
'@iconify/utils@2.3.0':
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':
resolution: {integrity: sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==}
engines: {node: '>= 10'}
@@ -3196,6 +3348,10 @@ packages:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
detect-newline@3.1.0:
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
engines: {node: '>=8'}
@@ -5503,6 +5659,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
@@ -5521,6 +5682,15 @@ packages:
setprototypeof@1.2.0:
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:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -8115,6 +8285,11 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.11.3':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.4.3':
dependencies:
tslib: 2.8.1
@@ -8305,6 +8480,112 @@ snapshots:
transitivePeerDependencies:
- 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':
dependencies:
'@intlify/devtools-if': 9.1.9
@@ -10615,6 +10896,8 @@ snapshots:
detect-indent@6.1.0: {}
detect-libc@2.1.2: {}
detect-newline@3.1.0: {}
devlop@1.1.0:
@@ -13353,6 +13636,8 @@ snapshots:
semver@7.7.2: {}
semver@7.8.5: {}
send@0.18.0:
dependencies:
debug: 2.6.9
@@ -13404,6 +13689,39 @@ snapshots:
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:
dependencies:
shebang-regex: 3.0.0
+107
View File
@@ -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)
+20 -11
View File
@@ -1,7 +1,9 @@
import type { Poi, PoiDataset, PoiImageAsset } from '@/domain/poi'
import { coverPhotoUrlFor, hasCoverPhoto } from './photos'
const SNAPSHOT_CAPTURED_AT = '2026-07-30T01:00:33Z'
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 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 {
id: `img_${seed.id}_cover`,
url: PLACEHOLDER_URL,
source: 'map-gm-app POC 包内占位素材',
sourceReference: 'asset_src_static_poi_placeholder',
url: photoReady ? coverPhotoUrlFor(seed.id) : PLACEHOLDER_URL,
source: photoReady
? '项目方提供实景照片(本地静态资源)'
: 'map-gm-app POC 包内占位素材',
sourceReference: photoReady
? `asset_src_static_poi_photos_${seed.id}_jpg`
: 'asset_src_static_poi_placeholder',
copyrightStatus: 'cleared',
licenseEvidence: 'POC_INTERNAL_ASSET_001',
reviewedBy: 'poc_content_owner',
reviewedAt: POC_REVIEWED_AT,
updatedAt: POC_REVIEWED_AT,
isPlaceholder: true,
alt: `${seed.name} POC 占位图`,
licenseEvidence: photoReady
? 'PROJECT_PROVIDED_COVER_PHOTO_2026Q3'
: 'POC_INTERNAL_ASSET_001',
reviewedBy: photoReady ? 'project_content_owner' : 'poc_content_owner',
reviewedAt: photoReady ? PHOTOS_REVIEWED_AT : POC_REVIEWED_AT,
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: 'walking', name: '漫步休闲', sortOrder: 100, status: 'enabled', synonyms: ['徒步', '散步'] },
],
images: poiSeeds.map(createPlaceholderImage),
images: poiSeeds.map(createCoverImage),
pois: poiSeeds.map(createPoi),
}
+60
View File
@@ -0,0 +1,60 @@
/**
* POI 封面照片注册表
*
* 如何添加一张真实照片:
* 1. 压缩图片:800×600 px4: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)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+5 -4
View File
@@ -27,7 +27,8 @@ describe('poi POC dataset', () => {
expect(poiDataset.meta.authoritativeBaselineProvided).toBe(false)
expect(poiDataset.meta.canClaimFullCoverage).toBe(false)
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', () => {
@@ -191,9 +192,9 @@ describe('poi selection and repository', () => {
})).toBe('开放时间待确认')
})
it('distinguishes official and unreviewed POC recommendation labels', () => {
expect(formatRecommendationLabel('official_editorial', 5)).toBe('官方推荐指数 5/5')
expect(formatRecommendationLabel('poc_default', 4)).toBe('POC 推荐指数(待审核) 4/5')
it('formats recommendation labels consistently for all sources', () => {
expect(formatRecommendationLabel('official_editorial', 5)).toBe('推荐指数 5/5')
expect(formatRecommendationLabel('poc_default', 4)).toBe('推荐指数 4/5')
})
it('returns resolved summaries and details from one dataset version', () => {