26/04/28 first update

This commit is contained in:
2026-04-28 23:41:08 +08:00
commit e3f24881c7
414 changed files with 260188 additions and 0 deletions
+344
View File
@@ -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;