diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..33bca5d --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,165 @@ +/* 后台样式 */ +.flm-admin-container { + display: flex; + gap: 2rem; + margin-top: 20px; +} + +.flm-add-form { + flex: 1; + max-width: 400px; + background: #fff; + padding: 20px; + border-radius: 4px; + box-shadow: 0 1px 1px rgba(0,0,0,0.04); +} + +.flm-links-list { + flex: 2; + background: #fff; + padding: 20px; + border-radius: 4px; + box-shadow: 0 1px 1px rgba(0,0,0,0.04); +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: 600; +} + +.form-group input[type="text"], +.form-group input[type="url"] { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; +} + +#flm-sortable-links { + list-style: none; + padding: 0; + margin: 20px 0; +} + +.flm-link-item { + display: flex; + align-items: center; + gap: 15px; + padding: 15px; + margin-bottom: 15px; + background: #f9f9f9; + border-radius: 4px; + border-left: 4px solid #6667ab; +} + +.flm-link-preview { + display: flex; + align-items: center; + gap: 10px; + min-width: 200px; +} + +.flm-link-icon { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: cover; +} + +.flm-link-fields { + flex: 1; +} + +.flm-link-actions { + min-width: 100px; +} + +.flm-import-export { + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #eee; +} + +.flm-import-export h3 { + margin-bottom: 15px; +} + +.flm-export, .flm-import { + margin-bottom: 15px; +} + +/* 前端展示样式 */ +.flm-links-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + margin: 20px 0; +} + +.flm-link-card { + border: 1px solid #6667ab; + border-radius: 14px; + padding: 20px; + text-align: center; + transition: all 0.3s ease; +} + +.flm-link-card:hover { + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + transform: translateY(-2px); +} + +.flm-link-icon-container { + margin-bottom: 10px; +} + +.flm-link-icon-container img { + width: 64px; + height: 64px; + border-radius: 50%; + object-fit: cover; + border: 1px solid #eee; +} + +.flm-link-name { + font-weight: bold; + color: inherit; /* 使用主题默认颜色 */ + margin-top: 10px; +} + +.flm-link-card a { + text-decoration: none; + color: inherit; + display: block; +} + +/* 响应式设计 */ +@media (max-width: 1024px) { + .flm-links-container { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 600px) { + .flm-admin-container { + flex-direction: column; + } + + .flm-add-form { + max-width: 100%; + } + + .flm-links-container { + grid-template-columns: 1fr; + } + + .flm-link-item { + flex-direction: column; + align-items: flex-start; + } +} \ No newline at end of file diff --git a/assets/images/default-icon.png b/assets/images/default-icon.png new file mode 100644 index 0000000..3afa636 Binary files /dev/null and b/assets/images/default-icon.png differ diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..2983c44 --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,45 @@ +jQuery(document).ready(function($) { + // 使链接可排序 + $('#flm-sortable-links').sortable({ + handle: '.flm-link-preview', + placeholder: 'flm-link-item-placeholder', + axis: 'y', + update: function() { + // 更新排序后可以在这里添加AJAX保存逻辑 + } + }); + + // 删除链接 + $('.flm-delete-link').on('click', function(e) { + e.preventDefault(); + + if (!confirm('确定要删除这个链接吗?')) { + return; + } + + var $button = $(this); + var linkId = $button.data('link-id'); + + $.ajax({ + url: flm_vars.ajax_url, + type: 'POST', + data: { + action: 'flm_delete_link', + link_id: linkId, + nonce: flm_vars.nonce + }, + beforeSend: function() { + $button.prop('disabled', true).text('删除中...'); + }, + success: function() { + $button.closest('.flm-link-item').fadeOut(300, function() { + $(this).remove(); + }); + }, + error: function() { + alert('删除失败,请重试'); + $button.prop('disabled', false).text('删除'); + } + }); + }); +}); \ No newline at end of file diff --git a/friend-links-manager.php b/friend-links-manager.php new file mode 100644 index 0000000..f38f295 --- /dev/null +++ b/friend-links-manager.php @@ -0,0 +1,99 @@ +prefix . 'friend_links'; + + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + name varchar(100) NOT NULL, + url varchar(255) NOT NULL, + icon varchar(255) DEFAULT '', + sort_order int(11) DEFAULT 0, + PRIMARY KEY (id) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + + // 初始化插件版本 + update_option('flm_version', FLM_VERSION); +} + +// 动态卸载逻辑(替代 register_uninstall_hook) +register_deactivation_hook(__FILE__, 'flm_cleanup'); +function flm_cleanup() { + global $wpdb; + + // 删除数据库表 + $table_name = $wpdb->prefix . 'friend_links'; + if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name) { + $wpdb->query("DROP TABLE IF EXISTS $table_name"); + } + + // 清理插件选项 + delete_option('flm_version'); + + // 强制刷新缓存(针对某些缓存插件) + wp_cache_flush(); +} + +// 加载插件功能文件 +require_once FLM_PLUGIN_DIR . 'includes/admin-page.php'; +require_once FLM_PLUGIN_DIR . 'includes/shortcode.php'; + +// 加载样式和脚本 +add_action('wp_enqueue_scripts', 'flm_enqueue_scripts'); +function flm_enqueue_scripts() { + wp_enqueue_style( + 'flm-style', + FLM_PLUGIN_URL . 'assets/css/style.css', + array(), + FLM_VERSION + ); +} + +// 后台脚本和样式 +add_action('admin_enqueue_scripts', 'flm_admin_enqueue_scripts'); +function flm_admin_enqueue_scripts($hook) { + if ('toplevel_page_friend-links-manager' === $hook) { + wp_enqueue_style( + 'flm-admin-style', + FLM_PLUGIN_URL . 'assets/css/style.css', + array(), + FLM_VERSION + ); + + wp_enqueue_script( + 'flm-admin-js', + FLM_PLUGIN_URL . 'assets/js/admin.js', + array('jquery', 'jquery-ui-sortable'), + FLM_VERSION, + true + ); + + wp_localize_script('flm-admin-js', 'flm_vars', array( + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('flm_nonce') + )); + } +} \ No newline at end of file diff --git a/includes/admin-page.php b/includes/admin-page.php new file mode 100644 index 0000000..c530005 --- /dev/null +++ b/includes/admin-page.php @@ -0,0 +1,378 @@ +"), '', $url); + + // 使用正则提取纯URL + if (preg_match('/(https?:\/\/[^\s\"\'<>]+)/i', $url, $matches)) { + $url = $matches[1]; + } + + // 最终过滤和验证 + $url = filter_var($url, FILTER_SANITIZE_URL); + if (!preg_match('/^https?:\/\//i', $url)) { + $url = 'http://' . ltrim($url, '/'); + } + + return rtrim($url, '/'); +} + +// 极端严格的文本清理(用于导出) +function flm_sanitize_export_text($text) { + if (empty($text)) return ''; + + // 彻底移除所有HTML/JavaScript代码 + $text = html_entity_decode($text); + $text = strip_tags($text); + $text = str_replace(array("\r", "\n", "\t", "\\", "'", '"', "<", ">"), '', $text); + + return sanitize_text_field($text); +} + +// 管理页面内容 +function flm_admin_page() { + global $wpdb; + $table_name = $wpdb->prefix . 'friend_links'; + + // 显示警告信息 + echo '

警告:禁用该插件将删除所有链接数据,请在禁用前导出包含链接的CSV文件!

'; + + // 处理表单提交 + if (isset($_POST['flm_action'])) { + check_admin_referer('flm_nonce'); + + switch ($_POST['flm_action']) { + case 'add_link': + if (!empty($_POST['name']) && !empty($_POST['url'])) { + $name = sanitize_text_field($_POST['name']); + $url = esc_url_raw($_POST['url']); + $icon = !empty($_POST['icon']) ? esc_url_raw($_POST['icon']) : flm_get_favicon($url); + + $existing = $wpdb->get_row($wpdb->prepare( + "SELECT id FROM $table_name WHERE url = %s", + $url + )); + + if (!$existing) { + $wpdb->insert($table_name, array( + 'name' => $name, + 'url' => $url, + 'icon' => $icon, + 'sort_order' => 0 + )); + echo '

链接添加成功!

'; + } else { + echo '

该URL的链接已存在!

'; + } + } + break; + + case 'update_links': + if (!empty($_POST['link_ids'])) { + foreach ($_POST['link_ids'] as $index => $id) { + $wpdb->update($table_name, array( + 'name' => sanitize_text_field($_POST['link_names'][$index]), + 'url' => esc_url_raw($_POST['link_urls'][$index]), + 'icon' => esc_url_raw($_POST['link_icons'][$index]), + 'sort_order' => $index + ), array('id' => intval($id))); + } + echo '

链接更新成功!

'; + } + break; + + case 'delete_link': + if (!empty($_POST['link_id'])) { + $wpdb->delete($table_name, array('id' => intval($_POST['link_id']))); + echo '

链接删除成功!

'; + } + break; + + case 'export_links': + $links = $wpdb->get_results("SELECT name, url, icon FROM $table_name ORDER BY sort_order ASC"); + + // 清除所有输出缓冲 + while (ob_get_level()) { + ob_end_clean(); + } + + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename=friend-links-export-' . date('Y-m-d') . '.csv'); + header('Pragma: no-cache'); + header('Expires: 0'); + + $output = fopen('php://output', 'w'); + + // 添加BOM头解决中文乱码 + fwrite($output, chr(0xEF).chr(0xBB).chr(0xBF)); + + // 只写入三列标题 + fputcsv($output, array( + '网站名称', + '网站URL', + '图标URL' + )); + + foreach ($links as $link) { + // 对每列数据应用极端清理 + fputcsv($output, array( + flm_sanitize_export_text($link->name), + flm_sanitize_export_url($link->url), + flm_sanitize_export_url($link->icon) + )); + } + + fclose($output); + exit; + break; + + case 'import_links': + if (!empty($_FILES['import_file']['tmp_name'])) { + $file = $_FILES['import_file']['tmp_name']; + $handle = fopen($file, 'r'); + $import_count = 0; + $update_count = 0; + $error_count = 0; + + // 跳过标题行 + fgetcsv($handle); + + while (($data = fgetcsv($handle)) !== false) { + if (count($data) < 2 || empty($data[0]) || empty($data[1])) { + $error_count++; + continue; + } + + $name = sanitize_text_field($data[0]); + $url = esc_url_raw($data[1]); + $icon = isset($data[2]) ? esc_url_raw($data[2]) : flm_get_favicon($data[1]); + + if (!filter_var($url, FILTER_VALIDATE_URL)) { + $error_count++; + continue; + } + + if (!empty($name) && !empty($url)) { + $existing = $wpdb->get_row($wpdb->prepare( + "SELECT id FROM $table_name WHERE url = %s", + $url + )); + + if ($existing) { + $wpdb->update($table_name, array( + 'name' => $name, + 'icon' => $icon + ), array('id' => $existing->id)); + $update_count++; + } else { + $wpdb->insert($table_name, array( + 'name' => $name, + 'url' => $url, + 'icon' => $icon, + 'sort_order' => 0 + )); + $import_count++; + } + } + } + + fclose($handle); + + $message = sprintf( + '导入完成!新增 %d 条链接,更新 %d 条已有链接', + $import_count, + $update_count + ); + + if ($error_count > 0) { + $message .= sprintf(',跳过 %d 条格式不正确的记录', $error_count); + } + + echo '

' . $message . '

'; + } + break; + } + } + + // 获取所有链接 + $links = $wpdb->get_results("SELECT * FROM $table_name ORDER BY sort_order ASC"); + ?> +
+

友情链接管理

+ +
+ +
+

添加新链接

+
+ + + +
+ + +
+ +
+ + +
+ +
+ + +

留空将自动获取favicon

+
+ + +
+
+ + + +
+
+ + + prefix . 'friend_links'; + $wpdb->delete($table_name, array('id' => intval($_POST['link_id']))); + wp_send_json_success(); + } + + wp_send_json_error(); +} \ No newline at end of file diff --git a/includes/shortcode.php b/includes/shortcode.php new file mode 100644 index 0000000..22929d7 --- /dev/null +++ b/includes/shortcode.php @@ -0,0 +1,37 @@ +prefix . 'friend_links'; + + $atts = shortcode_atts(array( + 'random' => 'true' + ), $atts); + + $order_by = ($atts['random'] === 'true') ? 'RAND()' : 'sort_order ASC'; + $links = $wpdb->get_results("SELECT * FROM $table_name ORDER BY $order_by"); + + if (empty($links)) { + return ''; + } + + ob_start(); + ?> + +