前期准备与项目搭建

在开始项目开发前,请确保已完成以下准备工作:

  1. 搭建完整的Cordova开发环境

  2. 注册GitCode账户并获取访问令牌

  3. 熟悉GitCode OpenAPI的基本使用

详细的前期准备步骤可参考:https://blog.csdn.net/yanwydxf/article/details/155266874?spm=1001.2014.3001.5502

口袋工具页面构建

界面设计理念

我们的GitCode口袋工具采用现代化设计语言,以简洁、直观的用户体验为核心,确保用户在移动设备上也能获得良好的操作体验。

页面结构分析

启动 DevEco Studio 打开 cordova 创建的项目,在 index.html 编辑页面:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>口袋工具 - GitCode搜索</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        :root {
            --primary-color: #4a6ee0;
            --primary-light: #e8edff;
            --secondary-color: #6c757d;
            --success-color: #28a745;
            --warning-color: #ffc107;
            --danger-color: #dc3545;
            --light-color: #f8f9fa;
            --dark-color: #343a40;
            --border-radius: 8px;
            --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            --transition: all 0.3s ease;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

        body {
            background-color: #f5f7fa;
            color: var(--dark-color);
            line-height: 1.6;
            min-height: 100vh;
        }

        header {
            background: linear-gradient(135deg, var(--primary-color), #6a82fb);
            color: white;
            padding: 2rem 1rem;
            text-align: center;
            border-bottom-left-radius: 20px;
            border-bottom-right-radius: 20px;
            margin-bottom: 2rem;
            box-shadow: var(--box-shadow);
        }

        header h1 {
            font-size: 2.2rem;
            margin-bottom: 0.5rem;
            font-weight: 700;
        }

        header p {
            font-size: 1.1rem;
            opacity: 0.9;
            max-width: 600px;
            margin: 0 auto;
        }

        main {
            max-width: 800px;
            margin: 0 auto;
            padding: 0 1.5rem;
        }

        .card {
            background: white;
            border-radius: var(--border-radius);
            box-shadow: var(--box-shadow);
            padding: 2rem;
            margin-bottom: 2rem;
            transition: var(--transition);
        }

        .card:hover {
            transform: translateY(-5px);
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
        }

        .form-group {
            margin-bottom: 1.5rem;
        }

        label {
            display: block;
            margin-bottom: 0.5rem;
            font-weight: 600;
            color: var(--dark-color);
        }

        input, select {
            width: 100%;
            padding: 0.75rem 1rem;
            border: 1px solid #ddd;
            border-radius: var(--border-radius);
            font-size: 1rem;
            transition: var(--transition);
        }

        input:focus, select:focus {
            outline: none;
            border-color: var(--primary-color);
            box-shadow: 0 0 0 3px var(--primary-light);
        }

        .btn {
            display: inline-block;
            background: var(--primary-color);
            color: white;
            border: none;
            padding: 0.75rem 2rem;
            border-radius: var(--border-radius);
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: var(--transition);
            text-align: center;
        }

        .btn:hover {
            background: #3a5ed0;
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(74, 110, 224, 0.3);
        }

        .btn:active {
            transform: translateY(0);
        }

        .btn:disabled {
            background: var(--secondary-color);
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .btn-secondary {
            background: var(--secondary-color);
        }

        .btn-secondary:hover {
            background: #5a6268;
        }

        .button-group {
            display: flex;
            gap: 1rem;
        }

        #result {
            min-height: 200px;
            background: white;
            border-radius: var(--border-radius);
            padding: 1.5rem;
            box-shadow: var(--box-shadow);
            margin-top: 2rem;
        }

        .result-placeholder {
            text-align: center;
            color: var(--secondary-color);
            padding: 3rem 1rem;
        }

        .result-placeholder i {
            font-size: 3rem;
            margin-bottom: 1rem;
            color: #ddd;
        }

        .loading {
            display: none;
            text-align: center;
            padding: 2rem;
        }

        .spinner {
            border: 4px solid rgba(0, 0, 0, 0.1);
            border-radius: 50%;
            border-top: 4px solid var(--primary-color);
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 0 auto 1rem;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .result-item {
            border-bottom: 1px solid #eee;
            padding: 1rem 0;
            display: flex;
            align-items: flex-start;
        }

        .result-item:last-child {
            border-bottom: none;
        }

        .result-avatar {
            width: 50px;
            height: 50px;
            border-radius: 50%;
            margin-right: 1rem;
            flex-shrink: 0;
            object-fit: cover;
        }

        .result-content {
            flex: 1;
        }

        .result-title {
            font-weight: 600;
            margin-bottom: 0.5rem;
            color: var(--primary-color);
            display: flex;
            align-items: center;
        }

        .result-title a {
            color: inherit;
            text-decoration: none;
        }

        .result-title a:hover {
            text-decoration: underline;
        }

        .result-desc {
            color: var(--secondary-color);
            font-size: 0.9rem;
            margin-bottom: 0.5rem;
        }

        .result-meta {
            display: flex;
            gap: 1rem;
            font-size: 0.8rem;
            color: var(--secondary-color);
            flex-wrap: wrap;
        }

        .result-meta span {
            display: flex;
            align-items: center;
        }

        .result-meta i {
            margin-right: 0.3rem;
        }

        .error-message {
            background-color: #ffeaea;
            color: var(--danger-color);
            padding: 1rem;
            border-radius: var(--border-radius);
            margin-bottom: 1rem;
            border-left: 4px solid var(--danger-color);
        }

        .success-message {
            background-color: #e8f5e9;
            color: var(--success-color);
            padding: 1rem;
            border-radius: var(--border-radius);
            margin-bottom: 1rem;
            border-left: 4px solid var(--success-color);
        }

        .no-results {
            text-align: center;
            padding: 2rem;
            color: var(--secondary-color);
        }

        .result-count {
            margin-bottom: 1rem;
            padding: 0.5rem;
            background: #f8f9fa;
            border-radius: 4px;
            font-weight: 600;
        }

        footer {
            text-align: center;
            padding: 2rem 1rem;
            color: var(--secondary-color);
            font-size: 0.9rem;
            margin-top: 2rem;
        }

        .token-info {
            font-size: 0.8rem;
            color: var(--secondary-color);
            margin-top: 0.5rem;
        }

        @media (min-width: 768px) {
            .form-row {
                display: flex;
                gap: 1.5rem;
            }

            .form-group {
                flex: 1;
            }

            .button-group {
                justify-content: flex-start;
            }

            .btn {
                width: auto;
                min-width: 150px;
            }
        }

        @media (max-width: 767px) {
            .button-group {
                flex-direction: column;
            }

            .btn {
                width: 100%;
            }
        }
    </style>
</head>
<body>
<header>
    <h1><i class="fas fa-search"></i> 口袋工具</h1>
    <p>基于GitCode API的智能搜索工具,快速找到您需要的用户和仓库</p>
</header>

<main>
    <div class="card">
        <div class="form-group">
            <label for="key"><i class="fas fa-key"></i> 访问令牌</label>
            <input type="text" id="key" placeholder="请输入您的GitCode个人访问令牌" value="" />
            <div class="token-info">
                您可以在GitCode的"设置" → "访问令牌"中创建个人访问令牌
            </div>
        </div>

        <div class="form-row">
            <div class="form-group">
                <label for="category"><i class="fas fa-filter"></i> 选择类别</label>
                <select id="category">
                    <option value="users">用户</option>
                    <option value="projects">仓库</option>
                </select>
            </div>

            <div class="form-group">
                <label for="keyword"><i class="fas fa-search"></i> 关键词</label>
                <input type="text" id="keyword" placeholder="请输入搜索关键词" />
            </div>
        </div>

        <div class="button-group">
            <button id="btnquery" class="btn">
                <i class="fas fa-search"></i> 检索
            </button>
            <button id="btnTest" class="btn btn-secondary">
                <i class="fas fa-plug"></i> 测试连接
            </button>
        </div>
    </div>

    <div id="result">
        <div class="result-placeholder">
            <i class="fas fa-search"></i>
            <p>请输入搜索条件并点击"检索"按钮查看结果</p>
        </div>
    </div>

    <div class="loading" id="loading">
        <div class="spinner"></div>
        <p>正在检索中,请稍候...</p>
    </div>
</main>

<footer>
    <p>© 2025 口袋工具 - 便捷检索GitCode信息 | 基于GitCode OpenAPI</p>
</footer>

 
</body>
</html>

 页面完成后,启动手机模拟器,预览效果如下:

当用户没有输入关键词进行检索时,会给出提示信息:

 口袋工具逻辑处理

继续完善点击【检索】按钮后的逻辑处理:

document.addEventListener('DOMContentLoaded', function() {
        const btnQuery = document.getElementById('btnquery');
        const btnTest = document.getElementById('btnTest');
        const resultDiv = document.getElementById('result');
        const loadingDiv = document.getElementById('loading');
        const keyInput = document.getElementById('key');
        const categorySelect = document.getElementById('category');
        const keywordInput = document.getElementById('keyword');

        // GitCode API基础URL
        const API_BASE_URL = 'https://api.gitcode.com/api/v5';

        // 获取当前用户信息 - 验证token
        async function getCurrentUser(token) {
            const url = `${API_BASE_URL}/user`;
            console.log('获取用户信息URL:', url);

            try {
                const response = await fetch(url, {
                    method: 'GET',
                    headers: {
                        'Authorization': `Bearer ${token}`,
                        'Content-Type': 'application/json',
                        'Accept': 'application/json'
                    }
                });

                console.log('用户信息响应状态:', response.status);

                if (!response.ok) {
                    let errorText = '未知错误';
                    try {
                        errorText = await response.text();
                    } catch (e) {
                        errorText = '无法读取错误信息';
                    }
                    throw new Error(`获取用户信息失败: HTTP ${response.status} - ${errorText}`);
                }

                const data = await response.json();
                console.log('当前用户信息:', data);
                return data;
            } catch (error) {
                console.error('获取用户信息异常:', error);
                throw error;
            }
        }

        // 搜索用户
        async function searchUsers(token, keyword) {
            // 尝试多种可能的端点
            const endpoints = [
                `${API_BASE_URL}/search/users?q=${encodeURIComponent(keyword)}`,
                `${API_BASE_URL}/users?search=${encodeURIComponent(keyword)}`,
                `${API_BASE_URL}/users?username=${encodeURIComponent(keyword)}`
            ];

            for (const url of endpoints) {
                try {
                    console.log('尝试搜索用户URL:', url);

                    const response = await fetch(url, {
                        method: 'GET',
                        headers: {
                            'Authorization': `Bearer ${token}`,
                            'Content-Type': 'application/json',
                            'Accept': 'application/json'
                        }
                    });

                    console.log('搜索用户响应状态:', response.status);

                    if (response.ok) {
                        const data = await response.json();
                        console.log('搜索用户成功,结果:', data);

                        // 统一数据格式
                        if (Array.isArray(data)) {
                            return { items: data, total_count: data.length };
                        } else if (data.items) {
                            return data;
                        } else {
                            return { items: [data], total_count: 1 };
                        }
                    } else if (response.status === 404) {
                        console.log('端点不存在,尝试下一个');
                        continue;
                    }
                } catch (error) {
                    console.log('搜索用户请求异常:', error);
                    continue;
                }
            }

            throw new Error('所有用户搜索端点都失败了');
        }

        // 搜索仓库
        async function searchProjects(token, keyword) {
            // 尝试多种可能的端点
            const endpoints = [
                `${API_BASE_URL}/search/repositories?q=${encodeURIComponent(keyword)}`,
                `${API_BASE_URL}/projects?search=${encodeURIComponent(keyword)}`,
                `${API_BASE_URL}/repositories?search=${encodeURIComponent(keyword)}`
            ];

            for (const url of endpoints) {
                try {
                    console.log('尝试搜索仓库URL:', url);

                    const response = await fetch(url, {
                        method: 'GET',
                        headers: {
                            'Authorization': `Bearer ${token}`,
                            'Content-Type': 'application/json',
                            'Accept': 'application/json'
                        }
                    });

                    console.log('搜索仓库响应状态:', response.status);

                    if (response.ok) {
                        const data = await response.json();
                        console.log('搜索仓库成功,结果:', data);

                        // 统一数据格式
                        if (Array.isArray(data)) {
                            return { items: data, total_count: data.length };
                        } else if (data.items) {
                            return data;
                        } else {
                            return { items: [data], total_count: 1 };
                        }
                    } else if (response.status === 404) {
                        console.log('端点不存在,尝试下一个');
                        continue;
                    }
                } catch (error) {
                    console.log('搜索仓库请求异常:', error);
                    continue;
                }
            }

            throw new Error('所有仓库搜索端点都失败了');
        }

        // 显示加载状态
        function showLoading() {
            loadingDiv.style.display = 'block';
            resultDiv.innerHTML = '';
            btnQuery.disabled = true;
            btnTest.disabled = true;
        }

        // 隐藏加载状态
        function hideLoading() {
            loadingDiv.style.display = 'none';
            btnQuery.disabled = false;
            btnTest.disabled = false;
        }

        // 显示错误信息
        function showError(message) {
            resultDiv.innerHTML = `
                <div class="error-message">
                    <i class="fas fa-exclamation-circle"></i>
                    <strong>错误:</strong> ${message}
                </div>
            `;
        }

        // 显示成功信息
        function showSuccess(message) {
            resultDiv.innerHTML = `
                <div class="success-message">
                    <i class="fas fa-check-circle"></i>
                    <strong>成功:</strong> ${message}
                </div>
            `;
        }

        // 格式化日期
        function formatDate(dateString) {
            if (!dateString) return '未知';
            try {
                const date = new Date(dateString);
                return date.toLocaleDateString('zh-CN');
            } catch (e) {
                return '未知';
            }
        }

        // 显示搜索结果
        function displayResults(results, category) {
            console.log('显示结果函数被调用:', { results, category });

            if (!results || (!results.items && !Array.isArray(results))) {
                resultDiv.innerHTML = `
                    <div class="no-results">
                        <i class="fas fa-inbox"></i>
                        <p>没有找到相关${category === 'users' ? '用户' : '仓库'}</p>
                    </div>
                `;
                return;
            }

            let resultsHtml = '';

            // 统一数据格式处理
            let items = [];
            let totalCount = 0;

            if (Array.isArray(results)) {
                items = results;
                totalCount = results.length;
            } else if (results.items) {
                items = results.items;
                totalCount = results.total_count || results.items.length;
            } else {
                items = [results];
                totalCount = 1;
            }

            console.log('处理的数据:', { items, totalCount });

            if (items.length === 0) {
                resultDiv.innerHTML = `
                    <div class="no-results">
                        <i class="fas fa-inbox"></i>
                        <p>没有找到相关${category === 'users' ? '用户' : '仓库'}</p>
                        <p style="font-size: 0.8rem; margin-top: 0.5rem; color: #666;">
                            共搜索到 ${totalCount} 个结果
                        </p>
                    </div>
                `;
                return;
            }

            // 显示总数信息
            resultsHtml += `
                <div class="result-count">
                    <i class="fas fa-chart-bar"></i> 共找到 ${totalCount} 个${category === 'users' ? '用户' : '仓库'}
                </div>
            `;

            if (category === 'users') {
                items.forEach(user => {
                    console.log('处理用户数据:', user);
                    resultsHtml += `
                        <div class="result-item">
                            <img src="${user.avatar_url || 'https://via.placeholder.com/50?text=User'}"
                                 alt="${user.login || user.username || '用户'}"
                                 class="result-avatar"
                                 onerror="this.src='https://via.placeholder.com/50?text=User'">
                            <div class="result-content">
                                <div class="result-title">
                                    <a href="${user.html_url || user.web_url || user.url || '#'}" target="_blank">
                                        <i class="fas fa-user"></i> ${user.login || user.username || '未知用户'}
                                    </a>
                                </div>
                                <div class="result-desc">
                                    ${user.bio || user.description || '该用户暂无简介'}
                                </div>
                                <div class="result-meta">
                                    <span><i class="fas fa-id-card"></i> ID: ${user.id || '未知'}</span>
                                    ${user.type ? `<span><i class="fas fa-tag"></i> ${user.type}</span>` : ''}
                                    ${(user.site_admin || user.is_admin) ? `<span style="color: var(--success-color);"><i class="fas fa-shield-alt"></i> 管理员</span>` : ''}
                                </div>
                            </div>
                        </div>
                    `;
                });
            } else if (category === 'projects') {
                items.forEach(repo => {
                    console.log('处理仓库数据:', repo);
                    resultsHtml += `
                        <div class="result-item">
                            <img src="${repo.owner?.avatar_url || repo.avatar_url || 'https://via.placeholder.com/50?text=Repo'}"
                                 alt="${repo.name}"
                                 class="result-avatar"
                                 onerror="this.src='https://via.placeholder.com/50?text=Repo'">
                            <div class="result-content">
                                <div class="result-title">
                                    <a href="${repo.html_url || repo.web_url || repo.url || '#'}" target="_blank">
                                        <i class="fas fa-code-branch"></i> ${repo.full_name || repo.name_with_namespace || repo.name || '未知项目'}
                                    </a>
                                </div>
                                <div class="result-desc">
                                    ${repo.description || '该项目暂无描述'}
                                </div>
                                <div class="result-meta">
                                    <span><i class="fas fa-star"></i> ${repo.stargazers_count || repo.star_count || 0}</span>
                                    <span><i class="fas fa-code-fork"></i> ${repo.forks_count || 0}</span>
                                    <span><i class="fas fa-eye"></i> ${repo.watchers_count || repo.watch_count || 0}</span>
                                    <span><i class="fas fa-code"></i> ${repo.language || '未知'}</span>
                                    ${repo.updated_at || repo.last_activity_at ? `<span><i class="fas fa-clock"></i> ${formatDate(repo.updated_at || repo.last_activity_at)}</span>` : ''}
                                </div>
                            </div>
                        </div>
                    `;
                });
            }

            console.log('生成的HTML长度:', resultsHtml.length);
            resultDiv.innerHTML = resultsHtml;
        }

        // 检索按钮点击事件
        btnQuery.addEventListener('click', async function() {
            // 获取输入值
            const token = keyInput.value.trim();
            const category = categorySelect.value;
            const keyword = keywordInput.value.trim();

            // 验证输入
            if (!token) {
                showError('请输入访问令牌');
                return;
            }

            if (!keyword) {
                showError('请输入搜索关键词');
                return;
            }

            // 显示加载状态
            showLoading();

            try {
                console.log('=== 开始GitCode API请求 ===');

                // 1. 先验证token
                console.log('步骤1: 验证token...');
                const userInfo = await getCurrentUser(token);
                console.log('✅ Token验证成功,当前用户:', userInfo.login || userInfo.username);

                // 2. 执行搜索
                console.log('步骤2: 执行搜索...');
                let results;
                if (category === 'users') {
                    results = await searchUsers(token, keyword);
                } else {
                    results = await searchProjects(token, keyword);
                }

                console.log('✅ 搜索完成,准备显示结果');

                // 3. 显示结果
                displayResults(results, category);

            } catch (error) {
                console.error('❌ API请求失败:', error);
                showError(`请求失败: ${error.message}`);
            } finally {
                // 隐藏加载状态
                hideLoading();
            }
        });

        // 测试连接按钮点击事件
        btnTest.addEventListener('click', async function() {
            const token = keyInput.value.trim();

            if (!token) {
                showError('请输入访问令牌');
                return;
            }

            showLoading();

            try {
                console.log('=== 测试GitCode API连接 ===');
                const userInfo = await getCurrentUser(token);

                showSuccess(`
                    ✅ 连接测试成功!

                    <div style="margin-top: 10px;">
                        <strong>用户信息:</strong><br>
                        - 用户名: ${userInfo.login || userInfo.username || '未知'}<br>
                        - 姓名: ${userInfo.name || '未设置'}<br>
                        - ID: ${userInfo.id || '未知'}<br>
                        ${userInfo.email ? `- 邮箱: ${userInfo.email}<br>` : ''}
                    </div>

                    <div style="margin-top: 10px;">
                        <strong>API状态:</strong> 正常
                    </div>
                `);

            } catch (error) {
                console.error('连接测试失败:', error);
                showError(`
                    ❌ 连接测试失败

                    <div style="margin-top: 10px;">
                        <strong>错误信息:</strong> ${error.message}
                    </div>

                    <div style="margin-top: 10px;">
                        <strong>可能的原因:</strong><br>
                        1. Token无效或已过期<br>
                        2. Token权限不足<br>
                        3. 网络连接问题<br>
                        4. 鸿蒙模拟器网络限制
                    </div>
                `);
            } finally {
                hideLoading();
            }
        });

        // 添加示例搜索关键词
        keywordInput.placeholder = "例如: john, react, vue 等";

        // 添加回车键搜索支持
        keywordInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                btnQuery.click();
            }
        });

        // 添加调试信息显示
        const debugInfo = document.createElement('div');
        debugInfo.style.cssText = 'position: fixed; bottom: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 10px; border-radius: 5px; font-size: 12px; z-index: 1000;';
        debugInfo.innerHTML = '调试模式已开启';
        document.body.appendChild(debugInfo);
    });

