feat(版本管理): 添加Markdown预览功能并改进版本删除逻辑
- 在版本上传和编辑表单中添加Markdown实时预览功能 - 使用marked.js库实现Markdown解析 - 改进版本删除逻辑,使用事务处理确保数据一致性 - 为历史版本日志添加Markdown渲染切换功能
This commit is contained in:
BIN
APP Store.zip
BIN
APP Store.zip
Binary file not shown.
@@ -81,39 +81,80 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_version'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理删除版本
|
// 处理版本删除请求
|
||||||
if (isset($_GET['delete_id']) && is_numeric($_GET['delete_id'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_version'])) {
|
||||||
$versionId = $_GET['delete_id'];
|
// 设置内容类型为纯文本
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
|
||||||
// 获取版本信息
|
$versionId = $_POST['version_id'];
|
||||||
$getVersionSql = "SELECT file_path FROM app_versions WHERE id = ? AND app_id = ?";
|
$appId = $_POST['app_id'];
|
||||||
$stmt = $conn->prepare($getVersionSql);
|
|
||||||
$stmt->bind_param("ii", $versionId, $appId);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
if ($result->num_rows === 1) {
|
// 初始化消息变量
|
||||||
$version = $result->fetch_assoc();
|
$message = '';
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
$conn->begin_transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取文件路径
|
||||||
|
$filePath = '';
|
||||||
|
$getFilePathSql = "SELECT file_path FROM app_versions WHERE id = ? AND app_id = ?";
|
||||||
|
$getFileStmt = $conn->prepare($getFilePathSql);
|
||||||
|
|
||||||
// 删除文件
|
if (!$getFileStmt) {
|
||||||
if (file_exists($version['file_path'])) {
|
throw new Exception("获取文件路径查询准备失败: " . $conn->error);
|
||||||
unlink($version['file_path']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除数据库记录
|
$getFileStmt->bind_param("ii", $versionId, $appId);
|
||||||
|
$getFileStmt->execute();
|
||||||
|
$fileResult = $getFileStmt->get_result();
|
||||||
|
|
||||||
|
if ($fileResult->num_rows > 0) {
|
||||||
|
$fileRow = $fileResult->fetch_assoc();
|
||||||
|
$filePath = $fileRow['file_path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从数据库删除版本记录
|
||||||
$deleteVersionSql = "DELETE FROM app_versions WHERE id = ? AND app_id = ?";
|
$deleteVersionSql = "DELETE FROM app_versions WHERE id = ? AND app_id = ?";
|
||||||
$stmt = $conn->prepare($deleteVersionSql);
|
$deleteVersionStmt = $conn->prepare($deleteVersionSql);
|
||||||
$stmt->bind_param("ii", $versionId, $appId);
|
|
||||||
|
|
||||||
if ($stmt->execute() === TRUE) {
|
if (!$deleteVersionStmt) {
|
||||||
header('Location: manage_versions.php?app_id=' . $appId . '&success=版本删除成功');
|
throw new Exception("版本删除查询准备失败: " . $conn->error);
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
$error = '版本删除失败: ' . $conn->error;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$error = '版本不存在';
|
$deleteVersionStmt->bind_param("ii", $versionId, $appId);
|
||||||
|
|
||||||
|
if (!$deleteVersionStmt->execute()) {
|
||||||
|
throw new Exception("版本删除执行失败: " . $conn->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有记录被删除
|
||||||
|
if ($deleteVersionStmt->affected_rows === 0) {
|
||||||
|
throw new Exception("未找到要删除的版本记录");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 如果数据库删除成功,尝试删除文件(即使文件删除失败也不回滚数据库操作)
|
||||||
|
if (!empty($filePath) && file_exists($filePath)) {
|
||||||
|
if (!unlink($filePath)) {
|
||||||
|
// 文件删除失败不影响数据库操作,继续处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
$conn->commit();
|
||||||
|
$message = '版本删除成功';
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// 回滚事务
|
||||||
|
$conn->rollback();
|
||||||
|
$message = '版本删除失败: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只输出消息,不包含任何HTML
|
||||||
|
echo $message;
|
||||||
|
|
||||||
|
// 确保脚本终止,不执行后续代码
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理编辑版本
|
// 处理编辑版本
|
||||||
|
|||||||
@@ -292,6 +292,8 @@ if (!$verStmt) {
|
|||||||
<link rel="stylesheet" href="/css/all.min.css">
|
<link rel="stylesheet" href="/css/all.min.css">
|
||||||
<link rel="stylesheet" href="../styles.css">
|
<link rel="stylesheet" href="../styles.css">
|
||||||
<script src="/js/sweetalert.js"></script>
|
<script src="/js/sweetalert.js"></script>
|
||||||
|
<!-- Marked.js库用于Markdown解析 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
.blur-bg {
|
.blur-bg {
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
@@ -358,10 +360,19 @@ if (!$verStmt) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating mb-3">
|
<div class="mb-3">
|
||||||
<textarea class="form-control" id="changelog" name="changelog" rows="3" placeholder="更新日志"></textarea>
|
<div class="mb-1">
|
||||||
<label for="changelog">更新日志(支持Markdown)</label>
|
<label for="changelog" class="form-label">更新日志(支持Markdown)</label>
|
||||||
</div>
|
<div class="btn-group float-end">
|
||||||
|
<button type="button" id="writeModeBtn" class="btn btn-sm btn-primary active">编辑</button>
|
||||||
|
<button type="button" id="previewModeBtn" class="btn btn-sm btn-secondary">预览</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="writeModeDiv">
|
||||||
|
<textarea class="form-control" id="changelog" name="changelog" rows="6" placeholder="更新日志(支持Markdown语法)"></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="previewModeDiv" class="p-3 border rounded bg-light" style="min-height: 200px; display: none;"></div>
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn btn-primary" name="upload_version"><i class="fas fa-cloud-upload-alt me-1"></i>上传新版本</button>
|
<button type="submit" class="btn btn-primary" name="upload_version"><i class="fas fa-cloud-upload-alt me-1"></i>上传新版本</button>
|
||||||
<a href="dashboard.php" class="btn btn-secondary ms-2"><i class="fas fa-arrow-left me-1"></i>返回</a>
|
<a href="dashboard.php" class="btn btn-secondary ms-2"><i class="fas fa-arrow-left me-1"></i>返回</a>
|
||||||
</form>
|
</form>
|
||||||
@@ -387,8 +398,11 @@ if (!$verStmt) {
|
|||||||
<tr>
|
<tr>
|
||||||
<td><?php echo htmlspecialchars($ver['version']); ?></td>
|
<td><?php echo htmlspecialchars($ver['version']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($ver['upload_time']); ?></td>
|
<td><?php echo htmlspecialchars($ver['upload_time']); ?></td>
|
||||||
<td><?php echo nl2br(htmlspecialchars($ver['changelog'] ?: '无')); ?></td>
|
|
||||||
<td>
|
<td>
|
||||||
|
<div class="changelog-content" data-markdown="<?php echo htmlspecialchars($ver['changelog'] ?: '无'); ?>">
|
||||||
|
<?php echo nl2br(htmlspecialchars($ver['changelog'] ?: '无')); ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<a href="../download.php?id=<?php echo $ver['id']; ?>&type=version" class="btn btn-sm btn-outline-primary"><i class="fas fa-download me-1"></i>下载</a>
|
<a href="../download.php?id=<?php echo $ver['id']; ?>&type=version" class="btn btn-sm btn-outline-primary"><i class="fas fa-download me-1"></i>下载</a>
|
||||||
<a href="#" class="btn btn-sm btn-outline-warning ms-2" onclick="openEditModal(<?php echo $ver['id']; ?>, '<?php echo htmlspecialchars($ver['version']); ?>', '<?php echo htmlspecialchars($ver['changelog']); ?>')"><i class="fas fa-edit me-1"></i>修改</a>
|
<a href="#" class="btn btn-sm btn-outline-warning ms-2" onclick="openEditModal(<?php echo $ver['id']; ?>, '<?php echo htmlspecialchars($ver['version']); ?>', '<?php echo htmlspecialchars($ver['changelog']); ?>')"><i class="fas fa-edit me-1"></i>修改</a>
|
||||||
<a href="#" class="btn btn-sm btn-outline-danger ms-2" onclick="confirmDelete(<?php echo $ver['id']; ?>)"><i class="fas fa-trash-alt me-1"></i>删除</a>
|
<a href="#" class="btn btn-sm btn-outline-danger ms-2" onclick="confirmDelete(<?php echo $ver['id']; ?>)"><i class="fas fa-trash-alt me-1"></i>删除</a>
|
||||||
@@ -408,6 +422,50 @@ if (!$verStmt) {
|
|||||||
|
|
||||||
<script src="../js/bootstrap.bundle.js"></script>
|
<script src="../js/bootstrap.bundle.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Markdown预览功能实现
|
||||||
|
function initMarkdownPreview() {
|
||||||
|
// 上传表单的Markdown预览
|
||||||
|
const writeModeBtn = document.getElementById('writeModeBtn');
|
||||||
|
const previewModeBtn = document.getElementById('previewModeBtn');
|
||||||
|
const writeModeDiv = document.getElementById('writeModeDiv');
|
||||||
|
const previewModeDiv = document.getElementById('previewModeDiv');
|
||||||
|
const changelogTextarea = document.getElementById('changelog');
|
||||||
|
|
||||||
|
writeModeBtn.addEventListener('click', function() {
|
||||||
|
writeModeBtn.classList.add('active');
|
||||||
|
writeModeBtn.classList.remove('btn-secondary');
|
||||||
|
writeModeBtn.classList.add('btn-primary');
|
||||||
|
previewModeBtn.classList.remove('active');
|
||||||
|
previewModeBtn.classList.remove('btn-primary');
|
||||||
|
previewModeBtn.classList.add('btn-secondary');
|
||||||
|
writeModeDiv.style.display = 'block';
|
||||||
|
previewModeDiv.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
previewModeBtn.addEventListener('click', function() {
|
||||||
|
previewModeBtn.classList.add('active');
|
||||||
|
previewModeBtn.classList.remove('btn-secondary');
|
||||||
|
previewModeBtn.classList.add('btn-primary');
|
||||||
|
writeModeBtn.classList.remove('active');
|
||||||
|
writeModeBtn.classList.remove('btn-primary');
|
||||||
|
writeModeBtn.classList.add('btn-secondary');
|
||||||
|
writeModeDiv.style.display = 'none';
|
||||||
|
previewModeDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// 解析Markdown并显示预览
|
||||||
|
if (changelogTextarea && marked) {
|
||||||
|
previewModeDiv.innerHTML = marked.parse(changelogTextarea.value || '无更新日志');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 实时更新预览
|
||||||
|
changelogTextarea.addEventListener('input', function() {
|
||||||
|
if (previewModeDiv.style.display !== 'none' && marked) {
|
||||||
|
previewModeDiv.innerHTML = marked.parse(changelogTextarea.value || '无更新日志');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function openEditModal(versionId, version, changelog) {
|
function openEditModal(versionId, version, changelog) {
|
||||||
const modal = `
|
const modal = `
|
||||||
<div class="modal fade" id="editVersionModal" tabindex="-1" aria-labelledby="editVersionModalLabel" aria-hidden="true">
|
<div class="modal fade" id="editVersionModal" tabindex="-1" aria-labelledby="editVersionModalLabel" aria-hidden="true">
|
||||||
@@ -424,9 +482,18 @@ if (!$verStmt) {
|
|||||||
<input type="text" class="form-control" id="editVersion" name="version" value="${version}" required>
|
<input type="text" class="form-control" id="editVersion" name="version" value="${version}" required>
|
||||||
<label for="editVersion">版本号</label>
|
<label for="editVersion">版本号</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating mb-3">
|
<div class="mb-3">
|
||||||
<textarea class="form-control" id="editChangelog" name="changelog" rows="3">${changelog}</textarea>
|
<div class="mb-1">
|
||||||
<label for="editChangelog">更新日志</label>
|
<label for="editChangelog" class="form-label">更新日志</label>
|
||||||
|
<div class="btn-group float-end">
|
||||||
|
<button type="button" id="editWriteModeBtn" class="btn btn-sm btn-primary active">编辑</button>
|
||||||
|
<button type="button" id="editPreviewModeBtn" class="btn btn-sm btn-secondary">预览</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="editWriteModeDiv">
|
||||||
|
<textarea class="form-control" id="editChangelog" name="changelog" rows="6">${changelog}</textarea>
|
||||||
|
</div>
|
||||||
|
<div id="editPreviewModeDiv" class="p-3 border rounded bg-light" style="min-height: 200px; display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="new_app_file" class="form-label">更新App文件 (可选)</label>
|
<label for="new_app_file" class="form-label">更新App文件 (可选)</label>
|
||||||
@@ -464,6 +531,52 @@ if (!$verStmt) {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 编辑模态框的Markdown预览功能
|
||||||
|
const editWriteModeBtn = document.getElementById('editWriteModeBtn');
|
||||||
|
const editPreviewModeBtn = document.getElementById('editPreviewModeBtn');
|
||||||
|
const editWriteModeDiv = document.getElementById('editWriteModeDiv');
|
||||||
|
const editPreviewModeDiv = document.getElementById('editPreviewModeDiv');
|
||||||
|
const editChangelogTextarea = document.getElementById('editChangelog');
|
||||||
|
|
||||||
|
editWriteModeBtn.addEventListener('click', function() {
|
||||||
|
editWriteModeBtn.classList.add('active');
|
||||||
|
editWriteModeBtn.classList.remove('btn-secondary');
|
||||||
|
editWriteModeBtn.classList.add('btn-primary');
|
||||||
|
editPreviewModeBtn.classList.remove('active');
|
||||||
|
editPreviewModeBtn.classList.remove('btn-primary');
|
||||||
|
editPreviewModeBtn.classList.add('btn-secondary');
|
||||||
|
editWriteModeDiv.style.display = 'block';
|
||||||
|
editPreviewModeDiv.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
editPreviewModeBtn.addEventListener('click', function() {
|
||||||
|
editPreviewModeBtn.classList.add('active');
|
||||||
|
editPreviewModeBtn.classList.remove('btn-secondary');
|
||||||
|
editPreviewModeBtn.classList.add('btn-primary');
|
||||||
|
editWriteModeBtn.classList.remove('active');
|
||||||
|
editWriteModeBtn.classList.remove('btn-primary');
|
||||||
|
editWriteModeBtn.classList.add('btn-secondary');
|
||||||
|
editWriteModeDiv.style.display = 'none';
|
||||||
|
editPreviewModeDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// 解析Markdown并显示预览
|
||||||
|
if (editChangelogTextarea && marked) {
|
||||||
|
editPreviewModeDiv.innerHTML = marked.parse(editChangelogTextarea.value || '无更新日志');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 实时更新预览
|
||||||
|
editChangelogTextarea.addEventListener('input', function() {
|
||||||
|
if (editPreviewModeDiv.style.display !== 'none' && marked) {
|
||||||
|
editPreviewModeDiv.innerHTML = marked.parse(editChangelogTextarea.value || '无更新日志');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模态框关闭时移除元素
|
||||||
|
document.getElementById('editVersionModal').addEventListener('hidden.bs.modal', function() {
|
||||||
|
this.remove();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete(versionId) {
|
function confirmDelete(versionId) {
|
||||||
@@ -527,6 +640,49 @@ if (!$verStmt) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 页面加载完成后初始化Markdown预览功能
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initMarkdownPreview();
|
||||||
|
|
||||||
|
// 为版本历史中的更新日志添加Markdown渲染
|
||||||
|
renderChangelogHistory();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染版本历史中的更新日志
|
||||||
|
function renderChangelogHistory() {
|
||||||
|
const changelogElements = document.querySelectorAll('.changelog-content');
|
||||||
|
|
||||||
|
changelogElements.forEach(element => {
|
||||||
|
const markdownText = element.getAttribute('data-markdown');
|
||||||
|
if (markdownText && marked) {
|
||||||
|
try {
|
||||||
|
// 创建一个切换按钮
|
||||||
|
const toggleBtn = document.createElement('button');
|
||||||
|
toggleBtn.className = 'btn btn-xs btn-outline-secondary mb-1';
|
||||||
|
toggleBtn.textContent = '查看格式化';
|
||||||
|
|
||||||
|
let isFormatted = false;
|
||||||
|
const originalContent = element.innerHTML;
|
||||||
|
|
||||||
|
toggleBtn.addEventListener('click', function() {
|
||||||
|
if (isFormatted) {
|
||||||
|
element.innerHTML = originalContent;
|
||||||
|
toggleBtn.textContent = '查看格式化';
|
||||||
|
} else {
|
||||||
|
element.innerHTML = marked.parse(markdownText);
|
||||||
|
toggleBtn.textContent = '查看原始';
|
||||||
|
}
|
||||||
|
isFormatted = !isFormatted;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将按钮添加到元素前
|
||||||
|
element.parentNode.insertBefore(toggleBtn, element);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Markdown解析错误:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user