IHG/script/outputall.php
2026-04-28 23:41:08 +08:00

344 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;