说明: 使用v5版本的API,确保功能完整性和稳定性。

项目运行效果

1. 初始界面

页面加载完成后,用户首先看到简洁的搜索界面,包含清晰的输入区域和引导信息。

2. 令牌验证测试

用户输入GitCode访问令牌后,可点击"测试连接"按钮验证令牌有效性:

  • 成功:显示用户基本信息,确认API连接正常

  • 失败:详细列出可能的原因和解决方案

3. 用户搜索演示

搜索流程:

  1. 选择"用户"类别

  2. 输入用户名关键词(如"john")

  3. 点击"检索"按钮

预期结果:

  • 显示匹配用户的头像、用户名、简介等信息

  • 展示搜索结果总数

  • 提供用户详情链接

运行效果如下:

这里报 HTTP 400 错误,根据 gitcode openapi 文档,400 是网络限制问题,此处需要修改 module.json5 增加如下代码:

 "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      }
    ]

重新访问选择【用户】输入关键词,点击【检索】按钮,运行效果如下:

4. 仓库搜索演示

搜索流程:

  1. 选择"仓库"类别

  2. 输入仓库关键词(如"react")

  3. 点击"检索"按钮

预期结果:

  • 显示匹配仓库的名称、描述、拥有者信息

  • 展示星标数、复刻数、编程语言等关键指标

  • 提供仓库详情链接

界面示例:

选择【仓库】输入关键词,点击【检索】按钮,运行效果如下:

总结

通过本项目的开发,我们成功构建了一个功能完善、界面美观的GitCode口袋工具。该项目不仅展示了Cordova开发混合应用的流程,也体现了良好的API集成实践和用户体验设计理念。

项目的核心价值在于:

  1. 便捷性:为开发者提供快速访问GitCode平台的方式

  2. 实用性:涵盖用户和仓库两大核心搜索功能

  3. 可扩展性:架构设计支持后续功能扩展

  4. 跨平台性:一次开发,多端部署

无论是作为学习项目还是实际应用,这个GitCode口袋工具都具备良好的参考价值和实用价值。开发者可以根据自身需求,在此基础之上进行进一步的优化和扩展,打造更加个性化的代码搜索工具。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