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
+217
View File
@@ -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";