1.部署
具体如何部署,可查看 用 Cloudflare+Pages+KV 无需服务器,轻松部署个人导航网站(CloudNav) 这篇文章,里面有详细说明。
2.修改图标API
2.1 修改链接的图标API服务
在 components/LinkModal.tsx 文件中找到
// Logic to fetch icon
const fetchIconFromUrl = (targetUrl: string) => {
if (!targetUrl) return;
try {
let normalizedUrl = targetUrl;
if (!targetUrl.startsWith('http')) {
normalizedUrl = 'https://' + targetUrl;
}
// Use Google's specialized favicon service which is more robust
// t2.gstatic.com is used by Chrome internal pages
// fallback_opts=TYPE,SIZE,URL ensures it tries multiple ways to get an icon
const newIcon = `https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(normalizedUrl)}&size=128`;
setIconUrl(newIcon);
} catch (e) {
// invalid url
}
};
替换成:
// Logic to fetch icon
const fetchIconFromUrl = (targetUrl: string) => {
if (!targetUrl) return;
try {
let normalizedUrl = targetUrl;
if (!targetUrl.startsWith('http')) {
normalizedUrl = 'https://' + targetUrl;
}
// 提取域名
const urlObj = new URL(normalizedUrl);
const domain = urlObj.hostname;
// 使用 faviconextractor.com 获取图标
const newIcon = `https://www.faviconextractor.com/favicon/${domain}?larger=true`;
setIconUrl(newIcon);
} catch (e) {
// invalid url
console.error('Failed to extract domain:', e);
}
};
2.2 修改搜索引擎的图标API服务
在 components/SearchSettingsModal.tsx 文件中找到
const fetchIconFromUrl = (targetUrl: string) => {
if (!targetUrl) return;
try {
let normalizedUrl = targetUrl;
if (!targetUrl.startsWith('http')) {
normalizedUrl = 'https://' + targetUrl;
}
// 尝试解析域名
const urlObj = new URL(normalizedUrl);
const origin = urlObj.origin;
// 使用 Google 的 favicon 服务获取图标
const newIconUrl = `https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(origin)}&size=128`;
setNewIcon(newIconUrl);
} catch (e) {
// invalid url
}
};
替换成:
const fetchIconFromUrl = (targetUrl: string) => {
if (!targetUrl) return;
try {
let normalizedUrl = targetUrl;
if (!targetUrl.startsWith('http')) {
normalizedUrl = 'https://' + targetUrl;
}
// 提取域名
const urlObj = new URL(normalizedUrl);
const domain = urlObj.hostname;
// 使用 faviconextractor.com 获取图标
const newIcon = `https://www.faviconextractor.com/favicon/${domain}?larger=true`;
setNewIcon(newIcon);
} catch (e) {
// invalid url
console.error('Failed to extract domain:', e);
}
};
可备用图标API站点
// Favicon Extractor
https://www.faviconextractor.com/favicon/${domain}?larger=true
// icon.bqb.cool
https://icon.bqb.cool?url=https://${domain}
// favicon.im
https://favicon.im/${domain}?larger=true
// Icon Horse
https://icon.horse/icon/${domain}
// keeweb
https://services.keeweb.info/favicon/${domain}/128
// DuckDuckGo Icons
https://icons.duckduckgo.com/ip3/${domain}.ico
// Favicon.kit
https://favicon.kit/favicon/${domain}
// Google Favicon API
https://www.google.com/s2/favicons?domain=${domain}&sz=128
// Cravatar 国内速度快,缺点:不能定义图标大小
https://cn.cravatar.com/favicon/api/index.php?url=${domain}// 自己部署1
https://keeweb-favicon.eeen.eu.cc/favicon/${domain}/256
// 自己部署2
https://favicons.eeen.eu.cc/${domain}.ico
3.修改网站前端样式
在 Github 仓库的根目录 App.tsx 中进行修改。
如需修改每行的显示个数:
在 App.tsx 中搜索 grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 应该能找到两处:
置顶链接区域:
<div className={`grid gap-3 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-5 lg:grid-cols-8 xl:grid-cols-10' : 'grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8'}`}>
分类链接区域
<div className={`grid gap-3 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-5 lg:grid-cols-8 xl:grid-cols-10' : 'grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8'}`}>
修改成
<div className="grid gap-3 grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
- 手机:每行2个
- 平板:每行3个
- 小电脑:每行4个
- 大屏幕:每行6个
把这两处都改掉,保存提交,会重新部署,成功之后就能看到效果了!
4. 左侧菜单设置按钮仅登录用户可见
找到
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">分类目录</span>
<button
onClick={() => { if(!authToken) setIsAuthOpen(true); else setIsCatManagerOpen(true); }}
className="p-1 text-slate-400 hover:text-blue-500 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"
title="管理分类"
>
<Settings size={14} />
</button>
整段替换成
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">分类目录</span>
{authToken && (
<button
onClick={() => { if(!authToken) setIsAuthOpen(true); else setIsCatManagerOpen(true); }}
className="p-1 text-slate-400 hover:text-blue-500 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"
title="管理分类"
>
<Settings size={14} />
</button>
)}
5.进阶功能:修改分类可见条件
修改分类模块(全员可见 / 全员隐藏 / 仅管理员可见)
①、在 Github 仓库的根目录下找到(App.tsx)文件:
修改此功能,🔧 需要修改两个地方
第一处:左侧侧边栏分类列表
找到这段代码:
{categories.map(cat => {
const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);
const isEmoji = cat.icon && cat.icon.length <= 4 && !/^[a-zA-Z]+$/.test(cat.icon);
return (
<button
key={cat.id}
onClick={() => scrollToCategory(cat.id)}
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all group ${
activeCategory === cat.id
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 font-medium'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
<div className={`p-1.5 rounded-lg transition-colors flex items-center justify-center ${activeCategory === cat.id ? 'bg-blue-100 dark:bg-blue-800' : 'bg-slate-100 dark:bg-slate-800'}`}>
{isLocked ? <Lock size={16} className="text-amber-500" /> : (isEmoji ? <span className="text-base leading-none">{cat.icon}</span> : <Icon name={cat.icon} size={16} />)}
</div>
<span className="truncate flex-1 text-left">{cat.name}</span>
{activeCategory === cat.id && <div className="w-1.5 h-1.5 rounded-full bg-blue-500"></div>}
</button>
);
})}
整段替换成:
{/* 左侧先过滤出可见的分类 */}
{categories
.filter(cat => {
// 如果是管理员(已登录),能看到"全员可见"和"仅管理员可见"的分类
if (authToken) {
// 管理员也看不到"全员隐藏"的分类
return cat.isVisible !== false;
}
// 如果是普通用户,只显示"全员可见"的分类
return cat.isVisible !== false && !cat.isAdminOnly;
})
.map(cat => {
const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);
const isEmoji = cat.icon && cat.icon.length <= 4 && !/^[a-zA-Z]+$/.test(cat.icon);
return (
<button
key={cat.id}
onClick={() => scrollToCategory(cat.id)}
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all group ${
activeCategory === cat.id
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 font-medium'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
<div className={`p-1.5 rounded-lg transition-colors flex items-center justify-center ${activeCategory === cat.id ? 'bg-blue-100 dark:bg-blue-800' : 'bg-slate-100 dark:bg-slate-800'}`}>
{isLocked ? <Lock size={16} className="text-amber-500" /> : (isEmoji ? <span className="text-base leading-none">{cat.icon}</span> : <Icon name={cat.icon} size={16} />)}
</div>
<span className="truncate flex-1 text-left">{cat.name}</span>
{activeCategory === cat.id && <div className="w-1.5 h-1.5 rounded-full bg-blue-500"></div>}
</button>
);
})}
第二处:右侧内容区域
找到这段代码:
{categories.map(cat => {
let catLinks = searchResults.filter(l => l.categoryId === cat.id);
const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);
// Logic Fix: If External Search, do NOT hide categories based on links
// Because external search doesn't filter links.
// However, the user probably wants to see the links grid even when typing for external search
// Current logic: if search query exists AND local search -> filter.
// If search query exists AND external search -> show all (searchResults returns all).
if (searchQuery && searchMode === 'local' && catLinks.length === 0) return null;
return (
<section key={cat.id} id={`cat-${cat.id}`} className="scroll-mt-24">
整段替换成:
{/* 先过滤出可见的分类,然后再渲染 */}
{categories
.filter(cat => {
// 如果是管理员(已登录)
if (authToken) {
// 管理员也看不到"全员隐藏"的分类
return cat.isVisible !== false;
}
// 如果是普通用户,只显示全员可见的分类
return cat.isVisible !== false && !cat.isAdminOnly;
})
.map(cat => {
let catLinks = searchResults.filter(l => l.categoryId === cat.id);
const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);
if (searchQuery && searchMode === 'local' && catLinks.length === 0) return null;
return (
<section key={cat.id} id={`cat-${cat.id}`} className="scroll-mt-24">
②、在 GitHub 上打开(components/CategoryManagerModal.tsx)文件
修改此功能,🔧 需要修改两个地方
第一处:添加 isVisible?: boolean
找到这段代码:
import React, { useState } from 'react';
import { X, ArrowUp, ArrowDown, Trash2, Edit2, Plus, Check, Lock, Merge, Smile } from 'lucide-react';
import { Category, LinkItem } from '../types';
import Icon from './Icon';
整段替换成:
import React, { useState } from 'react';
import { X, ArrowUp, ArrowDown, Trash2, Edit2, Plus, Check, Lock, Merge, Smile } from 'lucide-react';
import { Category, LinkItem } from '../types';
import Icon from './Icon';
interface CategoryWithVisibility extends Category {
isVisible?: boolean;
}
第二处:添加是否可见下拉选项框
找到这段代码:
{/* Actions */}
{editingId !== cat.id && mergingCatId !== cat.id && (
<div className="flex items-center gap-1 self-start mt-2">
<button onClick={() => startEdit(cat)} className="p-1.5 text-slate-400 hover:text-blue-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded" title="编辑">
<Edit2 size={14} />
</button>
<button onClick={() => openMerge(cat.id)} className="p-1.5 text-slate-400 hover:text-purple-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded" title="合并到其他分类">
<Merge size={14} />
</button>
<button
onClick={() => { if(confirm(`确定删除"${cat.name}"分类吗?该分类下的书签将移动到"常用推荐"。`)) onDeleteCategory(cat.id); }}
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded"
title="删除"
>
<Trash2 size={14} />
</button>
</div>
)}
整段替换成:
{/* Actions */}
{editingId !== cat.id && mergingCatId !== cat.id && (
<div className="flex items-center gap-1 self-start mt-2">
{/* ===== 是否可见下拉框(直接显示在列表里) ===== */}
<div className="flex items-center gap-2 mr-3 border-r border-slate-200 dark:border-slate-700 pr-3">
<select
value={
(cat as CategoryWithVisibility).isVisible === false ? "hidden" :
(cat as CategoryWithVisibility).isAdminOnly === true ? "admin" : "public"
}
onChange={(e) => {
const value = e.target.value;
let isVisible = true;
let isAdminOnly = false;
if (value === "hidden") {
isVisible = false;
isAdminOnly = false;
} else if (value === "admin") {
isVisible = true;
isAdminOnly = true;
} else { // public
isVisible = true;
isAdminOnly = false;
}
const updatedCategories = categories.map(c =>
c.id === cat.id ? { ...c, isVisible, isAdminOnly } : c
);
onUpdateCategories(updatedCategories);
}}
className="text-xs p-1.5 pr-8 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
backgroundPosition: 'right 0.5rem center',
backgroundRepeat: 'no-repeat',
backgroundSize: '1.5em 1.5em',
paddingRight: '2rem'
}}
>
<option value="public" className="dark:bg-slate-800">👥 全员可见</option>
<option value="admin" className="dark:bg-slate-800">👑 仅管理员可见</option>
<option value="hidden" className="dark:bg-slate-800">🚫 全员隐藏</option>
</select>
</div>
{/* ===== 下拉框结束 ===== */}
<button onClick={() => startEdit(cat)} className="p-1.5 text-slate-400 hover:text-blue-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded" title="编辑">
<Edit2 size={14} />
</button>
<button onClick={() => openMerge(cat.id)} className="p-1.5 text-slate-400 hover:text-purple-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded" title="合并到其他分类">
<Merge size={14} />
</button>
<button
onClick={() => { if(confirm(`确定删除"${cat.name}"分类吗?该分类下的书签将移动到"常用推荐"。`)) onDeleteCategory(cat.id); }}
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded"
title="删除"
>
<Trash2 size={14} />
</button>
</div>
)}
③、在 Github 中找到根目录下的主页面文件(types.ts)文件
找到这段代码:
export interface Category {
id: string;
name: string;
icon: string; // Lucide icon name or emoji
password?: string; // Optional password for category protection
}
整段替换成:
export interface Category {
id: string;
name: string;
icon?: string;
password?: string;
isVisible?: boolean; // false = 全员隐藏
isAdminOnly?: boolean; // true = 仅管理员可见
}
6.仅管理可见模式,增加 [管] 标识
当选择 仅管理可见 时,左侧的分类名称,和右侧的标题后面,添加一个[管]标识,需在(App.tsx)文件中做修改
第一处:左侧侧边栏分类列表
找到这段代码
<span className="truncate flex-1 text-left">{cat.name}</span>
替换成
<span className="truncate flex-1 text-left">
{cat.name}
{cat.isAdminOnly && authToken && (
<span className="ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400 border border-purple-200 dark:border-purple-800">管</span>
)}
</span>
第二处:右侧内容标题
找到这段代码
<h2 className="text-lg font-bold text-slate-800 dark:text-slate-200">
{cat.name}
</h2>
{isLocked && <Lock size={16} className="text-amber-500" />}
替换成
<div className="flex items-center gap-2">
<h2 className="text-lg font-bold text-slate-800 dark:text-slate-200">
{cat.name}
</h2>
{cat.isAdminOnly && authToken && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400 border border-purple-200 dark:border-purple-800">仅管理员</span>
)}
{isLocked && <Lock size={16} className="text-amber-500" />}
</div>
7.给登录验证页面增加一个关闭按钮
在 GitHub 上打开(components/AuthModal.tsx)文件
把里面整个代码替换成:
import React, { useState } from 'react';
import { Lock, ArrowRight, Loader2, X } from 'lucide-react'; // 导入 X 图标
interface AuthModalProps {
isOpen: boolean;
onLogin: (password: string) => Promise<boolean>;
onClose: () => void; // 添加 onClose 属性
}
const AuthModal: React.FC<AuthModalProps> = ({ isOpen, onLogin, onClose }) => {
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
const success = await onLogin(password);
if (!success) {
setError('密码错误或无法连接服务器');
}
setIsLoading(false);
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/80 backdrop-blur-md">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden border border-slate-200 dark:border-slate-700 p-8 relative">
{/* ===== 新增:关闭按钮 ===== */}
<button
onClick={onClose}
className="absolute right-4 top-4 p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-full transition-colors"
aria-label="关闭"
>
<X size={20} />
</button>
{/* ===== 新增结束 ===== */}
<div className="flex flex-col items-center mb-6">
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mb-4 text-blue-600 dark:text-blue-400">
<Lock size={32} />
</div>
<h2 className="text-xl font-bold dark:text-white">身份验证</h2>
<p className="text-sm text-slate-500 dark:text-slate-400 text-center mt-2">
请输入部署时设置的 PASSWORD 以同步数据
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-3 rounded-xl border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none transition-all text-center tracking-widest"
placeholder="访问密码"
autoFocus
/>
</div>
{error && (
<div className="text-red-500 text-sm text-center font-medium">
{error}
</div>
)}
<button
type="submit"
disabled={isLoading || !password}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-xl transition-colors shadow-lg shadow-blue-500/30 flex items-center justify-center gap-2"
>
{isLoading ? <Loader2 className="animate-spin" /> : <>解锁进入 <ArrowRight size={18} /></>}
</button>
</form>
</div>
</div>
);
};
export default AuthModal;
第二处:找到 App.tsx 里使用 AuthModal 的地方,把 onClose 传进去:
<AuthModal isOpen={isAuthOpen} onLogin={handleLogin} />
替换成
<AuthModal
isOpen={isAuthOpen}
onLogin={handleLogin}
onClose={() => setIsAuthOpen(false)}
/>
8.左侧底部调整
- 让底部的(导入、备份、设置)按钮只有管理员(已登录)可见;
- 增加退出登录按钮。
在 GitHub 上打开(App.tsx)文件
找到
<div className="p-4 border-t border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 shrink-0">
整段替换成
{authToken && (
<div className="p-4 border-t border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 shrink-0">
<div className="grid grid-cols-4 gap-2 mb-2">
<button
onClick={() => { if(!authToken) setIsAuthOpen(true); else setIsImportModalOpen(true); }}
className="flex flex-col items-center justify-center gap-1 p-2 text-xs text-slate-600 dark:text-slate-300 hover:bg-white dark:hover:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 transition-all"
title="导入书签"
>
<Upload size={14} />
<span>导入</span>
</button>
<button
onClick={() => { if(!authToken) setIsAuthOpen(true); else setIsBackupModalOpen(true); }}
className="flex flex-col items-center justify-center gap-1 p-2 text-xs text-slate-600 dark:text-slate-300 hover:bg-white dark:hover:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 transition-all"
title="备份与恢复"
>
<CloudCog size={14} />
<span>备份</span>
</button>
<button
onClick={() => setIsSettingsModalOpen(true)}
className="flex flex-col items-center justify-center gap-1 p-2 text-xs text-slate-600 dark:text-slate-300 hover:bg-white dark:hover:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 transition-all"
title="AI 设置"
>
<Settings size={14} />
<span>设置</span>
</button>
{/* 新增:退出登录按钮 */}
<button
onClick={() => {
if (confirm('确定要退出登录吗?')) {
setAuthToken('');
localStorage.removeItem(AUTH_KEY);
setSyncStatus('idle');
// 可选:刷新页面或跳转
window.location.reload();
}
}}
className="flex flex-col items-center justify-center gap-1 p-2 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800 transition-all"
title="退出登录"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" x2="9" y1="12" y2="12" />
</svg>
<span>退出</span>
</button>
</div>
<div className="flex items-center justify-between text-xs px-2 mt-2">
<div className="flex items-center gap-1 text-slate-400">
{syncStatus === 'saving' && <Loader2 className="animate-spin w-3 h-3 text-blue-500" />}
{syncStatus === 'saved' && <CheckCircle2 className="w-3 h-3 text-green-500" />}
{syncStatus === 'error' && <AlertCircle className="w-3 h-3 text-red-500" />}
<span className="text-green-600">已同步</span>
</div>
</div>
</div>
)}
{/* 未登录时显示简单的登录提示 */}
{!authToken && (
<div className="flex sm:hidden p-4 border-t border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 shrink-0">
<button
onClick={() => setIsAuthOpen(true)}
className="w-full flex items-center justify-center gap-2 py-2 text-xs text-slate-600 dark:text-slate-400 hover:bg-white dark:hover:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 transition-all"
>
<Lock size={14} />
<span>登录</span>
</button>
</div>
)}
9.顶部右侧调整
在 GitHub 上打开(App.tsx)文件
找到
<div className="flex items-center gap-2">
<div className="hidden md:flex bg-slate-100 dark:bg-slate-700 rounded-lg p-1 mr-2">
... 原有内容 ...
<Plus size={16} /> <span className="hidden sm:inline">添加</span>
</button>
</div>
整段替换成
<div className="flex items-center gap-2 ml-3">
{/* 模式切换按钮 - 仅登录用户显示 */}
{authToken && (
<div className="flex bg-slate-100 dark:bg-slate-700 rounded-lg p-1">
<button
onClick={() => updateData(links, categories, { ...siteSettings, cardStyle: 'simple' })}
title="简约模式"
className={`p-1.5 rounded transition-all ${
siteSettings.cardStyle === 'simple'
? 'bg-white dark:bg-slate-600 shadow text-blue-600'
: 'text-slate-400 hover:text-slate-600'
}`}
>
<LayoutGrid size={16} />
</button>
<button
onClick={() => updateData(links, categories, { ...siteSettings, cardStyle: 'detailed' })}
title="详情模式"
className={`p-1.5 rounded transition-all ${
siteSettings.cardStyle === 'detailed'
? 'bg-white dark:bg-slate-600 shadow text-blue-600'
: 'text-slate-400 hover:text-slate-600'
}`}
>
<List size={16} />
</button>
</div>
)}
<button onClick={toggleTheme} className="p-2 rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700">
{darkMode ? <Sun size={18} /> : <Moon size={18} />}
</button>
{!authToken && (
<button onClick={() => setIsAuthOpen(true)} className="flex items-center gap-2 bg-slate-200 dark:bg-slate-700 px-3 py-1.5 rounded-full text-xs font-medium">
<Cloud size={14} /> 登录
</button>
)}
{authToken && (
<button
onClick={() => { if(!authToken) setIsAuthOpen(true); else { setEditingLink(undefined); setIsModalOpen(true); }}}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-full text-sm font-medium shadow-lg shadow-blue-500/30"
>
<Plus size={16} /> <span className="hidden sm:inline">添加</span>
</button>
)}
</div>
10. 移动端显示搜索框
在 GitHub 上打开(App.tsx)文件
找到
{/* Redesigned Search Bar */}
<div className="relative w-full max-w-xl hidden sm:flex items-center gap-3">
{/* Search Mode Toggle (Pill) */}
<div className="bg-slate-100 dark:bg-slate-700 p-1 rounded-full flex items-center shrink-0">
替换成
{/* Redesigned Search Bar */}
<div className="relative w-full max-w-xl flex items-center gap-3">
{/* Search Mode Toggle (Pill) */}
<div className="bg-slate-100 dark:bg-slate-700 p-1 rounded-full hidden sm:flex items-center shrink-0">
11. 鼠标右键弹窗菜单修改
在 GitHub 上打开(App.tsx)文件
11.1 修改 renderLinkCard 函数,使移动端也可支持编辑链接
找到
const renderLinkCard = (link: LinkItem) => {
const iconDisplay = link.icon ? (
... 原有内容 ...
)}
</a>
);
};
整段替换成
const renderLinkCard = (link: LinkItem) => {
const iconDisplay = link.icon ? (
<img
src={link.icon}
alt=""
className="w-5 h-5 object-contain"
onError={(e) => {
e.currentTarget.style.display = 'none';
e.currentTarget.parentElement!.innerText = link.title.charAt(0);
}}
/>
) : link.title.charAt(0);
const isSimple = siteSettings.cardStyle === 'simple';
// 使用普通的 let 变量,而不是 useRef
let longPressTimer: NodeJS.Timeout | null = null;
const handleTouchStart = (e: React.TouchEvent, link: LinkItem) => {
longPressTimer = setTimeout(() => {
// 触发长按菜单
let x = e.touches[0].clientX;
let y = e.touches[0].clientY;
// 边界调整
if (x + 180 > window.innerWidth) x = window.innerWidth - 190;
if (y + 220 > window.innerHeight) y = window.innerHeight - 230;
setContextMenu({ x, y, link });
// 轻微震动(如果设备支持)
if (navigator.vibrate) {
navigator.vibrate(50);
}
}, 500); // 长按500ms触发
};
const handleTouchEnd = () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
};
const handleTouchMove = () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
};
return (
<a
key={link.id}
href={link.url}
target="_blank"
rel="noopener noreferrer"
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
let x = e.clientX;
let y = e.clientY;
if (x + 180 > window.innerWidth) x = window.innerWidth - 190;
if (y + 220 > window.innerHeight) y = window.innerHeight - 230;
setContextMenu({ x, y, link });
return false;
}}
// 移动端长按事件
onTouchStart={(e) => handleTouchStart(e, link)}
onTouchEnd={handleTouchEnd}
onTouchMove={handleTouchMove}
onTouchCancel={handleTouchEnd}
className={`group relative flex flex-col ${isSimple ? 'p-2' : 'p-3'} bg-white dark:bg-slate-800 rounded-xl border border-slate-100 dark:border-slate-700/50 shadow-sm hover:shadow-lg hover:border-blue-200 dark:hover:border-slate-600 hover:-translate-y-0.5 transition-all duration-200 hover:bg-blue-50 dark:hover:bg-slate-750`}
title={link.description || link.url}
>
<div className={`flex items-center gap-3 ${isSimple ? '' : 'mb-1.5'} pr-6`}>
<div className={`${isSimple ? 'w-6 h-6 text-xs' : 'w-8 h-8 text-sm'} rounded-lg bg-slate-50 dark:bg-slate-700 text-blue-600 dark:text-blue-400 flex items-center justify-center font-bold uppercase shrink-0 overflow-hidden`}>
{iconDisplay}
</div>
<h3 className="font-medium text-sm text-slate-800 dark:text-slate-200 truncate flex-1 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{link.title}
</h3>
</div>
{!isSimple && (
<div className="text-xs text-slate-500 dark:text-slate-400 line-clamp-1 h-4 w-full overflow-hidden">
{link.description || <span className="opacity-0">.</span>}
</div>
)}
</a>
);
};
11.2 未登录用户不显示编辑、置顶、删除
找到
{/* Right Click Context Menu */}
{contextMenu && (
<div
ref={contextMenuRef}
... 原有内容 ...
<Trash2 size={16}/> <span>删除链接</span>
</button>
</>
)}
</div>
)}
整段替换成
{/* Right Click Context Menu */}
{contextMenu && (
<div
ref={contextMenuRef}
className="fixed z-[9999] bg-white dark:bg-slate-800 rounded-xl shadow-2xl border border-slate-100 dark:border-slate-600 w-44 py-2 flex flex-col animate-in fade-in zoom-in duration-100 overflow-hidden"
style={{ top: contextMenu.y, left: contextMenu.x }}
onClick={(e) => e.stopPropagation()}
onContextMenu={(e) => e.preventDefault()}
>
{/* 复制链接 - 所有人都可以 */}
<button
onClick={() => { handleCopyLink(contextMenu.link!.url); setContextMenu(null); }}
className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors text-left"
>
<Copy size={16} className="text-slate-400"/> <span>复制链接</span>
</button>
{/* 显示二维码 - 所有人都可以 */}
<button
onClick={() => { setQrCodeLink(contextMenu.link); setContextMenu(null); }}
className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors text-left"
>
<QrCode size={16} className="text-slate-400"/> <span>显示二维码</span>
</button>
{/* 只有登录用户才显示下面的操作 */}
{authToken && (
<>
<div className="h-px bg-slate-100 dark:bg-slate-700 my-1 mx-2"/>
{/* 编辑链接 */}
<button
onClick={() => {
setEditingLink(contextMenu.link!);
setIsModalOpen(true);
setContextMenu(null);
}}
className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors text-left"
>
<Edit2 size={16} className="text-slate-400"/> <span>编辑链接</span>
</button>
{/* 置顶/取消置顶 */}
<button
onClick={() => {
togglePin(contextMenu.link!.id);
setContextMenu(null);
}}
className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors text-left"
>
<Pin size={16} className={contextMenu.link!.pinned ? "fill-current text-blue-500" : "text-slate-400"}/>
<span>{contextMenu.link!.pinned ? '取消置顶' : '置顶'}</span>
</button>
<div className="h-px bg-slate-100 dark:bg-slate-700 my-1 mx-2"/>
{/* 删除链接 */}
<button
onClick={() => {
handleDeleteLink(contextMenu.link!.id);
setContextMenu(null);
}}
className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-red-50 dark:hover:bg-red-900/20 text-red-500 transition-colors text-left"
>
<Trash2 size={16}/> <span>删除链接</span>
</button>
</>
)}
</div>
)}
12. 修改初始化数据
在 GitHub 上打开(types.ts)文件
12.1 默认分类
找到
export const DEFAULT_CATEGORIES: Category[] = [
{ id: 'common', name: '常用推荐', icon: 'Star' },
{ id: 'dev', name: '开发工具', icon: 'Code' },
{ id: 'design', name: '设计资源', icon: 'Palette' },
{ id: 'read', name: '阅读资讯', icon: 'BookOpen' },
{ id: 'ent', name: '休闲娱乐', icon: 'Gamepad2' },
{ id: 'ai', name: '人工智能', icon: 'Bot' },
];
在这里面修改
12.2 默认链接
找到
export const INITIAL_LINKS: LinkItem[] = [
{ id: '1', title: 'GitHub', url: 'https://github.com', categoryId: 'dev', createdAt: Date.now(), description: '代码托管平台', pinned: true },
{ id: '2', title: 'ChatGPT', url: 'https://chat.openai.com', categoryId: 'ai', createdAt: Date.now(), description: 'OpenAI聊天机器人', pinned: true },
{ id: '3', title: 'Gemini', url: 'https://gemini.google.com', categoryId: 'ai', createdAt: Date.now(), description: 'Google DeepMind AI' },
];
在这里面修改
12.3 默认搜索引擎
找到
export const DEFAULT_SEARCH_ENGINES: SearchEngine[] = [
{ id: 'local', name: '站内', url: '', icon: 'Search' },
{ id: 'google', name: 'Google', url: 'https://www.google.com/search?q=', icon: 'https://www.faviconextractor.com/favicon/www.google.com?larger=true' },
{ id: 'baidu', name: '百度', url: 'https://www.baidu.com/s?wd=', icon: 'https://www.faviconextractor.com/favicon/www.baidu.com?larger=true' },
{ id: 'bing', name: '必应', url: 'https://www.bing.com/search?q=', icon: 'https://www.faviconextractor.com/favicon/www.bing.com?larger=true' },
{ id: 'bilibili', name: 'B站', url: 'https://search.bilibili.com/all?keyword=', icon: 'https://www.faviconextractor.com/favicon/www.bilibili.com?larger=true' },
];
在这里面修改

评论 (0)