一为主题用Code Snippets添加云端书签功能教程(PHP代码版)

1. 安装插件“Code Snippets”—启用 点击Code Snippets子菜单+Add Snippet—点击Add Your Custom Code (New Snippet)弹窗弹框选择PHP Snippet

一为主题用Code Snippets添加云端书签功能教程(PHP代码版) 一为主题用Code Snippets添加云端书签功能教程(PHP代码版)

2. 进入新建的PHP Snippet截面 第一行输入“云书签系统”右上角的“Active”要点击显示按钮显示蓝色即为开

一为主题用Code Snippets添加云端书签功能教程(PHP代码版)

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://你的域名/我的云书签

 

© 版权声明

相关文章