344 lines
12 KiB
PHP
344 lines
12 KiB
PHP
<?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; |