[js] 解决数据库编辑后直接排序或被删问题

一个矫情的需求: 编辑的时候 别给我按照我之前的额情况给排序或者筛掉 下次点进来你在刷新吧

/**
 * 思源笔记数据库编辑防护脚本 v2.1
 * 
 * 功能:在编辑数据库单元格时,禁止自动排序和过滤,防止内容意外被移动或隐藏
 * 
 * 使用方法:
 * 1. 将此 JS 代码添加到 思源笔记 - 设置 - 外观 - 代码片段 - JS 片段中
 * 2. 重启思源笔记或刷新页面即可生效
 * 
 * 更新日志:
 * v2.1 - 优化性能和用户体验
 *      - 添加调试模式开关
 *      - 添加拦截统计
 *      - 优化日志输出
 *      - 添加配置选项
 * v2.0 - 完全重写拦截逻辑,采用更精准的编辑状态检测
 */

(function() {
    'use strict';
  
    // ==================== 配置选项 ====================
    const CONFIG = {
        debugMode: false,              // 是否启用详细调试日志
        showNotification: false,       // 是否显示拦截提示
        editDelay: 2000,              // 编辑后保持保护的时间(毫秒)
        focusOutDelay: 500,           // 失去焦点后保持保护的时间(毫秒)
        renderDelay: 500,             // 视图渲染延迟时间(毫秒)
    };
  
    // 全局启用/禁用状态(从 localStorage 读取)
    let globalEnabled = localStorage.getItem('dbProtection_enabled') !== 'false'; // 默认启用
  
    // 检测是否为移动端
    const isMobile = () => {
        return window.siyuan?.mobile !== undefined || 
               /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    };
  
    // ==================== 全局状态 ====================
    let isEditingDatabase = false;
    let isEditingProtyle = false;
    let editingTimeout = null;
    let lastOperationType = null;
  
    // 统计信息
    const stats = {
        blockedSorts: 0,
        blockedFilters: 0,
        delayedRenders: 0,
        startTime: Date.now()
    };
  
    // ==================== 日志工具 ====================
    const log = {
        info: (msg, ...args) => console.log(`[数据库编辑防护] ${msg}`, ...args),
        warn: (msg, ...args) => console.warn(`[数据库编辑防护] ${msg}`, ...args),
        error: (msg, ...args) => console.error(`[数据库编辑防护] ${msg}`, ...args),
        debug: (msg, ...args) => {
            if (CONFIG.debugMode) {
                console.log(`[数据库编辑防护 DEBUG] ${msg}`, ...args);
            }
        }
    };
  
    log.info('v2.2 脚本开始加载...');
  
    /**
     * 设置编辑状态
     */
    function setEditingState(type, value) {
        if (type === 'database') {
            isEditingDatabase = value;
            log.debug(value ? '🔒 数据库编辑模式已启用' : '🔓 数据库编辑模式已关闭');
        } else if (type === 'protyle') {
            isEditingProtyle = value;
            log.debug(value ? '🔒 编辑器编辑模式已启用' : '🔓 编辑器编辑模式已关闭');
        }
    }
  
    /**
     * 延迟重置编辑状态(防抖)
     */
    function delayResetEditingState(type, delay = 1000) {
        if (editingTimeout) {
            clearTimeout(editingTimeout);
        }
        editingTimeout = setTimeout(() => {
            setEditingState(type, false);
        }, delay);
    }
  
    /**
     * 监听数据库单元格的编辑事件
     */
    function monitorDatabaseEditing() {
        // 监听所有的 focusin 事件
        document.addEventListener('focusin', (e) => {
            const target = e.target;
          
            // 排除下拉菜单和面板(单选/多选)
            if (target.closest('.av__panel, .b3-menu, .b3-select, .protyle-util')) {
                return;
            }
          
            // 检查是否在数据库区域内
            const avCell = target.closest('.av__cell, .av__calc');
            if (avCell) {
                setEditingState('database', true);
                return;
            }
          
            // 检查是否在编辑器内
            const protyleWysiwyg = target.closest('.protyle-wysiwyg');
            if (protyleWysiwyg || target.getAttribute('contenteditable') === 'true') {
                setEditingState('protyle', true);
                return;
            }
        }, true);
      
        // 监听所有的 input 事件
        document.addEventListener('input', (e) => {
            const target = e.target;
          
            // 排除下拉菜单和面板
            if (target.closest('.av__panel, .b3-menu, .b3-select, .protyle-util')) {
                return;
            }
          
            // 检查是否在数据库区域内输入
            const avCell = target.closest('.av__cell, .av__calc, .av');
            if (avCell) {
                setEditingState('database', true);
                delayResetEditingState('database', CONFIG.editDelay);
                return;
            }
          
            // 检查是否在编辑器内输入
            const protyleWysiwyg = target.closest('.protyle-wysiwyg');
            if (protyleWysiwyg) {
                setEditingState('protyle', true);
                delayResetEditingState('protyle', CONFIG.editDelay);
                return;
            }
        }, true);
      
        // 监听所有的 click 事件(点击数据库单元格)
        document.addEventListener('click', (e) => {
            const target = e.target;
          
            // 排除下拉菜单、面板的点击(不触发编辑状态)
            if (target.closest('.av__panel, .b3-menu, .b3-select, .protyle-util')) {
                // 点击选项只是操作,不改变编辑状态
                // 如果已经在编辑中,保持编辑状态,延迟渲染
                return;
            }
          
            // 点击数据库单元格
            const avCell = target.closest('.av__cell');
            if (avCell && !avCell.classList.contains('av__cell--header')) {
                setEditingState('database', true);
                delayResetEditingState('database', CONFIG.editDelay);
                return;
            }
        }, true);
      
        // 监听 focusout 事件
        document.addEventListener('focusout', () => {
            // 延迟重置状态,给后续操作留时间
            delayResetEditingState('database', CONFIG.focusOutDelay);
            delayResetEditingState('protyle', CONFIG.focusOutDelay);
        }, true);
      
        // 监听键盘事件(在数据库中输入)
        document.addEventListener('keydown', (e) => {
            const target = e.target;
            const avCell = target.closest('.av__cell, .av__calc, .av');
            if (avCell) {
                setEditingState('database', true);
                delayResetEditingState('database', CONFIG.editDelay);
            }
        }, true);
      
        log.info('✓ 编辑状态监听器已启动');
    }
  
    /**
     * 拦截排序和过滤请求
     */
    function interceptRequests() {
        const originalFetch = window.fetch;
      
        window.fetch = function(...args) {
            const [url, options] = args;
          
            // 调试:记录所有 API 请求
            log.debug('📡 API请求:', url);
          
            // 拦截数据库视图渲染请求(检查全局开关)
            if (globalEnabled && url && url.includes('/api/av/renderAttributeView') && (isEditingDatabase || isEditingProtyle)) {
                // 如果是插入操作,立即渲染
                if (lastOperationType === 'insert') {
                    log.debug('ℹ️ 检测到插入操作,允许立即渲染');
                    lastOperationType = null;
                    return originalFetch.apply(this, args);
                }
              
                // 如果不是更新操作(可能是其他操作如添加表格等),也立即渲染
                if (lastOperationType !== 'update') {
                    log.debug('ℹ️ 非更新操作,允许立即渲染');
                    lastOperationType = null;
                    return originalFetch.apply(this, args);
                }
              
                // 所有单元格更新操作(文本、单选、多选等)都延迟渲染
                stats.delayedRenders++;
                log.debug('⚠️ 延迟渲染视图(单元格编辑中,包括单选/多选)');
              
                return new Promise((resolve) => {
                    const executeRender = () => {
                        log.debug('✅ 编辑完成,执行视图渲染');
                        lastOperationType = null;
                        originalFetch.apply(this, args).then(resolve);
                    };
                  
                    const checkAndExecute = () => {
                        if (!isEditingDatabase && !isEditingProtyle) {
                            executeRender();
                        } else {
                            log.debug('⏳ 仍在编辑中,继续等待...');
                            setTimeout(checkAndExecute, CONFIG.renderDelay);
                        }
                    };
                  
                    setTimeout(checkAndExecute, CONFIG.renderDelay);
                });
            }
          
            // 拦截 transactions API
            if (url && url.includes('/api/transactions') && options && options.body) {
                log.debug('🔍 检测到 transactions 请求');
                log.debug('当前编辑状态:', { isEditingDatabase, isEditingProtyle });
              
                try {
                    const body = JSON.parse(options.body);
                    log.debug('📦 请求体:', body);
                  
                    if (body.transactions && Array.isArray(body.transactions)) {
                        log.debug(`发现 ${body.transactions.length} 个 transaction`);
                      
                        let hasBlocked = false;
                        const filteredTransactions = [];
                      
                        body.transactions.forEach((transaction, tIndex) => {
                            if (!transaction.doOperations || !Array.isArray(transaction.doOperations)) {
                                filteredTransactions.push(transaction);
                                return;
                            }
                          
                            log.debug(`Transaction ${tIndex} 包含 ${transaction.doOperations.length} 个操作`);
                          
                            // 详细调试模式下才打印所有操作
                            if (CONFIG.debugMode) {
                                log.debug('========== 操作列表 START ==========');
                                try {
                                    log.debug('原始数组:', JSON.stringify(transaction.doOperations, null, 2));
                                } catch(e) {
                                    log.error('JSON序列化失败:', e);
                                }
                            }
                          
                            // 记录操作类型(优先级:insert > update > 其他)
                            let foundInsert = false;
                            let foundUpdate = false;
                          
                            for (let i = 0; i < transaction.doOperations.length; i++) {
                                const op = transaction.doOperations[i];
                                log.debug(`操作 #${i}: ${op.action}`);
                              
                                // 插入类操作(优先级最高)
                                if (op.action === 'insertAttrViewBlock' || 
                                    op.action === 'appendAttrViewBlock' || 
                                    op.action === 'insert' ||
                                    op.action === 'addAttrViewCol' ||
                                    op.action === 'addAttrViewView') {
                                    foundInsert = true;
                                }
                                // 所有单元格更新操作(包括文本、单选、多选等)
                                else if (op.action === 'updateAttrViewCell') {
                                    foundUpdate = true;
                                }
                            }
                          
                            // 设置操作类型(插入 > 更新 > 其他)
                            if (foundInsert) {
                                lastOperationType = 'insert';
                            } else if (foundUpdate) {
                                lastOperationType = 'update'; // 所有更新操作(包括单选/多选)都延迟
                            } else {
                                lastOperationType = 'other'; // 其他操作
                            }
                          
                            if (CONFIG.debugMode) {
                                log.debug('========== 操作列表 END ==========');
                                log.debug('最后操作类型:', lastOperationType);
                            }
                          
                            const filteredOps = transaction.doOperations.filter((op, opIndex) => {
                                // 检查全局开关,如果正在编辑数据库或编辑器,阻止排序和过滤操作
                                if (globalEnabled && (isEditingDatabase || isEditingProtyle) && 
                                    (op.action === 'setAttrViewSorts' || op.action === 'setAttrViewFilters')) {
                                  
                                    // 更新统计
                                    if (op.action === 'setAttrViewSorts') {
                                        stats.blockedSorts++;
                                    } else if (op.action === 'setAttrViewFilters') {
                                        stats.blockedFilters++;
                                    }
                                  
                                    log.warn(`⛔ 已阻止 ${op.action}(编辑中)`);
                                    hasBlocked = true;
                                  
                                    // 显示提示(如果启用)
                                    if (CONFIG.showNotification && window.siyuan && window.siyuan.showMessage) {
                                        window.siyuan.showMessage('⚠️ 编辑中,已暂停排序/过滤', 3000, 'info');
                                    }
                                  
                                    return false;
                                }
                                log.debug(`✓ 保留操作 ${opIndex}: ${op.action}`);
                                return true;
                            });
                          
                            if (filteredOps.length > 0) {
                                filteredTransactions.push({
                                    ...transaction,
                                    doOperations: filteredOps
                                });
                            }
                        });
                      
                        // 如果所有操作都被过滤,返回空成功响应
                        if (filteredTransactions.length === 0 && hasBlocked) {
                            log.debug('⏭️ 请求已被完全拦截');
                            return Promise.resolve(new Response(JSON.stringify({
                                code: 0,
                                msg: '',
                                data: [{doOperations: []}]
                            }), {
                                status: 200,
                                headers: {'Content-Type': 'application/json'}
                            }));
                        }
                      
                        // 更新请求体
                        if (hasBlocked) {
                            log.debug('✂️ 已过滤操作,更新请求体');
                            body.transactions = filteredTransactions;
                            options.body = JSON.stringify(body);
                        }
                    }
                } catch (e) {
                    log.error('❌ 解析请求失败:', e);
                }
            }
          
            return originalFetch.apply(this, args);
        };
      
        log.info('✓ 请求拦截器已注入');
    }
  
    /**
     * 创建UI开关按钮
     */
    function createToggleButton() {
        // 查找数据库工具栏
        const observer = new MutationObserver((mutations) => {
            // 查找所有数据库视图头部
            document.querySelectorAll('.av__header').forEach((header) => {
                // 检查是否已经添加过按钮
                if (header.querySelector('.db-protection-toggle')) {
                    return;
                }
              
                // 找到工具栏按钮区域(在搜索按钮旁边)
                const searchBtn = header.querySelector('[data-type="av-search-icon"]');
                if (!searchBtn) return;
              
                // 创建开关按钮
                const toggleBtn = document.createElement('button');
                toggleBtn.className = 'block__icon ariaLabel db-protection-toggle';
                toggleBtn.setAttribute('data-position', '8south');
              
                // 移动端使用简化的提示(因为长按才能看到)
                const mobileMode = isMobile();
                toggleBtn.setAttribute('aria-label', globalEnabled 
                    ? (mobileMode ? '🔒 锁定排序(已启用)' : '🔒 编辑时锁定排序/过滤(已启用)\n点击关闭:允许编辑时自动排序')
                    : (mobileMode ? '🔓 正常排序' : '🔓 编辑时自动排序/过滤(正常模式)\n点击启用:编辑时锁定排序'));
                toggleBtn.style.marginRight = '4px';
              
                // 设置图标和颜色
                const updateButtonStyle = () => {
                    toggleBtn.innerHTML = globalEnabled 
                        ? '<svg><use xlink:href="#iconLock"></use></svg>'  // 锁定图标
                        : '<svg><use xlink:href="#iconUnlock"></use></svg>'; // 解锁图标
                    toggleBtn.style.color = globalEnabled ? '#4CAF50' : '#999'; // 绿色=启用,灰色=禁用
                    toggleBtn.setAttribute('aria-label', globalEnabled 
                        ? (mobileMode ? '🔒 锁定排序(已启用)' : '🔒 编辑时锁定排序/过滤(已启用)\n点击关闭:允许编辑时自动排序')
                        : (mobileMode ? '🔓 正常排序' : '🔓 编辑时自动排序/过滤(正常模式)\n点击启用:编辑时锁定排序'));
                };
              
                updateButtonStyle();
              
                // 点击事件
                toggleBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    e.preventDefault();
                  
                    // 切换状态
                    globalEnabled = !globalEnabled;
                  
                    // 保存到 localStorage
                    localStorage.setItem('dbProtection_enabled', String(globalEnabled));
                  
                    // 更新按钮样式
                    updateButtonStyle();
                  
                    // 更新所有按钮(因为可能有多个数据库视图)
                    document.querySelectorAll('.db-protection-toggle').forEach(btn => {
                        if (btn !== toggleBtn) {
                            btn.innerHTML = toggleBtn.innerHTML;
                            btn.style.color = toggleBtn.style.color;
                            btn.setAttribute('aria-label', toggleBtn.getAttribute('aria-label'));
                        }
                    });
                  
                    // 显示提示(更详细的说明)
                    if (window.siyuan && window.siyuan.showMessage) {
                        const message = globalEnabled 
                            ? '🔒 已启用:编辑单元格时不会自动排序/过滤(内容不会被移走)' 
                            : '🔓 已关闭:编辑单元格时会自动排序/过滤(恢复正常行为)';
                        window.siyuan.showMessage(message, 3000, globalEnabled ? 'info' : 'error');
                    }
                  
                    log.info(globalEnabled ? '✅ 保护已启用' : '❌ 保护已禁用');
                });
              
                // 插入到搜索按钮之前
                searchBtn.parentElement.insertBefore(toggleBtn, searchBtn);
                searchBtn.parentElement.insertBefore(document.createElement('div').cloneNode(), searchBtn).className = 'fn__space';
            });
        });
      
        // 开始观察DOM变化
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
      
        log.info('✓ UI开关按钮已创建');
    }
  
    /**
     * 初始化
     */
    function init() {
        if (typeof window.siyuan === 'undefined') {
            log.debug('⏳ 等待思源 API 加载...');
            setTimeout(init, 300);
            return;
        }
      
        // 启动监听
        monitorDatabaseEditing();
      
        // 拦截请求
        interceptRequests();
      
        // 创建UI开关
        createToggleButton();
      
        log.info('✅ 启动成功!');
        log.info(`📱 运行环境:${isMobile() ? '移动端' : '桌面端'}`);
        log.info(`📌 当前状态:${globalEnabled ? '🔒 已启用 - 编辑时锁定排序/过滤' : '🔓 已禁用 - 编辑时正常排序/过滤'}`);
        log.info(`💡 提示:可在数据库工具栏点击锁图标切换${isMobile() ? '(长按查看提示)' : ''}`);
      
        // 暴露全局接口
        window.siyuanDbProtection = {
            version: '2.2',
            get enabled() { return globalEnabled; },
            get isMobile() { return isMobile(); },
            config: CONFIG,
          
            // 状态查询
            getState: () => ({
                globalEnabled,
                isMobile: isMobile(),
                isEditingDatabase,
                isEditingProtyle,
                lastOperationType
            }),
          
            // 统计信息
            getStats: () => ({
                ...stats,
                uptime: Math.floor((Date.now() - stats.startTime) / 1000),
                totalBlocked: stats.blockedSorts + stats.blockedFilters
            }),
          
            // 全局开关控制
            toggle: () => {
                globalEnabled = !globalEnabled;
                localStorage.setItem('dbProtection_enabled', String(globalEnabled));
              
                // 更新所有UI按钮
                document.querySelectorAll('.db-protection-toggle').forEach(btn => {
                    btn.innerHTML = globalEnabled 
                        ? '<svg><use xlink:href="#iconLock"></use></svg>' 
                        : '<svg><use xlink:href="#iconUnlock"></use></svg>';
                    btn.style.color = globalEnabled ? '#4CAF50' : '#999';
                    btn.setAttribute('aria-label', globalEnabled 
                        ? '🔒 编辑时锁定排序/过滤(已启用)\n点击关闭:允许编辑时自动排序' 
                        : '🔓 编辑时自动排序/过滤(正常模式)\n点击启用:编辑时锁定排序');
                });
              
                log.info(globalEnabled 
                    ? '✅ 已启用:编辑单元格时锁定排序/过滤' 
                    : '❌ 已禁用:编辑单元格时自动排序/过滤');
                return globalEnabled;
            },
          
            // 编辑状态控制(已废弃,保留用于兼容)
            enable: () => setEditingState('database', true),
            disable: () => {
                setEditingState('database', false);
                setEditingState('protyle', false);
            },
          
            // 配置功能
            enableDebug: () => {
                CONFIG.debugMode = true;
                log.info('✓ 调试模式已启用');
            },
            disableDebug: () => {
                CONFIG.debugMode = false;
                log.info('✓ 调试模式已关闭');
            },
            toggleNotification: () => {
                CONFIG.showNotification = !CONFIG.showNotification;
                log.info(`✓ 通知提示已${CONFIG.showNotification ? '启用' : '关闭'}`);
            },
          
            // 统计重置
            resetStats: () => {
                stats.blockedSorts = 0;
                stats.blockedFilters = 0;
                stats.delayedRenders = 0;
                stats.startTime = Date.now();
                log.info('✓ 统计数据已重置');
            },
          
            // 帮助信息
            help: () => {
                const mobile = isMobile();
                console.log(`
╔══════════════════════════════════════════════════════════╗
║     思源笔记数据库编辑防护 v2.2 - 使用帮助               ║
╠══════════════════════════════════════════════════════════╣
║ 🎯 功能说明:                                             ║
║  🔒 启用时:编辑单元格时锁定排序/过滤                   ║
║            防止编辑中的行被移走或过滤隐藏               ║
║  📝 保护范围:文本、单选、多选、数字等所有字段           ║
║  ⏱️  完成编辑后 0.5 秒自动应用排序/过滤                  ║
║  ✅ 智能识别:添加新行/列等操作立即显示                  ║
║  🔓 禁用时:恢复默认行为(编辑时自动排序/过滤)         ║
║  📱 当前环境:${mobile ? '移动端 ✓' : '桌面端 ✓'}                                     ║
║                                                           ║
║ 📊 查询命令:                                             ║
║  .enabled         - 查看是否启用(true/false)            ║
║  .isMobile        - 查看是否为移动端                      ║
║  .getState()      - 查看当前编辑状态                      ║
║  .getStats()      - 查看拦截统计                          ║
║  .config          - 查看配置选项                          ║
║                                                           ║
║ 🎛️  全局开关:                                            ║
║  .toggle()        - 切换启用/禁用                         ║
║  💡 推荐:在数据库工具栏点击锁图标切换                   ║
${mobile ? '║     移动端:长按图标可查看提示                         ║' : ''}
║                                                           ║
║ 🔧 调试命令:                                             ║
║  .enableDebug()   - 启用详细日志                          ║
║  .disableDebug()  - 关闭详细日志                          ║
║  .toggleNotification() - 切换拦截提示                     ║
║                                                           ║
║ 📈 其他命令:                                             ║
║  .resetStats()    - 重置统计数据                          ║
║  .help()          - 显示此帮助                            ║
╚══════════════════════════════════════════════════════════╝
                `);
            }
        };
      
        // 简化访问
        window.dbp = window.siyuanDbProtection;
    }
  
    // 启动
    log.debug('🚀 准备初始化...');
    log.debug('document.readyState:', document.readyState);
  
    if (document.readyState === 'loading') {
        log.debug('等待 DOMContentLoaded...');
        document.addEventListener('DOMContentLoaded', () => {
            log.debug('DOMContentLoaded 触发');
            init();
        });
    } else {
        log.debug('DOM 已就绪,延迟启动...');
        setTimeout(() => {
            log.debug('开始初始化...');
            init();
        }, 1000);
    }
  
})();

