26/04/28 first update
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$outDir = __DIR__ . '/dataset';
|
||||
if (!is_dir($outDir)) mkdir($outDir, 0777, true);
|
||||
|
||||
/**
|
||||
* 地区 + 城市(用于命名)
|
||||
*/
|
||||
$geoPoints = [
|
||||
|
||||
// ap = Asia Pacific
|
||||
['region'=>'ap','name'=>'hangzhou','lat'=>30.274086,'lon'=>120.155071],
|
||||
['region'=>'ap','name'=>'beijing','lat'=>39.9042,'lon'=>116.4074],
|
||||
['region'=>'ap','name'=>'guangzhou','lat'=>23.1291,'lon'=>113.2644],
|
||||
['region'=>'ap','name'=>'chengdu','lat'=>30.5728,'lon'=>104.0668],
|
||||
['region'=>'ap','name'=>'tokyo','lat'=>35.6762,'lon'=>139.6503],
|
||||
['region'=>'ap','name'=>'seoul','lat'=>37.5665,'lon'=>126.9780],
|
||||
['region'=>'ap','name'=>'bangkok','lat'=>13.7563,'lon'=>100.5018],
|
||||
['region'=>'ap','name'=>'singapore','lat'=>1.3521,'lon'=>103.8198],
|
||||
['region'=>'ap','name'=>'jakarta','lat'=>-6.2088,'lon'=>106.8456],
|
||||
['region'=>'ap','name'=>'delhi','lat'=>28.6139,'lon'=>77.2090],
|
||||
['region'=>'ap','name'=>'mumbai','lat'=>19.0760,'lon'=>72.8777],
|
||||
|
||||
// me = Middle East / Africa
|
||||
['region'=>'me','name'=>'dubai','lat'=>25.2048,'lon'=>55.2708],
|
||||
['region'=>'me','name'=>'istanbul','lat'=>41.0082,'lon'=>28.9784],
|
||||
['region'=>'me','name'=>'cairo','lat'=>30.0444,'lon'=>31.2357],
|
||||
['region'=>'me','name'=>'nairobi','lat'=>-1.2921,'lon'=>36.8219],
|
||||
['region'=>'me','name'=>'johannesburg','lat'=>-26.2041,'lon'=>28.0473],
|
||||
|
||||
// eu
|
||||
['region'=>'eu','name'=>'london','lat'=>51.5072,'lon'=>-0.1276],
|
||||
['region'=>'eu','name'=>'paris','lat'=>48.8566,'lon'=>2.3522],
|
||||
['region'=>'eu','name'=>'madrid','lat'=>40.4168,'lon'=>-3.7038],
|
||||
['region'=>'eu','name'=>'rome','lat'=>41.9028,'lon'=>12.4964],
|
||||
['region'=>'eu','name'=>'berlin','lat'=>52.5200,'lon'=>13.4050],
|
||||
['region'=>'eu','name'=>'warsaw','lat'=>52.2297,'lon'=>21.0122],
|
||||
['region'=>'eu','name'=>'moscow','lat'=>55.7558,'lon'=>37.6173],
|
||||
|
||||
// na
|
||||
['region'=>'na','name'=>'newyork','lat'=>40.7128,'lon'=>-74.0060],
|
||||
['region'=>'na','name'=>'chicago','lat'=>41.8781,'lon'=>-87.6298],
|
||||
['region'=>'na','name'=>'losangeles','lat'=>34.0522,'lon'=>-118.2437],
|
||||
['region'=>'na','name'=>'dallas','lat'=>32.7767,'lon'=>-96.7970],
|
||||
['region'=>'na','name'=>'toronto','lat'=>43.6532,'lon'=>-79.3832],
|
||||
['region'=>'na','name'=>'mexicocity','lat'=>19.4326,'lon'=>-99.1332],
|
||||
|
||||
// sa / oc
|
||||
['region'=>'sa','name'=>'saopaulo','lat'=>-23.5505,'lon'=>-46.6333],
|
||||
['region'=>'sa','name'=>'santiago','lat'=>-33.4489,'lon'=>-70.6693],
|
||||
['region'=>'oc','name'=>'sydney','lat'=>-33.8688,'lon'=>151.2093],
|
||||
];
|
||||
|
||||
$url = "https://apis.ihg.com/graphql/v1/hotels";
|
||||
|
||||
$headers = [
|
||||
'sec-ch-ua-platform: "Windows"',
|
||||
'Referer: https://www.ihg.com.cn/',
|
||||
'sec-ch-ua: "Microsoft Edge";v="147", "Not.A/Brand";v="8", "Chromium";v="147"',
|
||||
'ihg-language: zh-CN',
|
||||
'sec-ch-ua-mobile: ?0',
|
||||
'x-ihg-api-key: se9ym5iAzaW8pxfBjkmgbuGjJcr3Pj6Y',
|
||||
'IHG-SessionId: 6cc0bb0f-8850-4302-9d4b-80e2f2ef4860',
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0',
|
||||
'Accept: application/json, text/plain, */*',
|
||||
'Content-Type: application/json; charset=UTF-8',
|
||||
'IHG-TransactionId: 50fef06e-2f3b-416c-a6af-69ba809c7666',
|
||||
];
|
||||
|
||||
function buildPayload($lat, $lon) {
|
||||
|
||||
$query = <<<'GRAPHQL'
|
||||
query GetHotelDetails($detailsInput: HotelArgs, $mediaArgs: MediaArgs){
|
||||
getHotels(input:$detailsInput){
|
||||
hotelInfo{
|
||||
hotelCode
|
||||
callCenter{phoneNumber}
|
||||
address{
|
||||
street1 street2 street3 city zip
|
||||
state{code name}
|
||||
country{name code}
|
||||
checkInAddress{city country line1 state zipcode}
|
||||
}
|
||||
location{boardTypes{boardType}}
|
||||
distanceFrom{kilometers miles}
|
||||
marketing{optOutDateWeb optInDateWeb marketingText{welcomeMessage}}
|
||||
brandInfo{
|
||||
SPBrandName brandCode brandName chainCode
|
||||
futureBrandInfo{rebrandingDate hotelName chainCode brandName brandCode}
|
||||
spTransitionalBrandIdentifier
|
||||
}
|
||||
greenEngage{
|
||||
certificationPrograms{
|
||||
certifiedByGloballyRecognizedSustainableProgram
|
||||
environmentalCertificationProgram{listItem}
|
||||
}
|
||||
lowCarbon{lowCarbonHotelDescription isLowCarbonHotel}
|
||||
lowCarbonReady{lowCarbonReadyHotelDescription isLowCarbonReadyHotel}
|
||||
}
|
||||
room{hotelHighlights{hotelDisclaimer}}
|
||||
badges{name id}
|
||||
facilities{name id}
|
||||
parking{
|
||||
complimentaryDailySelfParking
|
||||
parkingDescription
|
||||
carParkingAvailable
|
||||
valetParkingAvailable
|
||||
}
|
||||
policies{
|
||||
pet{petsAllowed guideDogsOrServiceAnimalsAllowed description}
|
||||
}
|
||||
stripes{id name}
|
||||
renovationAlertsList{alertType flagEndDate flagStartDate other}
|
||||
profile{
|
||||
name
|
||||
webNonBrandedHotelLogo{url}
|
||||
seoCity
|
||||
nonIhgCrsUrl
|
||||
independentNonIHGWebsiteURL
|
||||
hotelLogo{originalUrl}
|
||||
averageReview
|
||||
tpiLevel2Violator
|
||||
primaryImageUrl{originalUrl}
|
||||
latLong{lon lat}
|
||||
hotelStatus
|
||||
preSellDate
|
||||
dateOpened
|
||||
totalReviews
|
||||
vatIncluded
|
||||
}
|
||||
media(input:$mediaArgs){
|
||||
primaryPhotos{
|
||||
allPhotos{
|
||||
type
|
||||
primary
|
||||
caption
|
||||
originalUrl
|
||||
formats{url aspectHeight aspectWidth}
|
||||
}
|
||||
}
|
||||
}
|
||||
foodAndBeverage{
|
||||
complimentaryBreakfastDetails{complimentaryGrabAndGoBreakfast}
|
||||
}
|
||||
restaurant{onSiteRestaurantsCount}
|
||||
tax{
|
||||
taxAndFeeDetail
|
||||
serviceCharge{
|
||||
startAndEndDate{startDate endDate}
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$data = json_encode([
|
||||
'operationName' => 'GetHotelDetails',
|
||||
'variables' => [
|
||||
'detailsInput' => [
|
||||
'rebrandStartDate' => '2026-04-28',
|
||||
'geoLocation' => [
|
||||
'lat' => $lat,
|
||||
'lon' => $lon,
|
||||
'radius' => 1336,
|
||||
],
|
||||
'geoLocationDistance' => [
|
||||
'distanceType' => 'STRAIGHT_LINE',
|
||||
'distanceUnit' => 'KM',
|
||||
],
|
||||
'size' => 3000,
|
||||
'fallbackSearch' => [
|
||||
'minHotels' => 1,
|
||||
'maxRadius' => 100,
|
||||
'incrementRadiusBy' => 70,
|
||||
],
|
||||
'sortBy' => 'DISTANCE',
|
||||
],
|
||||
'mediaArgs' => [
|
||||
'formats' => [
|
||||
['aspectHeight' => '3', 'aspectWidth' => '4'],
|
||||
['aspectHeight' => '5', 'aspectWidth' => '16'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'query' => $query,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
return $data;
|
||||
/**
|
||||
return json_encode([
|
||||
"operationName"=>"GetHotelDetails",
|
||||
"variables"=>[
|
||||
"detailsInput"=>[
|
||||
"geoLocation"=>[
|
||||
"lat"=>$lat,
|
||||
"lon"=>$lon,
|
||||
"radius"=>1300
|
||||
],
|
||||
"size"=>3000,
|
||||
"sortBy"=>"DISTANCE"
|
||||
]
|
||||
],
|
||||
"query"=>"query GetHotelDetails(\$detailsInput:HotelArgs){getHotels(input:\$detailsInput){hotelInfo{hotelCode}}}"
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
*/
|
||||
}
|
||||
|
||||
function fetch($url, $headers, $payload) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0, // 对应 --http2
|
||||
]);
|
||||
$res = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主循环
|
||||
*/
|
||||
foreach ($geoPoints as $p) {
|
||||
|
||||
$filename = "{$p['region']}-{$p['name']}.json";
|
||||
$filepath = $outDir . '/' . $filename;
|
||||
|
||||
echo "抓取 {$filename}...\n";
|
||||
|
||||
$payload = buildPayload($p['lat'], $p['lon']);
|
||||
$res = fetch($url, $headers, $payload);
|
||||
|
||||
if (!$res) {
|
||||
echo "失败: {$filename}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
file_put_contents($filepath, $res);
|
||||
|
||||
// 防封(非常重要)
|
||||
usleep(300000); // 0.3秒
|
||||
}
|
||||
|
||||
echo "全部完成\n";
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+174
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
$url = "https://apis.ihg.com/graphql/v1/hotels";
|
||||
|
||||
$headers = [
|
||||
'sec-ch-ua-platform: "Windows"',
|
||||
'Referer: https://www.ihg.com.cn/',
|
||||
'sec-ch-ua: "Microsoft Edge";v="147", "Not.A/Brand";v="8", "Chromium";v="147"',
|
||||
'ihg-language: zh-CN',
|
||||
'sec-ch-ua-mobile: ?0',
|
||||
'x-ihg-api-key: se9ym5iAzaW8pxfBjkmgbuGjJcr3Pj6Y',
|
||||
'IHG-SessionId: 6cc0bb0f-8850-4302-9d4b-80e2f2ef4860',
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0',
|
||||
'Accept: application/json, text/plain, */*',
|
||||
'Content-Type: application/json; charset=UTF-8',
|
||||
'IHG-TransactionId: 50fef06e-2f3b-416c-a6af-69ba809c7666',
|
||||
];
|
||||
|
||||
/**
|
||||
$data = <<<JSON
|
||||
{"operationName":"GetHotelDetails","variables":{"detailsInput":{"rebrandStartDate":"2026-04-28","geoLocation":{"lat":30.274086,"lon":120.155071,"radius":1336},"geoLocationDistance":{"distanceType":"STRAIGHT_LINE","distanceUnit":"KM"},"size":120,"fallbackSearch":{"minHotels":1,"maxRadius":100,"incrementRadiusBy":70},"sortBy":"DISTANCE"},"mediaArgs":{"formats":[{"aspectHeight":"3","aspectWidth":"4"},{"aspectHeight":"5","aspectWidth":"16"}]}},"query":"query GetHotelDetails(\$detailsInput:HotelArgs\$mediaArgs:MediaArgs){getHotels(input:\$detailsInput){hotelInfo{hotelCode callCenter{phoneNumber}address{street1 street2 street3 city zip state{code name}country{name code}checkInAddress{city country line1 state zipcode}}location{boardTypes{boardType}}distanceFrom{kilometers miles}marketing{optOutDateWeb optInDateWeb marketingText{welcomeMessage}}brandInfo{SPBrandName brandCode brandName chainCode futureBrandInfo{rebrandingDate hotelName chainCode brandName brandCode}spTransitionalBrandIdentifier}greenEngage{certificationPrograms{certifiedByGloballyRecognizedSustainableProgram environmentalCertificationProgram{listItem}}lowCarbon{lowCarbonHotelDescription isLowCarbonHotel}lowCarbonReady{lowCarbonReadyHotelDescription isLowCarbonReadyHotel}}room{hotelHighlights{hotelDisclaimer}}badges{name id}facilities{name id}parking{complimentaryDailySelfParking parkingDescription carParkingAvailable valetParkingAvailable}policies{pet{petsAllowed guideDogsOrServiceAnimalsAllowed description}}stripes{id name}renovationAlertsList{alertType flagEndDate flagStartDate other}profile{name webNonBrandedHotelLogo{url}seoCity nonIhgCrsUrl independentNonIHGWebsiteURL hotelLogo{originalUrl}averageReview tpiLevel2Violator primaryImageUrl{originalUrl}latLong{lon lat}hotelStatus preSellDate dateOpened totalReviews vatIncluded}media(input:\$mediaArgs){primaryPhotos{allPhotos{type primary caption originalUrl formats{url aspectHeight aspectWidth}}}}foodAndBeverage{complimentaryBreakfastDetails{complimentaryGrabAndGoBreakfast}}restaurant{onSiteRestaurantsCount}tax{taxAndFeeDetail serviceCharge{startAndEndDate{startDate endDate}description}}}}}"}
|
||||
JSON;
|
||||
*/
|
||||
|
||||
$query = <<<'GRAPHQL'
|
||||
query GetHotelDetails($detailsInput: HotelArgs, $mediaArgs: MediaArgs){
|
||||
getHotels(input:$detailsInput){
|
||||
hotelInfo{
|
||||
hotelCode
|
||||
callCenter{phoneNumber}
|
||||
address{
|
||||
street1 street2 street3 city zip
|
||||
state{code name}
|
||||
country{name code}
|
||||
checkInAddress{city country line1 state zipcode}
|
||||
}
|
||||
location{boardTypes{boardType}}
|
||||
distanceFrom{kilometers miles}
|
||||
marketing{optOutDateWeb optInDateWeb marketingText{welcomeMessage}}
|
||||
brandInfo{
|
||||
SPBrandName brandCode brandName chainCode
|
||||
futureBrandInfo{rebrandingDate hotelName chainCode brandName brandCode}
|
||||
spTransitionalBrandIdentifier
|
||||
}
|
||||
greenEngage{
|
||||
certificationPrograms{
|
||||
certifiedByGloballyRecognizedSustainableProgram
|
||||
environmentalCertificationProgram{listItem}
|
||||
}
|
||||
lowCarbon{lowCarbonHotelDescription isLowCarbonHotel}
|
||||
lowCarbonReady{lowCarbonReadyHotelDescription isLowCarbonReadyHotel}
|
||||
}
|
||||
room{hotelHighlights{hotelDisclaimer}}
|
||||
badges{name id}
|
||||
facilities{name id}
|
||||
parking{
|
||||
complimentaryDailySelfParking
|
||||
parkingDescription
|
||||
carParkingAvailable
|
||||
valetParkingAvailable
|
||||
}
|
||||
policies{
|
||||
pet{petsAllowed guideDogsOrServiceAnimalsAllowed description}
|
||||
}
|
||||
stripes{id name}
|
||||
renovationAlertsList{alertType flagEndDate flagStartDate other}
|
||||
profile{
|
||||
name
|
||||
webNonBrandedHotelLogo{url}
|
||||
seoCity
|
||||
nonIhgCrsUrl
|
||||
independentNonIHGWebsiteURL
|
||||
hotelLogo{originalUrl}
|
||||
averageReview
|
||||
tpiLevel2Violator
|
||||
primaryImageUrl{originalUrl}
|
||||
latLong{lon lat}
|
||||
hotelStatus
|
||||
preSellDate
|
||||
dateOpened
|
||||
totalReviews
|
||||
vatIncluded
|
||||
}
|
||||
media(input:$mediaArgs){
|
||||
primaryPhotos{
|
||||
allPhotos{
|
||||
type
|
||||
primary
|
||||
caption
|
||||
originalUrl
|
||||
formats{url aspectHeight aspectWidth}
|
||||
}
|
||||
}
|
||||
}
|
||||
foodAndBeverage{
|
||||
complimentaryBreakfastDetails{complimentaryGrabAndGoBreakfast}
|
||||
}
|
||||
restaurant{onSiteRestaurantsCount}
|
||||
tax{
|
||||
taxAndFeeDetail
|
||||
serviceCharge{
|
||||
startAndEndDate{startDate endDate}
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL;
|
||||
|
||||
$data = json_encode([
|
||||
'operationName' => 'GetHotelDetails',
|
||||
'variables' => [
|
||||
'detailsInput' => [
|
||||
'rebrandStartDate' => '2026-04-28',
|
||||
'geoLocation' => [
|
||||
'lat' => 30.274086,
|
||||
'lon' => 120.155071,
|
||||
'radius' => 1336,
|
||||
],
|
||||
'geoLocationDistance' => [
|
||||
'distanceType' => 'STRAIGHT_LINE',
|
||||
'distanceUnit' => 'KM',
|
||||
],
|
||||
'size' => 120,
|
||||
'fallbackSearch' => [
|
||||
'minHotels' => 1,
|
||||
'maxRadius' => 100,
|
||||
'incrementRadiusBy' => 70,
|
||||
],
|
||||
'sortBy' => 'DISTANCE',
|
||||
],
|
||||
'mediaArgs' => [
|
||||
'formats' => [
|
||||
['aspectHeight' => '3', 'aspectWidth' => '4'],
|
||||
['aspectHeight' => '5', 'aspectWidth' => '16'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'query' => $query,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
|
||||
$ch = curl_init($url);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $data,
|
||||
CURLOPT_ENCODING => '', // 支持 gzip / br / deflate
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0, // 对应 --http2
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
echo "Curl error: " . curl_error($ch);
|
||||
exit;
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
echo "HTTP Error: $httpCode\n";
|
||||
echo $response;
|
||||
exit;
|
||||
}
|
||||
|
||||
// 保存为 hotel.json
|
||||
file_put_contents(__DIR__ . '/hotel.json', $response);
|
||||
|
||||
echo "保存成功 -> hotel.json\n";
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$jsonFile = __DIR__ . '/hotels.json';
|
||||
$outDir = __DIR__ . '/../dist';
|
||||
$pageSize = 100;
|
||||
|
||||
if (!is_dir($outDir)) {
|
||||
mkdir($outDir, 0777, true);
|
||||
}
|
||||
|
||||
function e(mixed $v): string {
|
||||
return htmlspecialchars((string)($v ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function getv(array $arr, string $path, mixed $default = null): mixed {
|
||||
$cur = $arr;
|
||||
foreach (explode('.', $path) as $key) {
|
||||
if (!is_array($cur) || !array_key_exists($key, $cur)) return $default;
|
||||
$cur = $cur[$key];
|
||||
}
|
||||
return $cur;
|
||||
}
|
||||
|
||||
function pickImage(array $hotel): string {
|
||||
$photos = getv($hotel, 'media.primaryPhotos.allPhotos', []);
|
||||
foreach ($photos as $p) {
|
||||
foreach (($p['formats'] ?? []) as $f) {
|
||||
if (($f['aspectWidth'] ?? '') === '4' && ($f['aspectHeight'] ?? '') === '3') {
|
||||
return $f['url'] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return getv($hotel, 'profile.primaryImageUrl.originalUrl', '');
|
||||
}
|
||||
|
||||
function normalizeHotel(array $h): array {
|
||||
return [
|
||||
'code' => $h['hotelCode'] ?? '',
|
||||
'name' => getv($h, 'profile.name', ''),
|
||||
'brand' => getv($h, 'brandInfo.brandName', ''),
|
||||
'brandCode' => getv($h, 'brandInfo.brandCode', ''),
|
||||
'phone' => getv($h, 'callCenter.phoneNumber', ''),
|
||||
'address' => trim(implode(' ', array_filter([
|
||||
getv($h, 'address.street1', ''),
|
||||
getv($h, 'address.street3', ''),
|
||||
getv($h, 'address.city', ''),
|
||||
getv($h, 'address.state.name', ''),
|
||||
getv($h, 'address.zip', ''),
|
||||
]))),
|
||||
'km' => getv($h, 'distanceFrom.kilometers', ''),
|
||||
'miles' => getv($h, 'distanceFrom.miles', ''),
|
||||
'image' => pickImage($h),
|
||||
'website' => getv($h, 'profile.independentNonIHGWebsiteURL', ''),
|
||||
'facilities' => array_values(array_filter(array_map(
|
||||
fn($x) => $x['name'] ?? '',
|
||||
$h['facilities'] ?? []
|
||||
))),
|
||||
'tax' => getv($h, 'tax.taxAndFeeDetail', ''),
|
||||
'price' => getv($h, 'rate.price', getv($h, 'price.amount', null)),
|
||||
'currency' => getv($h, 'rate.currency', getv($h, 'price.currency', 'CNY')),
|
||||
];
|
||||
}
|
||||
|
||||
function pageName(string $type, int $page): string {
|
||||
if ($type === 'cards') return $page === 1 ? 'index.html' : "page-$page.html";
|
||||
return $page === 1 ? 'table.html' : "table-$page.html";
|
||||
}
|
||||
|
||||
function renderLayout(string $body, string $title): string {
|
||||
return '<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>' . e($title) . '</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body{margin:0;background:#f4f4f4;font-family:Arial,"Microsoft YaHei",sans-serif;color:#111}
|
||||
.wrap{max-width:1460px;margin:0 auto;padding:20px}
|
||||
.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
|
||||
.top a{margin-left:10px;color:#0b5c7a;text-decoration:none}
|
||||
.card{display:grid;grid-template-columns:438px 1fr 210px;background:#fff;border-radius:18px;margin-bottom:24px;overflow:hidden}
|
||||
.img{width:438px;height:292px;object-fit:cover;background:#ddd}
|
||||
.info{padding:28px}
|
||||
.brand{display:inline-block;background:#0b684d;color:#fff;border-radius:4px;padding:6px 9px;margin-right:10px;font-weight:bold}
|
||||
h2{display:inline;font-size:24px}
|
||||
.addr{color:#444;margin:8px 0 6px;font-size:17px}
|
||||
.dist{color:#555;margin-bottom:26px}
|
||||
.features{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:13px 40px;font-size:18px;color:#333}
|
||||
.features span:before{content:"◆";font-size:12px;color:#666;margin-right:9px}
|
||||
.side{padding:36px 24px;text-align:right;display:flex;flex-direction:column;justify-content:center}
|
||||
.price{font-size:32px;font-weight:bold}
|
||||
.btn{background:#c9310c;color:#fff;border-radius:7px;padding:18px 24px;text-decoration:none;font-weight:bold;display:inline-block;margin-top:28px}
|
||||
.tax{color:#555;margin-top:12px}
|
||||
table{width:100%;border-collapse:collapse;background:#fff}
|
||||
th,td{border:1px solid #ddd;padding:10px;vertical-align:top}
|
||||
th{background:#eee}
|
||||
.thumb{width:110px;height:78px;object-fit:cover}
|
||||
.pager{margin:22px 0;text-align:center}
|
||||
.pager a{margin:0 8px;color:#0b5c7a}
|
||||
@media(max-width:900px){
|
||||
.card{grid-template-columns:1fr}
|
||||
.img{width:100%;height:240px}
|
||||
.side{text-align:left}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">' . $body . '</div>
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
|
||||
function renderPager(string $type, int $page, int $totalPages): string {
|
||||
$html = '<div class="pager">';
|
||||
if ($page > 1) {
|
||||
$html .= '<a href="' . e(pageName($type, $page - 1)) . '">上一页</a>';
|
||||
}
|
||||
$html .= ' 第 ' . $page . ' / ' . $totalPages . ' 页 ';
|
||||
if ($page < $totalPages) {
|
||||
$html .= '<a href="' . e(pageName($type, $page + 1)) . '">下一页</a>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
function renderCardsPage(array $hotels, int $page, int $totalPages, int $total): string {
|
||||
$body = '<div class="top"><div>找到 ' . $total . ' 家酒店</div><div><a href="' . e(pageName('table', $page)) . '">表格模式</a></div></div>';
|
||||
|
||||
foreach ($hotels as $h) {
|
||||
$features = '';
|
||||
foreach (array_slice($h['facilities'], 0, 8) as $f) {
|
||||
$features .= '<span>' . e($f) . '</span>';
|
||||
}
|
||||
|
||||
$price = $h['price'] !== null
|
||||
? '<div class="price">' . e(number_format((float)$h['price'])) . ' <small>' . e($h['currency']) . '</small></div><div>每晚</div>'
|
||||
: '<div class="price">暂无价格</div>';
|
||||
|
||||
$body .= '
|
||||
<div class="card">
|
||||
<img class="img" src="' . e($h['image']) . '" loading="lazy">
|
||||
<div class="info">
|
||||
<div><span class="brand">' . e($h['brandCode']) . '</span><h2>' . e($h['brand'] . ' ' . $h['name']) . '</h2></div>
|
||||
<div class="addr">' . e($h['address']) . ' | ' . e($h['phone']) . '</div>
|
||||
<div class="dist">📍 ' . e($h['km']) . ' 公里,' . e($h['miles']) . ' 英里 距离市中心</div>
|
||||
<div class="features">' . $features . '</div>
|
||||
</div>
|
||||
<div class="side">
|
||||
<div>⚡ Only a few left from</div>
|
||||
' . $price . '
|
||||
<div class="tax">' . e($h['tax']) . '</div>
|
||||
<a class="btn" href="' . e($h['website'] ?: '#') . '">选择酒店</a>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
$body .= renderPager('cards', $page, $totalPages);
|
||||
return renderLayout($body, '酒店列表');
|
||||
}
|
||||
|
||||
function renderTablePage(array $hotels, int $page, int $totalPages, int $total): string {
|
||||
$body = '<div class="top"><div>找到 ' . $total . ' 家酒店</div><div><a href="' . e(pageName('cards', $page)) . '">卡片模式</a></div></div>';
|
||||
$body .= '<table><thead><tr>
|
||||
<th>图片</th><th>酒店</th><th>品牌</th><th>地址</th><th>距离</th><th>电话</th><th>设施</th><th>价格</th><th>税费</th>
|
||||
</tr></thead><tbody>';
|
||||
|
||||
foreach ($hotels as $h) {
|
||||
$price = $h['price'] !== null
|
||||
? number_format((float)$h['price']) . ' ' . $h['currency']
|
||||
: '暂无';
|
||||
|
||||
$body .= '<tr>
|
||||
<td><img class="thumb" src="' . e($h['image']) . '" loading="lazy"></td>
|
||||
<td>' . e($h['name']) . '<br><small>' . e($h['code']) . '</small></td>
|
||||
<td>' . e($h['brand']) . '</td>
|
||||
<td>' . e($h['address']) . '</td>
|
||||
<td>' . e($h['km']) . ' 公里 / ' . e($h['miles']) . ' 英里</td>
|
||||
<td>' . e($h['phone']) . '</td>
|
||||
<td>' . e(implode('、', array_slice($h['facilities'], 0, 8))) . '</td>
|
||||
<td>' . e($price) . '</td>
|
||||
<td>' . e($h['tax']) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$body .= '</tbody></table>';
|
||||
$body .= renderPager('table', $page, $totalPages);
|
||||
|
||||
return renderLayout($body, '酒店表格');
|
||||
}
|
||||
|
||||
$raw = file_get_contents($jsonFile);
|
||||
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$hotelsRaw = getv($data, 'data.getHotels.hotelInfo', []);
|
||||
$hotels = array_map('normalizeHotel', $hotelsRaw);
|
||||
|
||||
$total = count($hotels);
|
||||
$totalPages = max(1, (int)ceil($total / $pageSize));
|
||||
|
||||
for ($page = 1; $page <= $totalPages; $page++) {
|
||||
$chunk = array_slice($hotels, ($page - 1) * $pageSize, $pageSize);
|
||||
|
||||
file_put_contents(
|
||||
$outDir . '/' . pageName('cards', $page),
|
||||
renderCardsPage($chunk, $page, $totalPages, $total)
|
||||
);
|
||||
|
||||
file_put_contents(
|
||||
$outDir . '/' . pageName('table', $page),
|
||||
renderTablePage($chunk, $page, $totalPages, $total)
|
||||
);
|
||||
}
|
||||
|
||||
echo "生成完成:{$outDir}\n";
|
||||
echo "卡片首页:{$outDir}/index.html\n";
|
||||
echo "表格首页:{$outDir}/table.html\n";
|
||||
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$outDir = __DIR__ . '/../dist';
|
||||
$pageSizeCards = 20;
|
||||
$pageSizeTable = 100;
|
||||
|
||||
if (!is_dir($outDir)) {
|
||||
mkdir($outDir, 0777, true);
|
||||
}
|
||||
|
||||
$jsonFiles = array_slice($argv, 1);
|
||||
|
||||
if (!$jsonFiles) {
|
||||
$jsonFiles = glob(__DIR__ . '/dataset/*.json') ?: [];
|
||||
}
|
||||
|
||||
function e(mixed $v): string {
|
||||
return htmlspecialchars((string)($v ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function getv(array $arr, string $path, mixed $default = null): mixed {
|
||||
$cur = $arr;
|
||||
foreach (explode('.', $path) as $key) {
|
||||
if (!is_array($cur) || !array_key_exists($key, $cur)) {
|
||||
return $default;
|
||||
}
|
||||
$cur = $cur[$key];
|
||||
}
|
||||
return $cur;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
|
||||
function pickImage(array $hotel): string {
|
||||
$photos = getv($hotel, 'media.primaryPhotos.allPhotos', []);
|
||||
foreach ($photos as $p) {
|
||||
foreach (($p['formats'] ?? []) as $f) {
|
||||
if (($f['aspectWidth'] ?? '') === '4' && ($f['aspectHeight'] ?? '') === '3') {
|
||||
return $f['url'] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return getv($hotel, 'profile.primaryImageUrl.originalUrl', '');
|
||||
}
|
||||
*/
|
||||
|
||||
function pickImage(array $hotel): string {
|
||||
$primary = getv($hotel, 'profile.primaryImageUrl.originalUrl', '');
|
||||
if ($primary !== '') {
|
||||
return $primary;
|
||||
}
|
||||
|
||||
$photos = getv($hotel, 'media.primaryPhotos.allPhotos', []);
|
||||
foreach ($photos as $p) {
|
||||
foreach (($p['formats'] ?? []) as $f) {
|
||||
if (($f['aspectWidth'] ?? '') === '4' && ($f['aspectHeight'] ?? '') === '3') {
|
||||
return $f['url'] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
function normalizeHotel(array $h): array {
|
||||
$hotelCode = trim((string)($h['hotelCode'] ?? ''));
|
||||
|
||||
return [
|
||||
'hotelCode' => $hotelCode,
|
||||
'name' => getv($h, 'profile.name', ''),
|
||||
'brand' => getv($h, 'brandInfo.brandName', ''),
|
||||
'brandCode' => getv($h, 'brandInfo.brandCode', ''),
|
||||
'phone' => getv($h, 'callCenter.phoneNumber', ''),
|
||||
'address' => trim(implode(' ', array_filter([
|
||||
getv($h, 'address.street1', ''),
|
||||
getv($h, 'address.street2', ''),
|
||||
getv($h, 'address.street3', ''),
|
||||
getv($h, 'address.city', ''),
|
||||
getv($h, 'address.state.name', ''),
|
||||
getv($h, 'address.zip', ''),
|
||||
]))),
|
||||
'km' => getv($h, 'distanceFrom.kilometers', ''),
|
||||
'miles' => getv($h, 'distanceFrom.miles', ''),
|
||||
'welcomeMessage' => trim(strip_tags((string)getv($h, 'marketing.marketingText.welcomeMessage', ''))),
|
||||
'image' => pickImage($h),
|
||||
'website' => getv($h, 'profile.independentNonIHGWebsiteURL', ''),
|
||||
'rating' => getv($h, 'profile.averageReview', null),
|
||||
'reviews' => getv($h, 'profile.totalReviews', null),
|
||||
'facilities' => array_values(array_filter(array_map(
|
||||
fn($x) => $x['name'] ?? '',
|
||||
$h['facilities'] ?? []
|
||||
))),
|
||||
'tax' => getv($h, 'tax.taxAndFeeDetail', ''),
|
||||
'price' => getv($h, 'rate.price', getv($h, 'price.amount', null)),
|
||||
'currency' => getv($h, 'rate.currency', getv($h, 'price.currency', 'CNY')),
|
||||
];
|
||||
}
|
||||
|
||||
function loadHotelsFromFiles(array $jsonFiles): array {
|
||||
$merged = [];
|
||||
|
||||
foreach ($jsonFiles as $file) {
|
||||
if (!is_file($file)) {
|
||||
echo "跳过不存在文件:{$file}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
echo "跳过无效 JSON:{$file}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$hotels = getv($data, 'data.getHotels.hotelInfo', []);
|
||||
|
||||
if (!is_array($hotels)) {
|
||||
echo "跳过无 hotelInfo 文件:{$file}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($hotels as $hotel) {
|
||||
if (!is_array($hotel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hotelCode = trim((string)($hotel['hotelCode'] ?? ''));
|
||||
|
||||
if ($hotelCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 后读入的同 hotelCode 酒店覆盖前面的
|
||||
$merged[$hotelCode] = normalizeHotel($hotel);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($merged);
|
||||
}
|
||||
|
||||
function pageName(string $type, int $page): string {
|
||||
if ($type === 'cards') {
|
||||
return $page === 1 ? 'index.html' : "page-{$page}.html";
|
||||
}
|
||||
|
||||
return $page === 1 ? 'table.html' : "table-{$page}.html";
|
||||
}
|
||||
|
||||
function renderLayout(string $body, string $title): string {
|
||||
return '<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>' . e($title) . '</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body{margin:0;background:#f4f4f4;font-family:Arial,"Microsoft YaHei",sans-serif;color:#111}
|
||||
.wrap{max-width:1460px;margin:0 auto;padding:20px}
|
||||
.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
|
||||
.top a{margin-left:10px;color:#0b5c7a;text-decoration:none}
|
||||
.card{display:grid;grid-template-columns:438px 1fr 230px;background:#fff;border-radius:18px;margin-bottom:24px;overflow:hidden}
|
||||
.img{width:438px;height:100%;object-fit:cover;background:#ddd}
|
||||
.info{padding:28px}
|
||||
.brand{display:inline-block;background:#0b684d;color:#fff;border-radius:4px;padding:6px 9px;margin-right:10px;font-weight:bold}
|
||||
.code{display:inline-block;background:#eee;color:#333;border-radius:4px;padding:5px 8px;margin-left:8px;font-size:14px;font-weight:bold}
|
||||
h2{display:inline;font-size:24px}
|
||||
.addr{color:#444;margin:8px 0 6px;font-size:17px}
|
||||
.dist{color:#555;margin-bottom:22px}
|
||||
.features{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:13px 40px;font-size:18px;color:#333}
|
||||
.features span:before{content:"◆";font-size:12px;color:#666;margin-right:9px}
|
||||
.side{padding:36px 24px;text-align:right;display:flex;flex-direction:column;justify-content:center}
|
||||
.price{font-size:32px;font-weight:bold}
|
||||
.btn{background:#c9310c;color:#fff;border-radius:7px;padding:18px 24px;text-decoration:none;font-weight:bold;display:inline-block;margin-top:28px}
|
||||
.tax{color:#555;margin-top:12px}
|
||||
table{width:100%;border-collapse:collapse;background:#fff}
|
||||
th,td{border:1px solid #ddd;padding:10px;vertical-align:top}
|
||||
th{background:#eee}
|
||||
.hotel-code{font-weight:bold;color:#0b5c7a}
|
||||
.thumb{width:110px;height:78px;object-fit:cover}
|
||||
.pager{margin:22px 0;text-align:center}
|
||||
.pager a{margin:0 8px;color:#0b5c7a}
|
||||
@media(max-width:900px){
|
||||
.card{grid-template-columns:1fr}
|
||||
.img{width:100%;height:240px}
|
||||
.side{text-align:left}
|
||||
}
|
||||
.rating{margin:10px 0;color:#333;font-weight:bold}
|
||||
.welcome{margin:14px 0 18px;color:#444;line-height:1.55;font-size:15px}
|
||||
.welcome-text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.welcome.open .welcome-text{display:block}
|
||||
.welcome-toggle{margin-top:6px;border:0;background:none;color:#0b5c7a;cursor:pointer;padding:0;font-size:14px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">' . $body . '</div>
|
||||
<script>
|
||||
document.addEventListener("click", function(e) {
|
||||
if (!e.target.classList.contains("welcome-toggle")) return;
|
||||
const box = e.target.closest(".welcome");
|
||||
box.classList.toggle("open");
|
||||
e.target.textContent = box.classList.contains("open") ? "收起" : "展开";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
|
||||
function renderPager(string $type, int $page, int $totalPages): string {
|
||||
$html = '<div class="pager">';
|
||||
|
||||
if ($page > 1) {
|
||||
$html .= '<a href="' . e(pageName($type, $page - 1)) . '">上一页</a>';
|
||||
}
|
||||
|
||||
$html .= ' 第 ' . $page . ' / ' . $totalPages . ' 页 ';
|
||||
|
||||
if ($page < $totalPages) {
|
||||
$html .= '<a href="' . e(pageName($type, $page + 1)) . '">下一页</a>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
function renderCardsPage(array $hotels, int $page, int $totalPages, int $total): string {
|
||||
$body = '<div class="top"><div>找到 ' . $total . ' 家酒店</div><div><a href="' . e(pageName('table', $page)) . '">表格模式</a></div></div>';
|
||||
|
||||
foreach ($hotels as $h) {
|
||||
$features = '';
|
||||
|
||||
foreach (array_slice($h['facilities'], 0, 8) as $f) {
|
||||
$features .= '<span>' . e($f) . '</span>';
|
||||
}
|
||||
|
||||
$price = $h['price'] !== null
|
||||
? '<div class="price">' . e(number_format((float)$h['price'])) . ' <small>' . e($h['currency']) . '</small></div><div>每晚</div>'
|
||||
: '<div class="price">暂无价格</div>';
|
||||
|
||||
$body .= '
|
||||
<div class="card" id="hotel-' . e($h['hotelCode']) . '" data-hotel-code="' . e($h['hotelCode']) . '">
|
||||
<img class="img" src="' . e($h['image']) . '" loading="lazy">
|
||||
<div class="info">
|
||||
<div>
|
||||
<span class="brand">' . e($h['brandCode']) . '</span>
|
||||
<h2>' . e($h['brand'] . ' ' . $h['name']) . '</h2>
|
||||
<span class="code">' . e($h['hotelCode']) . '</span>
|
||||
</div>
|
||||
<div class="addr">' . e($h['address']) . ' | ' . e($h['phone']) . '</div>
|
||||
<div class="rating">评分:' . e($h['rating'] === null ? 'null' : number_format((float)$h['rating'], 2, '.', '')) . '</div>
|
||||
<div class="welcome">
|
||||
<div class="welcome-text">' . e($h['welcomeMessage']) . '</div>
|
||||
<button class="welcome-toggle" type="button">展开</button>
|
||||
</div>
|
||||
<div class="features">' . $features . '</div>
|
||||
</div>
|
||||
<div class="side">
|
||||
<div>酒店 ID</div>
|
||||
<div class="hotel-code">' . e($h['hotelCode']) . '</div>
|
||||
<div style="margin-top:14px">⚡ Only a few left from</div>
|
||||
' . $price . '
|
||||
<div class="tax">' . e($h['tax']) . '</div>
|
||||
<a class="btn" href="' . e($h['website'] ?: '#') . '">选择酒店</a>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
$body .= renderPager('cards', $page, $totalPages);
|
||||
return renderLayout($body, '酒店列表');
|
||||
}
|
||||
|
||||
function renderTablePage(array $hotels, int $page, int $totalPages, int $total): string {
|
||||
$body = '<div class="top"><div>找到 ' . $total . ' 家酒店</div><div><a href="' . e(pageName('cards', $page)) . '">卡片模式</a></div></div>';
|
||||
|
||||
$body .= '<table><thead><tr>
|
||||
<th>hotelCode</th>
|
||||
<th>酒店</th>
|
||||
<th>品牌</th>
|
||||
<th>地址</th>
|
||||
<th>评分</th>
|
||||
<th>电话</th>
|
||||
<th>设施</th>
|
||||
<th>价格</th>
|
||||
<th>税费</th>
|
||||
</tr></thead><tbody>';
|
||||
|
||||
foreach ($hotels as $h) {
|
||||
$price = $h['price'] !== null
|
||||
? number_format((float)$h['price']) . ' ' . $h['currency']
|
||||
: '暂无';
|
||||
|
||||
$body .= '<tr data-hotel-code="' . e($h['hotelCode']) . '">
|
||||
<td class="hotel-code">' . e($h['hotelCode']) . '</td>
|
||||
<td>' . e($h['name']) . '</td>
|
||||
<td>' . e($h['brand']) . '</td>
|
||||
<td>' . e($h['address']) . '</td>
|
||||
<td>' . e($h['rating'] === null ? 'null' : $h['rating']) . '</td>
|
||||
<td>' . e($h['phone']) . '</td>
|
||||
<td>' . e(implode('、', array_slice($h['facilities'], 0, 8))) . '</td>
|
||||
<td>' . e($price) . '</td>
|
||||
<td>' . e($h['tax']) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$body .= '</tbody></table>';
|
||||
$body .= renderPager('table', $page, $totalPages);
|
||||
|
||||
return renderLayout($body, '酒店表格');
|
||||
}
|
||||
|
||||
$hotels = loadHotelsFromFiles($jsonFiles);
|
||||
|
||||
usort($hotels, function (array $a, array $b): int {
|
||||
return strcmp($a['hotelCode'], $b['hotelCode']);
|
||||
});
|
||||
|
||||
$total = count($hotels);
|
||||
|
||||
$totalCardPages = max(1, (int)ceil($total / $pageSizeCards));
|
||||
$totalTablePages = max(1, (int)ceil($total / $pageSizeTable));
|
||||
|
||||
for ($page = 1; $page <= $totalCardPages; $page++) {
|
||||
$chunk = array_slice($hotels, ($page - 1) * $pageSizeCards, $pageSizeCards);
|
||||
|
||||
file_put_contents(
|
||||
$outDir . '/' . pageName('cards', $page),
|
||||
renderCardsPage($chunk, $page, $totalCardPages, $total)
|
||||
);
|
||||
}
|
||||
|
||||
for ($page = 1; $page <= $totalTablePages; $page++) {
|
||||
$chunk = array_slice($hotels, ($page - 1) * $pageSizeTable, $pageSizeTable);
|
||||
|
||||
file_put_contents(
|
||||
$outDir . '/' . pageName('table', $page),
|
||||
renderTablePage($chunk, $page, $totalTablePages, $total)
|
||||
);
|
||||
}
|
||||
|
||||
echo "输入 JSON 文件数:" . count($jsonFiles) . PHP_EOL;
|
||||
echo "去重后酒店数:{$total}" . PHP_EOL;
|
||||
echo "卡片页数:{$totalCardPages}" . PHP_EOL;
|
||||
echo "表格页数:{$totalTablePages}" . PHP_EOL;
|
||||
echo "输出目录:{$outDir}" . PHP_EOL;
|
||||
Reference in New Issue
Block a user