Files
leonapp/developer/upload_app.php

678 lines
32 KiB
PHP
Raw Normal View History

2025-09-20 22:20:08 +08:00
<?php
require_once('../includes/logger.php');
set_time_limit(0); // 添加此行取消脚本超时限制
// 引入配置文件
require_once '../config.php';
session_start();
// 检查开发者是否已登录
if (!isset($_SESSION['developer_id']) || !is_numeric($_SESSION['developer_id'])) {
log_error('开发者会话ID不存在或无效', __FILE__, __LINE__);
header('Location: login.php');
exit;
}
$developerId = (int)$_SESSION['developer_id'];
log_info("上传应用的开发者ID: $developerId", __FILE__, __LINE__);
log_info("上传应用的开发者ID: $developerId", __FILE__, __LINE__);
$error = '';
$success = '';
// 检查开发者邮箱是否已验证
$stmt = $conn->prepare('SELECT is_verified FROM developers WHERE id = ?');
if (!$stmt) {
log_error('准备验证状态查询失败: ' . $conn->error, __FILE__, __LINE__);
$error = '系统错误,请稍后再试';
} else {
$stmt->bind_param('i', $developerId);
$stmt->execute();
$result = $stmt->get_result();
$developer = $result->fetch_assoc();
$stmt->close();
log_info("开发者验证状态: " . ($developer ? ($developer['is_verified'] ? "已验证" : "未验证") : "开发者不存在"), __FILE__, __LINE__);
if (!$developer) {
$error = '开发者账号不存在,请重新登录。';
} elseif (!$developer['is_verified']) {
$error = '您的邮箱尚未验证,请先验证邮箱后再上传应用。';
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 创建上传目录(如果不存在)
$uploadDirs = ['../uploads/apps', '../uploads/images'];
foreach ($uploadDirs as $dir) {
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
// 获取表单数据
$appName = trim($_POST['name']);
$appDescription = trim($_POST['description']);
$tags = $_POST['tags'] ?? [];
$ageRating = $_POST['age_rating'] ?? '';
$ageRatingDescription = $_POST['age_rating_description'] ?? '';
$platforms = isset($_POST['platforms']) ? $_POST['platforms'] : [];
$version = trim($_POST['version']);
$changelog = trim($_POST['changelog']);
// 验证表单数据
if (empty($appName) || empty($appDescription)) {
$error = '应用名称和描述不能为空';
} elseif (empty($changelog)) {
$error = '更新日志不能为空';
} elseif (empty($platforms)) {
$error = '请至少选择一个适用平台';
} elseif (in_array($ageRating, ['12+', '17+']) && empty($ageRatingDescription)) {
$error = '年龄分级为12+或以上时,必须提供年龄分级说明';
} else {
// 检查数据库连接是否为 MySQLi 对象
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
} else {
// 处理应用文件上传
// 获取选中的平台
$selectedPlatforms = $_POST['platforms'] ?? [];
// 处理应用文件上传
$appFile = $_FILES['app_file'] ?? null;
$appFilePath = '';
if ($appFile && $appFile['error'] === UPLOAD_ERR_OK) {
// 验证文件大小 (100MB)
if ($appFile['size'] > 500 * 1024 * 1024) {
log_error('应用文件过大: ' . number_format($appFile['size'] / 1024 / 1024, 2) . 'MB', __FILE__, __LINE__);
$error = '应用文件大小不能超过500MB';
}
$appExtension = pathinfo($appFile['name'], PATHINFO_EXTENSION);
// 生成6位随机数
$randomNumber = str_pad(mt_rand(0, 999999), 6, '0', STR_PAD_LEFT);
// 清理应用名称中的特殊字符
$cleanAppName = preg_replace('/[^a-zA-Z0-9_]/', '_', $appName);
$appFileName = $cleanAppName . '_' . $randomNumber . '.' . $appExtension;
2025-09-20 22:20:08 +08:00
$appRelativePath = 'uploads/apps/' . $appFileName;
$appFilePath = __DIR__ . '/../' . $appRelativePath;
if (!move_uploaded_file($appFile['tmp_name'], $appFilePath)) {
log_error('应用文件移动失败', __FILE__, __LINE__);
$error = '应用文件上传失败';
}
} else {
// 验证标签ID是否存在
if (!empty($tags)) {
$tagIds = implode(',', array_map('intval', $tags));
$tagCheckStmt = $conn->prepare("SELECT id FROM tags WHERE id IN ($tagIds)");
if (!$tagCheckStmt) {
log_error('标签验证查询准备失败: ' . $conn->error, __FILE__, __LINE__);
$error = '系统错误,请稍后再试';
} else {
$tagCheckStmt->execute();
$tagResult = $tagCheckStmt->get_result();
$validTagIds = [];
while ($tag = $tagResult->fetch_assoc()) {
$validTagIds[] = $tag['id'];
}
$tagCheckStmt->close();
$invalidTags = array_diff($tags, $validTagIds);
if (!empty($invalidTags)) {
log_error('无效的标签ID: ' . implode(',', $invalidTags), __FILE__, __LINE__);
$error = '选择了无效的标签,请刷新页面重试';
}
}
}
$error = '应用文件上传错误: ' . ($appFile ? $appFile['error'] : '未找到文件');
}
// 处理图片上传
$imagePaths = [];
$images = $_FILES['images'] ?? null;
if ($images && is_array($images['tmp_name'])) {
foreach ($images['tmp_name'] as $key => $tmpName) {
if ($images['error'][$key] === UPLOAD_ERR_OK) {
// 验证图片大小 (10MB)
if ($images['size'][$key] > 10 * 1024 * 1024) {
log_error('图片过大: ' . $images['name'][$key] . ' (' . number_format($images['size'][$key] / 1024 / 1024, 2) . 'MB)', __FILE__, __LINE__);
$error = '图片 ' . $images['name'][$key] . ' 大小不能超过10MB';
}
$imageRelativePath = 'uploads/images/' . uniqid() . '.' . pathinfo($images['name'][$key], PATHINFO_EXTENSION);
$imagePath = __DIR__ . '/../' . $imageRelativePath;
$target_dir = dirname($imagePath);
if (!is_dir($target_dir)) {
mkdir($target_dir, 0755, true);
}
if (move_uploaded_file($tmpName, $imagePath)) {
$imagePaths[] = $imageRelativePath;
} else {
log_error('图片文件移动失败: ' . $images['name'][$key], __FILE__, __LINE__);
}
}
}
}
if (empty($error)) {
// 开始数据库事务
$conn->begin_transaction();
try {
// 确保必要变量存在,防止空值导致 SQL 错误
if (!isset($appName) || !isset($appDescription) || !isset($developerId) || !isset($version) || !isset($changelog) || !isset($ageRating) || !isset($ageRatingDescription)) {
throw new Exception('缺少必要的上传参数');
}
// 获取开发者邮箱
$userStmt = $conn->prepare('SELECT email FROM developers WHERE id = ?');
$userStmt->bind_param('i', $developerId);
$userStmt->execute();
$userResult = $userStmt->get_result();
$user = $userResult->fetch_assoc();
$developerEmail = $user['email'] ?? '';
$userStmt->close();
if (empty($developerEmail)) {
throw new Exception('无法获取开发者邮箱信息');
}
// 插入应用基本信息
$stmt = $conn->prepare('INSERT INTO apps (name, description, platforms, status, age_rating, age_rating_description, version, changelog, file_path, developer_id, developer_email, created_at) VALUES (?, ?, ?, \'pending\', ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)');
if (!$stmt) {
throw new Exception('应用基本信息查询准备失败: ' . $conn->error);
}
// 确保平台数据正确编码
$platforms = $_POST['platforms'] ?? [];
$platforms_json = json_encode($platforms);
// 此处需确认预处理语句占位符数量,确保与 bind_param 参数数量一致,示例仅示意,实际需根据表结构调整
// 修正参数绑定添加file_path参数以匹配SQL占位符数量
// 修正参数类型字符串长度确保与10个参数匹配
// 修正类型字符串长度10个参数对应10个类型字符
// 最终修正10个参数对应10个类型字符
// 根据参数实际类型修正类型字符串整数用i字符串用s
// 移除多余的$status参数匹配SQL中9个占位符
// 修复变量名错误:使用已验证的$appFilePath替换未定义的$file_path
$stmt->bind_param('ssssssssis', $appName, $appDescription, $platforms_json, $ageRating, $ageRatingDescription, $version, $changelog, $appRelativePath, $developerId, $developerEmail);
if (!$stmt->execute()) {
throw new Exception('应用基本信息查询执行失败: ' . $stmt->error);
}
$appId = $stmt->insert_id;
log_info("应用已插入数据库: ID=$appId, 状态=pending", __FILE__, __LINE__);
$stmt->close();
// 发送审核邮件给管理员
require_once '../vendor/autoload.php';
$subject = '新应用待审核';
$body = "开发者提交了新应用应用ID: $appId名称: $appName请及时审核。";
$mail = new PHPMailer\PHPMailer\PHPMailer();
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USERNAME;
$mail->Password = SMTP_PASSWORD;
$mail->SMTPSecure = SMTP_ENCRYPTION;
$mail->Port = SMTP_PORT;
$mail->setFrom(SMTP_FROM_EMAIL, SMTP_FROM_NAME);
$mail->addAddress(ADMIN_EMAIL);
$mail->isHTML(false);
$mail->CharSet = 'utf-8';
$mail->Subject = $subject;
$mail->Body = $body;
if(!$mail->send()) {
log_error('发送审核邮件失败: ' . $mail->ErrorInfo, __FILE__, __LINE__);
} else {
log_info('已成功发送审核邮件给管理员', __FILE__, __LINE__);
}
log_info("开始处理应用关联数据: ID=$appId", __FILE__, __LINE__);
// 插入应用标签关联
foreach ($tags as $tagId) {
$tagStmt = $conn->prepare('INSERT INTO app_tags (app_id, tag_id) VALUES (?, ?)');
if (!$tagStmt) {
throw new Exception('标签关联查询准备失败: ' . $conn->error);
}
$tagStmt->bind_param('ii', $appId, $tagId);
if (!$tagStmt->execute()) {
throw new Exception('标签关联查询执行失败: ' . $tagStmt->error);
}
$tagStmt->close();
}
// 插入应用图片
foreach ($imagePaths as $imageRelativePath) {
$imageStmt = $conn->prepare('INSERT INTO app_images (app_id, image_path) VALUES (?, ?)');
if (!$imageStmt) {
throw new Exception('图片关联查询准备失败: ' . $conn->error);
}
$imageStmt->bind_param('is', $appId, $imageRelativePath);
if (!$imageStmt->execute()) {
throw new Exception('图片关联查询执行失败: ' . $imageStmt->error);
}
$imageStmt->close();
}
// 插入应用版本信息
$versionStmt = $conn->prepare('INSERT INTO app_versions (app_id, version, changelog, file_path, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)');
if (!$versionStmt) {
throw new Exception('版本信息查询准备失败: ' . $conn->error);
}
$versionStmt->bind_param('isss', $appId, $version, $changelog, $appRelativePath);
if (!$versionStmt->execute()) {
throw new Exception('版本信息查询执行失败: ' . $versionStmt->error);
}
$versionStmt->close();
log_info("所有关联数据处理完成,准备提交事务: ID=$appId", __FILE__, __LINE__);
// 提交事务
$conn->commit();
log_info("应用上传成功: ID=$appId, 状态=pending", __FILE__, __LINE__);
$success = '应用上传成功,请等待管理员审核';
} catch (Exception $e) {
// 回滚事务
$conn->rollback();
log_error('应用上传事务失败(ID=$appId): ' . $e->getMessage(), __FILE__, __LINE__);
$error = '上传应用时发生错误,请稍后再试';
}
}
}
}
}
?>
<!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_STORE_NAME; ?></title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/css/bootstrap.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="/css/all.min.css">
<!-- 自定义CSS -->
<link rel="stylesheet" href="../styles.css">
<script src="/js/sweetalert.js"></script>
<!-- Markdown解析库 -->
<script src="/js/marked.js"></script>
<!-- Fluent Design 模糊效果和Markdown预览样式 -->
2025-09-20 22:20:08 +08:00
<style>
.blur-bg {
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.5);
}
.form-group {
margin-bottom: 1rem;
}
.btn-primary {
background-color: #007BFF;
border-color: #007BFF;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.back-link {
text-align: center;
margin-top: 1rem;
}
#ageRatingDescriptionGroup {
display: none;
}
/* Markdown预览样式 */
.markdown-content {
line-height: 1.6;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 1rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
.markdown-content p {
margin-bottom: 1rem;
}
.markdown-content ul,
.markdown-content ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-content pre {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
margin-bottom: 1rem;
}
.markdown-content code {
background-color: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Courier New', Courier, monospace;
}
.markdown-content blockquote {
border-left: 4px solid #007BFF;
padding-left: 1rem;
margin-left: 0;
margin-bottom: 1rem;
color: #6c757d;
}
.markdown-content img {
max-width: 100%;
height: auto;
margin-bottom: 1rem;
}
.markdown-content a {
color: #007BFF;
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
2025-09-20 22:20:08 +08:00
</style>
</head>
<!-- 导航栏 -->
<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="dashboard.php">应用仪表盘</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="upload_app.php">上传应用</a>
</li>
<li class="nav-item">
<a class="nav-link" href="logout.php">退出登录</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<script>
// 年龄分级说明显示控制
document.addEventListener('DOMContentLoaded', function() {
const ageRating = document.getElementById('age_rating');
const ageDescGroup = document.getElementById('ageRatingDescriptionGroup');
const ageDescInput = document.getElementById('age_rating_description');
function toggleAgeDescription() {
if (['12+', '17+'].includes(ageRating.value)) {
ageDescGroup.style.display = 'block';
ageDescInput.required = true;
} else {
ageDescGroup.style.display = 'none';
ageDescInput.required = false;
}
}
ageRating.addEventListener('change', toggleAgeDescription);
toggleAgeDescription(); // 初始状态检查
// 文件类型验证
const appFileInput = document.getElementById('app_file');
const imageInput = document.getElementById('images');
const allowedAppTypes = { 'android': ['apk'], 'ios': ['ipa'] };
const allowedImageTypes = ['jpg', 'jpeg', 'png', 'gif'];
appFileInput.addEventListener('change', function(e) {
if (this.files.length > 0) {
const file = this.files[0];
const ext = file.name.split('.').pop().toLowerCase();
if (file.size > 500 * 1024 * 1024) { // 100MB限制
Swal.fire({
title: '提示',
text: '文件大小不能超过500MB',
icon: 'warning',
confirmButtonText: '确定'
});
this.value = '';
}
}
});
imageInput.addEventListener('change', function(e) {
if (this.files.length > 0) {
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
if (file.size > 10 * 1024 * 1024) { // 10MB限制
Swal.fire({
title: '提示',
text: `图片 ${file.name} 大小不能超过10MB`,
icon: 'warning',
confirmButtonText: '确定'
});
this.value = '';
return;
}
}
}
});
// 平台子选项显示控制
document.getElementById('windows').addEventListener('change', function() {
const suboptions = document.getElementById('windows_suboptions');
suboptions.style.display = this.checked ? 'block' : 'none';
if (!this.checked) {
document.querySelectorAll('input[name="windows_version"]').forEach(radio => radio.checked = false);
}
});
document.getElementById('linux').addEventListener('change', function() {
const suboptions = document.getElementById('linux_suboptions');
suboptions.style.display = this.checked ? 'block' : 'none';
if (!this.checked) {
document.querySelectorAll('input[name="linux_distribution"]').forEach(radio => radio.checked = false);
}
});
// Markdown预览功能
const descriptionTextarea = document.getElementById('description');
const markdownPreview = document.getElementById('markdown-preview');
const previewContent = document.getElementById('preview-content');
const togglePreviewBtn = document.getElementById('toggle-preview');
const closePreviewBtn = document.getElementById('close-preview');
function updatePreview() {
const markdownText = descriptionTextarea.value;
if (markdownText.trim() === '') {
previewContent.innerHTML = '<div class="text-muted">请输入Markdown内容以查看预览</div>';
} else {
// 使用marked.js解析Markdown
try {
const html = marked.parse(markdownText);
previewContent.innerHTML = html;
} catch (error) {
previewContent.innerHTML = '<div class="text-danger">Markdown解析错误: ' + error.message + '</div>';
}
}
}
togglePreviewBtn.addEventListener('click', function() {
markdownPreview.style.display = 'block';
updatePreview();
});
closePreviewBtn.addEventListener('click', function() {
markdownPreview.style.display = 'none';
});
descriptionTextarea.addEventListener('input', function() {
if (markdownPreview.style.display === 'block') {
updatePreview();
}
});
2025-09-20 22:20:08 +08:00
});
</script>
</head>
<body>
<div class="container mt-5 mb-5 col-md-8 col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">上传应用</h2>
</div>
<div class="card-body p-4">
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo $error; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php echo $success; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data">
<div class="alert alert-warning mb-3">
<strong>警告:</strong>如果该应用非您本人开发,请务必添加"转载"标签。
</div>
<div class="form-group mb-3">
<label for="name" class="form-label">应用名称</label>
<input type="text" id="name" name="name" class="form-control" required>
</div>
<div class="form-group mb-3">
<label for="tags" class="form-label">标签</label>
<select id="tags" name="tags[]" multiple class="form-select" size="3">
<?php
$tagResult = $conn->query("SELECT id, name FROM tags");
while ($tag = $tagResult->fetch_assoc()):
?>
<option value="<?php echo $tag['id']; ?>"><?php echo htmlspecialchars($tag['name']); ?></option>
<?php endwhile; ?>
</select>
<small>按住Ctrl键可选择多个标签</small>
</div>
<div class="form-group mb-3">
<label for="description" class="form-label">应用描述支持Markdown</label>
<div class="mb-2">
<button type="button" id="toggle-preview" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-eye"></i> 预览
</button>
</div>
2025-09-20 22:20:08 +08:00
<textarea id="description" name="description" rows="5" class="form-control" required></textarea>
<div id="markdown-preview" class="mt-3 p-3 border rounded bg-light" style="display: none;">
<div class="preview-header d-flex justify-content-between items-center mb-2">
<h5 class="mb-0">Markdown预览</h5>
<button type="button" id="close-preview" class="btn-close"></button>
</div>
<div id="preview-content" class="markdown-content"></div>
</div>
2025-09-20 22:20:08 +08:00
</div>
<div class="form-group mb-3">
<label for="age_rating" class="form-label">年龄分级</label>
<select class="form-select" id="age_rating" name="age_rating" required>
<option value="3+">3+</option>
<option value="7+">7+</option>
<option value="12+">12+</option>
<option value="17+">17+</option>
</select>
</div>
<div class="form-group mb-3" id="ageRatingDescriptionGroup">
<label for="age_rating_description" class="form-label">年龄分级说明</label>
<textarea class="form-control" id="age_rating_description" name="age_rating_description" rows="3" placeholder="请说明为何需要此年龄分级"></textarea>
<small>当年龄分级为12+或以上时,此项为必填</small>
</div>
<div class="alert alert-info mb-3">
<strong>信息:</strong>我们尝试了开发多选子平台的功能但是就会导致以前的app会全部显示“未知平台”如果你是Windows的话选择Win7以上选项如果你是Linux的话选择APP支持平台中的任意一个即可。
</div>
<div class="form-group mb-3">
<label class="form-label">适用平台</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="android" id="android" name="platforms[]">
<label class="form-check-label" for="android">Android</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="ios" id="ios" name="platforms[]">
<label class="form-check-label" for="ios">iOS</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="windows" id="windows" name="platforms[]">
<label class="form-check-label" for="windows">Windows</label>
</div>
<div id="windows_suboptions" class="ms-4 mt-2" style="display: none;">
<div class="form-check">
<input class="form-check-input" type="radio" name="windows_version" id="windows_xp" value="windows_xp">
<label class="form-check-label" for="windows_xp">XP以前</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="windows_version" id="windows_win7" value="windows_win7">
<label class="form-check-label" for="windows_win7">Win7以后</label>
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="macos" id="macos" name="platforms[]">
<label class="form-check-label" for="macos">macOS</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="linux" id="linux" name="platforms[]">
<label class="form-check-label" for="linux">Linux</label>
</div>
<div id="linux_suboptions" class="ms-4 mt-2" style="display: none;">
<div class="form-check">
<input class="form-check-input" type="radio" name="linux_distribution" id="linux_ubuntu" value="linux_ubuntu">
<label class="form-check-label" for="linux_ubuntu">Ubuntu</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="linux_distribution" id="linux_arch" value="linux_arch">
<label class="form-check-label" for="linux_arch">Arch Linux</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="linux_distribution" id="linux_centos" value="linux_centos">
<label class="form-check-label" for="linux_centos">CentOS</label>
</div>
</div>
</div>
<div class="form-group mb-3">
<label for="app_file" class="form-label">应用文件</label>
<input type="file" id="app_file" name="app_file" class="form-control" required>
</div>
<div class="form-group mb-3">
<label for="version" class="form-label">版本号</label>
<input type="text" id="version" name="version" class="form-control" required placeholder="例如: 1.0.0">
</div>
<div class="form-group mb-3">
<label for="changelog" class="form-label">更新日志</label>
<textarea id="changelog" name="changelog" rows="3" class="form-control" required></textarea>
</div>
<div class="form-group mb-4">
<label for="images" class="form-label">预览图片</label>
<input type="file" id="images" name="images[]" multiple accept="image/*" class="form-control">
<small>可选择多张图片</small>
</div>
<div class="form-group mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="age_doc_confirm" name="age_doc_confirm" required>
<label class="form-check-label" for="age_doc_confirm">
我已阅读并理解<a href="../docs/age_rating.php" target="_blank">年龄说明文档</a>
</label>
</div>
</div>
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-upload me-1"></i>上传应用</button>
</form>
<div class="back-link mt-4">
<a href="dashboard.php" class="btn btn-outline-secondary w-100">返回仪表盘</a>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS with Popper -->
<script src="/js/bootstrap.bundle.js"></script>
2025-09-20 22:20:08 +08:00
</body>
</html>