// 立即执行标记
console.log('[数据库编辑防护] ✓ v2.1 脚本文件已加载');


  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28443 引用 • 119762 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    285 引用 • 1984 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Access
    1 引用 • 3 回帖 • 13 关注
  • jsoup

    jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 jQuery 的操作方法来取出和操作数据。

    6 引用 • 1 回帖 • 517 关注
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    29 引用 • 202 回帖 • 54 关注
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    9 引用 • 75 回帖 • 1 关注
  • OpenStack

    OpenStack 是一个云操作系统,通过数据中心可控制大型的计算、存储、网络等资源池。所有的管理通过前端界面管理员就可以完成,同样也可以通过 Web 接口让最终用户部署资源。

    10 引用 • 9 关注
  • Elasticsearch

    Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

    117 引用 • 99 回帖 • 191 关注
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖 • 9 关注
  • Sphinx

    Sphinx 是一个基于 SQL 的全文检索引擎,可以结合 MySQL、PostgreSQL 做全文搜索,它可以提供比数据库本身更专业的搜索功能,使得应用程序更容易实现专业化的全文检索。

    1 引用 • 259 关注
  • Excel
    32 引用 • 29 回帖 • 1 关注
  • NGINX

    NGINX 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。 NGINX 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日。

    316 引用 • 547 回帖 • 4 关注
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    16 引用 • 143 回帖 • 6 关注
  • 域名

    域名(Domain Name),简称域名、网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。

    43 引用 • 208 回帖
  • 禅道

    禅道是一款国产的开源项目管理软件,她的核心管理思想基于敏捷方法 scrum,内置了产品管理和项目管理,同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能,在一个软件中就可以将软件研发中的需求、任务、bug、用例、计划、发布等要素有序的跟踪管理起来,完整地覆盖了项目管理的核心流程。

    11 引用 • 15 回帖 • 1 关注
  • 知乎

    知乎是网络问答社区,连接各行各业的用户。用户分享着彼此的知识、经验和见解,为中文互联网源源不断地提供多种多样的信息。

    10 引用 • 66 回帖
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    269 引用 • 666 回帖 • 1 关注
  • Solo

    Solo 是一款小而美的开源博客系统,专为程序员设计。Solo 有着非常活跃的社区,可将文章作为帖子推送到社区,来自社区的回帖将作为博客评论进行联动(具体细节请浏览 B3log 构思 - 分布式社区网络)。

    这是一种全新的网络社区体验,让热爱记录和分享的你不再感到孤单!

    1449 引用 • 10092 回帖 • 489 关注
  • 宕机

    宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。

    13 引用 • 82 回帖 • 75 关注
  • LeetCode

    LeetCode(力扣)是一个全球极客挚爱的高质量技术成长平台,想要学习和提升专业能力从这里开始,充足技术干货等你来啃,轻松拿下 Dream Offer!

    209 引用 • 72 回帖 • 1 关注
  • Kotlin

    Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,由 JetBrains 设计开发并开源。Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言。

    19 引用 • 33 回帖 • 87 关注
  • Telegram

    Telegram 是一个非盈利性、基于云端的即时消息服务。它提供了支持各大操作系统平台的开源的客户端,也提供了很多强大的 APIs 给开发者创建自己的客户端和机器人。

    5 引用 • 35 回帖
  • 千千插件

    千千块(自定义块 css 和 js)
    可以用 ai 提示词来无限创作思源笔记

    32 引用 • 69 回帖 • 1 关注
  • 京东

    京东是中国最大的自营式电商企业,2015 年第一季度在中国自营式 B2C 电商市场的占有率为 56.3%。2014 年 5 月,京东在美国纳斯达克证券交易所正式挂牌上市(股票代码:JD),是中国第一个成功赴美上市的大型综合型电商平台,与腾讯、百度等中国互联网巨头共同跻身全球前十大互联网公司排行榜。

    14 引用 • 102 回帖 • 261 关注
  • Notion

    Notion - The all-in-one workspace for your notes, tasks, wikis, and databases.

    10 引用 • 80 回帖 • 1 关注
  • 星云链

    星云链是一个开源公链,业内简单的将其称为区块链上的谷歌。其实它不仅仅是区块链搜索引擎,一个公链的所有功能,它基本都有,比如你可以用它来开发部署你的去中心化的 APP,你可以在上面编写智能合约,发送交易等等。3 分钟快速接入星云链 (NAS) 测试网

    3 引用 • 16 回帖
  • OpenShift

    红帽提供的 PaaS 云,支持多种编程语言,为开发人员提供了更为灵活的框架、存储选择。

    14 引用 • 20 回帖 • 687 关注
  • OkHttp

    OkHttp 是一款 HTTP & HTTP/2 客户端库,专为 Android 和 Java 应用打造。

    16 引用 • 6 回帖 • 99 关注
  • flomo

    flomo 是新一代 「卡片笔记」 ,专注在碎片化时代,促进你的记录,帮你积累更多知识资产。

    6 引用 • 144 回帖