This repository has been archived on 2026-08-27. You can view files and clone it, but cannot push or open issues or pull requests.
Files
RECS-WEB-MODLE-REACT/app/icons/page.tsx
2026-08-27 20:15:24 +08:00

174 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { CSSProperties, useEffect, useMemo, useState } from "react";
import iconData from "../data/web-icons.json";
import styles from "./IconLibrary.module.css";
type IconItem = (typeof iconData.icons)[number];
type CodeMode = "character" | "html" | "react" | "css";
type PreviewBackground = "light" | "soft" | "dark";
export default function IconLibraryPage() {
const [query, setQuery] = useState("");
const [category, setCategory] = useState("all");
const [selectedId, setSelectedId] = useState("home");
const [favorites, setFavorites] = useState<string[]>([]);
const [view, setView] = useState<"grid" | "list">("grid");
const [size, setSize] = useState(28);
const [color, setColor] = useState("#27324a");
const [background, setBackground] = useState<PreviewBackground>("soft");
const [codeMode, setCodeMode] = useState<CodeMode>("html");
const [onlyFavorites, setOnlyFavorites] = useState(false);
const [toast, setToast] = useState("");
useEffect(() => {
try {
const saved = window.localStorage.getItem("orbit-icon-favorites");
if (saved) setFavorites(JSON.parse(saved));
} catch { /* use an empty favorite list */ }
}, []);
useEffect(() => {
window.localStorage.setItem("orbit-icon-favorites", JSON.stringify(favorites));
}, [favorites]);
const selected = iconData.icons.find((icon) => icon.id === selectedId) ?? iconData.icons[0];
const filteredIcons = useMemo(() => iconData.icons.filter((icon) => {
const text = `${icon.name}${icon.cn}${icon.id}${icon.keywords.join("")}`.toLowerCase();
const matchesQuery = text.includes(query.toLowerCase());
const matchesCategory = category === "all" || icon.category === category;
const matchesFavorite = !onlyFavorites || favorites.includes(icon.id);
return matchesQuery && matchesCategory && matchesFavorite;
}), [query, category, onlyFavorites, favorites]);
const categoryName = iconData.categories.find((item) => item.id === selected.category)?.name ?? "通用";
const code = getIconCode(selected, codeMode, size, color);
const previewStyle = { "--preview-size": `${size}px`, "--preview-color": color } as CSSProperties;
const notify = (message: string) => {
setToast(message);
window.setTimeout(() => setToast(""), 1800);
};
const copy = async (value: string, label: string) => {
try {
await navigator.clipboard.writeText(value);
} catch {
const input = document.createElement("textarea");
input.value = value;
input.style.position = "fixed";
input.style.opacity = "0";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
input.remove();
}
notify(`${label}已复制`);
};
const toggleFavorite = (id: string) => setFavorites((current) => current.includes(id) ? current.filter((item) => item !== id) : [...current, id]);
const exportFavorites = () => {
const items = iconData.icons.filter((icon) => favorites.includes(icon.id));
const blob = new Blob([JSON.stringify(items, null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "web-icons-favorites.json";
link.click();
URL.revokeObjectURL(url);
notify("收藏图标已导出");
};
return (
<div className={styles.page}>
<header className={styles.topbar}>
<Link href="/" className={styles.brand}><span>G</span><strong>GlyphKit</strong><small>WEB ICON LIBRARY</small></Link>
<nav><Link href="/simple-list"></Link><Link href="/repeatable-form"></Link><Link className={styles.activeNav} href="/icons"></Link><Link href="/markdown"></Link></nav>
<div className={styles.headerActions}><button onClick={() => setOnlyFavorites((value) => !value)} className={onlyFavorites ? styles.favoriteFilterActive : ""}> {favorites.length}</button><button onClick={exportFavorites} disabled={favorites.length === 0}></button><span></span></div>
</header>
<div className={styles.workspace}>
<aside className={styles.sidebar}>
<div className={styles.sidebarTitle}><span>ICON SET</span><strong></strong><small>{iconData.icons.length} </small></div>
<nav className={styles.categories} aria-label="图标分类">
{iconData.categories.map((item) => {
const count = item.id === "all" ? iconData.icons.length : iconData.icons.filter((icon) => icon.category === item.id).length;
return <button key={item.id} className={category === item.id ? styles.categoryActive : ""} onClick={() => { setCategory(item.id); setOnlyFavorites(false); }}><span>{categorySymbol(item.id)}</span>{item.name}<b>{count}</b></button>;
})}
</nav>
<div className={styles.sidebarHelp}><span>i</span><p><strong></strong><small> HTMLCSS React </small></p></div>
</aside>
<main className={styles.main}>
<section className={styles.hero}>
<div><p>SYMBOL PICKER</p><h1></h1><span>使</span></div>
<div className={styles.heroSamples}>{["⌂","✓","⌕","✎","▦"].map((symbol,index) => <span key={symbol} style={{transform:`translateY(${index % 2 ? 7 : -2}px)`}}>{symbol}</span>)}</div>
</section>
<div className={styles.toolbar}>
<label className={styles.search}><span></span><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索图标名称、功能或关键词例如编辑、upload" />{query && <button onClick={() => setQuery("")}>×</button>}<kbd> K</kbd></label>
<div className={styles.sizeControl}><span></span><input type="range" min="18" max="42" value={size} onChange={(event) => setSize(Number(event.target.value))} /><b>{size}px</b></div>
<div className={styles.viewSwitch}><button className={view === "grid" ? styles.viewActive : ""} onClick={() => setView("grid")} aria-label="网格视图"></button><button className={view === "list" ? styles.viewActive : ""} onClick={() => setView("list")} aria-label="列表视图"></button></div>
</div>
<div className={styles.resultHeading}><div><strong>{onlyFavorites ? "我的收藏" : iconData.categories.find((item) => item.id === category)?.name}</strong><span>{filteredIcons.length} </span></div>{(query || onlyFavorites || category !== "all") && <button onClick={() => { setQuery(""); setCategory("all"); setOnlyFavorites(false); }}></button>}</div>
<div className={`${styles.iconCollection} ${view === "list" ? styles.listView : ""}`} style={previewStyle}>
{filteredIcons.map((icon) => {
const favorite = favorites.includes(icon.id);
return <article key={icon.id} className={selected.id === icon.id ? styles.selectedCard : ""}>
<button className={styles.selectIcon} onClick={() => setSelectedId(icon.id)}>
<span className={styles.iconSymbol}>{icon.symbol}</span>
<span className={styles.iconLabel}><strong>{icon.cn}</strong><small>{icon.name}</small></span>
</button>
<button className={`${styles.favoriteButton} ${favorite ? styles.isFavorite : ""}`} onClick={() => toggleFavorite(icon.id)} aria-label={favorite ? `取消收藏 ${icon.cn}` : `收藏 ${icon.cn}`}>{favorite ? "★" : "☆"}</button>
<button className={styles.quickCopy} onClick={() => copy(icon.symbol, "图标字符")} aria-label={`复制 ${icon.cn}`}></button>
</article>;
})}
{filteredIcons.length === 0 && <div className={styles.empty}><span></span><h3></h3><p></p><button onClick={() => { setQuery(""); setCategory("all"); setOnlyFavorites(false); }}></button></div>}
</div>
</main>
<aside className={styles.inspector}>
<div className={styles.inspectorHeader}><div><span>SELECTED ICON</span><strong></strong></div><button className={favorites.includes(selected.id) ? styles.inspectorFavorite : ""} onClick={() => toggleFavorite(selected.id)}>{favorites.includes(selected.id) ? "★" : "☆"}</button></div>
<div className={`${styles.largePreview} ${styles[`preview_${background}`]}`} style={previewStyle}><span>{selected.symbol}</span><small>{size}px</small></div>
<div className={styles.iconMeta}><div><h2>{selected.cn}</h2><p>{selected.name}</p></div><button onClick={() => copy(selected.symbol, "图标字符")}></button></div>
<dl className={styles.details}><div><dt> ID</dt><dd>{selected.id}</dd></div><div><dt></dt><dd>{categoryName}</dd></div><div><dt>Unicode</dt><dd>U+{unicodeValue(selected.symbol)}</dd></div></dl>
<section className={styles.appearance}><h3></h3><label><span></span><input type="range" min="14" max="64" value={size} onChange={(event) => setSize(Number(event.target.value))} /><b>{size}px</b></label><div className={styles.colorRow}><span></span>{["#27324a","#3d6fe8","#20a26a","#e45252","#8a5bd1"].map((value) => <button key={value} className={color === value ? styles.colorActive : ""} style={{background:value}} onClick={() => setColor(value)} aria-label={`颜色 ${value}`} />)}<input type="color" value={color} onChange={(event) => setColor(event.target.value)} aria-label="自定义颜色" /></div><div className={styles.backgroundSwitch}>{(["light","soft","dark"] as PreviewBackground[]).map((value) => <button key={value} className={background === value ? styles.backgroundActive : ""} onClick={() => setBackground(value)}>{value === "light" ? "白色" : value === "soft" ? "浅色" : "深色"}</button>)}</div></section>
<section className={styles.codeSection}>
<h3>使</h3>
<div className={styles.codeTabs}>{(["character","html","react","css"] as CodeMode[]).map((mode) => <button key={mode} className={codeMode === mode ? styles.codeTabActive : ""} onClick={() => setCodeMode(mode)}>{mode === "character" ? "字符" : mode.toUpperCase()}</button>)}</div>
<pre><code>{code}</code></pre>
<button className={styles.copyCode} onClick={() => copy(code, `${codeMode.toUpperCase()} 代码`)}> </button>
</section>
<p className={styles.compatibility}><span></span><strong></strong><small>使 Segoe UI SymbolArial Unicode MS 线</small></p>
</aside>
</div>
{toast && <div className={styles.toast}> {toast}</div>}
</div>
);
}
function categorySymbol(id: string) {
return ({ all: "✣", navigation: "⌖", actions: "✎", status: "◉", media: "▶", files: "▤", business: "▦", communication: "✉", identity: "⚥", biology: "⚛", breeding: "♂♀", pets: "🐾︎" } as Record<string,string>)[id] ?? "◇";
}
function unicodeValue(symbol: string) {
return Array.from(symbol).map((char) => char.codePointAt(0)?.toString(16).toUpperCase().padStart(4, "0")).join(" ");
}
function cssEscaped(symbol: string) {
return Array.from(symbol).map((char) => `\\${char.codePointAt(0)?.toString(16).toUpperCase()} `).join("");
}
function getIconCode(icon: IconItem, mode: CodeMode, size: number, color: string) {
if (mode === "character") return icon.symbol;
if (mode === "html") return `<span class="web-icon" aria-hidden="true">${icon.symbol}</span>`;
if (mode === "react") return `<span className="web-icon" aria-hidden>${icon.symbol}</span>`;
return `.web-icon::before {\n content: "${cssEscaped(icon.symbol)}";\n color: ${color};\n font: ${size}px/1 "Segoe UI Symbol", sans-serif;\n}`;
}