Files
argon-theme/feedback.php

1765 lines
77 KiB
PHP
Raw Normal View History

<?php
/**
* 问题反馈页面
* @package Argon
*/
2026-01-20 16:14:10 +08:00
// 在加载 WordPress 之前初始化 session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$wp_load_path = dirname(dirname(dirname(dirname(__FILE__)))) . '/wp-load.php';
if (!file_exists($wp_load_path)) $wp_load_path = $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php';
require_once($wp_load_path);
2026-01-20 16:14:10 +08:00
// 引入反馈邮件模板
require_once(get_template_directory() . '/email-templates/feedback-notify.php');
// 权限检查
$is_admin = current_user_can('manage_options');
2026-01-20 16:14:10 +08:00
// 处理验证码刷新请求AJAX
if (isset($_GET['action']) && $_GET['action'] === 'refresh_captcha') {
header('Content-Type: application/json; charset=utf-8');
if (function_exists('get_comment_captcha_seed')) {
get_comment_captcha_seed(true);
}
$captcha = function_exists('get_comment_captcha') ? get_comment_captcha() : '';
echo json_encode(['success' => true, 'captcha' => $captcha]);
exit;
}
// 授权链接访问检查
$auth_view_id = null;
$auth_view_token = null;
$is_auth_view = false;
$can_view_private = false; // 是否可以查看私密留言
if (isset($_GET['id']) && isset($_GET['token'])) {
$auth_view_id = sanitize_text_field($_GET['id']);
$auth_view_token = sanitize_text_field($_GET['token']);
if (argon_verify_feedback_token($auth_view_id, $auth_view_token)) {
$is_auth_view = true;
$can_view_private = true; // 授权链接可以查看私密留言
}
}
// 管理员也可以查看私密留言
if ($is_admin) {
$can_view_private = true;
}
// AJAX 处理图片上传
if (isset($_POST['feedback_action']) && $_POST['feedback_action'] === 'upload_image') {
header('Content-Type: application/json; charset=utf-8');
// 验证 nonce防止 CSRF 攻击)
if (!isset($_POST['feedback_upload_nonce']) || !wp_verify_nonce($_POST['feedback_upload_nonce'], 'argon_feedback_upload')) {
echo json_encode(['success' => false, 'message' => '安全验证失败']);
exit;
}
// IP 黑名单检查
if (argon_is_ip_blocked_global()) {
echo json_encode(['success' => false, 'message' => '您的 IP 已被限制访问']);
exit;
}
// 频率限制(防止滥用)
$user_identifier = argon_get_feedback_user_identifier();
$rate_limit_key = 'feedback_upload_' . $user_identifier;
$upload_count = get_transient($rate_limit_key);
$max_uploads = intval(get_option('argon_feedback_upload_limit', 20));
if ($upload_count !== false && $upload_count >= $max_uploads) {
echo json_encode(['success' => false, 'message' => '上传过于频繁,请稍后再试']);
exit;
}
$image_data = $_POST['image_data'] ?? '';
if (empty($image_data)) {
echo json_encode(['success' => false, 'message' => '图片数据为空']);
exit;
}
$saved_url = argon_save_feedback_image($image_data);
if ($saved_url) {
// 更新频率限制计数
$limit_period = intval(get_option('argon_feedback_upload_period', 3600));
if ($upload_count === false) {
set_transient($rate_limit_key, 1, $limit_period);
} else {
set_transient($rate_limit_key, $upload_count + 1, $limit_period);
}
echo json_encode(['success' => true, 'url' => $saved_url]);
} else {
echo json_encode(['success' => false, 'message' => '图片保存失败']);
}
exit;
}
// AJAX 处理反馈管理
if (isset($_POST['feedback_action'])) {
2026-01-20 16:14:10 +08:00
header('Content-Type: application/json; charset=utf-8');
$response = ['success' => false, 'message' => ''];
$action = sanitize_text_field($_POST['feedback_action']);
// 管理员操作
if (in_array($action, ['delete', 'toggle_public', 'reply', 'update_status', 'get', 'reply_and_resolve'])) {
if (!$is_admin) {
echo json_encode(['success' => false, 'message' => '权限不足']);
exit;
}
if (!isset($_POST['feedback_nonce']) || !wp_verify_nonce($_POST['feedback_nonce'], 'argon_feedback_manage')) {
echo json_encode(['success' => false, 'message' => '安全验证失败']);
exit;
}
}
switch ($action) {
case 'submit':
$response = argon_handle_feedback_submit($_POST);
break;
case 'delete':
$id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : '';
$response = ['success' => argon_delete_feedback($id)];
break;
case 'toggle_public':
$id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : '';
$response = argon_toggle_feedback_public($id);
break;
case 'reply':
$id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : '';
$reply = isset($_POST['reply']) ? sanitize_textarea_field($_POST['reply']) : '';
$is_private = !empty($_POST['is_private']);
$response = argon_reply_feedback($id, $reply, false, $is_private);
break;
case 'reply_and_resolve':
$id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : '';
$reply = isset($_POST['reply']) ? sanitize_textarea_field($_POST['reply']) : '';
$is_private = !empty($_POST['is_private']);
$response = argon_reply_feedback($id, $reply, true, $is_private);
break;
case 'update_status':
$id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : '';
$status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
$response = argon_update_feedback_status($id, $status, false);
break;
case 'get':
$id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : '';
$feedback = argon_get_feedback($id);
if ($feedback) {
// 如果是未查看状态,自动改为已查看
if ($feedback['status'] === 'pending') {
argon_update_feedback_status($id, 'viewed', false);
$feedback['status'] = 'viewed';
}
// 确保 images 字段存在(兼容旧数据)
if (!isset($feedback['images'])) {
$feedback['images'] = [];
}
$response = ['success' => true, 'data' => $feedback];
} else {
$response = ['success' => false, 'message' => '反馈不存在'];
}
break;
}
echo json_encode($response);
exit;
}
// ==================== 反馈数据操作函数 ====================
function argon_get_feedback_user_identifier() {
2026-01-20 16:14:10 +08:00
if (is_user_logged_in()) {
return 'user_' . get_current_user_id();
}
$ip = $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
if (strpos($ip, ',') !== false) $ip = trim(explode(',', $ip)[0]);
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
return substr(hash('sha256', $ip . '|' . $user_agent), 0, 16);
}
function argon_get_feedbacks($filter = 'all') {
2026-01-20 16:14:10 +08:00
$feedbacks = get_option('argon_feedbacks', []);
if (!is_array($feedbacks)) $feedbacks = [];
usort($feedbacks, function($a, $b) { return ($b['created_at'] ?? 0) - ($a['created_at'] ?? 0); });
if ($filter === 'public') {
return array_values(array_filter($feedbacks, function($f) { return !empty($f['is_public']); }));
}
return $feedbacks;
}
function argon_get_feedback($id) {
2026-01-20 16:14:10 +08:00
foreach (argon_get_feedbacks() as $f) {
if ($f['id'] === $id) return $f;
}
return null;
}
2026-01-20 16:14:10 +08:00
function argon_get_feedback_stats() {
$feedbacks = argon_get_feedbacks();
$stats = ['total' => count($feedbacks), 'pending' => 0, 'viewed' => 0, 'processing' => 0, 'resolved' => 0, 'closed' => 0];
foreach ($feedbacks as $f) {
$status = $f['status'] ?? 'pending';
if (isset($stats[$status])) $stats[$status]++;
}
return $stats;
}
function argon_add_feedback($data) {
2026-01-20 16:14:10 +08:00
$feedbacks = get_option('argon_feedbacks', []);
if (!is_array($feedbacks)) $feedbacks = [];
$id = 'fb_' . uniqid();
// 处理图片上传
$images = [];
if (!empty($data['feedback_images']) && is_array($data['feedback_images'])) {
$upload_dir = wp_upload_dir();
$feedback_url_prefix = $upload_dir['baseurl'] . '/feedback-images/';
foreach ($data['feedback_images'] as $image_url) {
if (!empty($image_url) && count($images) < 10) {
// 验证是否是本站上传的图片 URL已经通过 upload_image 接口上传)
if (strpos($image_url, $feedback_url_prefix) === 0) {
$images[] = $image_url;
}
}
}
}
$feedback = [
'id' => $id,
'title' => sanitize_text_field($data['title'] ?? ''),
'content' => sanitize_textarea_field($data['content'] ?? ''),
'type' => sanitize_text_field($data['type'] ?? 'suggestion'),
'name' => sanitize_text_field($data['name'] ?? ''),
'email' => sanitize_email($data['email'] ?? ''),
'url' => esc_url_raw($data['url'] ?? ''),
'is_public' => !empty($data['is_public']),
'status' => 'pending',
'user_identifier' => argon_get_feedback_user_identifier(),
'user_id' => is_user_logged_in() ? get_current_user_id() : 0,
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'created_at' => time(),
'replies' => [],
'images' => $images,
];
$feedbacks[] = $feedback;
update_option('argon_feedbacks', $feedbacks);
argon_send_feedback_new_notify($feedback);
return $id;
}
function argon_delete_feedback($id) {
2026-01-20 16:14:10 +08:00
$feedbacks = get_option('argon_feedbacks', []);
if (!is_array($feedbacks)) return false;
foreach ($feedbacks as $key => $f) {
if ($f['id'] === $id) {
unset($feedbacks[$key]);
update_option('argon_feedbacks', array_values($feedbacks));
return true;
}
}
return false;
}
function argon_toggle_feedback_public($id) {
2026-01-20 16:14:10 +08:00
$feedbacks = get_option('argon_feedbacks', []);
if (!is_array($feedbacks)) return ['success' => false];
foreach ($feedbacks as &$f) {
if ($f['id'] === $id) {
$f['is_public'] = !$f['is_public'];
update_option('argon_feedbacks', $feedbacks);
return ['success' => true, 'is_public' => $f['is_public']];
}
}
return ['success' => false];
}
2026-01-20 16:14:10 +08:00
function argon_reply_feedback($id, $reply, $resolve = false, $is_private = false) {
$feedbacks = get_option('argon_feedbacks', []);
if (!is_array($feedbacks)) return ['success' => false];
foreach ($feedbacks as &$f) {
if ($f['id'] === $id) {
// 检查是否已完结,已完结的反馈不允许修改状态,只能追加留言
$is_archived = in_array($f['status'], ['resolved', 'closed']);
// 初始化 replies 数组(兼容旧数据)
if (!isset($f['replies'])) {
$f['replies'] = [];
// 迁移旧的 reply 字段
if (!empty($f['reply'])) {
$f['replies'][] = [
'content' => $f['reply'],
'time' => $f['reply_at'] ?? time(),
'is_private' => false,
];
}
}
// 添加新留言
if (!empty($reply)) {
$new_reply = [
'content' => $reply,
'time' => time(),
'is_private' => $is_private,
];
$f['replies'][] = $new_reply;
// 只有未完结的反馈才能修改状态
if (!$is_archived) {
if ($resolve) {
$f['status'] = 'resolved';
} elseif ($f['status'] === 'pending' || $f['status'] === 'viewed') {
$f['status'] = 'processing';
}
}
update_option('argon_feedbacks', $feedbacks);
// 发送邮件通知
if ($resolve) {
argon_send_feedback_resolved_notify($f);
} elseif (!$is_private) {
// 只有非私密留言才发送通知
if ($is_archived) {
// 已完结的反馈发送留言追加通知
argon_send_feedback_reply_append_notify($f, $reply);
} else {
// 未完结的反馈发送普通回复通知
argon_send_feedback_reply_notify($f, $reply);
}
}
} else {
// 没有留言内容,只修改状态
if (!$is_archived && $resolve) {
$f['status'] = 'resolved';
update_option('argon_feedbacks', $feedbacks);
argon_send_feedback_resolved_notify($f);
}
}
return ['success' => true, 'is_archived' => $is_archived];
}
}
return ['success' => false];
}
2026-01-20 16:14:10 +08:00
function argon_update_feedback_status($id, $status, $send_notify = true) {
$valid_statuses = ['pending', 'viewed', 'processing', 'resolved', 'closed'];
if (!in_array($status, $valid_statuses)) return ['success' => false, 'message' => '无效状态'];
$feedbacks = get_option('argon_feedbacks', []);
if (!is_array($feedbacks)) return ['success' => false];
foreach ($feedbacks as &$f) {
if ($f['id'] === $id) {
// 检查是否已完结
$is_archived = in_array($f['status'], ['resolved', 'closed']);
if ($is_archived) {
return ['success' => false, 'message' => '已完结的反馈不允许修改状态'];
}
$old_status = $f['status'];
$f['status'] = $status;
update_option('argon_feedbacks', $feedbacks);
// 只有明确要求发送通知且状态变为 resolved 时才发送
if ($send_notify && $status === 'resolved' && $old_status !== 'resolved') {
argon_send_feedback_resolved_notify($f);
}
return ['success' => true];
}
}
return ['success' => false];
}
2026-01-20 16:14:10 +08:00
function argon_is_feedback_captcha_enabled() {
$mode = get_option('argon_feedback_captcha_mode', 'global');
if ($mode === 'enabled') return true;
if ($mode === 'disabled') return false;
return function_exists('argon_is_captcha_enabled') && argon_is_captcha_enabled();
}
2026-01-20 16:14:10 +08:00
function argon_save_feedback_image($base64_data) {
// 验证 base64 数据
if (strpos($base64_data, 'data:image/') !== 0) {
return false;
}
// 解析 base64
$data_parts = explode(',', $base64_data);
if (count($data_parts) !== 2) {
return false;
}
$image_data = base64_decode($data_parts[1]);
if ($image_data === false) {
return false;
}
// 检查大小250KB
if (strlen($image_data) > 250 * 1024) {
return false;
}
// 获取 MIME 类型
preg_match('/data:image\/(\w+);/', $data_parts[0], $matches);
$ext = $matches[1] ?? 'jpg';
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$ext = 'jpg';
}
// 生成文件名
$upload_dir = wp_upload_dir();
$feedback_dir = $upload_dir['basedir'] . '/feedback-images';
if (!file_exists($feedback_dir)) {
wp_mkdir_p($feedback_dir);
}
$filename = 'fb_' . uniqid() . '_' . time() . '.' . $ext;
$filepath = $feedback_dir . '/' . $filename;
// 保存文件
if (file_put_contents($filepath, $image_data) === false) {
return false;
}
// 返回相对 URL
return $upload_dir['baseurl'] . '/feedback-images/' . $filename;
}
2026-01-20 16:14:10 +08:00
function argon_handle_feedback_submit($data) {
// IP 黑名单检查
if (argon_is_ip_blocked_global()) {
return ['success' => false, 'message' => __('您的 IP 已被限制访问', 'argon')];
}
// 提交频率限制
$user_identifier = argon_get_feedback_user_identifier();
$submit_limit_key = 'feedback_submit_' . $user_identifier;
$submit_count = get_transient($submit_limit_key);
$max_submits = intval(get_option('argon_feedback_submit_limit', 5));
$limit_period = intval(get_option('argon_feedback_submit_period', 3600));
if ($submit_count !== false && $submit_count >= $max_submits) {
return ['success' => false, 'message' => __('提交过于频繁,请稍后再试', 'argon')];
}
if (empty($data['feedback_email'])) return ['success' => false, 'message' => __('请填写邮箱', 'argon')];
if (empty($data['feedback_name'])) return ['success' => false, 'message' => __('请填写昵称', 'argon')];
if (empty($data['feedback_content'])) return ['success' => false, 'message' => __('请填写反馈内容', 'argon')];
if (!is_email($data['feedback_email'])) return ['success' => false, 'message' => __('邮箱格式错误', 'argon')];
// 验证码检查
if (argon_is_feedback_captcha_enabled()) {
$captcha_type = get_option('argon_captcha_type', 'math');
if ($captcha_type === 'geetest') {
if (function_exists('geetest_validate')) {
$lot_number = $data['geetest_lot_number'] ?? '';
$captcha_output = $data['geetest_captcha_output'] ?? '';
$pass_token = $data['geetest_pass_token'] ?? '';
$gen_time = $data['geetest_gen_time'] ?? '';
if (empty($lot_number) || empty($captcha_output)) {
return ['success' => false, 'message' => __('请完成验证码验证', 'argon')];
}
$result = geetest_validate($lot_number, $captcha_output, $pass_token, $gen_time);
if ($result !== true) return ['success' => false, 'message' => __('验证码验证失败', 'argon')];
}
} else {
$captcha_input = $data['feedback_captcha'] ?? '';
if (empty($captcha_input)) {
return ['success' => false, 'message' => __('请输入验证码', 'argon')];
}
if (function_exists('get_comment_captcha_answer')) {
$correct_answer = get_comment_captcha_answer();
if ($captcha_input != $correct_answer) {
if (function_exists('get_comment_captcha_seed')) get_comment_captcha_seed(true);
return ['success' => false, 'message' => __('验证码错误', 'argon')];
}
if (function_exists('get_comment_captcha_seed')) get_comment_captcha_seed(true);
}
}
}
$id = argon_add_feedback([
'title' => $data['feedback_title'] ?? '',
'content' => $data['feedback_content'],
'type' => $data['feedback_type'] ?? 'suggestion',
'name' => $data['feedback_name'],
'email' => $data['feedback_email'],
'url' => $data['feedback_url'] ?? '',
'is_public' => !empty($data['feedback_public']),
'feedback_images' => $data['feedback_images'] ?? [],
]);
if ($id) {
// 更新提交频率计数
if ($submit_count === false) {
set_transient($submit_limit_key, 1, $limit_period);
} else {
set_transient($submit_limit_key, $submit_count + 1, $limit_period);
}
return ['success' => true, 'message' => __('反馈提交成功,感谢您的建议!', 'argon')];
}
return ['success' => false, 'message' => __('提交失败,请稍后重试', 'argon')];
}
// ==================== 页面渲染 ====================
$all_feedbacks = argon_get_feedbacks();
$public_feedbacks = argon_get_feedbacks('public');
2026-01-20 16:14:10 +08:00
$stats = argon_get_feedback_stats();
$captcha_enabled = argon_is_feedback_captcha_enabled();
$captcha_type = get_option('argon_captcha_type', 'math');
2026-01-20 16:14:10 +08:00
$saved_name = $_COOKIE['comment_author_' . COOKIEHASH] ?? '';
$saved_email = $_COOKIE['comment_author_email_' . COOKIEHASH] ?? '';
$saved_url = $_COOKIE['comment_author_url_' . COOKIEHASH] ?? '';
if (is_user_logged_in()) {
2026-01-20 16:14:10 +08:00
$current_user = wp_get_current_user();
$saved_name = $current_user->display_name;
$saved_email = $current_user->user_email;
$saved_url = $current_user->user_url;
}
$single_feedback = null;
if ($is_auth_view && $auth_view_id) {
$single_feedback = argon_get_feedback($auth_view_id);
}
get_header();
2026-01-20 16:14:10 +08:00
$type_labels = ['bug' => __('Bug', 'argon'), 'suggestion' => __('建议', 'argon'), 'question' => __('问题', 'argon'), 'other' => __('其他', 'argon')];
$status_labels = ['pending' => __('未查看', 'argon'), 'viewed' => __('已查看', 'argon'), 'processing' => __('进行中', 'argon'), 'resolved' => __('已完成', 'argon'), 'closed' => __('已关闭', 'argon')];
?>
<div class="page-information-card-container"></div>
<?php get_sidebar(); ?>
<div id="primary" class="content-area">
2026-01-20 16:14:10 +08:00
<?php
// 获取全局毛玻璃设置
$feedback_card_blur = intval(get_option('argon_card_blur', '20'));
$feedback_card_saturate = intval(get_option('argon_card_saturate', '180'));
$feedback_card_opacity = get_option('argon_post_background_opacity', '0.7');
2026-03-03 14:58:17 +08:00
if ($feedback_card_opacity == '') {
2026-01-20 16:14:10 +08:00
$feedback_card_opacity = '0.7';
}
?>
<style id="feedback-page-style">
/* 页面布局 */
@media screen and (min-width: 901px) {
2026-01-20 16:14:10 +08:00
body.feedback-page #leftbar_part, body.feedback-page #leftbar { display: none !important; }
}
@media screen and (max-width: 900px) {
2026-01-20 16:14:10 +08:00
body.feedback-page #leftbar {
display: block; position: fixed; background: var(--color-foreground);
top: 0; left: -300px; height: 100vh; width: 280px; padding: 0;
overflow-y: auto; z-index: 1002;
box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07) !important;
transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
html.leftbar-opened body.feedback-page #leftbar { left: 0px; }
}
body.feedback-page #primary { width: 100% !important; max-width: 900px !important; margin: 0 auto !important; float: none !important; }
body.feedback-page #content { margin-top: -50vh !important; }
2026-01-20 16:14:10 +08:00
body.feedback-page #main { max-width: 900px !important; margin: 0 auto !important; }
body.feedback-page .site-footer { max-width: 900px !important; margin: 0 auto !important; box-sizing: border-box !important; }
/* 头部卡片 */
2026-01-20 16:14:10 +08:00
.feedback-header-card { text-align: center; padding: 40px 24px 32px; background: transparent !important; box-shadow: none !important; border: none !important; }
.feedback-header-icon { width: 64px; height: 64px; margin: 0 auto 20px; background: var(--themecolor-gradient); border-radius: var(--card-radius); display: flex; align-items: center; justify-content: center; font-size: 28px; color: #fff; box-shadow: 0 4px 12px rgba(var(--themecolor-rgbstr), 0.2); transition: transform var(--animation-fast) var(--ease-standard); }
.feedback-header-icon:hover { transform: translateY(-2px); }
.feedback-title { font-size: 24px; font-weight: 600; margin: 0 0 12px; color: var(--color-text-deeper); background: none !important; }
.feedback-title::before, .feedback-title::after { display: none !important; }
2026-01-20 16:14:10 +08:00
.feedback-subtitle { font-size: 14px; color: #888; margin: 0 0 20px; line-height: 1.6; }
.feedback-stats { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; }
.feedback-stat { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; background: rgba(var(--themecolor-rgbstr), 0.08); border-radius: var(--card-radius-sm); font-size: 13px; font-weight: 500; color: var(--themecolor); transition: all var(--animation-fast) var(--ease-standard); }
.feedback-stat:hover { background: rgba(var(--themecolor-rgbstr), 0.12); }
html.darkmode .feedback-subtitle { color: #aaa; }
2026-01-20 16:14:10 +08:00
/* 管理员统计 */
.feedback-admin-stats { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; }
2026-03-03 14:58:17 +08:00
.feedback-admin-stat { flex: 1; min-width: 120px; padding: 20px; background-color: rgba(255, 255, 255, var(--card-opacity)); backdrop-filter: blur(var(--card-blur)) saturate(var(--card-saturate)); -webkit-backdrop-filter: blur(var(--card-blur)) saturate(var(--card-saturate)); border-radius: var(--card-radius); text-align: center; border: 1px solid var(--color-border); transition: all var(--animation-fast) var(--ease-standard); }
2026-01-20 16:14:10 +08:00
.feedback-admin-stat:hover { transform: translateY(-2px); border-color: rgba(var(--themecolor-rgbstr), 0.3); box-shadow: 0 4px 12px rgba(var(--themecolor-rgbstr), 0.15); }
.feedback-admin-stat .stat-value { font-size: 32px; font-weight: 600; color: var(--themecolor); line-height: 1.2; }
.feedback-admin-stat .stat-label { font-size: 13px; color: #888; margin-top: 6px; }
.feedback-admin-stat.pending { border-color: rgba(217, 119, 6, 0.3); }
.feedback-admin-stat.pending .stat-value { color: #d97706; }
.feedback-admin-stat.resolved { border-color: rgba(5, 150, 105, 0.3); }
.feedback-admin-stat.resolved .stat-value { color: #059669; }
2026-03-03 14:58:17 +08:00
html.darkmode .feedback-admin-stat { background-color: rgba(66, 66, 66, var(--card-opacity)); }
2026-01-20 16:14:10 +08:00
html.darkmode .feedback-admin-stat .stat-label { color: #aaa; }
/* 表单样式 */
.feedback-form-card { padding: 24px 28px; }
.feedback-form-title { font-size: 17px; font-weight: 600; margin: 0 0 20px; color: var(--color-text-deeper); display: flex; align-items: center; gap: 10px; }
.feedback-form-title i { color: var(--themecolor); font-size: 18px; }
.feedback-form .form-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 16px; }
.feedback-form .form-row.full { grid-template-columns: 1fr; }
.feedback-form .form-group { margin-bottom: 0; }
2026-01-20 16:14:10 +08:00
.feedback-form label { font-size: 14px; font-weight: 600; color: var(--color-text-deeper); margin-bottom: 6px; display: block; }
.feedback-form label .required { color: #f5365c; margin-left: 2px; }
.feedback-form input, .feedback-form textarea, .feedback-form select {
2026-01-20 16:14:10 +08:00
width: 100%; padding: 12px 14px; font-size: 14px;
border: 1.5px solid var(--color-border); border-radius: var(--card-radius-sm);
background: var(--color-widgets); color: var(--color-text-deeper);
font-family: var(--font); box-sizing: border-box;
transition: all var(--animation-fast) var(--ease-standard);
}
.feedback-form input:focus, .feedback-form textarea:focus, .feedback-form select:focus {
2026-01-20 16:14:10 +08:00
border-color: var(--themecolor);
box-shadow: 0 0 0 3px rgba(var(--themecolor-rgbstr), 0.1), 0 2px 8px rgba(var(--themecolor-rgbstr), 0.15);
outline: none;
transform: translateY(-1px);
}
2026-01-20 16:14:10 +08:00
.feedback-form textarea { resize: vertical; min-height: 140px; line-height: 1.6; }
.feedback-form .char-count { font-size: 12px; color: #666; float: right; margin-top: 4px; }
.feedback-public-option { display: flex; align-items: center; gap: 10px; margin: 20px 0; padding: 14px 18px; background: rgba(var(--themecolor-rgbstr), 0.06); border-radius: var(--card-radius-sm); border: 1.5px solid rgba(var(--themecolor-rgbstr), 0.12); transition: all var(--animation-fast) var(--ease-standard); }
.feedback-public-option:hover { background: rgba(var(--themecolor-rgbstr), 0.08); border-color: rgba(var(--themecolor-rgbstr), 0.18); }
.feedback-public-option input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--themecolor); cursor: pointer; }
.feedback-public-option label { font-size: 14px; color: var(--color-text-deeper); cursor: pointer; margin: 0; font-weight: 500; }
.feedback-public-option .hint { font-size: 12px; color: #666; margin-left: auto; }
.feedback-image-upload { display: flex; flex-wrap: wrap; gap: 12px; }
.feedback-image-preview { display: contents; }
.feedback-image-item { position: relative; width: 100px; height: 100px; border-radius: var(--card-radius-sm); overflow: hidden; border: 2px solid var(--color-border); transition: all var(--animation-fast) var(--ease-standard); }
.feedback-image-item:hover { border-color: var(--themecolor); transform: scale(1.05); }
.feedback-image-item img { width: 100%; height: 100%; object-fit: cover; }
.feedback-image-item .remove-btn { position: absolute; top: 4px; right: 4px; width: 24px; height: 24px; background: rgba(245,54,92,0.9); color: #fff; border: none; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: all var(--animation-fast) var(--ease-standard); }
.feedback-image-item .remove-btn:hover { background: #f5365c; transform: scale(1.1); }
.feedback-image-add-btn { width: 100px; height: 100px; border: 2px dashed var(--color-border); border-radius: var(--card-radius-sm); display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: all var(--animation-fast) var(--ease-standard); color: #666; font-size: 13px; }
.feedback-image-add-btn:hover { border-color: var(--themecolor); color: var(--themecolor); background: rgba(var(--themecolor-rgbstr), 0.05); }
.feedback-image-add-btn.disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }
.feedback-image-add-btn i { font-size: 24px; margin-bottom: 6px; }
.feedback-image-item.uploading { background: var(--color-border-on-foreground); display: flex; align-items: center; justify-content: center; }
.uploading-spinner { font-size: 24px; color: var(--themecolor); }
.feedback-images-display { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.feedback-images-display img { max-width: 120px; max-height: 120px; border-radius: var(--card-radius-sm); cursor: pointer; transition: all var(--animation-fast) var(--ease-standard); border: 2px solid var(--color-border); }
.feedback-images-display img:hover { transform: scale(1.05); border-color: var(--themecolor); }
.feedback-captcha { margin-top: 16px; }
.feedback-captcha .input-group { max-width: 300px; }
.feedback-captcha-text { background: var(--color-background); font-family: monospace; font-size: 15px; letter-spacing: 3px; cursor: pointer; user-select: none; padding: 10px 18px; border-radius: 0 var(--card-radius-sm) var(--card-radius-sm) 0; font-weight: 600; transition: all var(--animation-fast) var(--ease-standard); }
.feedback-captcha-text:hover { background: var(--color-border-on-foreground); }
.feedback-submit { margin-top: 20px; text-align: center; }
.feedback-submit .btn { padding: 12px 40px; font-size: 15px; font-weight: 600; border-radius: var(--card-radius-sm); }
.feedback-divider { height: 1px; background: linear-gradient(to right, transparent, var(--color-border), transparent); margin: 32px 0; }
/* 反馈列表 */
2026-01-20 16:14:10 +08:00
.feedback-list-title { font-size: 17px; font-weight: 600; margin: 0 0 20px; color: var(--color-text-deeper); display: flex; align-items: center; gap: 10px; }
.feedback-list-title i { color: var(--themecolor); font-size: 18px; }
.feedback-list-title .count { margin-left: auto; font-size: 13px; font-weight: 600; color: var(--themecolor); background: rgba(var(--themecolor-rgbstr), 0.1); padding: 4px 14px; border-radius: 14px; border: 1px solid rgba(var(--themecolor-rgbstr), 0.2); }
.feedback-item { padding: 20px 0; background: transparent; border-radius: 0; margin-bottom: 0; position: relative; cursor: default; transition: all var(--animation-fast) var(--ease-standard); border: none; border-bottom: 1px solid var(--color-border-on-foreground); }
.feedback-item:last-child { margin-bottom: 0; border-bottom: none; }
.feedback-item.clickable { cursor: pointer; }
.feedback-item.clickable:hover {
background: rgba(var(--themecolor-rgbstr), 0.03);
padding-left: 12px;
padding-right: 12px;
margin-left: -12px;
margin-right: -12px;
border-radius: var(--card-radius-sm);
border-bottom-color: transparent;
}
.feedback-item-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 12px; }
.feedback-item-avatar { width: 44px; height: 44px; border-radius: 50%; background: var(--themecolor-gradient); display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; color: #fff; flex-shrink: 0; overflow: hidden; box-shadow: 0 2px 8px rgba(var(--themecolor-rgbstr), 0.2); transition: transform var(--animation-fast) var(--ease-standard); }
.feedback-item.clickable:hover .feedback-item-avatar { transform: scale(1.05); }
.feedback-item-avatar img { width: 100%; height: 100%; object-fit: cover; }
.feedback-item-meta { flex: 1; min-width: 0; }
2026-01-20 16:14:10 +08:00
.feedback-item-name { font-size: 15px; font-weight: 600; color: var(--color-text-deeper); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.feedback-item-time { font-size: 13px; color: #666; }
.feedback-item-badges { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.feedback-badge { font-size: 12px; padding: 3px 10px; border-radius: 12px; font-weight: 600; transition: all var(--animation-fast) var(--ease-standard); }
.feedback-badge.type-bug { background: #fee2e2; color: #dc2626; }
.feedback-badge.type-suggestion { background: #dbeafe; color: #2563eb; }
.feedback-badge.type-question { background: #fef3c7; color: #d97706; }
.feedback-badge.type-other { background: #e5e7eb; color: #6b7280; }
.feedback-badge.status-pending { background: #fef3c7; color: #d97706; }
2026-01-20 16:14:10 +08:00
.feedback-badge.status-viewed { background: #e0e7ff; color: #6366f1; }
.feedback-badge.status-processing { background: #dbeafe; color: #2563eb; }
.feedback-badge.status-resolved { background: #d1fae5; color: #059669; }
.feedback-badge.status-closed { background: #e5e7eb; color: #6b7280; }
2026-01-20 16:14:10 +08:00
.feedback-item-title { font-size: 16px; font-weight: 600; color: var(--color-text-deeper); margin-bottom: 10px; line-height: 1.5; }
.feedback-item-content { font-size: 14px; color: #555; line-height: 1.7; white-space: pre-wrap; word-break: break-word; }
.feedback-item-reply { margin-top: 16px; padding: 14px 16px; background: rgba(var(--themecolor-rgbstr), 0.06); border-radius: var(--card-radius-sm); border-left: 3px solid var(--themecolor); transition: all var(--animation-fast) var(--ease-standard); }
.feedback-item-reply:hover { background: rgba(var(--themecolor-rgbstr), 0.08); }
.feedback-item-reply-header { font-size: 13px; font-weight: 600; color: var(--themecolor); margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
.feedback-item-reply-content { font-size: 14px; color: #555; line-height: 1.7; white-space: pre-wrap; }
.feedback-empty { padding: 60px 24px; text-align: center; }
2026-01-20 16:14:10 +08:00
.feedback-empty i { font-size: 56px; color: var(--color-border); margin-bottom: 20px; display: block; opacity: 0.5; }
.feedback-empty p { font-size: 15px; color: #666; margin: 0; line-height: 1.6; }
.feedback-admin-card { padding: 20px 24px; }
.feedback-admin-title { font-size: 16px; font-weight: 600; color: var(--themecolor); margin: 0 0 16px; display: flex; align-items: center; gap: 10px; }
2026-01-20 16:14:10 +08:00
/* 弹窗样式 */
.feedback-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); z-index: 9998; display: none; align-items: center; justify-content: center; animation: fadeIn var(--animation-fast) var(--ease-standard); }
.feedback-modal-overlay.show { display: flex; }
2026-01-20 16:14:10 +08:00
.feedback-modal { background: var(--color-foreground); border-radius: var(--card-radius); padding: 0; width: 90%; max-width: 600px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.3), 0 8px 24px rgba(0,0,0,0.2); animation: modalSlideUp var(--animation-normal) var(--ease-emphasized-decelerate); }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes modalSlideUp { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
.feedback-modal-header { padding: 20px 24px; border-bottom: 1.5px solid var(--color-border); display: flex; align-items: center; justify-content: space-between; background: linear-gradient(to bottom, var(--color-foreground), rgba(var(--themecolor-rgbstr), 0.02)); }
.feedback-modal-title { font-size: 18px; font-weight: 600; margin: 0; display: flex; align-items: center; gap: 10px; color: var(--color-text-deeper); }
.feedback-modal-title i { color: var(--themecolor); }
.feedback-modal-close { background: none; border: none; font-size: 22px; cursor: pointer; color: #666; padding: 0; line-height: 1; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all var(--animation-fast) var(--ease-standard); }
.feedback-modal-close:hover { background: rgba(0,0,0,0.05); color: var(--color-text-deeper); transform: rotate(90deg); }
.feedback-modal-body { padding: 24px; overflow-y: auto; flex: 1; }
.feedback-modal-footer { padding: 16px 24px; border-top: 1.5px solid var(--color-border); display: flex; gap: 10px; justify-content: flex-end; background: rgba(var(--themecolor-rgbstr), 0.02); }
.feedback-modal-footer .btn { min-width: 90px; }
/* 详情弹窗内容 */
.feedback-detail-section { margin-bottom: 16px; }
.feedback-detail-section:last-child { margin-bottom: 0; }
.feedback-detail-label { font-size: 12px; font-weight: 600; color: #666; margin-bottom: 4px; }
.feedback-detail-value { font-size: 14px; color: var(--color-text-deeper); line-height: 1.6; }
.feedback-detail-value.content { white-space: pre-wrap; word-break: break-word; padding: 12px; background: var(--color-background); border-radius: calc(var(--card-radius) * 0.6); }
.feedback-detail-meta { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; padding: 12px; background: var(--color-background); border-radius: calc(var(--card-radius) * 0.6); }
.feedback-detail-meta-item { font-size: 12px; }
.feedback-detail-meta-item .label { color: #666; }
.feedback-detail-meta-item .value { color: var(--color-text-deeper); word-break: break-all; }
.feedback-reply-input { width: 100%; padding: 10px 12px; font-size: 14px; border: 1px solid var(--color-border); border-radius: calc(var(--card-radius) * 0.6); min-height: 80px; resize: vertical; box-sizing: border-box; background: var(--color-widgets); color: var(--color-text-deeper); font-family: var(--font); }
.feedback-reply-input:focus { border-color: var(--themecolor); box-shadow: 0 0 0 3px rgba(var(--themecolor-rgbstr), 0.1); outline: none; }
/* 授权链接查看 */
.feedback-auth-view { padding: 20px 24px; }
.feedback-auth-notice { padding: 12px 16px; background: rgba(var(--themecolor-rgbstr), 0.1); border-radius: var(--card-radius); margin-bottom: 16px; font-size: 13px; color: var(--themecolor); display: flex; align-items: center; gap: 8px; }
2026-01-20 16:14:10 +08:00
/* 响应式 */
@media (max-width: 768px) {
2026-01-20 16:14:10 +08:00
.feedback-form .form-row { grid-template-columns: 1fr; }
.feedback-public-option { flex-wrap: wrap; }
.feedback-public-option .hint { width: 100%; margin-left: 26px; margin-top: 4px; }
.feedback-admin-stats { gap: 8px; }
.feedback-admin-stat { min-width: 70px; padding: 12px 8px; }
.feedback-admin-stat .stat-value { font-size: 20px; }
.feedback-detail-meta { grid-template-columns: 1fr; }
.feedback-modal-footer { flex-wrap: wrap; }
.feedback-modal-footer .btn { flex: 1; min-width: 70px; }
.feedback-image-item, .feedback-image-add-btn { width: 80px; height: 80px; }
.feedback-images-display img { max-width: 100px; max-height: 100px; }
}
2026-01-20 16:14:10 +08:00
/* 夜间模式 */
html.darkmode .feedback-item { background: rgba(255, 255, 255, 0.03); }
html.darkmode .feedback-modal { background: var(--color-background); }
2026-01-20 16:14:10 +08:00
html.darkmode .feedback-badge.type-bug { background: rgba(220, 38, 38, 0.2); }
html.darkmode .feedback-badge.type-suggestion { background: rgba(37, 99, 235, 0.2); }
html.darkmode .feedback-badge.type-question { background: rgba(217, 119, 6, 0.2); }
html.darkmode .feedback-badge.type-other { background: rgba(107, 114, 128, 0.2); }
html.darkmode .feedback-badge.status-pending { background: rgba(217, 119, 6, 0.2); }
html.darkmode .feedback-badge.status-viewed { background: rgba(99, 102, 241, 0.2); }
html.darkmode .feedback-badge.status-processing { background: rgba(37, 99, 235, 0.2); }
html.darkmode .feedback-badge.status-resolved { background: rgba(5, 150, 105, 0.2); }
html.darkmode .feedback-badge.status-closed { background: rgba(107, 114, 128, 0.2); }
html.darkmode .feedback-subtitle { color: #aaa; }
html.darkmode .feedback-admin-stat .stat-label { color: #aaa; }
html.darkmode .feedback-form .char-count { color: #aaa; }
html.darkmode .feedback-public-option .hint { color: #aaa; }
html.darkmode .feedback-item-time { color: #aaa; }
html.darkmode .feedback-item-content { color: #bbb; }
html.darkmode .feedback-item-reply-content { color: #bbb; }
html.darkmode .feedback-empty p { color: #aaa; }
html.darkmode .feedback-detail-label { color: #aaa; }
html.darkmode .feedback-detail-meta-item .label { color: #aaa; }
html.darkmode .feedback-modal-close { color: #aaa; }
html.darkmode .feedback-image-item { border-color: #555; }
html.darkmode .feedback-image-add-btn { border-color: #555; color: #aaa; }
html.darkmode .feedback-images-display img { border-color: #555; }
</style>
<script data-pjax>
(function() {
2026-01-20 16:14:10 +08:00
document.body.classList.add('feedback-page');
if (typeof jQuery !== 'undefined') {
let $ = jQuery;
$(document).off('pjax:start.feedback');
$(document).on('pjax:start.feedback', function() {
document.body.classList.remove('feedback-page');
let s = document.getElementById('feedback-page-style');
if (s) s.remove();
$(document).off('pjax:start.feedback');
});
}
})();
</script>
<main id="main" class="site-main" role="main">
2026-01-20 16:14:10 +08:00
<?php if ($is_auth_view && $single_feedback) : ?>
<!-- 授权链接查看单个反馈 -->
<div class="feedback-header-card" style="margin-bottom: 16px;">
2026-01-20 16:14:10 +08:00
<div class="feedback-header-icon"><i class="fa fa-commenting"></i></div>
<h1 class="feedback-title"><?php _e('反馈详情', 'argon'); ?></h1>
<p class="feedback-subtitle"><?php _e('您可以在此查看反馈的详细信息和回复', 'argon'); ?></p>
</div>
2026-01-20 16:14:10 +08:00
<article class="post card shadow-sm bg-white border-0 feedback-auth-view" style="margin-bottom: 16px;">
<div class="feedback-auth-notice">
<i class="fa fa-lock"></i>
<?php _e('这是您的专属链接,请妥善保管', 'argon'); ?>
</div>
<?php
$type = $single_feedback['type'] ?? 'other';
$status = $single_feedback['status'] ?? 'pending';
$avatar_url = get_avatar_url($single_feedback['email'], ['size' => 80]);
?>
<div class="feedback-item" style="background: transparent; padding: 0;">
<div class="feedback-item-header">
<div class="feedback-item-avatar">
<?php if ($avatar_url) : ?><img src="<?php echo esc_url($avatar_url); ?>" alt=""><?php else : echo mb_substr($single_feedback['name'], 0, 1); endif; ?>
</div>
<div class="feedback-item-meta">
<div class="feedback-item-name">
<?php echo esc_html($single_feedback['name']); ?>
<span class="feedback-item-time"><?php echo date('Y-m-d H:i', $single_feedback['created_at']); ?></span>
</div>
<div class="feedback-item-badges">
<span class="feedback-badge type-<?php echo $type; ?>"><?php echo $type_labels[$type] ?? $type; ?></span>
<span class="feedback-badge status-<?php echo $status; ?>"><?php echo $status_labels[$status] ?? $status; ?></span>
</div>
</div>
</div>
<?php if (!empty($single_feedback['title'])) : ?>
<div class="feedback-item-title"><?php echo esc_html($single_feedback['title']); ?></div>
<?php endif; ?>
<div class="feedback-item-content"><?php echo esc_html($single_feedback['content']); ?></div>
<?php if (!empty($single_feedback['images'])) : ?>
<div class="feedback-images-display">
<?php foreach ($single_feedback['images'] as $img_url) : ?>
<img src="<?php echo esc_url($img_url); ?>" alt="" onclick="window.open(this.src)">
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php
// 兼容旧数据格式
$replies = $single_feedback['replies'] ?? [];
if (empty($replies) && !empty($single_feedback['reply'])) {
$replies = [[
'content' => $single_feedback['reply'],
'time' => $single_feedback['reply_at'] ?? time(),
'is_private' => false,
]];
}
if (!empty($replies)) :
foreach ($replies as $reply_item) :
if (!empty($reply_item['is_private']) && !$can_view_private) : ?>
<div class="feedback-item-reply" style="background:rgba(245,54,92,0.06);border-left-color:#f5365c;">
<div class="feedback-item-reply-header"><i class="fa fa-lock"></i><?php _e('私密留言', 'argon'); ?> · <?php echo date('Y-m-d H:i', $reply_item['time']); ?></div>
<div class="feedback-item-reply-content" style="color:#999;font-style:italic;"><?php _e('此留言仅反馈者和管理员可见', 'argon'); ?></div>
</div>
<?php elseif (!empty($reply_item['is_private']) && $can_view_private) : ?>
<div class="feedback-item-reply" style="background:rgba(245,54,92,0.06);border-left-color:#f5365c;">
<div class="feedback-item-reply-header"><i class="fa fa-lock"></i><?php _e('私密留言', 'argon'); ?> · <?php echo date('Y-m-d H:i', $reply_item['time']); ?></div>
<div class="feedback-item-reply-content"><?php echo esc_html($reply_item['content']); ?></div>
</div>
<?php else : ?>
<div class="feedback-item-reply">
<div class="feedback-item-reply-header"><i class="fa fa-reply"></i><?php _e('博主回复', 'argon'); ?> · <?php echo date('Y-m-d H:i', $reply_item['time']); ?></div>
<div class="feedback-item-reply-content"><?php echo esc_html($reply_item['content']); ?></div>
</div>
<?php endif;
endforeach;
endif; ?>
</div>
<div style="margin-top: 20px; text-align: center;">
<a href="<?php echo home_url('/'); ?>?argon_feedback_view=1" class="btn btn-primary btn-sm">
<i class="fa fa-arrow-left"></i> <?php _e('返回反馈页面', 'argon'); ?>
</a>
</div>
</article>
2026-01-20 16:14:10 +08:00
<?php else : ?>
<!-- 正常反馈页面 -->
<div class="feedback-header-card" style="margin-bottom: 16px;">
<div class="feedback-header-icon"><i class="fa fa-commenting"></i></div>
<h1 class="feedback-title"><?php _e('问题反馈', 'argon'); ?></h1>
<p class="feedback-subtitle"><?php _e('有任何建议或问题?欢迎告诉我!', 'argon'); ?></p>
<div class="feedback-stats">
<div class="feedback-stat"><i class="fa fa-comments"></i><span><?php echo count($public_feedbacks); ?> <?php _e('条公开反馈', 'argon'); ?></span></div>
<?php if ($is_admin && $stats['pending'] > 0) : ?>
<div class="feedback-stat" style="background:rgba(245,54,92,0.1);color:#f5365c;"><i class="fa fa-clock-o"></i><span><?php echo $stats['pending']; ?> <?php _e('条待处理', 'argon'); ?></span></div>
<?php endif; ?>
</div>
</div>
<?php if ($is_admin) : ?>
<div class="feedback-admin-stats">
<div class="feedback-admin-stat">
<div class="stat-value"><?php echo $stats['total']; ?></div>
<div class="stat-label"><?php _e('总数', 'argon'); ?></div>
</div>
<div class="feedback-admin-stat resolved">
<div class="stat-value"><?php echo $stats['resolved']; ?></div>
<div class="stat-label"><?php _e('已完成', 'argon'); ?></div>
</div>
<div class="feedback-admin-stat pending">
<div class="stat-value"><?php echo $stats['pending'] + $stats['processing']; ?></div>
<div class="stat-label"><?php _e('待解决', 'argon'); ?></div>
</div>
<div class="feedback-admin-stat">
<div class="stat-value"><?php echo $stats['closed']; ?></div>
<div class="stat-label"><?php _e('已关闭', 'argon'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if (!$is_admin) : ?>
2026-01-20 16:14:10 +08:00
<!-- 访客提交反馈表单 -->
<article class="post card shadow-sm bg-white border-0 feedback-form-card" style="margin-bottom: 0;">
<h2 class="feedback-form-title"><i class="fa fa-pencil-square-o"></i><?php _e('提交反馈', 'argon'); ?></h2>
<form class="feedback-form" id="feedback-form">
<div class="form-row">
<div class="form-group">
<label><?php _e('昵称', 'argon'); ?> <span class="required">*</span></label>
<input type="text" name="feedback_name" id="feedback-name" required placeholder="<?php _e('您的称呼', 'argon'); ?>" value="<?php echo esc_attr($saved_name); ?>">
</div>
<div class="form-group">
<label><?php _e('邮箱', 'argon'); ?> <span class="required">*</span></label>
<input type="email" name="feedback_email" id="feedback-email" required placeholder="<?php _e('用于接收回复通知', 'argon'); ?>" value="<?php echo esc_attr($saved_email); ?>">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label><?php _e('网站', 'argon'); ?></label>
<input type="url" name="feedback_url" id="feedback-url" placeholder="<?php _e('选填', 'argon'); ?>" value="<?php echo esc_attr($saved_url); ?>">
</div>
<div class="form-group">
<label><?php _e('反馈类型', 'argon'); ?></label>
<select name="feedback_type" id="feedback-type">
<option value="suggestion"><?php _e('建议', 'argon'); ?></option>
<option value="bug"><?php _e('Bug 报告', 'argon'); ?></option>
<option value="question"><?php _e('问题咨询', 'argon'); ?></option>
<option value="other"><?php _e('其他', 'argon'); ?></option>
</select>
</div>
</div>
<div class="form-row full">
<div class="form-group">
<label><?php _e('标题', 'argon'); ?></label>
<input type="text" name="feedback_title" id="feedback-title" placeholder="<?php _e('简要描述您的反馈(选填)', 'argon'); ?>" maxlength="100">
</div>
</div>
<div class="form-row full">
<div class="form-group">
<label><?php _e('详细内容', 'argon'); ?> <span class="required">*</span> <span class="char-count"><span id="content-count">0</span>/2000</span></label>
<textarea name="feedback_content" id="feedback-content" required placeholder="<?php _e('请详细描述您的建议或遇到的问题...', 'argon'); ?>" maxlength="2000" oninput="document.getElementById('content-count').textContent=this.value.length"></textarea>
</div>
</div>
<div class="form-row full">
<div class="form-group">
<label><?php _e('上传图片', 'argon'); ?> <span style="font-size:12px;color:#666;font-weight:400;"><?php _e('选填最多10张单张最大250KB', 'argon'); ?></span></label>
<div class="feedback-image-upload">
<div class="feedback-image-preview" id="feedback-image-preview"></div>
<label class="feedback-image-add-btn" id="feedback-image-add-btn" for="feedback-image-input">
<i class="fa fa-plus"></i>
<span><?php _e('添加图片', 'argon'); ?></span>
</label>
<input type="file" id="feedback-image-input" accept="image/*" multiple style="display:none;">
</div>
</div>
</div>
<div class="feedback-public-option">
<input type="checkbox" name="feedback_public" id="feedback-public" value="1">
<label for="feedback-public"><?php _e('公开此反馈', 'argon'); ?></label>
<span class="hint"><?php _e('公开后其他访客可以看到', 'argon'); ?></span>
</div>
<?php if ($captcha_enabled) : ?>
<?php if ($captcha_type === 'geetest') : ?>
<div class="feedback-captcha">
<input type="hidden" name="geetest_lot_number" id="geetest_lot_number">
<input type="hidden" name="geetest_captcha_output" id="geetest_captcha_output">
<input type="hidden" name="geetest_pass_token" id="geetest_pass_token">
<input type="hidden" name="geetest_gen_time" id="geetest_gen_time">
</div>
<?php else : ?>
<div class="feedback-captcha">
<div class="input-group input-group-sm">
<input type="text" name="feedback_captcha" id="feedback-captcha" class="form-control" placeholder="<?php _e('验证码', 'argon'); ?>" required style="max-width:120px;">
<div class="input-group-append">
<span class="input-group-text feedback-captcha-text" id="feedback-captcha-text" onclick="refreshFeedbackCaptcha()" title="<?php _e('点击刷新', 'argon'); ?>"><?php echo function_exists('get_comment_captcha') ? get_comment_captcha() : ''; ?></span>
</div>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<div class="feedback-submit">
<button type="submit" class="btn btn-primary" id="feedback-submit-btn"><i class="fa fa-paper-plane"></i> <?php _e('提交反馈', 'argon'); ?></button>
</div>
</form>
</article>
2026-01-20 16:14:10 +08:00
<!-- 分割线 -->
<div class="feedback-divider"></div>
<?php endif; ?>
2026-01-20 16:14:10 +08:00
<!-- 公开反馈列表 -->
<article class="post card shadow-sm bg-white border-0 feedback-form-card" style="margin-bottom: 16px;">
<h2 class="feedback-list-title"><i class="fa fa-comments-o"></i><?php _e('公开反馈', 'argon'); ?><span class="count"><?php echo count($public_feedbacks); ?></span></h2>
<?php if (!empty($public_feedbacks)) : ?>
<?php foreach ($public_feedbacks as $fb) :
$type = $fb['type'] ?? 'other';
$status = $fb['status'] ?? 'pending';
$avatar_url = get_avatar_url($fb['email'], ['size' => 80]);
?>
<div class="feedback-item <?php echo $is_admin ? 'clickable' : ''; ?>" data-id="<?php echo esc_attr($fb['id']); ?>" <?php if ($is_admin) : ?>onclick="openFeedbackDetail('<?php echo esc_attr($fb['id']); ?>')"<?php endif; ?>>
<div class="feedback-item-header">
<div class="feedback-item-avatar">
<?php if ($avatar_url) : ?><img src="<?php echo esc_url($avatar_url); ?>" alt=""><?php else : echo mb_substr($fb['name'], 0, 1); endif; ?>
</div>
<div class="feedback-item-meta">
<div class="feedback-item-name">
<?php echo esc_html($fb['name']); ?>
<span class="feedback-item-time"><?php echo date('Y-m-d H:i', $fb['created_at']); ?></span>
</div>
<div class="feedback-item-badges">
<span class="feedback-badge type-<?php echo $type; ?>"><?php echo $type_labels[$type] ?? $type; ?></span>
<span class="feedback-badge status-<?php echo $status; ?>"><?php echo $status_labels[$status] ?? $status; ?></span>
</div>
</div>
</div>
<?php if (!empty($fb['title'])) : ?>
<div class="feedback-item-title"><?php echo esc_html($fb['title']); ?></div>
<?php endif; ?>
<div class="feedback-item-content"><?php echo esc_html($fb['content']); ?></div>
<?php if (!empty($fb['images'])) : ?>
<div class="feedback-images-display">
<?php foreach ($fb['images'] as $img_url) : ?>
<img src="<?php echo esc_url($img_url); ?>" alt="" onclick="window.open(this.src)">
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php
// 兼容旧数据格式
$replies = $fb['replies'] ?? [];
if (empty($replies) && !empty($fb['reply'])) {
$replies = [[
'content' => $fb['reply'],
'time' => $fb['reply_at'] ?? time(),
'is_private' => false,
]];
}
if (!empty($replies)) :
foreach ($replies as $reply_item) :
if (!empty($reply_item['is_private']) && !$can_view_private) : ?>
<div class="feedback-item-reply" style="background:rgba(245,54,92,0.06);border-left-color:#f5365c;">
<div class="feedback-item-reply-header"><i class="fa fa-lock"></i><?php _e('私密留言', 'argon'); ?> · <?php echo date('Y-m-d H:i', $reply_item['time']); ?></div>
<div class="feedback-item-reply-content" style="color:#999;font-style:italic;"><?php _e('此留言仅反馈者和管理员可见', 'argon'); ?></div>
</div>
<?php elseif (!empty($reply_item['is_private']) && $can_view_private) : ?>
<div class="feedback-item-reply" style="background:rgba(245,54,92,0.06);border-left-color:#f5365c;">
<div class="feedback-item-reply-header"><i class="fa fa-lock"></i><?php _e('私密留言', 'argon'); ?> · <?php echo date('Y-m-d H:i', $reply_item['time']); ?></div>
<div class="feedback-item-reply-content"><?php echo esc_html($reply_item['content']); ?></div>
</div>
<?php else : ?>
<div class="feedback-item-reply">
<div class="feedback-item-reply-header"><i class="fa fa-reply"></i><?php _e('博主回复', 'argon'); ?> · <?php echo date('Y-m-d H:i', $reply_item['time']); ?></div>
<div class="feedback-item-reply-content"><?php echo esc_html($reply_item['content']); ?></div>
</div>
<?php endif;
endforeach;
endif; ?>
</div>
<?php endforeach; ?>
<?php else : ?>
<div class="feedback-empty">
<i class="fa fa-inbox"></i>
<p><?php _e('暂无公开反馈', 'argon'); ?></p>
</div>
<?php endif; ?>
</article>
<?php if ($is_admin) :
$private_feedbacks = array_filter($all_feedbacks, function($f) { return empty($f['is_public']); });
if (!empty($private_feedbacks)) :
?>
<!-- 管理员:私密反馈 -->
<article class="post card shadow-sm bg-white border-0 feedback-admin-card" style="margin-bottom: 16px;">
<h3 class="feedback-admin-title"><i class="fa fa-lock"></i><?php _e('私密反馈', 'argon'); ?> (<?php echo count($private_feedbacks); ?>)</h3>
<?php foreach ($private_feedbacks as $fb) :
$type = $fb['type'] ?? 'other';
$status = $fb['status'] ?? 'pending';
$avatar_url = get_avatar_url($fb['email'], ['size' => 80]);
?>
<div class="feedback-item clickable" data-id="<?php echo esc_attr($fb['id']); ?>" onclick="openFeedbackDetail('<?php echo esc_attr($fb['id']); ?>')">
<div class="feedback-item-header">
<div class="feedback-item-avatar">
<?php if ($avatar_url) : ?><img src="<?php echo esc_url($avatar_url); ?>" alt=""><?php else : echo mb_substr($fb['name'], 0, 1); endif; ?>
</div>
<div class="feedback-item-meta">
<div class="feedback-item-name">
<?php echo esc_html($fb['name']); ?>
<span class="feedback-item-time"><?php echo date('Y-m-d H:i', $fb['created_at']); ?></span>
</div>
<div class="feedback-item-badges">
<span class="feedback-badge type-<?php echo $type; ?>"><?php echo $type_labels[$type] ?? $type; ?></span>
<span class="feedback-badge status-<?php echo $status; ?>"><?php echo $status_labels[$status] ?? $status; ?></span>
</div>
</div>
</div>
<?php if (!empty($fb['title'])) : ?>
<div class="feedback-item-title"><?php echo esc_html($fb['title']); ?></div>
<?php endif; ?>
<div class="feedback-item-content"><?php echo esc_html(mb_substr($fb['content'], 0, 100)); ?><?php if (mb_strlen($fb['content']) > 100) echo '...'; ?></div>
</div>
<?php endforeach; ?>
</article>
<?php endif; ?>
<!-- 管理员详情弹窗 -->
<div class="feedback-modal-overlay" id="feedback-detail-modal">
<div class="feedback-modal">
<div class="feedback-modal-header">
<h3 class="feedback-modal-title"><i class="fa fa-info-circle"></i> <?php _e('反馈详情', 'argon'); ?></h3>
<button type="button" class="feedback-modal-close" onclick="closeFeedbackDetail()">&times;</button>
</div>
<div class="feedback-modal-body" id="feedback-detail-content">
<!-- 动态填充 -->
</div>
<div class="feedback-modal-footer">
<button type="button" class="btn btn-secondary btn-sm" onclick="setProcessing()"><?php _e('计划进行', 'argon'); ?></button>
<button type="button" class="btn btn-secondary btn-sm" onclick="submitReplyAndResolve()"><?php _e('确定', 'argon'); ?></button>
<button type="button" class="btn btn-secondary btn-sm" onclick="closeFeedbackDetail()"><?php _e('取消', 'argon'); ?></button>
</div>
</div>
</div>
<?php endif; ?>
2026-01-20 16:14:10 +08:00
<?php endif; // end normal page ?>
<script data-pjax>
var feedbackNonce = '<?php echo wp_create_nonce('argon_feedback_manage'); ?>';
2026-01-20 16:14:10 +08:00
var feedbackUploadNonce = '<?php echo wp_create_nonce('argon_feedback_upload'); ?>';
var feedbackAjaxUrl = '<?php echo esc_url($_SERVER['REQUEST_URI']); ?>';
2026-01-20 16:14:10 +08:00
var feedbackCaptchaType = '<?php echo esc_js($captcha_type); ?>';
var feedbackCaptchaEnabled = <?php echo $captcha_enabled ? 'true' : 'false'; ?>;
var currentFeedbackId = null;
var feedbackImages = []; // 存储已上传的图片 URL
var maxFeedbackImages = 10;
var maxImageSize = 250 * 1024; // 250KB
var maxImageDimension = 4096; // 最大分辨率
var uploadingCount = 0; // 正在上传的图片数量
function refreshFeedbackCaptcha() {
2026-01-20 16:14:10 +08:00
let currentUrl = window.location.pathname;
fetch(currentUrl + '?action=refresh_captcha')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.captcha) {
document.getElementById('feedback-captcha-text').textContent = res.captcha;
}
});
}
<?php if ($captcha_enabled && $captcha_type === 'geetest' && !$is_admin) : ?>
let feedbackGeetestCaptcha = null;
let feedbackGeetestValidated = false;
let feedbackGeetestLoading = false;
function loadFeedbackGeetestScript(callback) {
if (typeof initGeetest4 !== 'undefined') {
if (callback) callback();
return;
}
if (feedbackGeetestLoading) {
let t0 = Date.now();
(function wait() {
if (typeof initGeetest4 !== 'undefined') {
if (callback) callback();
return;
}
if (Date.now() - t0 > 5000) {
feedbackGeetestLoading = false;
return;
}
setTimeout(wait, 100);
})();
return;
}
let existing = Array.prototype.slice.call(document.getElementsByTagName('script')).some(function(s) {
return (s.src || '').indexOf('static.geetest.com/v4/gt4.js') !== -1;
});
if (!existing) {
let s = document.createElement('script');
s.src = 'https://static.geetest.com/v4/gt4.js';
s.async = true;
s.onload = function() {
feedbackGeetestLoading = false;
if (callback) callback();
};
s.onerror = function() {
feedbackGeetestLoading = false;
};
document.head.appendChild(s);
feedbackGeetestLoading = true;
} else {
feedbackGeetestLoading = true;
let t1 = Date.now();
(function wait2() {
if (typeof initGeetest4 !== 'undefined') {
feedbackGeetestLoading = false;
if (callback) callback();
return;
}
if (Date.now() - t1 > 5000) {
feedbackGeetestLoading = false;
return;
}
setTimeout(wait2, 100);
})();
}
}
function initFeedbackGeetestCore() {
initGeetest4({
captchaId: '<?php echo esc_js(get_option('argon_geetest_captcha_id', '')); ?>',
product: 'bind'
}, function(captcha) {
feedbackGeetestCaptcha = captcha;
captcha.onSuccess(function() {
let result = captcha.getValidate();
document.getElementById('geetest_lot_number').value = result.lot_number;
document.getElementById('geetest_captcha_output').value = result.captcha_output;
document.getElementById('geetest_pass_token').value = result.pass_token;
document.getElementById('geetest_gen_time').value = result.gen_time;
feedbackGeetestValidated = true;
submitFeedbackForm();
});
});
}
function initFeedbackGeetest() {
loadFeedbackGeetestScript(function() {
initFeedbackGeetestCore();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initFeedbackGeetest);
} else {
initFeedbackGeetest();
}
<?php endif; ?>
<?php if (!$is_admin) : ?>
// ==================== 图片上传处理 ====================
function compressAndUploadImage(file, callback) {
let reader = new FileReader();
reader.onload = function(e) {
let img = new Image();
img.onload = function() {
// 检查分辨率
if (img.width > maxImageDimension || img.height > maxImageDimension) {
if (typeof iziToast !== 'undefined') {
iziToast.error({
title: '<?php _e('图片分辨率过大', 'argon'); ?>',
message: '<?php _e('图片分辨率不能超过 4096x4096请使用较小的图片', 'argon'); ?>',
position: 'topRight',
timeout: 5000
});
} else {
alert('<?php _e('图片分辨率不能超过 4096x4096', 'argon'); ?>');
}
callback(null);
return;
}
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
// 初始质量
let quality = 0.9;
let width = img.width;
let height = img.height;
// 如果图片太大,先缩小尺寸
let maxDimension = 2048;
if (width > maxDimension || height > maxDimension) {
if (width > height) {
height = (height / width) * maxDimension;
width = maxDimension;
} else {
width = (width / height) * maxDimension;
height = maxDimension;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// 尝试压缩
let tryCompress = function(q) {
let dataUrl = canvas.toDataURL('image/jpeg', q);
let size = Math.round((dataUrl.length - 'data:image/jpeg;base64,'.length) * 0.75);
if (size <= maxImageSize || q <= 0.1) {
if (size > maxImageSize) {
// 无法压缩到目标大小
if (typeof iziToast !== 'undefined') {
iziToast.error({
title: '<?php _e('图片无法压缩', 'argon'); ?>',
message: '<?php _e('图片内容过于复杂,无法压缩到 250KB 以下,请使用更小的图片', 'argon'); ?>',
position: 'topRight',
timeout: 5000
});
} else {
alert('<?php _e('图片无法压缩到 250KB 以下', 'argon'); ?>');
}
callback(null);
} else {
// 上传到服务器
uploadingCount++;
updateImagePreview();
let formData = new FormData();
formData.append('feedback_action', 'upload_image');
formData.append('feedback_upload_nonce', feedbackUploadNonce);
formData.append('image_data', dataUrl);
fetch(feedbackAjaxUrl, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
uploadingCount--;
if (res.success) {
callback(res.url);
} else {
if (typeof iziToast !== 'undefined') {
iziToast.error({
title: '<?php _e('上传失败', 'argon'); ?>',
message: res.message || '<?php _e('请稍后重试', 'argon'); ?>',
position: 'topRight',
timeout: 5000
});
} else {
alert(res.message || '<?php _e('上传失败', 'argon'); ?>');
}
callback(null);
}
updateImagePreview();
})
.catch(function() {
uploadingCount--;
if (typeof iziToast !== 'undefined') {
iziToast.error({
title: '<?php _e('上传失败', 'argon'); ?>',
message: '<?php _e('网络错误,请稍后重试', 'argon'); ?>',
position: 'topRight',
timeout: 5000
});
} else {
alert('<?php _e('上传失败', 'argon'); ?>');
}
callback(null);
updateImagePreview();
});
}
} else {
// 继续降低质量
tryCompress(q - 0.1);
}
};
tryCompress(quality);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function updateImagePreview() {
let preview = document.getElementById('feedback-image-preview');
let addBtn = document.getElementById('feedback-image-add-btn');
preview.innerHTML = '';
// 显示已上传的图片
feedbackImages.forEach(function(img, index) {
let item = document.createElement('div');
item.className = 'feedback-image-item';
item.innerHTML = '<img src="' + img + '" alt=""><button type="button" class="remove-btn" onclick="removeFeedbackImage(' + index + ')">&times;</button>';
preview.appendChild(item);
});
// 显示上传中的占位符
for (let i = 0; i < uploadingCount; i++) {
let item = document.createElement('div');
item.className = 'feedback-image-item uploading';
item.innerHTML = '<div class="uploading-spinner"><i class="fa fa-spinner fa-spin"></i></div>';
preview.appendChild(item);
}
if (feedbackImages.length + uploadingCount >= maxFeedbackImages) {
addBtn.classList.add('disabled');
} else {
addBtn.classList.remove('disabled');
}
}
function removeFeedbackImage(index) {
feedbackImages.splice(index, 1);
updateImagePreview();
}
document.getElementById('feedback-image-input')?.addEventListener('change', function(e) {
let files = Array.from(e.target.files);
let remaining = maxFeedbackImages - feedbackImages.length - uploadingCount;
if (files.length > remaining) {
if (typeof iziToast !== 'undefined') {
iziToast.warning({
title: '<?php _e('最多上传 10 张图片', 'argon'); ?>',
position: 'topRight',
timeout: 3000
});
}
files = files.slice(0, remaining);
}
files.forEach(function(file) {
if (!file.type.startsWith('image/')) {
return;
}
compressAndUploadImage(file, function(url) {
if (url) {
feedbackImages.push(url);
updateImagePreview();
}
});
});
e.target.value = '';
});
2026-01-20 16:14:10 +08:00
function submitFeedbackForm() {
let form = document.getElementById('feedback-form');
let btn = document.getElementById('feedback-submit-btn');
let originalText = btn.innerHTML;
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> <?php _e('提交中...', 'argon'); ?>';
btn.disabled = true;
let formData = new FormData(form);
formData.append('feedback_action', 'submit');
// 添加已上传的图片 URL
feedbackImages.forEach(function(img, index) {
formData.append('feedback_images[' + index + ']', img);
});
fetch(feedbackAjaxUrl, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
btn.innerHTML = originalText;
btn.disabled = false;
if (res.success) {
if (typeof iziToast !== 'undefined') {
iziToast.success({ title: res.message, position: 'topRight', timeout: 3000 });
} else {
alert(res.message);
}
form.reset();
document.getElementById('content-count').textContent = '0';
feedbackImages = [];
updateImagePreview();
<?php if ($captcha_enabled && $captcha_type === 'geetest') : ?>
feedbackGeetestValidated = false;
if (feedbackGeetestCaptcha) feedbackGeetestCaptcha.reset();
<?php endif; ?>
setTimeout(function() { location.reload(); }, 1500);
} else {
if (typeof iziToast !== 'undefined') {
iziToast.error({ title: res.message, position: 'topRight', timeout: 5000 });
} else {
alert(res.message);
}
<?php if ($captcha_enabled) : ?>
if (feedbackCaptchaType === 'math') {
refreshFeedbackCaptcha();
} else if (typeof feedbackGeetestCaptcha !== 'undefined' && feedbackGeetestCaptcha) {
feedbackGeetestValidated = false;
feedbackGeetestCaptcha.reset();
}
<?php endif; ?>
}
})
.catch(function() {
btn.innerHTML = originalText;
btn.disabled = false;
alert('<?php _e('提交失败,请稍后重试', 'argon'); ?>');
});
}
document.getElementById('feedback-form')?.addEventListener('submit', function(e) {
e.preventDefault();
// 检查是否有图片正在上传
if (uploadingCount > 0) {
if (typeof iziToast !== 'undefined') {
iziToast.warning({ title: '<?php _e('请等待图片上传完成', 'argon'); ?>', position: 'topRight', timeout: 3000 });
} else {
alert('<?php _e('请等待图片上传完成', 'argon'); ?>');
}
return;
}
let name = document.getElementById('feedback-name').value.trim();
let email = document.getElementById('feedback-email').value.trim();
let content = document.getElementById('feedback-content').value.trim();
if (!name || !email || !content) {
if (typeof iziToast !== 'undefined') {
iziToast.warning({ title: '<?php _e('请填写必填项', 'argon'); ?>', position: 'topRight', timeout: 3000 });
} else {
alert('<?php _e('请填写必填项', 'argon'); ?>');
}
return;
}
<?php if ($captcha_enabled && $captcha_type === 'geetest') : ?>
if (!feedbackGeetestValidated) {
if (feedbackGeetestCaptcha) {
feedbackGeetestCaptcha.showCaptcha();
} else {
if (typeof iziToast !== 'undefined') {
iziToast.error({ title: '<?php _e('验证码加载中,请稍后重试', 'argon'); ?>', position: 'topRight', timeout: 3000 });
} else {
alert('<?php _e('验证码加载中,请稍后重试', 'argon'); ?>');
}
}
return;
}
<?php endif; ?>
submitFeedbackForm();
});
<?php endif; ?>
<?php if ($is_admin) : ?>
2026-01-20 16:14:10 +08:00
var typeLabels = <?php echo json_encode($type_labels); ?>;
var statusLabels = <?php echo json_encode($status_labels); ?>;
function openFeedbackDetail(id) {
currentFeedbackId = id;
let formData = new FormData();
formData.append('feedback_action', 'get');
formData.append('feedback_nonce', feedbackNonce);
formData.append('id', id);
fetch(feedbackAjaxUrl, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
renderFeedbackDetail(res.data);
document.getElementById('feedback-detail-modal').classList.add('show');
}
});
}
function renderFeedbackDetail(data) {
let html = '';
let isArchived = data.status === 'resolved' || data.status === 'closed';
// 基本信息
html += '<div class="feedback-detail-section">';
html += '<div class="feedback-item-badges" style="margin-bottom:12px;">';
html += '<span class="feedback-badge type-' + data.type + '">' + (typeLabels[data.type] || data.type) + '</span>';
html += '<span class="feedback-badge status-' + data.status + '">' + (statusLabels[data.status] || data.status) + '</span>';
if (data.is_public) html += '<span class="feedback-badge" style="background:#d1fae5;color:#059669;"><?php _e('公开', 'argon'); ?></span>';
else html += '<span class="feedback-badge" style="background:#e5e7eb;color:#6b7280;"><?php _e('私密', 'argon'); ?></span>';
if (isArchived) html += '<span class="feedback-badge" style="background:#fef3c7;color:#d97706;"><?php _e('已存档', 'argon'); ?></span>';
html += '</div>';
html += '</div>';
// 标题
if (data.title) {
html += '<div class="feedback-detail-section">';
html += '<div class="feedback-detail-label"><?php _e('标题', 'argon'); ?></div>';
html += '<div class="feedback-detail-value">' + escapeHtml(data.title) + '</div>';
html += '</div>';
}
// 内容
html += '<div class="feedback-detail-section">';
html += '<div class="feedback-detail-label"><?php _e('反馈内容', 'argon'); ?></div>';
html += '<div class="feedback-detail-value content">' + escapeHtml(data.content) + '</div>';
html += '</div>';
// 图片
if (data.images && data.images.length > 0) {
html += '<div class="feedback-detail-section">';
html += '<div class="feedback-detail-label"><?php _e('附件图片', 'argon'); ?> (' + data.images.length + ')</div>';
html += '<div class="feedback-images-display">';
data.images.forEach(function(img) {
html += '<img src="' + escapeHtml(img) + '" alt="" onclick="window.open(this.src)">';
});
html += '</div>';
html += '</div>';
}
// 用户信息
html += '<div class="feedback-detail-section">';
html += '<div class="feedback-detail-label"><?php _e('提交者信息', 'argon'); ?></div>';
html += '<div class="feedback-detail-meta">';
html += '<div class="feedback-detail-meta-item"><span class="label"><?php _e('昵称', 'argon'); ?>: </span><span class="value">' + escapeHtml(data.name) + '</span></div>';
html += '<div class="feedback-detail-meta-item"><span class="label"><?php _e('邮箱', 'argon'); ?>: </span><span class="value">' + escapeHtml(data.email) + '</span></div>';
html += '<div class="feedback-detail-meta-item"><span class="label">IP: </span><span class="value">' + escapeHtml(data.ip || '-') + '</span></div>';
html += '<div class="feedback-detail-meta-item"><span class="label"><?php _e('时间', 'argon'); ?>: </span><span class="value">' + formatTime(data.created_at) + '</span></div>';
if (data.url) html += '<div class="feedback-detail-meta-item" style="grid-column:span 2;"><span class="label"><?php _e('网站', 'argon'); ?>: </span><span class="value"><a href="' + escapeHtml(data.url) + '" target="_blank">' + escapeHtml(data.url) + '</a></span></div>';
html += '</div>';
html += '</div>';
// 显示已有留言
let replies = data.replies || [];
if (replies.length === 0 && data.reply) {
replies = [{content: data.reply, time: data.reply_at || Date.now()/1000, is_private: false}];
}
if (replies.length > 0) {
html += '<div class="feedback-detail-section">';
html += '<div class="feedback-detail-label"><?php _e('留言记录', 'argon'); ?> (' + replies.length + ')</div>';
html += '<div style="max-height:300px;overflow-y:auto;border:1px solid var(--color-border);border-radius:calc(var(--card-radius) * 0.6);padding:12px;background:var(--color-background);">';
for (let i = 0; i < replies.length; i++) {
let r = replies[i];
let privateLabel = r.is_private ? ' <span style="color:#f5365c;font-size:11px;"><i class="fa fa-lock"></i> <?php _e('私密', 'argon'); ?></span>' : '';
html += '<div style="padding:10px;background:var(--color-foreground);border-radius:calc(var(--card-radius) * 0.5);margin-bottom:8px;border-left:3px solid ' + (r.is_private ? '#f5365c' : 'var(--themecolor)') + ';">';
html += '<div style="font-size:12px;color:#666;margin-bottom:6px;"><i class="fa fa-reply"></i> ' + formatTime(r.time) + privateLabel + '</div>';
html += '<div style="font-size:14px;color:var(--color-text-deeper);white-space:pre-wrap;">' + escapeHtml(r.content) + '</div>';
html += '</div>';
}
html += '</div>';
html += '</div>';
}
// 新增留言区域
html += '<div class="feedback-detail-section">';
if (isArchived) {
html += '<div class="feedback-detail-label"><?php _e('追加留言', 'argon'); ?> <span style="color:#d97706;font-size:12px;">(<?php _e('已完结,仅可追加留言', 'argon'); ?>)</span></div>';
} else {
html += '<div class="feedback-detail-label"><?php _e('新增留言', 'argon'); ?></div>';
}
html += '<textarea class="feedback-reply-input" id="detail-reply-content" placeholder="' + (isArchived ? '<?php _e('输入追加留言...', 'argon'); ?>' : '<?php _e('输入新留言...', 'argon'); ?>') + '"></textarea>';
html += '<div style="margin-top:8px;"><label style="display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:13px;"><input type="checkbox" id="detail-reply-private" style="width:16px;height:16px;"> <?php _e('设为私密留言(用户不可见)', 'argon'); ?></label></div>';
html += '</div>';
// 操作按钮
html += '<div class="feedback-detail-section" style="display:flex;gap:8px;flex-wrap:wrap;">';
html += '<button type="button" class="btn btn-sm btn-secondary" onclick="togglePublicFromDetail()">';
html += data.is_public ? '<?php _e('设为私密', 'argon'); ?>' : '<?php _e('设为公开', 'argon'); ?>';
html += '</button>';
html += '<button type="button" class="btn btn-sm btn-secondary" onclick="deleteFromDetail()"><?php _e('删除', 'argon'); ?></button>';
html += '</div>';
document.getElementById('feedback-detail-content').innerHTML = html;
}
function escapeHtml(text) {
if (!text) return '';
let div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatTime(timestamp) {
let d = new Date(timestamp * 1000);
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0') + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
}
function closeFeedbackDetail() {
document.getElementById('feedback-detail-modal').classList.remove('show');
currentFeedbackId = null;
}
function setProcessing() {
if (!currentFeedbackId) return;
let reply = document.getElementById('detail-reply-content').value;
let isPrivate = document.getElementById('detail-reply-private')?.checked || false;
// 先保存回复(如果有)
let formData = new FormData();
formData.append('feedback_action', 'reply');
formData.append('feedback_nonce', feedbackNonce);
formData.append('id', currentFeedbackId);
formData.append('reply', reply);
if (isPrivate) formData.append('is_private', '1');
fetch(feedbackAjaxUrl, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
// 设置状态为进行中
let statusData = new FormData();
statusData.append('feedback_action', 'update_status');
statusData.append('feedback_nonce', feedbackNonce);
statusData.append('id', currentFeedbackId);
statusData.append('status', 'processing');
return fetch(feedbackAjaxUrl, { method: 'POST', body: statusData });
})
.then(function() {
closeFeedbackDetail();
location.reload();
});
}
function submitReplyAndResolve() {
if (!currentFeedbackId) return;
let reply = document.getElementById('detail-reply-content').value;
let isPrivate = document.getElementById('detail-reply-private')?.checked || false;
let formData = new FormData();
formData.append('feedback_action', 'reply_and_resolve');
formData.append('feedback_nonce', feedbackNonce);
formData.append('id', currentFeedbackId);
formData.append('reply', reply);
if (isPrivate) formData.append('is_private', '1');
fetch(feedbackAjaxUrl, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
closeFeedbackDetail();
location.reload();
}
});
}
function togglePublicFromDetail() {
if (!currentFeedbackId) return;
let formData = new FormData();
formData.append('feedback_action', 'toggle_public');
formData.append('feedback_nonce', feedbackNonce);
formData.append('id', currentFeedbackId);
fetch(feedbackAjaxUrl, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
closeFeedbackDetail();
location.reload();
}
});
}
function deleteFromDetail() {
if (!currentFeedbackId) return;
if (!confirm('<?php _e('确定要删除这条反馈吗?', 'argon'); ?>')) return;
let formData = new FormData();
formData.append('feedback_action', 'delete');
formData.append('feedback_nonce', feedbackNonce);
formData.append('id', currentFeedbackId);
fetch(feedbackAjaxUrl, { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
closeFeedbackDetail();
location.reload();
}
});
}
<?php endif; ?>
</script>
<?php get_footer(); ?>