1. 安装插件“Code Snippets”—启用 点击Code Snippets子菜单+Add Snippet—点击Add Your Custom Code (New Snippet)弹窗弹框选择PHP Snippet
2. 进入新建的PHP Snippet截面 第一行输入“云书签系统”右上角的“Active”要点击显示按钮显示蓝色即为开
Code Preview 下面的代码输入
<?php
//AJAX:抓取网站标题
add_action('wp_ajax_get_website_title', function() {
if (empty($_POST['url'])) wp_send_json_error(['message' => '未提供URL']);
$raw = sanitize_text_field($_POST['url']);
$url = preg_match('/^https?:\/\//', $raw) ? $raw : 'https://'.$raw;
$response = wp_remote_get($url, [
'timeout' => 10,
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win; x64) Chrome/120.0.0.0 Safari/537.36',
'sslverify' => false
]);
$title = '';
if (!is_wp_error($response)) {
$body = wp_remote_retrieve_body($response);
if (preg_match('/<title[^>]*>([^<]+)<\/title>/is', $body, $m)) {
$title = trim($m[1]);
$title = html_entity_decode($title, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
}
if (empty($title)) $title = parse_url($url, PHP_URL_HOST);
wp_send_json_success(['title' => $title]);
wp_die();
});
//添加书签
add_action('wp_ajax_add_bookmark', function() {
if (!is_user_logged_in()) wp_send_json_error();
$uid = get_current_user_id();
$list = get_user_meta($uid, 'user_bookmarks', true);
if (!is_array($list)) $list = [];
$url = esc_url_raw($_POST['url']);
$title = sanitize_text_field($_POST['title']);
$desc = sanitize_text_field($_POST['desc']);
$currGroup = isset($_POST['group_id']) ? sanitize_text_field($_POST['group_id']) : 'default';
if (!preg_match('/^https?:\/\//', $url)) $url = 'https://'.$url;
//后端重复校验
foreach($list as $item){
if($item['url'] === $url){
wp_send_json_error(['msg'=>'该网址已存在,请勿重复添加']);
}
}
$list[] = [
'id' => uniqid('bm_'),
'url' => $url,
'title' => $title,
'desc' => $desc,
'group' => $currGroup,
'sort' => count($list)+1
];
update_user_meta($uid, 'user_bookmarks', $list);
wp_send_json_success([
'id' => end($list)['id'],
'url' => $url,
'title' => $title,
'desc' => $desc,
'host' => parse_url($url, PHP_URL_HOST),
'group' => $currGroup
]);
wp_die();
});
//单条删除
add_action('wp_ajax_del_bookmark', function() {
if (!is_user_logged_in()) wp_send_json_error();
$uid = get_current_user_id();
$list = get_user_meta($uid, 'user_bookmarks', true);
if (!is_array($list)) $list = [];
$delId = trim(sanitize_text_field($_POST['id']));
$keep = [];
foreach ($list as $item) {
if (isset($item['id']) && $item['id'] !== $delId) {
$keep[] = $item;
}
}
update_user_meta($uid, 'user_bookmarks', $keep);
wp_send_json_success();
wp_die();
});
//编辑书签
add_action('wp_ajax_edit_bookmark', function() {
if (!is_user_logged_in()) wp_send_json_error();
$uid = get_current_user_id();
$list = get_user_meta($uid, 'user_bookmarks', true);
if (!is_array($list)) $list = [];
$bmId = sanitize_text_field($_POST['id']);
$newUrl = esc_url_raw($_POST['url']);
$newTitle = sanitize_text_field($_POST['title']);
$newDesc = sanitize_text_field($_POST['desc']);
$newGroup = sanitize_text_field($_POST['group']);
if (!preg_match('/^https?:\/\//', $newUrl)) $newUrl = 'https://'.$newUrl;
foreach($list as $item){
if($item['url'] === $newUrl && $item['id']!=$bmId){
wp_send_json_error(['msg'=>'已存在相同网址,无法修改']);
}
}
foreach ($list as &$item) {
if ($item['id'] === $bmId) {
$item['url'] = $newUrl;
$item['title'] = $newTitle;
$item['desc'] = $newDesc;
$item['group'] = $newGroup;
}
}
unset($item);
update_user_meta($uid, 'user_bookmarks', $list);
wp_send_json_success(['host' => parse_url($newUrl, PHP_URL_HOST)]);
wp_die();
});
//新增分组
add_action('wp_ajax_add_group', function() {
if (!is_user_logged_in()) wp_send_json_error();
$uid = get_current_user_id();
$groups = get_user_meta($uid, 'bookmark_groups', true);
if (!is_array($groups)) $groups = [['id' => 'default', 'name' => '默认分组']];
$gName = sanitize_text_field($_POST['name']);
if (trim($gName) === '') wp_send_json_error(['msg'=>'分组名不能为空']);
$newGid = uniqid('g_');
$groups[] = ['id' => $newGid, 'name' => $gName];
update_user_meta($uid, 'bookmark_groups', $groups);
wp_send_json_success(['id' => $newGid, 'name' => $gName]);
wp_die();
});
//重命名分组
add_action('wp_ajax_rename_group', function() {
if (!is_user_logged_in()) wp_send_json_error();
$uid = get_current_user_id();
$groups = get_user_meta($uid, 'bookmark_groups', true);
if (!is_array($groups)) $groups = [];
$gid = sanitize_text_field($_POST['id']);
$newName = sanitize_text_field($_POST['name']);
foreach($groups as &$g) {
if($g['id'] === $gid) $g['name'] = $newName;
}
update_user_meta($uid, 'bookmark_groups', $groups);
wp_send_json_success();
wp_die();
});
//删除分组
add_action('wp_ajax_delete_group', function() {
if (!is_user_logged_in()) wp_send_json_error(['msg'=>'未登录']);
$uid = get_current_user_id();
$gid = sanitize_text_field($_POST['id']);
if($gid === 'default') wp_send_json_error(['msg'=>'默认分组不能删除']);
$groups = get_user_meta($uid, 'bookmark_groups', true);
if (!is_array($groups)) $groups = [['id' => 'default', 'name' => '默认分组']];
$newGroups = [];
foreach($groups as $g){
if($g['id'] !== $gid){
$newGroups[] = $g;
}
}
update_user_meta($uid, 'bookmark_groups', $newGroups);
$list = get_user_meta($uid, 'user_bookmarks', true);
if (!is_array($list)) $list = [];
foreach($list as &$item){
if(isset($item['group']) && $item['group'] === $gid) $item['group'] = 'default';
}
update_user_meta($uid, 'user_bookmarks', $list);
wp_send_json_success();
wp_die();
});
//获取全部分组
add_action('wp_ajax_get_groups', function() {
if (!is_user_logged_in()) wp_send_json_error();
$uid = get_current_user_id();
$groups = get_user_meta($uid, 'bookmark_groups', true);
if (!is_array($groups) || empty($groups)){
$groups = [['id' => 'default', 'name' => '默认分组']];
update_user_meta($uid,'bookmark_groups',$groups);
}
wp_send_json_success($groups);
wp_die();
});
//导出书签
add_action('wp_ajax_export_bm',function(){
if(!is_user_logged_in())wp_send_json_error();
$uid=get_current_user_id();
$data['groups']=get_user_meta($uid,'bookmark_groups',true);
if (!is_array($data['groups'])) $data['groups'] = [['id'=>'default','name'=>'默认分组']];
$data['bookmarks']=get_user_meta($uid,'user_bookmarks',true);
if (!is_array($data['bookmarks'])) $data['bookmarks'] = [];
wp_send_json_success($data);
wp_die();
});
//导入书签
add_action('wp_ajax_import_bm',function(){
if(!is_user_logged_in())wp_send_json_error();
$uid=get_current_user_id();
if(!isset($_FILES['import_file']))wp_send_json_error(['msg'=>'请选择文件']);
$file=$_FILES['import_file'];
if($file['error']!==UPLOAD_ERR_OK)wp_send_json_error(['msg'=>'文件上传失败']);
$raw=file_get_contents($file['tmp_name']);
$json=json_decode($raw,true);
$oldGroups=get_user_meta($uid,'bookmark_groups',true);
if (!is_array($oldGroups)) $oldGroups = [['id'=>'default','name'=>'默认分组']];
$oldBms=get_user_meta($uid,'user_bookmarks',true);
if (!is_array($oldBms)) $oldBms = [];
if(!empty($json['bookmarks'])){
foreach($json['groups'] as $g){
$exist=false;
foreach($oldGroups as $og){if($og['name']==$g['name'])$exist=true;}
if(!$exist && $g['id']!='default')$oldGroups[]=['id'=>uniqid('g_'),'name'=>$g['name']];
}
foreach($json['bookmarks'] as $bm){
$has=false;
foreach($oldBms as $obm){if($obm['url']==$bm['url'])$has=true;}
if(!$has){
$bm['id']=uniqid('bm_');
$oldBms[]=$bm;
}
}
}else{
$pattern='/<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/is';
preg_match_all($pattern,$raw,$matches);
for($i=0;$i<count($matches[0]);$i++){
$url=$matches[1][$i];
$title=$matches[2][$i];
if(strpos($url,'javascript:')===0 || strpos($url,'data:')===0)continue;
$has=false;
foreach($oldBms as $obm){if($obm['url']==$url)$has=true;}
if(!$has){
$oldBms[]=[
'id'=>uniqid('bm_'),
'url'=>$url,
'title'=>$title,
'desc'=>'',
'group'=>'default',
'sort'=>count($oldBms)+1
];
}
}
}
update_user_meta($uid,'bookmark_groups',$oldGroups);
update_user_meta($uid,'user_bookmarks',$oldBms);
wp_send_json_success();
wp_die();
});
//短代码渲染
add_shortcode('my_bookmark', function() {
if (!is_user_logged_in()) {
$loginUrl = wp_login_url( get_permalink() );
return '
<div style="padding:30px;text-align:center;">
<a href="'.$loginUrl.'" style="color:#007cba;font-size:16px;text-decoration:none;">
请登录后使用(点击登录)
</a>
</div>';
}
$uid = get_current_user_id();
$bookmarks = get_user_meta($uid, 'user_bookmarks', true);
if (!is_array($bookmarks)) $bookmarks = [];
usort($bookmarks, function($a, $b) {
$sa = isset($a['sort']) ? $a['sort'] : 0;
$sb = isset($b['sort']) ? $b['sort'] : 0;
return $sa - $sb;
});
$groups = get_user_meta($uid, 'bookmark_groups', true);
if (!is_array($groups)) $groups = [['id' => 'default', 'name' => '默认分组']];
ob_start();
?>
<style>
#__my_bookmark_container {
--bm-bg:#ffffff; --bm-text:#222222; --bm-card:#ffffff; --bm-border:#eeeeee;
--bm-tabbg:#f5f7fa; --bm-btn:#3b82f6; max-width:1280px; margin:30px auto; padding:0 12px;
touch-action: manipulation !important;
}
#__my_bookmark_container *{
touch-action: manipulation !important;
}
html.dark #__my_bookmark_container,html.io-black-mode #__my_bookmark_container {
--bm-bg:#121212; --bm-text:#e0e0e0; --bm-card:#1e1e1e; --bm-border:#333333;
--bm-tabbg:#2a2a2a; --bm-btn:#3b82f6;
}
#__my_bookmark_container *{box-sizing:border-box;margin:0;padding:0;transition:all 0.25s ease;}
#__my_bookmark_container .bm-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px;background:var(--bm-tabbg);padding:10px;border-radius:14px;}
#__my_bookmark_container .bm-tab{padding:8px 12px;padding-right:36px;border-radius:10px;background:var(--bm-card);border:1px solid var(--bm-border);cursor:pointer;font-size:14px;color:var(--bm-text);position:relative;white-space:nowrap;max-width:140px;overflow:hidden;text-overflow:ellipsis;}
#__my_bookmark_container .bm-tab::after{content:attr(data-fullname);position:absolute;left:8px;top:calc(100% + 6px);background:#222;color:#fff;padding:4px 8px;border-radius:6px;font-size:12px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity 0.2s;z-index:99;}
#__my_bookmark_container .bm-tab:hover::after{opacity:1;}
#__my_bookmark_container .bm-tab.active{background:#3b82f6;color:#fff;border-color:#3b82f6;}
#__my_bookmark_container .group-actions{position:absolute;right:8px;top:50%;transform:translateY(-50%);display:none;gap:4px;}
#__my_bookmark_container .bm-tab:hover .group-actions{display:inline-flex;}
#__my_bookmark_container .group-edit{font-size:12px;color:#666;cursor:pointer;}
#__my_bookmark_container .group-del{font-size:12px;color:#f43f5e;cursor:pointer;}
#__my_bookmark_container .bm-toolbar{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px;align-items:center;}
#__my_bookmark_container .bm-toolbar input,#__my_bookmark_container .bm-toolbar button{padding:8px 12px;border:1px solid var(--bm-border);border-radius:10px;outline:none;background:var(--bm-card);color:var(--bm-text);}
#__my_bookmark_container .bm-toolbar button{background:var(--bm-btn);color:white;border:none;cursor:pointer;}
#__my_bookmark_container .bm-tip{color:#999;font-size:12px;margin-bottom:12px;}
#__my_bookmark_container .bm-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;}
@media(max-width:768px){
#__my_bookmark_container .bm-grid{grid-template-columns:repeat(2,1fr);}
}
#__my_bookmark_container .bm-item{background:var(--bm-card);border:1px solid var(--bm-border);border-radius:14px;padding:12px;display:flex;align-items:center;gap:10px;position:relative;height:68px;cursor:pointer;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.04);}
#__my_bookmark_container .bm-item:hover{border-color:#3b82f6;transform:translateY(-2px);box-shadow:0 4px 10px rgba(59,130,246,0.1);}
#__my_bookmark_container .bm-icon{width:42px;height:42px;background:var(--bm-tabbg);border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;}
#__my_bookmark_container .bm-icon img{width:24px;height:24px;object-fit:contain;display:none;}
#__my_bookmark_container .default-icon{width:24px !important;height:24px !important;display:block !important;background:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEuMSAxOC45M2MtMy45NS0uNDktNy0zLjg1LTctNy45MyAwLS42Mi4wOC0xLjIxLjIxLTEuNzlMOSAxNXYxYzAgMS4xLjkgMiAyIDJ2MS45M202LjktMi41NGMtLjI2LS44MS0xLTEuMzktMS45LTEuMzloLTF2LTRjMC0uNTUtLjQ1LTEtMS0xSDh2LTJoMmMuNTUgMCAxLS40NSAxLTFWN2gyYzEuMSAwIDItLjkgMi0ydi0uNDFjMi45MyAxLjE5IDUgNC4wNiA1IDcuNDEgMCAyLjA4LS44IDMuOTctMi4xIDUuMzl6IiBmaWxsPSIjNjY3MDgwIi8+PC9zdmc+') center/contain no-repeat;}
html.dark #__my_bookmark_container .default-icon,html.io-black-mode #__my_bookmark_container .default-icon{background-image:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEuMSAxOC45M2MtMy45NS0uNDktNy0zLjg1LTctNy45MyAwLS42Mi4wOC0xLjIxLjIxLTEuNzlMOSAxNXYxYzAgMS4xLjkgMiAyIDJ2MS45M202LjktMi41NGMtLjI2LS44MS0xLTEuMzktMS45LTEuMzloLTF2LTRjMC0uNTUtLjQ1LTEtMS0xSDh2LTJoMmMuNTUgMCAxLS40NSAxLTFWN2gyYzEuMSAwIDItLjkgMi0ydi0uNDFjMi45MyAxLjE5IDUgNC4wNiA1IDcuNDEgMCAyLjA4LS44IDMuOTctMi4xIDUuMzl6IiBmaWxsPSIjYWFiYWMxIi8+PC9zdmc+');}
#__my_bookmark_container .bm-texts{flex:1;min-width:0;}
#__my_bookmark_container .bm-title{font-size:14px;font-weight:500;color:var(--bm-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
#__my_bookmark_container .bm-url{font-size:12px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:3px;}
#__my_bookmark_container .bm-item-op{position:absolute;top:6px;right:6px;display:none;gap:6px;}
#__my_bookmark_container .bm-item.active .bm-item-op{display:flex;}
#__my_bookmark_container .bm-op-btn{font-size:12px;padding:2px 6px;border-radius:4px;background:#3b82f6;color:#fff;border:none;cursor:pointer;}
#__my_bookmark_container .bm-op-del{background:#ef4444;}
#__my_bookmark_container .modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.4);backdrop-filter:blur(8px);display:none;align-items:center;justify-content:center;z-index:9999;}
#__my_bookmark_container .modal.show{display:flex;}
#__my_bookmark_container .modal-box{background:var(--bm-card);width:390px;max-width:95%;color:var(--bm-text);border-radius:16px;padding:26px;box-shadow:0 12px 40px rgba(0,0,0,0.15);}
#__my_bookmark_container .modal-box h4{margin:0 0 18px 0;font-size:17px;font-weight:600;}
#__my_bookmark_container .modal-box input,#__my_bookmark_container .modal-box select,#__my_bookmark_container .modal-box textarea{width:100%;padding:12px;margin-bottom:14px;border:1px solid var(--bm-border);border-radius:10px;background:var(--bm-bg);color:var(--bm-text);}
#__my_bookmark_container .modal-btns{display:flex;gap:10px;justify-content:flex-end;margin-top:10px;}
#__my_bookmark_container .modal-btns button{padding:10px 16px;border-radius:10px;border:none;cursor:pointer;font-weight:500;}
#__my_bookmark_container .modal-btns .confirm{background:#3b82f6;color:#fff;}
#__my_bookmark_container .modal-btns .cancel{background:#64748b;color:#fff;}
#__my_bookmark_container .rightmenu{position:fixed;background:var(--bm-card);border:1px solid var(--bm-border);border-radius:10px;padding:8px;min-width:150px;z-index:99999;display:none;box-shadow:0 6px 20px rgba(0,0,0,0.1);}
#__my_bookmark_container .rightmenu>div{padding:8px 12px;cursor:pointer;font-size:14px;border-radius:6px;}
#__my_bookmark_container .rightmenu>div:hover{background:var(--bm-tabbg);}
#__my_bookmark_container .rightmenu .split{border-top:1px solid var(--bm-border);margin:6px 0;}
#__my_bookmark_container .file-upload-area{border:2px dashed var(--bm-border);border-radius:12px;padding:24px;text-align:center;cursor:pointer;color:#94a3b8;}
#__my_bookmark_container .file-upload-area:hover{border-color:#3b82f6;}
</style>
<div id="__my_bookmark_container">
<div class="bm-tabs" id="__group_tabs">
<button type="button" class="bm-tab active" data-group="all" data-fullname="全部书签">全部</button>
<?php foreach($groups as $g): ?>
<button type="button" class="bm-tab" data-group="<?php echo $g['id'] ?>" data-fullname="<?php echo esc_attr($g['name']) ?>">
<?php echo $g['name'] ?>
<?php if($g['id'] != 'default'): ?>
<span class="group-actions">
<span class="group-edit" title="重命名">✏️</span>
<span class="group-del" title="删除">×</span>
</span>
<?php endif; ?>
</button>
<?php endforeach; ?>
</div>
<div class="bm-toolbar">
<input type="text" id="__search_key" placeholder="搜索标题/网址">
<button type="button" id="__add_bookmark_btn">➕ 添加书签</button>
<button type="button" id="__new_group_btn">新建分组</button>
<button type="button" id="__export_btn">导出JSON/HTML</button>
<button type="button" id="__import_btn">导入书签</button>
</div>
<div class="bm-tip">💡 提示:电脑右键菜单,手机单击一次调出编辑/删除,再次单击打开链接</div>
<div class="bm-grid" id="__bm_list">
<?php if(empty($bookmarks)): ?>
<div style="grid-column:span 6;padding:30px;text-align:center;color:#999;">暂无书签</div>
<?php else: foreach($bookmarks as $item):
$host = parse_url($item['url'], PHP_URL_HOST);
?>
<div class="bm-item" data-id="<?php echo $item['id'] ?>" data-group="<?php echo $item['group'] ?>" data-host="<?php echo $host ?>" data-url="<?php echo $item['url'] ?>" data-desc="<?php echo esc_attr($item['desc']??'') ?>">
<div class="bm-icon">
<img class="favicon-img">
<div class="default-icon"></div>
</div>
<div class="bm-texts">
<div class="bm-title"><?php echo $item['title'] ?></div>
<div class="bm-url"><?php echo $host ?></div>
</div>
<div class="bm-item-op">
<button class="bm-op-btn edit-btn">编辑</button>
<button class="bm-op-btn bm-op-del del-btn">删除</button>
</div>
</div>
<?php endforeach; endif; ?>
</div>
<div class="rightmenu" id="__rightMenu">
<div data-type="open">在新标签打开</div>
<div data-type="copy">复制链接地址</div>
<div class="split"></div>
<div data-type="edit">编辑书签</div>
<div data-type="move">移动到分组</div>
<div data-type="del">删除书签</div>
</div>
<div class="modal" id="__add_modal">
<div class="modal-box">
<h4>添加书签</h4>
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:6px;">选择分类</label>
<select id="__add_group_sel"></select>
</div>
<div style="margin-bottom:12px;display:flex;gap:8px;align-items:center;">
<div style="flex:1;">
<label style="display:block;margin-bottom:6px;">书签网址</label>
<input type="text" id="__add_url" placeholder="粘贴网址">
</div>
<button type="button" class="fetch-btn" id="__fetch_title" style="padding:10px 16px;border:none;border-radius:6px;background:#28a745;color:#fff;cursor:pointer;">抓取网站</button>
</div>
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:6px;">书签标题</label>
<input type="text" id="__add_title" placeholder="自定义标题">
</div>
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:6px;">书签描述</label>
<textarea id="__add_desc" placeholder="添加描述(可选)" rows="3"></textarea>
</div>
<div class="modal-btns">
<button type="button" class="cancel">取消</button>
<button type="button" class="confirm" id="add_save_btn">保存</button>
</div>
</div>
</div>
<div class="modal" id="__edit_modal">
<div class="modal-box">
<h4>编辑书签</h4>
<input type="hidden" id="__edit_id">
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:6px;">选择分类</label>
<select id="__edit_group_sel"></select>
</div>
<div style="margin-bottom:12px;display:flex;gap:8px;align-items:center;">
<div style="flex:1;">
<label style="display:block;margin-bottom:6px;">书签网址</label>
<input type="text" id="__edit_url">
</div>
<button type="button" class="fetch-btn" id="__edit_fetch" style="padding:10px 16px;border:none;border-radius:6px;background:#28a745;color:#fff;cursor:pointer;">抓取网站</button>
</div>
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:6px;">书签标题</label>
<input type="text" id="__edit_title">
</div>
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:6px;">书签描述</label>
<textarea id="__edit_desc" rows="3"></textarea>
</div>
<div class="modal-btns">
<button type="button" class="cancel">取消</button>
<button type="button" class="confirm">保存</button>
</div>
</div>
</div>
<div class="modal" id="__import_modal">
<div class="modal-box">
<h4>导入书签</h4>
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:6px;">导入分组</label>
<select id="__import_group_sel">
<option value="default">浏览器书签</option>
</select>
</div>
<div class="file-upload-area" id="__file_upload_area">
<input type="file" id="__import_file" accept=".html,.json" style="display:none;">
<p style="margin:0;">📁 点击选择HTML/JSON书签文件</p>
</div>
<div style="background:#e3f2fd;padding:12px;border-radius:8px;margin-top:12px;color:#1976d2;font-size:12px;">
💡 导入浏览器导出html书签文件
</div>
<div class="modal-btns">
<button type="button" class="cancel">取消</button>
<button type="button" class="confirm" id="__do_import">导入</button>
</div>
</div>
</div>
<div class="modal" id="__new_group_modal"><div class="modal-box"><h4>新建分组</h4><input type="text" id="__new_group_name" placeholder="分组名称"><div class="modal-btns"><button type="button" class="cancel">取消</button><button type="button" class="confirm">创建</button></div></div></div>
<div class="modal" id="__rename_group_modal"><div class="modal-box"><h4>重命名分组</h4><input type="hidden" id="__rename_group_id"><input type="text" id="__rename_group_name" placeholder="新名称"><div class="modal-btns"><button type="button" class="cancel">取消</button><button type="button" class="confirm">保存</button></div></div></div>
<div class="modal" id="__move_modal"><div class="modal-box"><h4>移动书签至分组</h4><input type="hidden" id="__move_bid"><select id="__move_group_sel"></select><div class="modal-btns"><button type="button" class="cancel">取消</button><button type="button" class="confirm" id="__save_move">确认移动</button></div></div></div>
</div>
<script>
(function() {
const AJAX_URL = '<?php echo admin_url('admin-ajax.php') ?>';
const FAVICON_APIS = [h=>`https://favicone.com/${h}?s=64`,h=>`https://www.google.com/s2/favicons?domain=${h}&sz=64`,h=>`https://icon.horse/icon/${h}`];
let rightClickItem=null;
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const clickEvt = isTouchDevice ? 'touchend' : 'click';
let addSubmitLock = false;
const darkObserver = new MutationObserver(()=>{
const wrap=document.getElementById('__my_bookmark_container');
wrap.style.opacity='0.99';
setTimeout(()=>wrap.style.opacity='1',50);
});
darkObserver.observe(document.documentElement,{attributes:true,attributeFilter:['class']});
function loadFavicon(el,host,idx=0){
const img=el.querySelector('.favicon-img');
const def=el.querySelector('.default-icon');
if(idx>=FAVICON_APIS.length){img.style.display='none';def.style.display='block';return;}
img.src=FAVICON_APIS[idx](host);
img.onload=()=>{img.style.display='block';def.style.display='none';};
img.onerror=()=>loadFavicon(el,host,idx+1);
}
function refreshGroups(){
fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'action=get_groups'})
.then(r=>r.json())
.then(d=>{
if(!d.success || !Array.isArray(d.data) || d.data.length===0){
return;
}
const g=d.data;
const t=document.getElementById('__group_tabs');
const a=document.getElementById('__add_group_sel');
const e=document.getElementById('__edit_group_sel');
const m=document.getElementById('__move_group_sel');
const i=document.getElementById('__import_group_sel');
t.innerHTML=`<button type="button" class="bm-tab active" data-group="all" data-fullname="全部书签">全部</button>`;
a.innerHTML='';e.innerHTML='';m.innerHTML='';i.innerHTML='<option value="default">浏览器书签</option>';
g.forEach(group=>{
t.innerHTML+=`<button type="button" class="bm-tab" data-group="${group.id}" data-fullname="${group.name}">${group.name}${group.id!=='default'?`<span class="group-actions"><span class="group-edit">✏️</span><span class="group-del">×</span></span>`:''}</button>`;
a.innerHTML+=`<option value="${group.id}">${group.name}</option>`;
e.innerHTML+=`<option value="${group.id}">${group.name}</option>`;
m.innerHTML+=`<option value="${group.id}">${group.name}</option>`;
});
bindTabClick();
bindTabActions();
filterList();
document.querySelectorAll('.bm-item').forEach(item=>loadFavicon(item,item.dataset.host));
})
.catch(()=>{
return;
});
}
function bindTabClick(){
document.querySelectorAll('.bm-tab').forEach(t=>{
t.onclick = function(e){
if(e.target.closest('.group-actions')) return;
document.querySelectorAll('.bm-tab').forEach(i=>i.classList.remove('active'));
this.classList.add('active');
filterList();
}
});
}
function bindTabActions(){
document.querySelectorAll('.group-edit').forEach(btn=>{
btn.onclick = function(e){
e.stopPropagation();
const tab=this.closest('.bm-tab');
document.getElementById('__rename_group_id').value=tab.dataset.group;
document.getElementById('__rename_group_name').value=tab.dataset.fullname;
document.getElementById('__rename_group_modal').classList.add('show');
}
});
document.querySelectorAll('.group-del').forEach(btn=>{
btn.onclick = function(e){
e.stopPropagation();
const gid=this.closest('.bm-tab').dataset.group;
if(!confirm('确定删除该分组?书签移入默认分组'))return;
fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'action=delete_group&id='+gid})
.then(res=>res.json())
.then(d=>{
if(d.success){
refreshGroups();
}else{
alert(d.msg||'删除失败');
}
});
}
});
}
function filterList(){
const activeG=document.querySelector('.bm-tab.active').dataset.group;
const key=document.getElementById('__search_key').value.toLowerCase();
document.querySelectorAll('.bm-item').forEach(item=>{
let show=true;
if(activeG!='all' && item.dataset.group!==activeG) show=false;
if(key && !item.querySelector('.bm-title').innerText.toLowerCase().includes(key) && !item.dataset.host.includes(key)) show=false;
item.style.display=show?'flex':'none';
});
}
function initRightMenu(){
const menu=document.getElementById('__rightMenu');
const itemList = document.querySelectorAll('.bm-item');
if(isTouchDevice){
itemList.forEach(item=>{
item.addEventListener(clickEvt,function(ev){
const opBtn = ev.target.closest('.bm-op-btn');
if(opBtn) return;
document.querySelectorAll('.bm-item.active').forEach(s=>{
if(s !== this)s.classList.remove('active');
})
if(this.classList.contains('active')){
window.open(this.dataset.url,'_blank');
this.classList.remove('active');
}else{
this.classList.add('active');
}
})
item.querySelector('.edit-btn').addEventListener(clickEvt,()=>{
rightClickItem = item;
document.getElementById('__edit_id').value=item.dataset.id;
document.getElementById('__edit_url').value=item.dataset.url;
document.getElementById('__edit_title').value=item.querySelector('.bm-title').innerText;
document.getElementById('__edit_desc').value=item.dataset.desc;
document.getElementById('__edit_group_sel').value=item.dataset.group;
document.getElementById('__edit_modal').classList.add('show');
item.classList.remove('active');
})
item.querySelector('.del-btn').addEventListener(clickEvt,()=>{
if(!confirm('确认删除?'))return;
fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'action=del_bookmark&id='+item.dataset.id})
.then(r=>r.json()).then(d=>{
if(d.success) item.remove();
})
})
})
}else{
document.getElementById('__bm_list').addEventListener('contextmenu',e=>{
const item=e.target.closest('.bm-item');
if(!item)return e.preventDefault();
rightClickItem=item;
menu.style.left=e.pageX+'px';menu.style.top=e.pageY+'px';menu.style.display='block';
e.preventDefault();
});
itemList.forEach(item=>{
item.addEventListener(clickEvt,function(){
window.open(this.dataset.url,'_blank');
})
})
}
document.addEventListener(clickEvt,()=>menu.style.display='none');
menu.querySelectorAll('div[data-type]').forEach(btn=>{
btn.addEventListener(clickEvt,(e)=>{
e.preventDefault();
const type=btn.dataset.type;
const item=rightClickItem;
const url=item.dataset.url;
const bid=item.dataset.id;
if(type==='open')window.open(url,'_blank');
if(type==='copy'){navigator.clipboard.writeText(url);alert('链接已复制');}
if(type==='edit'){
document.getElementById('__edit_id').value=bid;
document.getElementById('__edit_url').value=url;
document.getElementById('__edit_title').value=item.querySelector('.bm-title').innerText;
document.getElementById('__edit_desc').value=item.dataset.desc;
document.getElementById('__edit_group_sel').value=item.dataset.group;
document.getElementById('__edit_modal').classList.add('show');
}
if(type==='move'){
document.getElementById('__move_bid').value=bid;
document.getElementById('__move_modal').classList.add('show');
}
if(type==='del'){
if(!confirm('确认删除书签?'))return;
fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'action=del_bookmark&id='+bid})
.then(r=>r.json()).then(d=>d.success&&item.remove());
}
menu.style.display='none';
});
});
document.getElementById('__save_move').addEventListener(clickEvt,async(e)=>{
e.preventDefault();
const bid=document.getElementById('__move_bid').value;
const newG=document.getElementById('__move_group_sel').value;
const item=document.querySelector(`.bm-item[data-id="${bid}"]`);
const url=item.dataset.url;
const title=item.querySelector('.bm-title').innerText;
const desc=item.dataset.desc;
const res=await fetch(AJAX_URL,{
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`action=edit_bookmark&id=${bid}&url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&desc=${encodeURIComponent(desc)}&group=${newG}`
});
const ret=await res.json();
if(ret.success){
item.dataset.group=newG;
document.getElementById('__move_modal').classList.remove('show');
filterList();
loadFavicon(item,item.dataset.host);
}else alert(ret.msg);
});
}
document.addEventListener('DOMContentLoaded',()=>{
document.querySelectorAll('.bm-item').forEach(i=>loadFavicon(i,i.dataset.host));
refreshGroups();
initRightMenu();
bindTabClick();
bindTabActions();
document.getElementById('__add_bookmark_btn').addEventListener(clickEvt,(e)=>{
e.preventDefault();
document.getElementById('__add_url').value='';
document.getElementById('__add_title').value='';
document.getElementById('__add_desc').value='';
addSubmitLock = false;
document.getElementById('add_save_btn').disabled = false;
document.getElementById('__add_modal').classList.add('show');
});
document.getElementById('__fetch_title').addEventListener(clickEvt,async(e)=>{
e.preventDefault();
let u=document.getElementById('__add_url').value.trim();
if(!u)return alert('填写网址');
const r=await fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:`action=get_website_title&url=${encodeURIComponent(u)}`});
const d=await r.json();
if(d.success)document.getElementById('__add_title').value=d.data.title;
});
//【重点修复】双重锁:按钮禁用+JS锁,重复点击不会刷新跳出短代码
document.getElementById('add_save_btn').addEventListener(clickEvt,async(e)=>{
e.preventDefault();
if(addSubmitLock) return;
addSubmitLock = true;
let saveBtn = document.getElementById('add_save_btn');
saveBtn.disabled = true;
saveBtn.innerText = '提交中...';
const u=document.getElementById('__add_url').value.trim();
const t=document.getElementById('__add_title').value.trim();
const desc=document.getElementById('__add_desc').value.trim();
const g=document.getElementById('__add_group_sel').value;
if(!u){
addSubmitLock = false;
saveBtn.disabled = false;
saveBtn.innerText = '保存';
return alert('必填网址');
}
const res=await fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:`action=add_bookmark&url=${encodeURIComponent(u)}&title=${encodeURIComponent(t)}&desc=${encodeURIComponent(desc)}&group_id=${g}`});
const ret=await res.json();
if(ret.success){
location.reload();
}else{
addSubmitLock = false;
saveBtn.disabled = false;
saveBtn.innerText = '保存';
alert(ret.msg);
}
});
document.getElementById('__edit_fetch').addEventListener(clickEvt,async(e)=>{
e.preventDefault();
let u=document.getElementById('__edit_url').value.trim();
if(!u)return alert('填写网址');
const r=await fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:`action=get_website_title&url=${encodeURIComponent(u)}`});
const d=await r.json();
if(d.success)document.getElementById('__edit_title').value=d.data.title;
});
document.querySelector('#__edit_modal .confirm').addEventListener(clickEvt,async(e)=>{
e.preventDefault();
const bid=document.getElementById('__edit_id').value;
const u=document.getElementById('__edit_url').value.trim();
const t=document.getElementById('__edit_title').value.trim();
const desc=document.getElementById('__edit_desc').value.trim();
const g=document.getElementById('__edit_group_sel').value;
const res=await fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:`action=edit_bookmark&id=${bid}&url=${encodeURIComponent(u)}&title=${encodeURIComponent(t)}&desc=${encodeURIComponent(desc)}&group=${g}`});
const ret=await res.json();
if(ret.success){
const item=document.querySelector(`.bm-item[data-id="${bid}"]`);
item.dataset.group=g;
item.dataset.desc=desc;
document.getElementById('__edit_modal').classList.remove('show');
loadFavicon(item,item.dataset.host);
filterList();
}else alert(ret.msg);
});
document.getElementById('__new_group_btn').addEventListener(clickEvt,(e)=>{
e.preventDefault();
document.getElementById('__new_group_name').value='';
document.getElementById('__new_group_modal').classList.add('show');
});
document.querySelector('#__new_group_modal .confirm').addEventListener(clickEvt,async(e)=>{
e.preventDefault();
let n=document.getElementById('__new_group_name').value.trim();
if(!n)return alert('分组名不能为空');
const r=await fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:`action=add_group&name=${encodeURIComponent(n)}`});
const d=await r.json();
if(d.success){
document.getElementById('__new_group_modal').classList.remove('show');
refreshGroups();
}
});
document.querySelector('#__rename_group_modal .confirm').addEventListener(clickEvt,async(e)=>{
e.preventDefault();
const gid=document.getElementById('__rename_group_id').value;
const name=document.getElementById('__rename_group_name').value.trim();
const r=await fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:`action=rename_group&id=${gid}&name=${encodeURIComponent(name)}`});
const d=await r.json();
if(d.success){
document.getElementById('__rename_group_modal').classList.remove('show');
refreshGroups();
}
});
document.getElementById('__export_btn').addEventListener(clickEvt,(e)=>{
e.preventDefault();
(async()=>{
const res=await fetch(AJAX_URL,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'action=export_bm'});
const d=await res.json();
const jsonBlob=new Blob([JSON.stringify(d.data,null,2)],{type:'application/json'});
let a=document.createElement('a');
a.href=URL.createObjectURL(jsonBlob);
a.download='书签备份_'+Date.now()+'.json';a.click();URL.revokeObjectURL(a.href);
let html='<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n';
d.data.groups.forEach(g=>{
html+=`<DT><H3>${g.name}</H3><DL><p>\n`;
d.data.bookmarks.forEach(bm=>{
if(bm.group===g.id)html+=`<DT><A HREF="${bm.url}">${bm.title}</A>\n`;
});
html+='</DL><p>\n';
});
html+='</DL><p>';
const htmlBlob=new Blob([html],{type:'text/html'});
a=document.createElement('a');a.href=URL.createObjectURL(htmlBlob);a.download='书签备份_'+Date.now()+'.html';a.click();URL.revokeObjectURL(a.href);
})();
});
document.getElementById('__import_btn').addEventListener(clickEvt,(e)=>{
e.preventDefault();
document.getElementById('__import_modal').classList.add('show');
});
document.getElementById('__file_upload_area').addEventListener(clickEvt,()=>{
document.getElementById('__import_file').click();
});
document.getElementById('__do_import').addEventListener(clickEvt,async(e)=>{
e.preventDefault();
const file=document.getElementById('__import_file').files[0];
if(!file)return alert('请选择文件');
const fd=new FormData();fd.append('action','import_bm');fd.append('import_file',file);
const res=await fetch(AJAX_URL,{method:'POST',body:fd});
const d=await res.json();
if(d.success){alert('导入成功');location.reload();}else alert(d.msg||'导入失败');
});
document.querySelectorAll('.cancel').forEach(c=>{
c.addEventListener(clickEvt,()=>{
document.querySelectorAll('.modal').forEach(m=>m.classList.remove('show'));
addSubmitLock = false;
let saveBtn = document.getElementById('add_save_btn');
saveBtn.disabled = false;
saveBtn.innerText = '保存';
});
});
document.getElementById('__search_key').addEventListener('input',filterList);
});
})();
</script>
<?php
return ob_get_clean();
});
二、新建页面
1.标题写入“我的云书签” 内容写入
[my_bookmark请把此中文删掉全部]
打开页面为https://你的域名/我的云书签
