Files
leonapp/app.php
Leonmmcoset d4dc9e1edc feat: 实现管理员权限系统并本地化sweetalert资源
- 添加管理员权限系统,支持all/say/review三种权限类型
- 为各管理页面添加权限检查逻辑
- 将sweetalert从CDN改为本地资源
- 添加统一的登出确认弹窗和logout.php处理
- 更新config.php中的数据库和SMTP配置
2025-07-15 21:30:51 +08:00

497 lines
25 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
session_start();
require_once 'config.php';
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header('Location: index.php');
exit;
}
$appId = $_GET['id'];
// 获取App信息
$sqlApp = "SELECT apps.*, apps.developer_id, developers.username as developer_name, AVG(reviews.rating) as avg_rating
FROM apps
LEFT JOIN developers ON apps.developer_id = developers.id
LEFT JOIN reviews ON apps.id = reviews.app_id
WHERE apps.id = $appId
GROUP BY apps.id, apps.developer_id, developers.username";
$resultApp = $conn->query($sqlApp);
if (!$resultApp) {
die("<h1>数据库查询错误</h1><p>错误信息: " . htmlspecialchars($conn->error) . "</p><p>SQL语句: " . htmlspecialchars($sqlApp) . "</p>");
}
$app = $resultApp->fetch_assoc();
$developerId = $app['developer_id'] ?? 0;
$developerName = ($developerId == 0) ? '管理员' : ($app['developer_name'] ?? '未知开发者');
if (!$app) {
die("<h1>错误:应用不存在</h1><p>找不到ID为 $appId 的应用。请检查ID是否正确。</p>");
}
// 检查应用审核状态
if ($app['status'] != 'approved') {
echo '<script>
document.addEventListener("DOMContentLoaded", function() {
Swal.fire({
title: "应用审核中",
text: "该应用正在审核中,暂时无法访问。",
icon: "info",
confirmButtonText: "确定"
}).then((result) => {
if (result.isConfirmed) {
window.history.back();
}
});
});
</script>';
}
// 处理评价加载请求
if (isset($_GET['action']) && $_GET['action'] === 'load_reviews') {
header('Content-Type: text/html; charset=UTF-8');
// 获取评论数据
$sqlReviews = "SELECT * FROM reviews WHERE app_id = ? ORDER BY created_at DESC, id DESC LIMIT 10 OFFSET ?";
$stmt = $conn->prepare($sqlReviews);
$offset = isset($_GET['offset']) ? intval($_GET['offset']) : 0;
$stmt->bind_param("ii", $appId, $offset);
$stmt->execute();
$resultReviews = $stmt->get_result();
if (!$resultReviews) {
die("Error fetching reviews: " . htmlspecialchars($conn->error));
}
while ($review = $resultReviews->fetch_assoc()) {
?>
<div class="card mb-3 blur-bg">
<div class="card-body">
<?php
$rating = $review['rating'] !== null ? $review['rating'] : 0;
echo '<p class="card-text">评分: ';
for ($i = 1; $i <= 5; $i++) {
if ($i <= floor($rating)) {
echo '<span class="fas fa-star text-warning"></span>';
} elseif ($i - $rating <= 0.5) {
echo '<span class="fas fa-star-half-alt text-warning"></span>';
} else {
echo '<span class="far fa-star text-warning"></span>';
}
}
echo '</p>';
?>
<p class="card-text"><small class="text-muted">评价时间: <?php echo $review['created_at']; ?></small></p>
</div>
</div>
<?php
}
exit;
}
// 获取App版本信息
$sqlVersions = "SELECT * FROM app_versions WHERE app_id = $appId ORDER BY created_at DESC";
$resultVersions = $conn->query($sqlVersions);
// 获取App预览图片
$sqlImages = "SELECT * FROM app_images WHERE app_id = $appId";
$resultImages = $conn->query($sqlImages);
// 获取评价总数
$sqlReviewCount = "SELECT COUNT(*) as total FROM reviews WHERE app_id = $appId";
$resultReviewCount = $conn->query($sqlReviewCount);
$reviewCount = $resultReviewCount->fetch_assoc()['total'];
// 分页参数
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$hasMore = ($page * $limit) < $reviewCount;
// 获取评价信息
$sqlReviews = "SELECT * FROM reviews WHERE app_id = $appId ORDER BY created_at DESC, id DESC LIMIT 10 OFFSET $offset";
$resultReviews = $conn->query($sqlReviews);
// 获取评分分布
$sqlRatingDistribution = "SELECT rating, COUNT(*) as count FROM reviews WHERE app_id = $appId GROUP BY rating ORDER BY rating DESC";
$resultRatingDistribution = $conn->query($sqlRatingDistribution);
$ratingDistribution = [];
while ($row = $resultRatingDistribution->fetch_assoc()) {
$ratingDistribution[$row['rating']] = $row['count'];
}
// 处理评价提交
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['rating'])) {
$rating = $_POST['rating'];
$ipAddress = $_SERVER['REMOTE_ADDR'];
$insertSql = "INSERT INTO reviews (app_id, rating) VALUES ($appId, $rating)"; if ($conn->query($insertSql) === TRUE) { header("Location: app.php?id=$appId"); exit; }
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $app['name']; ?> - <?php echo APP_STORE_NAME; ?></title>
<!-- Bootstrap CSS -->
<link href="css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="/css/all.min.css">
<!-- SweetAlert2 -->
<link rel="stylesheet" href="/js/sweetalert.js/dist/sweetalert2.min.css">
<script src="/js/sweetalert.js/dist/sweetalert2.all.min.js"></script>
<!-- 本地 Chart.js -->
<script src="js/charts.js"></script>
<!-- 自定义CSS -->
<link rel="stylesheet" href="styles.css">
<!-- Fluent Design 模糊效果 -->
<style>
.blur-bg {
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.5);
}
.page-transition {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
</head>
<body class="page-transition">
<!-- 导航栏 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
document.body.classList.add('page-transition');
});
</script>
<nav class="navbar navbar-expand-lg navbar-light blur-bg">
<div class="container">
<a href="index.php"><img src="/favicon.jpeg" alt="Logo" style="height: 30px; margin-right: 10px; border-radius: var(--border-radius);"></a>
<a class="navbar-brand" href="index.php"><?php echo APP_STORE_NAME; ?></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="index.php">首页</a>
</li>
<?php if (isset($_SESSION['admin'])): ?>
<li class="nav-item">
<a class="nav-link" href="/admin/">管理</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="row">
<div class="col-md-6">
<h1><i class="fas fa-box-open"></i> <?php echo $app['name']; ?></h1>
<p class="lead"><i class="fas fa-info-circle"></i> <?php echo $app['description']; ?></p>
<p>年龄分级:
<?php
$svgHeader = '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><filter id="shadow"><feGaussianBlur in="SourceAlpha" stdDeviation="2"/><feOffset dx="0" dy="2" result="offsetblur"/><feFlood flood-color="rgba(0,0,0,0.2)"/><feComposite in2="offsetblur" operator="in"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>';
$esrbIcons = [
'3+' => "{$svgHeader}<circle cx='20' cy='20' r='18' fill='#34A853' filter='url(#shadow)'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='14' font-weight='bold'>3+</text></svg>",
'7+' => "{$svgHeader}<circle cx='20' cy='20' r='18' fill='#FBBC05' filter='url(#shadow)'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='14' font-weight='bold'>7+</text></svg>",
'12+' => "{$svgHeader}<circle cx='20' cy='20' r='18' fill='#FF8C00' filter='url(#shadow)'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='14' font-weight='bold'>12+</text></svg>",
// '16+' => "{$svgHeader}<circle cx='20' cy='20' r='18' fill='#FBBC05' filter='url(#shadow)'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='14' font-weight='bold'>16+</text></svg>",
'17+' => "{$svgHeader}<circle cx='20' cy='20' r='18' fill='#EA4335' filter='url(#shadow)'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='14' font-weight='bold'>17+</text></svg>"
];
echo isset($esrbIcons[$app['age_rating']]) ? $esrbIcons[$app['age_rating']] : '<i class="fas fa-user-check"></i>';
?> <?php echo $app['age_rating']; ?></p>
<?php if (!empty($app['age_rating_description'])): ?>
<div class="age-rating-description">
<h4>年龄分级说明</h4>
<p><?php echo nl2br(htmlspecialchars($app['age_rating_description'])); ?></p>
</div>
<?php endif; ?>
<p>适用平台: <?php
$platforms = json_decode($app['platforms'], true) ?? [];
$platformIcons = [
'windows' => '<i class="fab fa-windows"></i>',
'windows_win7' => '<i class="fab fa-windows"></i>',
'windows_xp' => '<i class="fab fa-windows"></i>',
'macos' => '<i class="fab fa-apple"></i>',
'linux' => '<i class="fab fa-linux"></i>',
'linux_arch' => '<i class="fab fa-linux"></i>',
'linux_ubuntu' => '<i class="fab fa-linux"></i>',
'android' => '<i class="fab fa-android"></i>',
'ios' => '<i class="fab fa-app-store-ios"></i>'
];
$platformMap = [
'android' => 'Android',
'ios' => 'iOS',
'windows_win7' => 'WindowsWindows 7以上',
'windows_xp' => 'Windows XP',
'macos' => 'MacOS',
'linux_arch' => 'Linux适用于Arch Linux',
'linux_ubuntu' => 'Linux适用于Ubuntu',
'windows' => 'Windows',
'linux' => 'Linux',
];
$platformTexts = [];
foreach ($platforms as $platform) {
$icon = $platformIcons[strtolower($platform)] ?? '';
$readableName = $platformMap[strtolower($platform)] ?? ucfirst($platform);
$platformTexts[] = $icon . ' ' . $readableName;
}
echo implode(', ', $platformTexts);
?></p>
<p>评分: <?php echo round($app['avg_rating'], 1); ?>/5</p>
<p>开发者: <?php if ($developerId == 0 || empty($developerName)): ?>管理员<?php else: ?><a href="developer_apps.php?id=<?php echo $developerId; ?>"><?php echo htmlspecialchars($developerName); ?></a><?php endif; ?></p>
<?php
// 获取应用标签
$sqlTags = "SELECT tags.name FROM app_tags JOIN tags ON app_tags.tag_id = tags.id WHERE app_tags.app_id = ?";
$stmtTags = $conn->prepare($sqlTags);
$stmtTags->bind_param("i", $appId);
$stmtTags->execute();
$resultTags = $stmtTags->get_result();
$tags = [];
while ($tag = $resultTags->fetch_assoc()) {
$tags[] = $tag['name'];
}
if (!empty($tags)): ?>
<p>标签: <?php echo implode(', ', $tags); ?></p>
<?php endif; ?>
</div>
<div class="col-md-6">
<div id="imageCarousel" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-inner">
<?php
$first = true;
while ($image = $resultImages->fetch_assoc()) {
$active = $first ? 'active' : '';
echo '<div class="carousel-item '. $active . '">';
echo '<img src="'. $image['image_path'] . '" class="d-block w-100" alt="App Image">';
echo '</div>';
$first = false;
}
?>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#imageCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#imageCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<h2>版本历史</h2>
<?php while ($version = $resultVersions->fetch_assoc()): ?>
<div class="card mb-3 blur-bg">
<div class="card-body">
<h5 class="card-title">版本 <?php echo $version['version']; ?></h5>
<p class="card-text"><?php echo $version['changelog']; ?></p>
<a href="<?php echo htmlspecialchars($version['file_path']); ?>" class="btn btn-primary btn-lg" download>立即下载</a>
<a href="version_list.php?id=<?php echo $app['id']; ?>" class="btn btn-outline-secondary">查看版本历史</a>
</div>
</div>
<?php endwhile; ?>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<h2>提交评价</h2>
<form method="post" action="">
<div class="mb-3">
<label for="rating" class="form-label">评分</label>
<select class="form-select" id="rating" name="rating" required>
<option value="1">1星</option>
<option value="2">2星</option>
<option value="3">3星</option>
<option value="4">4星</option>
<option value="5">5星</option>
</select>
</div>
<button type="submit" class="btn btn-primary">提交评价</button>
</form>
<h2>评价</h2>
<div id="reviews-container">
<?php while ($review = $resultReviews->fetch_assoc()): ?>
<div class="card mb-3 blur-bg">
<div class="card-body">
<?php
$rating = $review['rating'] !== null ? $review['rating'] : 0;
echo '<p class="card-text">评分: ';
for ($i = 1; $i <= 5; $i++) {
if ($i <= floor($rating)) {
echo '<span class="fas fa-star text-warning"></span>';
} elseif ($i - $rating <= 0.5) {
echo '<span class="fas fa-star-half-alt text-warning"></span>';
} else {
echo '<span class="far fa-star text-warning"></span>';
}
}
echo '</p>';
?>
<p class="card-text"><small class="text-muted">评价时间: <?php echo $review['created_at']; ?></small></p>
</div>
</div>
<?php endwhile; ?>
</div>
<?php if ($hasMore): ?>
<button id="load-more" class="btn btn-secondary" data-page="<?php echo $page + 1; ?>">加载更多</button>
<?php endif; ?>
</div>
<div class="col-md-6">
<h2>评分分布</h2>
<div id="ratingChartSkeleton" class="skeleton-chart"></div>
<canvas id="ratingChart" width="400" height="200"></canvas>
<script>
// 加载更多评价功能
document.addEventListener('DOMContentLoaded', function() {
const loadMoreBtn = document.getElementById('load-more');
if (loadMoreBtn) {
loadMoreBtn.addEventListener('click', function() {
const button = this;
const page = parseInt(button.getAttribute('data-page'));
const offset = (page - 1) * 10;
const appId = <?php echo $appId; ?>;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 加载中...';
button.disabled = true;
fetch(`app.php?id=${appId}&offset=${offset}&action=load_reviews`)
.then(response => response.text())
.then(html => {
if (html.trim() === '') {
button.style.display = 'none';
return;
}
document.getElementById('reviews-container').insertAdjacentHTML('beforeend', html);
button.innerHTML = '加载更多';
button.disabled = false;
button.setAttribute('data-page', parseInt(page) + 1);
})
.catch(error => {
console.error('加载评价失败:', error);
button.innerHTML = '加载更多';
button.disabled = false;
});
});
}
});
// 评分图表
const ctx = document.getElementById('ratingChart').getContext('2d');
new Chart(ctx).Bar({
labels: ['5星', '4星', '3星', '2星', '1星'],
datasets: [
// {
// label: '评分数量',
// fillColor: 'rgba(75, 192, 192, 0.6)',
// strokeColor: 'rgba(75, 192, 192, 1)',
// highlightFill: 'rgba(75, 192, 192, 0.8)',
// highlightStroke: 'rgba(75, 192, 192, 1)',
// data: [
// <?php echo $ratingDistribution[5] ?? 0; ?>,
// <?php echo $ratingDistribution[4] ?? 0; ?>,
// <?php echo $ratingDistribution[3] ?? 0; ?>,
// <?php echo $ratingDistribution[2] ?? 0; ?>,
// <?php echo $ratingDistribution[1] ?? 0; ?>
// ]
// },
// {
// label: '评分数量',
// fillColor: 'rgba(153, 102, 255, 0.6)',
// strokeColor: 'rgba(153, 102, 255, 1)',
// highlightFill: 'rgba(153, 102, 255, 0.8)',
// highlightStroke: 'rgba(153, 102, 255, 1)',
// data: [
// <?php echo $ratingDistribution[5] ?? 0; ?>,
// <?php echo $ratingDistribution[4] ?? 0; ?>,
// <?php echo $ratingDistribution[3] ?? 0; ?>,
// <?php echo $ratingDistribution[2] ?? 0; ?>,
// <?php echo $ratingDistribution[1] ?? 0; ?>
// ]
// },
// {
// label: '评分数量',
// fillColor: 'rgba(255, 206, 86, 0.6)',
// strokeColor: 'rgba(255, 206, 86, 1)',
// highlightFill: 'rgba(255, 206, 86, 0.8)',
// highlightStroke: 'rgba(255, 206, 86, 1)',
// data: [
// <?php echo $ratingDistribution[5] ?? 0; ?>,
// <?php echo $ratingDistribution[4] ?? 0; ?>,
// <?php echo $ratingDistribution[3] ?? 0; ?>,
// <?php echo $ratingDistribution[2] ?? 0; ?>,
// <?php echo $ratingDistribution[1] ?? 0; ?>
// ]
// },
// {
// label: '评分数量',
// fillColor: 'rgba(255, 99, 132, 0.6)',
// strokeColor: 'rgba(255, 99, 132, 1)',
// highlightFill: 'rgba(255, 99, 132, 0.8)',
// highlightStroke: 'rgba(255, 99, 132, 1)',
// data: [
// <?php echo $ratingDistribution[5] ?? 0; ?>,
// <?php echo $ratingDistribution[4] ?? 0; ?>,
// <?php echo $ratingDistribution[3] ?? 0; ?>,
// <?php echo $ratingDistribution[2] ?? 0; ?>,
// <?php echo $ratingDistribution[1] ?? 0; ?>
// ]
// },
{
label: '评分数量',
fillColor: 'rgba(54, 162, 235, 0.6)',
strokeColor: 'rgba(54, 162, 235, 1)',
highlightFill: 'rgba(54, 162, 235, 0.8)',
highlightStroke: 'rgba(54, 162, 235, 1)',
data: [
<?php echo $ratingDistribution[5] ?? 0; ?>,
<?php echo $ratingDistribution[4] ?? 0; ?>,
<?php echo $ratingDistribution[3] ?? 0; ?>,
<?php echo $ratingDistribution[2] ?? 0; ?>,
<?php echo $ratingDistribution[1] ?? 0; ?>
]
}
]
}, {
scaleBeginAtZero: true,
scales: {
y: {
beginAtZero: true
}
}
});
document.getElementById('ratingChartSkeleton').style.display = 'none';
</script>
</div>
</div>
</div>
<!-- Bootstrap JS Bundle with Popper -->
<script src="/js/bootstrap.bundle.js"></script>
<script>
// 导航栏滚动效果
window.addEventListener('scroll', function() {
const navbar = document.querySelector('.navbar');
if (window.scrollY > 10) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
});
</script>
</body>
</html>