PHP反馈系统源码
完善的反馈系统,带有图片上传,视频上传,后台带有标记已处理,回复用户等
1.index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>反馈中心</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
background: #f5f7fb;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, sans-serif;
padding: 20px 16px 40px;
color: #1e293b;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.card {
background: #ffffff;
border-radius: 32px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.03);
padding: 24px 20px;
margin-bottom: 24px;
border: 1px solid #eef2f8;
}
h1 {
font-size: 1.75rem;
font-weight: 620;
letter-spacing: -0.3px;
background: linear-gradient(135deg, #1e2b3c, #2d3e50);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.subhead {
font-size: 0.85rem;
color: #5b6e8c;
border-left: 3px solid #3b82f6;
padding-left: 12px;
margin: 8px 0 20px 0;
}
.form-group {
margin-bottom: 24px;
}
label {
font-weight: 500;
font-size: 0.9rem;
color: #1e293b;
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
textarea {
width: 100%;
border: 1.5px solid #e2e8f0;
border-radius: 24px;
padding: 14px 18px;
font-size: 0.95rem;
font-family: inherit;
resize: vertical;
background: #fff;
transition: 0.2s;
}
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.1);
}
.upload-area {
border: 1.5px dashed #cbd5e1;
border-radius: 28px;
padding: 16px;
background: #fefefe;
}
.upload-trigger {
background: #f1f5f9;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 40px;
font-size: 0.85rem;
font-weight: 500;
color: #1e293b;
cursor: pointer;
margin-bottom: 16px;
transition: 0.2s;
}
.upload-trigger:active {
background: #e2e8f0;
}
.selected-files {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 8px;
}
.file-item {
display: flex;
align-items: center;
gap: 12px;
background: #f8fafc;
padding: 8px 12px;
border-radius: 20px;
border: 1px solid #eef2ff;
}
.file-thumb {
width: 48px;
height: 48px;
background: #eef2ff;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.file-thumb img, .file-thumb video {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-info {
flex: 1;
font-size: 0.75rem;
color: #334155;
word-break: break-all;
}
.remove-file {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #94a3b8;
padding: 6px;
}
.progress-wrap {
display: none;
align-items: center;
gap: 12px;
background: #f1f5f9;
padding: 10px 16px;
border-radius: 60px;
margin: 16px 0 8px;
}
.progress-bar-bg {
flex: 1;
height: 6px;
background: #e2e8f0;
border-radius: 6px;
overflow: hidden;
}
.progress-fill {
width: 0%;
height: 100%;
background: #3b82f6;
border-radius: 6px;
transition: width 0.2s;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
button {
background: #3b82f6;
border: none;
padding: 14px 20px;
border-radius: 60px;
font-weight: 600;
font-size: 1rem;
color: white;
width: 100%;
cursor: pointer;
transition: 0.2s;
margin-top: 12px;
}
button:active { transform: scale(0.97); background: #2563eb; }
button:disabled { opacity: 0.6; transform: none; }
.feedback-item {
background: white;
border-radius: 24px;
padding: 18px;
margin-bottom: 16px;
border: 1px solid #edf2f7;
}
.feedback-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 8px;
}
.time {
font-size: 0.7rem;
color: #6c7a91;
}
.status-badge {
font-size: 0.7rem;
padding: 4px 12px;
border-radius: 40px;
font-weight: 500;
}
.status-resolved {
background: #dcfce7;
color: #15803d;
}
.status-pending {
background: #fff3e3;
color: #b45309;
}
.feedback-content {
font-size: 0.9rem;
line-height: 1.45;
margin: 8px 0;
}
.reply-section {
background: #f8fafc;
border-radius: 20px;
padding: 12px;
margin-top: 12px;
font-size: 0.85rem;
color: #2c3e66;
border-left: 3px solid #3b82f6;
}
.reply-label {
font-weight: 600;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.media-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.media-grid a {
display: block;
width: 80px;
height: 80px;
border-radius: 16px;
overflow: hidden;
background: #f1f5f9;
border: 1px solid #eef2ff;
}
.media-grid img, .media-grid video {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
.refresh-btn {
background: #f1f5f9;
color: #1e293b;
margin-top: 0;
width: auto;
padding: 10px 18px;
font-size: 0.85rem;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
visibility: hidden;
opacity: 0;
transition: 0.2s;
}
.modal-mask.active {
visibility: visible;
opacity: 1;
}
.modal-card {
background: white;
border-radius: 48px;
padding: 28px 24px;
text-align: center;
width: 280px;
}
.success-icon {
width: 56px;
height: 56px;
background: #3b82f6;
border-radius: 60px;
margin: 0 auto 16px;
display: flex;
align-items: center;
justify-content: center;
}
.svg-icon {
width: 24px;
height: 24px;
stroke-width: 1.5;
stroke: currentColor;
fill: none;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>反馈中心</h1>
<div class="subhead">告诉我们你的想法,我们会认真对待</div>
<form id="feedbackForm">
<div class="form-group">
<label>反馈内容</label>
<textarea id="content" rows="4" placeholder="请详细描述... (必填)" required></textarea>
</div>
<div class="form-group">
<label>附件 (图片/视频,最多4个)</label>
<div class="upload-area">
<div class="upload-trigger" id="uploadTrigger">
<svg class="svg-icon" viewBox="0 0 24 24" stroke="currentColor"><path d="M12 4v16m8-8H4"/></svg>
<span>选择文件</span>
</div>
<div id="selectedFilesList" class="selected-files"></div>
<input type="file" id="fileInput" multiple accept="image/jpeg,image/png,image/gif,video/mp4,video/quicktime" style="display:none">
</div>
</div>
<div id="progressContainer" class="progress-wrap">
<div class="spinner"></div>
<div class="progress-bar-bg"><div class="progress-fill" id="progressFill"></div></div>
<span id="progressPercent" style="font-size:0.75rem; min-width:45px;">0%</span>
</div>
<button type="submit" id="submitBtn">提交反馈</button>
</form>
</div>
<div class="card">
<div class="flex-between">
<h2 style="font-size:1.3rem;">所有反馈</h2>
<button id="refreshListBtn" class="refresh-btn">刷新状态</button>
</div>
<div id="feedbacksList" style="display: flex; flex-direction: column; gap: 14px;">
<div style="text-align:center; padding:24px;">加载中...</div>
</div>
</div>
</div>
<div id="successModal" class="modal-mask">
<div class="modal-card">
<div class="success-icon">
<svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5">
<path d="M20 6L9 17L4 12" stroke="white" stroke-linecap="round"/>
</svg>
</div>
<h3 style="margin-bottom: 8px;">提交成功</h3>
<p style="color:#475569;">感谢您的反馈!</p>
<button onclick="closeModal()" style="margin-top: 20px; background:#eef2ff; color:#1e293b;">关闭</button>
</div>
</div>
<script>
// 多文件管理
let selectedFiles = [];
const fileInput = document.getElementById('fileInput');
const selectedContainer = document.getElementById('selectedFilesList');
const maxFiles = 4;
function updateFileListUI() {
if (!selectedContainer) return;
if (selectedFiles.length === 0) {
selectedContainer.innerHTML = '<div style="font-size:0.7rem; color:#94a3b8;">未选择任何文件</div>';
return;
}
selectedContainer.innerHTML = '';
selectedFiles.forEach((file, idx) => {
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
let previewHTML = '';
if (isImage) {
const url = URL.createObjectURL(file);
previewHTML = `<img src="${url}" style="width:100%; height:100%; object-fit:cover;" onload="URL.revokeObjectURL('${url}')">`;
} else if (isVideo) {
const url = URL.createObjectURL(file);
previewHTML = `<video src="${url}" style="width:100%; height:100%; object-fit:cover;" preload="metadata"></video>`;
} else {
previewHTML = '<span>文件</span>';
}
const itemDiv = document.createElement('div');
itemDiv.className = 'file-item';
itemDiv.innerHTML = `
<div class="file-thumb">${previewHTML}</div>
<div class="file-info">${file.name.length>30?file.name.slice(0,27)+'...':file.name}<br>${(file.size/1024).toFixed(0)} KB</div>
<button class="remove-file" data-index="${idx}">✕</button>
`;
selectedContainer.appendChild(itemDiv);
});
document.querySelectorAll('.remove-file').forEach(btn => {
btn.addEventListener('click', (e) => {
const idx = parseInt(btn.getAttribute('data-index'));
if (!isNaN(idx)) {
selectedFiles.splice(idx, 1);
updateFileListUI();
fileInput.value = '';
}
});
});
}
document.getElementById('uploadTrigger').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
const newFiles = Array.from(e.target.files);
if (selectedFiles.length + newFiles.length > maxFiles) {
alert(`最多只能上传 ${maxFiles} 个文件`);
return;
}
for (let f of newFiles) {
if (f.size > 20 * 1024 * 1024) {
alert(`文件 ${f.name} 超过 20MB,请压缩后上传`);
return;
}
}
selectedFiles.push(...newFiles);
updateFileListUI();
fileInput.value = '';
});
// 表单提交
const form = document.getElementById('feedbackForm');
const submitBtn = document.getElementById('submitBtn');
const progressContainer = document.getElementById('progressContainer');
const progressFill = document.getElementById('progressFill');
const progressPercent = document.getElementById('progressPercent');
const contentText = document.getElementById('content');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const content = contentText.value.trim();
if (!content) {
alert("请填写反馈内容");
return;
}
if (selectedFiles.length > maxFiles) {
alert(`最多上传 ${maxFiles} 个文件`);
return;
}
const formData = new FormData();
formData.append('content', content);
for (let i = 0; i < selectedFiles.length; i++) {
formData.append('media[]', selectedFiles[i]);
}
progressContainer.style.display = 'flex';
submitBtn.disabled = true;
progressFill.style.width = '0%';
progressPercent.innerText = '0%';
const xhr = new XMLHttpRequest();
xhr.open('POST', 'submit.php', true);
xhr.upload.onprogress = (ev) => {
if (ev.lengthComputable) {
const percent = Math.round((ev.loaded / ev.total) * 100);
progressFill.style.width = percent + '%';
progressPercent.innerText = percent + '%';
}
};
xhr.onload = () => {
progressContainer.style.display = 'none';
submitBtn.disabled = false;
if (xhr.status === 200) {
try {
const resp = JSON.parse(xhr.responseText);
if (resp.success) {
contentText.value = '';
selectedFiles = [];
updateFileListUI();
showSuccessModal();
loadFeedbacks();
} else {
alert("提交失败: " + (resp.error || "未知错误"));
}
} catch(err) { alert("服务器响应异常"); }
} else {
alert("网络错误,请检查网络后重试");
}
};
xhr.onerror = () => {
progressContainer.style.display = 'none';
submitBtn.disabled = false;
alert("请求失败,请稍后重试");
};
xhr.send(formData);
});
function showSuccessModal() {
const modal = document.getElementById('successModal');
modal.classList.add('active');
setTimeout(() => closeModal(), 2000);
}
window.closeModal = function() {
document.getElementById('successModal').classList.remove('active');
};
// 加载反馈列表
async function loadFeedbacks() {
try {
const res = await fetch('admin.php?action=getFeedbacks');
const list = await res.json();
const container = document.getElementById('feedbacksList');
if (!list.length) {
container.innerHTML = '<div style="text-align:center; padding:24px;">暂无反馈,来提交第一条吧~</div>';
return;
}
container.innerHTML = list.map(item => {
const statusClass = item.status == 1 ? 'status-resolved' : 'status-pending';
const statusText = item.status == 1 ? '已处理' : '待处理';
const replyHtml = item.reply ? `
<div class="reply-section">
<div class="reply-label">官方回复:</div>
<div>${escapeHtml(item.reply)}</div>
</div>
` : '';
return `
<div class="feedback-item">
<div class="feedback-header">
<span class="time">${escapeHtml(item.time)}</span>
<span class="status-badge ${statusClass}">${statusText}</span>
</div>
<div class="feedback-content">${escapeHtml(item.content).replace(/\n/g,'<br>')}</div>
${replyHtml}
<div class="media-grid">
${(item.media || []).map(url => {
const ext = url.split('.').pop().toLowerCase();
if (['jpg','jpeg','png','gif'].includes(ext)) {
return `<a href="${url}" target="_blank"><img src="${url}" loading="lazy"></a>`;
} else if (['mp4','mov','avi','webm'].includes(ext)) {
return `<a href="${url}" target="_blank"><video src="${url}" preload="metadata" style="width:100%;height:100%;object-fit:cover;"></video></a>`;
}
return '';
}).join('')}
</div>
</div>
`;
}).join('');
} catch(e) {
console.error(e);
document.getElementById('feedbacksList').innerHTML = '<div style="text-align:center;">加载失败,刷新重试</div>';
}
}
function escapeHtml(str) {
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
document.getElementById('refreshListBtn').addEventListener('click', loadFeedbacks);
loadFeedbacks();
updateFileListUI();
</script>
</body>
</html>
2.admin.php(管理员后台)
<?php
session_start();
$data_dir = __DIR__ . '/data';
$password_file = $data_dir . '/password.txt';
if (!is_dir($data_dir)) mkdir($data_dir, 0777, true);
if (!file_exists($password_file)) {
file_put_contents($password_file, "admin:admin123\n");
}
function authenticate($username, $password) {
global $password_file;
$lines = file($password_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(':', $line, 2);
if (count($parts) === 2 && trim($parts[0]) === $username && trim($parts[1]) === $password) {
return true;
}
}
return false;
}
// 处理 API 请求
$request_uri = $_SERVER['REQUEST_URI'];
$query_str = parse_url($request_uri, PHP_URL_QUERY);
parse_str($query_str, $query_params);
if (isset($query_params['action'])) {
$action = $query_params['action'];
$data_file = $data_dir . '/feedbacks.json';
$feedbacks = [];
if (file_exists($data_file)) {
$feedbacks = json_decode(file_get_contents($data_file), true);
if (!is_array($feedbacks)) $feedbacks = [];
}
// 获取列表(公开,无需登录)
if ($action === 'getFeedbacks') {
header('Content-Type: application/json');
echo json_encode(array_reverse($feedbacks));
exit;
}
// 以下操作需要管理员登录
$is_logged_in = isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true;
if (!$is_logged_in) {
header('HTTP/1.1 401 Unauthorized');
echo json_encode(['success' => false, 'error' => '未登录']);
exit;
}
// 切换处理状态
if ($action === 'updateStatus') {
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$newStatus = isset($input['status']) ? (int)$input['status'] : -1;
if (!$id || !in_array($newStatus, [0,1])) {
echo json_encode(['success' => false]);
exit;
}
foreach ($feedbacks as &$fb) {
if ($fb['id'] === $id) {
$fb['status'] = $newStatus;
break;
}
}
file_put_contents($data_file, json_encode($feedbacks, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo json_encode(['success' => true]);
exit;
}
// 回复反馈
if ($action === 'reply') {
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$reply = isset($input['reply']) ? trim($input['reply']) : '';
if (!$id || $reply === '') {
echo json_encode(['success' => false, 'error' => '回复内容不能为空']);
exit;
}
$found = false;
foreach ($feedbacks as &$fb) {
if ($fb['id'] === $id) {
$fb['reply'] = $reply;
$found = true;
break;
}
}
if ($found) {
file_put_contents($data_file, json_encode($feedbacks, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => '反馈不存在']);
}
exit;
}
// 删除反馈(同时删除关联的媒体文件)
if ($action === 'delete') {
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
if (!$id) {
echo json_encode(['success' => false]);
exit;
}
$newFeedbacks = [];
$deleted = false;
foreach ($feedbacks as $fb) {
if ($fb['id'] === $id) {
// 删除对应的媒体文件
if (isset($fb['media']) && is_array($fb['media'])) {
foreach ($fb['media'] as $filePath) {
$fullPath = __DIR__ . '/' . $filePath;
if (file_exists($fullPath)) {
unlink($fullPath);
}
}
}
$deleted = true;
continue;
}
$newFeedbacks[] = $fb;
}
if ($deleted) {
file_put_contents($data_file, json_encode($newFeedbacks, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false]);
}
exit;
}
}
// 普通页面请求:显示登录表单或管理界面
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (authenticate($username, $password)) {
$_SESSION['admin_logged_in'] = true;
header("Location: admin.php");
exit();
} else {
$error = "用户名或密码错误!";
}
}
$is_logged_in = isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true;
if (!$is_logged_in) {
// 显示登录表单
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>管理员登录</title>
<style>
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: #f0f2f5;
margin: 0;
padding: 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
background: white;
padding: 24px 20px 32px;
border-radius: 32px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
width: 100%;
max-width: 340px;
}
h2 {
margin: 0 0 20px 0;
text-align: center;
font-size: 1.6rem;
font-weight: 500;
}
input {
width: 100%;
padding: 14px 12px;
margin-bottom: 16px;
border: 1px solid #ddd;
border-radius: 60px;
font-size: 1rem;
background: #fff;
}
input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.2);
}
button {
width: 100%;
padding: 14px;
background: #3b82f6;
color: white;
border: none;
border-radius: 60px;
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
}
.error {
color: #e5484d;
font-size: 0.85rem;
margin-bottom: 16px;
text-align: center;
}
</style>
</head>
<body>
<div class="login-card">
<h2>管理员登录</h2>
<?php if (isset($error)) echo "<div class='error'>$error</div>"; ?>
<form method="POST">
<input type="hidden" name="login" value="1">
<input type="text" name="username" placeholder="用户名" required autofocus>
<input type="password" name="password" placeholder="密码" required>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
3.submit.php(文件处理)
<?php
header('Content-Type: application/json');
session_start();
$data_dir = __DIR__ . '/data';
$upload_dir = __DIR__ . '/uploads/';
if (!is_dir($data_dir)) mkdir($data_dir, 0777, true);
if (!is_dir($upload_dir)) mkdir($upload_dir, 0777, true);
$data_file = $data_dir . '/feedbacks.json';
$content = isset($_POST['content']) ? trim($_POST['content']) : '';
if (empty($content)) {
echo json_encode(['success' => false, 'error' => '内容不能为空']);
exit;
}
$mediaPaths = [];
if (isset($_FILES['media']) && is_array($_FILES['media']['name'])) {
$fileCount = count($_FILES['media']['name']);
if ($fileCount > 4) {
echo json_encode(['success' => false, 'error' => '最多上传4个文件']);
exit;
}
$allowedImg = ['jpg','jpeg','png','gif'];
$allowedVideo = ['mp4','mov','avi','webm'];
for ($i = 0; $i < $fileCount; $i++) {
if ($_FILES['media']['error'][$i] !== UPLOAD_ERR_OK) continue;
$ext = strtolower(pathinfo($_FILES['media']['name'][$i], PATHINFO_EXTENSION));
if (!in_array($ext, array_merge($allowedImg, $allowedVideo))) {
continue;
}
if ($_FILES['media']['size'][$i] > 20 * 1024 * 1024) {
continue;
}
$newName = time() . '_' . uniqid() . '.' . $ext;
$target = $upload_dir . $newName;
if (move_uploaded_file($_FILES['media']['tmp_name'][$i], $target)) {
$mediaPaths[] = 'uploads/' . $newName;
}
}
}
$feedbacks = [];
if (file_exists($data_file)) {
$feedbacks = json_decode(file_get_contents($data_file), true);
if (!is_array($feedbacks)) $feedbacks = [];
}
$newFeedback = [
'id' => uniqid(),
'content' => $content,
'media' => $mediaPaths,
'time' => date('Y-m-d H:i:s'),
'status' => 0, // 0未处理 1已处理
'reply' => '' // 管理员回复内容
];
$feedbacks[] = $newFeedback;
file_put_contents($data_file, json_encode($feedbacks, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo json_encode(['success' => true]);
说明:在使用的时候,确保三个文件在同一个目录,然后呢,新建一个文件夹,名字为data,文件夹里新建一个文件名为:password.txt,这个文件里用于存放你的后台管理账号和密码,格式为:后台账号:后台密码
以下是效果

所有评论(0)