1410 lines
40 KiB
PHP
1410 lines
40 KiB
PHP
<?php
|
||
/**
|
||
* 🍳 菜谱分享页面
|
||
*
|
||
* 二维码扫码后展示菜谱详情的网页
|
||
* 支持通过 code 或 id 参数获取菜谱数据并渲染美观的分享页面
|
||
* 使用本地JSON文件存储菜谱数据,不依赖外部API
|
||
*
|
||
* 接口列表:
|
||
* GET ?code=CP00001 或 ?id=123 查看菜谱分享页面
|
||
* GET ?act=api&code=CP00001 获取菜谱JSON数据
|
||
* POST ?act=create 创建菜谱分享数据
|
||
* POST ?act=update 更新菜谱分享数据
|
||
* DELETE ?id=xxx 删除菜谱分享数据 (RESTful)
|
||
* GET ?act=list 菜谱分享列表
|
||
* GET ?act=stats 查看分享统计
|
||
* GET ?act=log 查看访问日志
|
||
* GET ?act=index 接口说明
|
||
*
|
||
* @file recipe_share.php
|
||
* @date 2026-04-18
|
||
* @version 1.3.0
|
||
* @desc 菜谱分享页面,扫码展示菜谱详情,本地JSON存储,支持统计和管理,数据3天后自动过期
|
||
* @lastUpdate 2026-04-18 适配完整菜谱数据传输,新增食材/营养/步骤等完整内容显示
|
||
*/
|
||
|
||
// ─── CORS预检请求处理 ───
|
||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('Access-Control-Allow-Origin: *');
|
||
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS');
|
||
header('Access-Control-Allow-Headers: Content-Type');
|
||
header('Access-Control-Max-Age: 86400');
|
||
http_response_code(204);
|
||
exit;
|
||
}
|
||
|
||
$startTime = microtime(true);
|
||
|
||
header('Access-Control-Allow-Origin: *');
|
||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||
header('Content-Type: text/html; charset=utf-8');
|
||
|
||
// ─── POST请求参数解析 ───
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||
$postData = array();
|
||
|
||
if (stripos($contentType, 'application/json') !== false) {
|
||
$rawBody = file_get_contents('php://input');
|
||
$json = json_decode($rawBody, true);
|
||
if (is_array($json)) {
|
||
$postData = $json;
|
||
}
|
||
} else {
|
||
$postData = $_POST;
|
||
}
|
||
|
||
foreach ($postData as $key => $value) {
|
||
if (!isset($_GET[$key])) {
|
||
$_GET[$key] = $value;
|
||
}
|
||
}
|
||
}
|
||
|
||
$act = strtolower(trim($_GET['act'] ?? 'share'));
|
||
|
||
// ─── RESTful DELETE 支持 ───
|
||
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||
$act = 'delete';
|
||
if (!isset($_GET['id']) && isset($_REQUEST['id'])) {
|
||
$_GET['id'] = $_REQUEST['id'];
|
||
}
|
||
}
|
||
|
||
// ─── 数据目录配置(与kitchen.php共享) ───
|
||
$dataDir = dirname(__FILE__) . '/cache/kitchen/';
|
||
if (!is_dir($dataDir)) {
|
||
if (!@mkdir($dataDir, 0755, true)) {
|
||
$tmpDir = sys_get_temp_dir() . '/kitchen/';
|
||
if (!is_dir($tmpDir)) {
|
||
@mkdir($tmpDir, 0755, true);
|
||
}
|
||
$dataDir = $tmpDir;
|
||
}
|
||
}
|
||
if (!is_writable($dataDir)) {
|
||
$tmpDir = sys_get_temp_dir() . '/kitchen/';
|
||
if (!is_dir($tmpDir)) {
|
||
@mkdir($tmpDir, 0755, true);
|
||
}
|
||
$dataDir = $tmpDir;
|
||
}
|
||
|
||
$statsFile = $dataDir . 'share_stats.json';
|
||
$logFile = $dataDir . 'access_log.json';
|
||
|
||
// ─── 路由分发 ───
|
||
switch ($act) {
|
||
case 'create':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(create_recipe_share(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'update':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(update_recipe_share(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'delete':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(delete_recipe_share(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'list':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(list_recipe_data(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'cleanup':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(cleanup_expired_recipes(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'stats':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(get_share_stats(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'log':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(get_access_log(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'api':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(fetch_recipe_data(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'index':
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode(array(
|
||
'code' => 200,
|
||
'message' => '🍳 菜谱分享页面',
|
||
'data' => array(
|
||
'version' => '1.3.0',
|
||
'description' => '菜谱分享页面,本地JSON存储,支持CRUD和统计,数据3天后自动过期删除,完整数据传输',
|
||
'endpoints' => array(
|
||
'share' => '?code=CP00001 或 ?id=123',
|
||
'api' => '?act=api&code=CP00001 (JSON数据)',
|
||
'create' => 'POST ?act=create {recipe JSON}',
|
||
'update' => 'POST ?act=update {recipe JSON}',
|
||
'delete' => 'DELETE ?id=xxx (RESTful)',
|
||
'list' => 'GET ?act=list',
|
||
'cleanup' => 'GET ?act=cleanup (清理过期数据)',
|
||
'stats' => '?act=stats (分享统计)',
|
||
'log' => '?act=log (访问日志)',
|
||
'index' => '?act=index (接口说明)',
|
||
),
|
||
'storage' => 'JSON文件 (' . $dataDir . 'recipe_*.json)',
|
||
'expire_policy' => '数据3天后自动过期删除,读取时自动检查并清理',
|
||
'display' => '完整显示:封面/标题/简介/标签/食材/营养/过敏原/步骤',
|
||
),
|
||
), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
break;
|
||
case 'share':
|
||
default:
|
||
render_share_page();
|
||
break;
|
||
}
|
||
|
||
exit;
|
||
|
||
// ══════════════════════════════════════════════════════════════
|
||
// 数据获取函数
|
||
// ══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* 读取菜谱分享数据(本地JSON文件)
|
||
* 自动检查过期,过期则删除并返回null
|
||
*/
|
||
function read_recipe_data($id) {
|
||
global $dataDir;
|
||
$file = $dataDir . 'recipe_' . (int)$id . '.json';
|
||
if (!file_exists($file)) {
|
||
return null;
|
||
}
|
||
|
||
// 检查是否过期
|
||
if (is_recipe_expired($file)) {
|
||
@unlink($file);
|
||
return null;
|
||
}
|
||
|
||
$content = file_get_contents($file);
|
||
$data = json_decode($content, true);
|
||
return is_array($data) ? $data : null;
|
||
}
|
||
|
||
/**
|
||
* 检查菜谱数据是否过期
|
||
*/
|
||
function is_recipe_expired($file) {
|
||
if (!file_exists($file)) return true;
|
||
|
||
$content = file_get_contents($file);
|
||
$data = json_decode($content, true);
|
||
if (!is_array($data)) return true;
|
||
|
||
$expiresAt = $data['expiresAt'] ?? null;
|
||
if (empty($expiresAt)) {
|
||
// 没有过期时间,默认3天后过期(从创建时间算起)
|
||
$createdAt = $data['createdAt'] ?? $data['updatedAt'] ?? '';
|
||
if (empty($createdAt)) return false;
|
||
$expiresAt = date('c', strtotime($createdAt . ' +3 days'));
|
||
}
|
||
|
||
return strtotime($expiresAt) < time();
|
||
}
|
||
|
||
/**
|
||
* 清理所有过期的菜谱分享数据
|
||
*/
|
||
function cleanup_expired_recipes() {
|
||
global $dataDir;
|
||
$files = glob($dataDir . 'recipe_*.json');
|
||
if ($files === false) return array('deleted' => 0, 'errors' => array());
|
||
|
||
$deleted = 0;
|
||
$errors = array();
|
||
|
||
foreach ($files as $file) {
|
||
if (is_recipe_expired($file)) {
|
||
if (@unlink($file)) {
|
||
$deleted++;
|
||
} else {
|
||
$errors[] = basename($file);
|
||
}
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'deleted' => $deleted,
|
||
'errors' => $errors,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 写入菜谱分享数据(本地JSON文件)
|
||
* 注意:数据3天后自动过期删除
|
||
*/
|
||
function write_recipe_data($id, $data) {
|
||
global $dataDir;
|
||
$file = $dataDir . 'recipe_' . (int)$id . '.json';
|
||
|
||
// 添加过期时间(3天后)
|
||
$data['expiresAt'] = date('c', strtotime('+3 days'));
|
||
$data['updatedAt'] = date('c');
|
||
|
||
$fp = fopen($file, 'c');
|
||
if (flock($fp, LOCK_EX)) {
|
||
ftruncate($fp, 0);
|
||
rewind($fp);
|
||
fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||
flock($fp, LOCK_UN);
|
||
}
|
||
fclose($fp);
|
||
}
|
||
|
||
/**
|
||
* 删除菜谱分享数据
|
||
*/
|
||
function delete_recipe_data($id) {
|
||
global $dataDir;
|
||
$file = $dataDir . 'recipe_' . (int)$id . '.json';
|
||
if (file_exists($file)) {
|
||
return @unlink($file);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 获取菜谱分享列表
|
||
*/
|
||
function list_recipe_data() {
|
||
global $dataDir;
|
||
$recipes = array();
|
||
$files = glob($dataDir . 'recipe_*.json');
|
||
if ($files === false) return $recipes;
|
||
|
||
foreach ($files as $file) {
|
||
$content = file_get_contents($file);
|
||
$data = json_decode($content, true);
|
||
if (is_array($data)) {
|
||
$recipes[] = $data;
|
||
}
|
||
}
|
||
|
||
// 按更新时间倒序
|
||
usort($recipes, function($a, $b) {
|
||
$timeA = $a['updatedAt'] ?? $a['createdAt'] ?? '';
|
||
$timeB = $b['updatedAt'] ?? $b['createdAt'] ?? '';
|
||
return strcmp($timeB, $timeA);
|
||
});
|
||
|
||
return $recipes;
|
||
}
|
||
|
||
/**
|
||
* 将code解析为ID(支持多种格式)
|
||
* 支持格式: CP00001, CP123, cp123, cpxxxx, 123
|
||
*/
|
||
function resolve_code_to_id($code) {
|
||
// 支持 CP/cp/cpxxxx 格式,提取数字部分
|
||
if (preg_match('/^CPX?(\d+)$/i', $code, $matches)) {
|
||
return (int)$matches[1];
|
||
}
|
||
return (int)$code;
|
||
}
|
||
|
||
/**
|
||
* 获取API基础URL(用于页面链接)
|
||
*/
|
||
function get_api_base_url() {
|
||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||
$host = $_SERVER['HTTP_HOST'] ?? 'eat.wktyl.com';
|
||
return $protocol . '://' . $host;
|
||
}
|
||
|
||
/**
|
||
* 通过本地JSON文件获取菜谱数据
|
||
*/
|
||
function fetch_recipe_data() {
|
||
$code = trim($_GET['code'] ?? '');
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
|
||
if (empty($code) && $id <= 0) {
|
||
return array('code' => 400, 'message' => '缺少 code 或 id 参数');
|
||
}
|
||
|
||
$resolvedId = $id > 0 ? $id : resolve_code_to_id($code);
|
||
$recipe = read_recipe_data($resolvedId);
|
||
|
||
if ($recipe === null) {
|
||
return array('code' => 404, 'message' => '菜谱不存在');
|
||
}
|
||
|
||
record_share_access($recipe, $code, $id);
|
||
|
||
return array(
|
||
'code' => 200,
|
||
'message' => 'success',
|
||
'data' => $recipe,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 获取菜谱完整数据(用于页面渲染)
|
||
*/
|
||
function fetch_recipe_full($id) {
|
||
$recipe = read_recipe_data((int)$id);
|
||
if ($recipe === null) return array();
|
||
|
||
return $recipe;
|
||
}
|
||
|
||
/**
|
||
* 创建菜谱分享数据
|
||
* POST ?act=create
|
||
* Body: {recipe JSON}
|
||
*/
|
||
function create_recipe_share() {
|
||
$recipeJson = $_GET['recipe'] ?? $_GET['data'] ?? null;
|
||
|
||
if ($recipeJson === null) {
|
||
$rawBody = file_get_contents('php://input');
|
||
$recipeData = json_decode($rawBody, true);
|
||
} else {
|
||
$recipeData = json_decode($recipeJson, true);
|
||
}
|
||
|
||
if (!is_array($recipeData)) {
|
||
return array('code' => 400, 'message' => '无效的菜谱数据,需要JSON格式');
|
||
}
|
||
|
||
// 补全字段
|
||
$id = (int)($recipeData['id'] ?? 0);
|
||
if ($id <= 0) {
|
||
return array('code' => 400, 'message' => '缺少菜谱ID');
|
||
}
|
||
|
||
if (empty($recipeData['createdAt'])) {
|
||
$recipeData['createdAt'] = date('c');
|
||
}
|
||
$recipeData['updatedAt'] = date('c');
|
||
$recipeData['createdBy'] = get_client_ip();
|
||
|
||
write_recipe_data($id, $recipeData);
|
||
|
||
return array(
|
||
'code' => 200,
|
||
'message' => '创建成功',
|
||
'data' => $recipeData,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 更新菜谱分享数据
|
||
* POST ?act=update
|
||
* Body: {recipe JSON with id}
|
||
*/
|
||
function update_recipe_share() {
|
||
$recipeJson = $_GET['recipe'] ?? $_GET['data'] ?? null;
|
||
|
||
if ($recipeJson === null) {
|
||
$rawBody = file_get_contents('php://input');
|
||
$recipeData = json_decode($rawBody, true);
|
||
} else {
|
||
$recipeData = json_decode($recipeJson, true);
|
||
}
|
||
|
||
if (!is_array($recipeData) || empty($recipeData['id'])) {
|
||
return array('code' => 400, 'message' => '无效的菜谱数据,缺少id字段');
|
||
}
|
||
|
||
$id = (int)$recipeData['id'];
|
||
$existing = read_recipe_data($id);
|
||
|
||
if ($existing === null) {
|
||
return array('code' => 404, 'message' => '菜谱不存在');
|
||
}
|
||
|
||
// 合并数据
|
||
$recipeData = array_merge($existing, $recipeData);
|
||
$recipeData['updatedAt'] = date('c');
|
||
|
||
write_recipe_data($id, $recipeData);
|
||
|
||
return array(
|
||
'code' => 200,
|
||
'message' => '更新成功',
|
||
'data' => $recipeData,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 删除菜谱分享数据
|
||
* GET ?act=delete&id=xxx
|
||
*/
|
||
function delete_recipe_share() {
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
if ($id <= 0) {
|
||
return array('code' => 400, 'message' => '缺少菜谱ID参数');
|
||
}
|
||
|
||
if (!delete_recipe_data($id)) {
|
||
return array('code' => 404, 'message' => '菜谱不存在');
|
||
}
|
||
|
||
return array(
|
||
'code' => 200,
|
||
'message' => '删除成功',
|
||
'data' => array('deleted_id' => $id),
|
||
);
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════
|
||
// 统计和日志函数
|
||
// ══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* 记录分享访问
|
||
*/
|
||
function record_share_access($recipe, $code, $id) {
|
||
global $statsFile, $logFile;
|
||
|
||
$recipeId = $recipe['id'] ?? $id;
|
||
$recipeTitle = $recipe['title'] ?? '未知菜谱';
|
||
|
||
// 更新统计
|
||
$stats = read_json_file($statsFile, array(
|
||
'total_views' => 0,
|
||
'today_views' => 0,
|
||
'today_date' => date('Y-m-d'),
|
||
'recipes' => array(),
|
||
));
|
||
|
||
$today = date('Y-m-d');
|
||
if ($stats['today_date'] !== $today) {
|
||
$stats['today_views'] = 0;
|
||
$stats['today_date'] = $today;
|
||
}
|
||
|
||
$stats['total_views']++;
|
||
$stats['today_views']++;
|
||
|
||
$key = 'r_' . $recipeId;
|
||
if (!isset($stats['recipes'][$key])) {
|
||
$stats['recipes'][$key] = array(
|
||
'id' => $recipeId,
|
||
'title' => $recipeTitle,
|
||
'code' => $code,
|
||
'views' => 0,
|
||
'first_access' => date('c'),
|
||
'last_access' => date('c'),
|
||
);
|
||
}
|
||
$stats['recipes'][$key]['views']++;
|
||
$stats['recipes'][$key]['last_access'] = date('c');
|
||
if (!empty($recipeTitle) && $recipeTitle !== '未知菜谱') {
|
||
$stats['recipes'][$key]['title'] = $recipeTitle;
|
||
}
|
||
|
||
write_json_file($statsFile, $stats);
|
||
|
||
// 记录访问日志(保留最近200条)
|
||
$log = read_json_file($logFile, array());
|
||
array_unshift($log, array(
|
||
'time' => date('c'),
|
||
'recipe_id' => $recipeId,
|
||
'title' => $recipeTitle,
|
||
'code' => $code,
|
||
'ip' => get_client_ip(),
|
||
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||
'referer' => $_SERVER['HTTP_REFERER'] ?? '',
|
||
));
|
||
$log = array_slice($log, 0, 200);
|
||
write_json_file($logFile, $log);
|
||
}
|
||
|
||
/**
|
||
* 获取分享统计
|
||
*/
|
||
function get_share_stats() {
|
||
global $statsFile;
|
||
$stats = read_json_file($statsFile, array(
|
||
'total_views' => 0,
|
||
'today_views' => 0,
|
||
'today_date' => date('Y-m-d'),
|
||
'recipes' => array(),
|
||
));
|
||
return array(
|
||
'code' => 200,
|
||
'data' => $stats,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 获取访问日志
|
||
*/
|
||
function get_access_log() {
|
||
global $logFile;
|
||
$log = read_json_file($logFile, array());
|
||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||
$limit = min(50, max(1, (int)($_GET['limit'] ?? 20)));
|
||
$offset = ($page - 1) * $limit;
|
||
return array(
|
||
'code' => 200,
|
||
'data' => array(
|
||
'list' => array_slice($log, $offset, $limit),
|
||
'total' => count($log),
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
),
|
||
);
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════
|
||
// 工具函数
|
||
// ══════════════════════════════════════════════════════════════
|
||
|
||
function read_json_file($file, $default = array()) {
|
||
if (!file_exists($file)) return $default;
|
||
$content = file_get_contents($file);
|
||
$data = json_decode($content, true);
|
||
return is_array($data) ? $data : $default;
|
||
}
|
||
|
||
function write_json_file($file, $data) {
|
||
$fp = fopen($file, 'c');
|
||
if (flock($fp, LOCK_EX)) {
|
||
ftruncate($fp, 0);
|
||
rewind($fp);
|
||
fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||
flock($fp, LOCK_UN);
|
||
}
|
||
fclose($fp);
|
||
}
|
||
|
||
function get_client_ip() {
|
||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||
return trim($ips[0]);
|
||
}
|
||
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||
return $_SERVER['HTTP_X_REAL_IP'];
|
||
}
|
||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||
}
|
||
|
||
function safe_html($str) {
|
||
return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
function format_time_ago($timestamp) {
|
||
if (empty($timestamp)) return '';
|
||
$now = time();
|
||
$ts = is_numeric($timestamp) ? (int)$timestamp : strtotime($timestamp);
|
||
$diff = $now - $ts;
|
||
if ($diff < 60) return '刚刚';
|
||
if ($diff < 3600) return floor($diff / 60) . '分钟前';
|
||
if ($diff < 86400) return floor($diff / 3600) . '小时前';
|
||
if ($diff < 604800) return floor($diff / 86400) . '天前';
|
||
return date('Y-m-d', $ts);
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════
|
||
// 页面渲染
|
||
// ══════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* 渲染分享页面
|
||
*/
|
||
function render_share_page() {
|
||
$code = trim($_GET['code'] ?? '');
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
|
||
$recipe = array();
|
||
$error = '';
|
||
|
||
if (!empty($code) || $id > 0) {
|
||
$resolvedId = $id > 0 ? $id : resolve_code_to_id($code);
|
||
$recipe = fetch_recipe_full($resolvedId);
|
||
if (!empty($recipe)) {
|
||
record_share_access($recipe, $code, $id);
|
||
} else {
|
||
$error = '菜谱不存在或暂时无法加载';
|
||
}
|
||
} else {
|
||
$error = '缺少菜谱参数';
|
||
}
|
||
|
||
// 提取数据用于显示
|
||
$title = !empty($recipe) ? safe_html($recipe['title'] ?? '菜谱分享') : '🍳 菜谱分享';
|
||
$cover = !empty($recipe) ? ($recipe['cover'] ?? '') : '';
|
||
$intro = !empty($recipe) ? ($recipe['intro'] ?? '') : '';
|
||
$content = !empty($recipe) ? ($recipe['content'] ?? '') : '';
|
||
$categoryName = !empty($recipe) ? ($recipe['category_name'] ?? ($recipe['categoryName'] ?? '')) : '';
|
||
$recipeCode = !empty($recipe) ? ($recipe['code'] ?? $code) : $code;
|
||
$viewCount = !empty($recipe) ? (($recipe['statistics']['views'] ?? 0) ?: ($recipe['view_count'] ?? 0)) : 0;
|
||
$likeCount = !empty($recipe) ? (($recipe['statistics']['likes'] ?? 0) ?: ($recipe['like_count'] ?? 0)) : 0;
|
||
$ratingScore = !empty($recipe) ? (($recipe['rating']['score'] ?? 0) ?: 0) : 0;
|
||
$ratingNums = !empty($recipe) ? (($recipe['rating']['nums'] ?? 0) ?: 0) : 0;
|
||
$author = !empty($recipe) ? ($recipe['author'] ?? null) : null;
|
||
$tags = !empty($recipe) ? ($recipe['tags'] ?? array()) : array();
|
||
$ingredients = !empty($recipe) ? ($recipe['ingredients'] ?? array()) : array();
|
||
$nutrition = !empty($recipe) ? ($recipe['nutrition'] ?? null) : null;
|
||
$meta = !empty($recipe) ? ($recipe['meta'] ?? null) : null;
|
||
$allergens = !empty($recipe) ? ($recipe['allergens'] ?? array()) : array();
|
||
|
||
$queryTime = round((microtime(true) - $GLOBALS['startTime']) * 1000, 2);
|
||
|
||
echo '<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>' . $title . ' - 小妈厨房</title>
|
||
<meta name="description" content="' . safe_html($intro) . '">
|
||
<meta name="theme-color" content="#FF6B35">
|
||
<meta property="og:title" content="' . $title . '">
|
||
<meta property="og:description" content="' . safe_html($intro) . '">
|
||
<meta property="og:type" content="article">
|
||
' . (!empty($cover) ? '<meta property="og:image" content="' . safe_html($cover) . '">' : '') . '
|
||
<meta property="og:site_name" content="小妈厨房">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍳</text></svg>">
|
||
<style>
|
||
:root {
|
||
--primary: #FF6B35;
|
||
--primary-light: #FF8F5E;
|
||
--primary-dark: #E55A2B;
|
||
--bg: #F5F5F7;
|
||
--surface: #FFFFFF;
|
||
--text1: #1D1D1F;
|
||
--text2: #6E6E73;
|
||
--text3: #AEAEB2;
|
||
--border: rgba(0,0,0,0.06);
|
||
--radius-sm: 8px;
|
||
--radius-md: 12px;
|
||
--radius-lg: 16px;
|
||
--radius-xl: 20px;
|
||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04);
|
||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||
--shadow-lg: 0 8px 30px rgba(0,0,0,0.12);
|
||
--font: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
--space1: 4px; --space2: 8px; --space3: 16px; --space4: 24px; --space5: 32px;
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--bg: #000000;
|
||
--surface: #1C1C1E;
|
||
--text1: #F5F5F7;
|
||
--text2: #98989D;
|
||
--text3: #636366;
|
||
--border: rgba(255,255,255,0.08);
|
||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.2);
|
||
--shadow-md: 0 4px 12px rgba(0,0,0,0.3);
|
||
--shadow-lg: 0 8px 30px rgba(0,0,0,0.4);
|
||
}
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: var(--font);
|
||
background: var(--bg);
|
||
color: var(--text1);
|
||
min-height: 100vh;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
.container {
|
||
max-width: 640px;
|
||
margin: 0 auto;
|
||
padding: var(--space3);
|
||
}
|
||
|
||
/* 顶部导航 */
|
||
.nav {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: var(--space3) 0;
|
||
margin-bottom: var(--space2);
|
||
}
|
||
|
||
.nav-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space2);
|
||
text-decoration: none;
|
||
color: var(--text1);
|
||
}
|
||
|
||
.nav-brand-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
border-radius: var(--radius-md);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.nav-brand-text {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.2px;
|
||
}
|
||
|
||
.nav-badge {
|
||
font-size: 11px;
|
||
color: var(--text3);
|
||
background: var(--surface);
|
||
padding: var(--space1) var(--space2);
|
||
border-radius: 20px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
/* 封面图 */
|
||
.cover-section {
|
||
position: relative;
|
||
border-radius: var(--radius-xl);
|
||
overflow: hidden;
|
||
margin-bottom: var(--space4);
|
||
aspect-ratio: 4/3;
|
||
background: var(--surface);
|
||
}
|
||
|
||
.cover-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
|
||
.cover-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, var(--primary-light), var(--primary));
|
||
color: white;
|
||
}
|
||
|
||
.cover-placeholder-emoji {
|
||
font-size: 64px;
|
||
margin-bottom: var(--space3);
|
||
}
|
||
|
||
.cover-placeholder-text {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.cover-overlay {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: var(--space5) var(--space4) var(--space4);
|
||
background: linear-gradient(transparent, rgba(0,0,0,0.6));
|
||
}
|
||
|
||
.cover-category {
|
||
display: inline-block;
|
||
background: rgba(255,255,255,0.2);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
color: white;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
padding: var(--space1) var(--space2);
|
||
border-radius: 20px;
|
||
margin-bottom: var(--space2);
|
||
}
|
||
|
||
.cover-title {
|
||
color: white;
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.3px;
|
||
line-height: 1.3;
|
||
text-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
/* 信息卡片 */
|
||
.info-card {
|
||
background: var(--surface);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--space4);
|
||
margin-bottom: var(--space3);
|
||
box-shadow: var(--shadow-sm);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space3);
|
||
padding: var(--space2) 0;
|
||
}
|
||
|
||
.info-row + .info-row {
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.info-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: var(--radius-sm);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.info-icon.views { background: rgba(59,130,246,0.1); }
|
||
.info-icon.rating { background: rgba(245,158,11,0.1); }
|
||
.info-icon.code { background: rgba(139,92,246,0.1); }
|
||
|
||
.info-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 12px;
|
||
color: var(--text3);
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--text1);
|
||
}
|
||
|
||
/* 评分星星 */
|
||
.stars {
|
||
display: inline-flex;
|
||
gap: 2px;
|
||
margin-right: var(--space1);
|
||
}
|
||
|
||
.star { color: #F59E0B; font-size: 14px; }
|
||
.star.empty { color: var(--text3); }
|
||
|
||
/* 简介 */
|
||
.intro-text {
|
||
font-size: 15px;
|
||
line-height: 1.7;
|
||
color: var(--text2);
|
||
}
|
||
|
||
/* 区块标题 */
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--text1);
|
||
margin-bottom: var(--space3);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space2);
|
||
}
|
||
|
||
/* 标签列表 */
|
||
.tags-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space2);
|
||
}
|
||
|
||
.tag-item {
|
||
display: inline-block;
|
||
padding: var(--space1) var(--space2);
|
||
background: var(--primary);
|
||
color: white;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
/* 食材列表 */
|
||
.ingredients-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space2);
|
||
}
|
||
|
||
.ingredient-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: var(--space2) var(--space3);
|
||
background: var(--bg);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.ingredient-name {
|
||
font-size: 14px;
|
||
color: var(--text1);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.ingredient-amount {
|
||
font-size: 13px;
|
||
color: var(--text2);
|
||
}
|
||
|
||
/* 营养成分网格 */
|
||
.nutrition-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: var(--space2);
|
||
}
|
||
|
||
.nutrition-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space2);
|
||
padding: var(--space2) var(--space3);
|
||
background: var(--bg);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.nutrition-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.nutrition-label {
|
||
font-size: 13px;
|
||
color: var(--text2);
|
||
flex: 1;
|
||
}
|
||
|
||
.nutrition-value {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--text1);
|
||
}
|
||
|
||
/* 过敏原警告 */
|
||
.warning-card {
|
||
background: linear-gradient(135deg, #FEF3C7, #FDE68A);
|
||
border: 1px solid #F59E0B;
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
.warning-card {
|
||
background: linear-gradient(135deg, #78350F, #92400E);
|
||
border-color: #D97706;
|
||
}
|
||
}
|
||
|
||
.allergens-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space2);
|
||
}
|
||
|
||
.allergen-item {
|
||
display: inline-block;
|
||
padding: var(--space1) var(--space2);
|
||
background: rgba(245, 158, 11, 0.2);
|
||
color: #B45309;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
border-radius: 20px;
|
||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
.allergen-item {
|
||
color: #FCD34D;
|
||
background: rgba(245, 158, 11, 0.15);
|
||
border-color: rgba(245, 158, 11, 0.3);
|
||
}
|
||
}
|
||
|
||
/* 步骤内容 */
|
||
.steps-content {
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
color: var(--text2);
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
/* CTA按钮 */
|
||
.cta-section {
|
||
margin-top: var(--space4);
|
||
margin-bottom: var(--space5);
|
||
}
|
||
|
||
.cta-button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--space2);
|
||
width: 100%;
|
||
padding: var(--space3) var(--space4);
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: white;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
border: none;
|
||
border-radius: var(--radius-lg);
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
box-shadow: 0 4px 14px rgba(255,107,53,0.3);
|
||
}
|
||
|
||
.cta-button:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 6px 20px rgba(255,107,53,0.4);
|
||
}
|
||
|
||
.cta-button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* 页脚 */
|
||
.footer {
|
||
text-align: center;
|
||
padding: var(--space5) 0 var(--space4);
|
||
color: var(--text3);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.footer a {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
/* 错误页面 */
|
||
.error-section {
|
||
text-align: center;
|
||
padding: var(--space5) var(--space4);
|
||
}
|
||
|
||
.error-emoji {
|
||
font-size: 64px;
|
||
margin-bottom: var(--space3);
|
||
}
|
||
|
||
.error-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--text1);
|
||
margin-bottom: var(--space2);
|
||
}
|
||
|
||
.error-desc {
|
||
font-size: 15px;
|
||
color: var(--text2);
|
||
margin-bottom: var(--space4);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 加载动画 */
|
||
.loading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: var(--space5);
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 3px solid var(--border);
|
||
border-top-color: var(--primary);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
margin-bottom: var(--space3);
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 14px;
|
||
color: var(--text3);
|
||
}
|
||
|
||
/* 动画 */
|
||
@keyframes fadeInUp {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.animate-in {
|
||
animation: fadeInUp 0.5s ease-out forwards;
|
||
}
|
||
|
||
.animate-delay-1 { animation-delay: 0.1s; opacity: 0; }
|
||
.animate-delay-2 { animation-delay: 0.2s; opacity: 0; }
|
||
.animate-delay-3 { animation-delay: 0.3s; opacity: 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">';
|
||
|
||
if (!empty($error)) {
|
||
echo '
|
||
<div class="nav">
|
||
<a href="' . get_api_base_url() . '/api/" class="nav-brand">
|
||
<div class="nav-brand-icon">🍳</div>
|
||
<span class="nav-brand-text">小妈厨房</span>
|
||
</a>
|
||
</div>
|
||
<div class="error-section animate-in">
|
||
<div class="error-emoji">😕</div>
|
||
<div class="error-title">无法加载菜谱</div>
|
||
<div class="error-desc">' . safe_html($error) . '</div>
|
||
<a href="' . get_api_base_url() . '/api/" class="cta-button" style="max-width:240px;margin:0 auto;">
|
||
🍳 返回首页
|
||
</a>
|
||
</div>';
|
||
} else {
|
||
$starsHtml = '';
|
||
$fullStars = floor($ratingScore);
|
||
$halfStar = ($ratingScore - $fullStars) >= 0.5;
|
||
for ($i = 0; $i < 5; $i++) {
|
||
if ($i < $fullStars) {
|
||
$starsHtml .= '<span class="star">★</span>';
|
||
} elseif ($i === $fullStars && $halfStar) {
|
||
$starsHtml .= '<span class="star">★</span>';
|
||
} else {
|
||
$starsHtml .= '<span class="star empty">★</span>';
|
||
}
|
||
}
|
||
|
||
echo '
|
||
<div class="nav animate-in">
|
||
<a href="' . get_api_base_url() . '/api/" class="nav-brand">
|
||
<div class="nav-brand-icon">🍳</div>
|
||
<span class="nav-brand-text">小妈厨房</span>
|
||
</a>
|
||
<span class="nav-badge">扫码访问</span>
|
||
</div>
|
||
|
||
<div class="cover-section animate-in animate-delay-1">';
|
||
if (!empty($cover)) {
|
||
echo '<img class="cover-img" src="' . safe_html($cover) . '" alt="' . $title . '" loading="lazy" onerror="this.parentElement.innerHTML=\'<div class=cover-placeholder><div class=cover-placeholder-emoji>🍳</div><div class=cover-placeholder-text>' . $title . '</div></div>\'">';
|
||
} else {
|
||
echo '<div class="cover-placeholder">
|
||
<div class="cover-placeholder-emoji">🍳</div>
|
||
<div class="cover-placeholder-text">' . $title . '</div>
|
||
</div>';
|
||
}
|
||
echo '
|
||
<div class="cover-overlay">';
|
||
if (!empty($categoryName)) {
|
||
echo '<span class="cover-category">' . safe_html($categoryName) . '</span>';
|
||
}
|
||
echo '<h1 class="cover-title">' . $title . '</h1>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-card animate-in animate-delay-2">';
|
||
if ($viewCount > 0) {
|
||
echo '
|
||
<div class="info-row">
|
||
<div class="info-icon views">👁️</div>
|
||
<div class="info-content">
|
||
<div class="info-label">浏览量</div>
|
||
<div class="info-value">' . number_format($viewCount) . ' 次</div>
|
||
</div>
|
||
</div>';
|
||
}
|
||
if ($ratingScore > 0) {
|
||
echo '
|
||
<div class="info-row">
|
||
<div class="info-icon rating">⭐</div>
|
||
<div class="info-content">
|
||
<div class="info-label">评分</div>
|
||
<div class="info-value">
|
||
<span class="stars">' . $starsHtml . '</span>
|
||
' . number_format($ratingScore, 1) . ' 分' . ($ratingNums > 0 ? ' · ' . $ratingNums . ' 人评' : '') . '
|
||
</div>
|
||
</div>
|
||
</div>';
|
||
}
|
||
if (!empty($recipeCode)) {
|
||
echo '
|
||
<div class="info-row">
|
||
<div class="info-icon code">🔢</div>
|
||
<div class="info-content">
|
||
<div class="info-label">菜谱编码</div>
|
||
<div class="info-value">' . safe_html($recipeCode) . '</div>
|
||
</div>
|
||
</div>';
|
||
}
|
||
|
||
// 显示作者信息
|
||
if (!empty($author) && is_array($author)) {
|
||
$authorName = $author['name'] ?? ($author['nickname'] ?? '');
|
||
$authorAvatar = $author['avatar'] ?? '';
|
||
if (!empty($authorName)) {
|
||
echo '
|
||
<div class="info-row">
|
||
<div class="info-icon">👨🍳</div>
|
||
<div class="info-content">
|
||
<div class="info-label">作者</div>
|
||
<div class="info-value">' . safe_html($authorName) . '</div>
|
||
</div>
|
||
</div>';
|
||
}
|
||
}
|
||
|
||
// 显示烹饪时间和难度
|
||
if (!empty($meta) && is_array($meta)) {
|
||
$difficulty = $meta['difficulty'] ?? '';
|
||
$time = $meta['time'] ?? '';
|
||
if (!empty($time)) {
|
||
echo '
|
||
<div class="info-row">
|
||
<div class="info-icon">⏱️</div>
|
||
<div class="info-content">
|
||
<div class="info-label">烹饪时间</div>
|
||
<div class="info-value">' . safe_html($time) . '</div>
|
||
</div>
|
||
</div>';
|
||
}
|
||
if (!empty($difficulty)) {
|
||
echo '
|
||
<div class="info-row">
|
||
<div class="info-icon">📊</div>
|
||
<div class="info-content">
|
||
<div class="info-label">难度</div>
|
||
<div class="info-value">' . safe_html($difficulty) . '</div>
|
||
</div>
|
||
</div>';
|
||
}
|
||
}
|
||
|
||
echo '
|
||
</div>';
|
||
|
||
// 显示简介
|
||
if (!empty($intro)) {
|
||
echo '
|
||
<div class="info-card animate-in animate-delay-3">
|
||
<h3 class="section-title">📝 简介</h3>
|
||
<p class="intro-text">' . nl2br(safe_html($intro)) . '</p>
|
||
</div>';
|
||
}
|
||
|
||
// 显示标签
|
||
if (!empty($tags) && is_array($tags) && count($tags) > 0) {
|
||
echo '
|
||
<div class="info-card animate-in animate-delay-3">
|
||
<h3 class="section-title">🏷️ 标签</h3>
|
||
<div class="tags-list">';
|
||
foreach ($tags as $tag) {
|
||
$tagName = is_array($tag) ? ($tag['name'] ?? '') : $tag;
|
||
if (!empty($tagName)) {
|
||
echo '<span class="tag-item">' . safe_html($tagName) . '</span>';
|
||
}
|
||
}
|
||
echo '
|
||
</div>
|
||
</div>';
|
||
}
|
||
|
||
// 显示食材列表
|
||
if (!empty($ingredients) && is_array($ingredients) && count($ingredients) > 0) {
|
||
echo '
|
||
<div class="info-card animate-in animate-delay-3">
|
||
<h3 class="section-title">🥗 食材清单</h3>
|
||
<div class="ingredients-list">';
|
||
foreach ($ingredients as $ing) {
|
||
$ingName = is_array($ing) ? ($ing['name'] ?? '') : $ing;
|
||
$ingAmount = is_array($ing) ? ($ing['amount'] ?? '') : '';
|
||
$ingUnit = is_array($ing) ? ($ing['unit'] ?? '') : '';
|
||
if (!empty($ingName)) {
|
||
echo '
|
||
<div class="ingredient-item">
|
||
<span class="ingredient-name">' . safe_html($ingName) . '</span>
|
||
<span class="ingredient-amount">' . safe_html(trim($ingAmount . ' ' . $ingUnit)) . '</span>
|
||
</div>';
|
||
}
|
||
}
|
||
echo '
|
||
</div>
|
||
</div>';
|
||
}
|
||
|
||
// 显示营养信息
|
||
if (!empty($nutrition) && is_array($nutrition)) {
|
||
$hasNutrition = false;
|
||
$nutritionItems = array();
|
||
$nutritionFields = array(
|
||
'calories' => array('label' => '热量', 'unit' => 'kcal', 'icon' => '🔥'),
|
||
'protein' => array('label' => '蛋白质', 'unit' => 'g', 'icon' => '💪'),
|
||
'fat' => array('label' => '脂肪', 'unit' => 'g', 'icon' => '🧈'),
|
||
'carbs' => array('label' => '碳水', 'unit' => 'g', 'icon' => '🍞'),
|
||
'fiber' => array('label' => '膳食纤维', 'unit' => 'g', 'icon' => '🥬'),
|
||
);
|
||
foreach ($nutritionFields as $key => $info) {
|
||
$value = $nutrition[$key] ?? null;
|
||
if ($value !== null && $value !== '') {
|
||
$hasNutrition = true;
|
||
$nutritionItems[] = '<div class="nutrition-item"><span class="nutrition-icon">' . $info['icon'] . '</span><span class="nutrition-label">' . $info['label'] . '</span><span class="nutrition-value">' . $value . ' ' . $info['unit'] . '</span></div>';
|
||
}
|
||
}
|
||
if ($hasNutrition) {
|
||
echo '
|
||
<div class="info-card animate-in animate-delay-3">
|
||
<h3 class="section-title">📊 营养成分</h3>
|
||
<div class="nutrition-grid">' . implode('', $nutritionItems) . '</div>
|
||
</div>';
|
||
}
|
||
}
|
||
|
||
// 显示过敏原警告
|
||
if (!empty($allergens) && is_array($allergens) && count($allergens) > 0) {
|
||
echo '
|
||
<div class="info-card warning-card animate-in animate-delay-3">
|
||
<h3 class="section-title">⚠️ 过敏原提示</h3>
|
||
<div class="allergens-list">';
|
||
foreach ($allergens as $allergen) {
|
||
echo '<span class="allergen-item">' . safe_html($allergen) . '</span>';
|
||
}
|
||
echo '
|
||
</div>
|
||
</div>';
|
||
}
|
||
|
||
// 显示步骤内容
|
||
if (!empty($content)) {
|
||
echo '
|
||
<div class="info-card animate-in animate-delay-3">
|
||
<h3 class="section-title">👨🍳 制作步骤</h3>
|
||
<div class="steps-content">' . nl2br(safe_html($content)) . '</div>
|
||
</div>';
|
||
}
|
||
|
||
echo '
|
||
<div class="cta-section animate-in animate-delay-3">
|
||
<a href="' . get_api_base_url() . '/api/api.php?act=detail&id=' . ($recipe['id'] ?? $id) . '&viewnums=true" class="cta-button">
|
||
🍳 在小妈厨房中查看完整菜谱
|
||
</a>
|
||
</div>';
|
||
}
|
||
|
||
echo '
|
||
<div class="footer">
|
||
🍳 小妈厨房 · 妈妈的味道<br>
|
||
<a href="' . get_api_base_url() . '/api/">' . get_api_base_url() . '/api/</a>
|
||
<br><span style="color:var(--text3);font-size:11px;">' . $queryTime . 'ms</span>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>';
|
||
}
|