From 55ef0312171c711636f7ddf38caeb14575bf944e Mon Sep 17 00:00:00 2001 From: sundakai Date: Fri, 17 Oct 2025 12:36:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=86=85=E7=BD=91?= =?UTF-8?q?=E7=BD=91=E5=9D=80=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=86=85=E5=A4=96=E7=BD=91=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database/init.db.go | 5 + service/tools.go | 20 +- types/dto.go | 32 +- types/types.go | 17 +- ui/src/components/CardV2/index.css | 34 ++ ui/src/components/CardV2/index.tsx | 221 +++++----- ui/src/components/Content/index.css | 44 ++ ui/src/components/Content/index.tsx | 499 ++++++++++++---------- ui/src/components/NetworkSwitch/index.css | 9 + ui/src/components/NetworkSwitch/index.tsx | 77 ++++ ui/src/index.css | 3 + ui/src/pages/admin/tabs/Tools.tsx | 25 +- 12 files changed, 605 insertions(+), 381 deletions(-) create mode 100644 ui/src/components/NetworkSwitch/index.css create mode 100644 ui/src/components/NetworkSwitch/index.tsx diff --git a/database/init.db.go b/database/init.db.go index d87db52..bfa6c8e 100644 --- a/database/init.db.go +++ b/database/init.db.go @@ -109,6 +109,11 @@ func InitDB() { DB.Exec(`ALTER TABLE nav_table ADD COLUMN hide BOOLEAN;`) } + // tools数据表结构升级-20250101-【内外网切换】 + if !columnExists("nav_table", "intranetUrl") { + DB.Exec(`ALTER TABLE nav_table ADD COLUMN intranetUrl TEXT;`) + } + // 分类表 sql_create_table = ` CREATE TABLE IF NOT EXISTS nav_catelog ( diff --git a/service/tools.go b/service/tools.go index 90b154a..ef91dc4 100644 --- a/service/tools.go +++ b/service/tools.go @@ -18,12 +18,12 @@ func ImportTools(data []types.Tool) { catelogs = append(catelogs, v.Catelog) } sql_add_tool := ` - INSERT INTO nav_table (id, name, catelog, url, logo, desc) - VALUES (?, ?, ?, ?, ?, ?); + INSERT INTO nav_table (id, name, catelog, url, intranetUrl, logo, desc) + VALUES (?, ?, ?, ?, ?, ?, ?); ` stmt, err := database.DB.Prepare(sql_add_tool) utils.CheckErr(err) - res, err := stmt.Exec(v.Id, v.Name, v.Catelog, v.Url, v.Logo, v.Desc) + res, err := stmt.Exec(v.Id, v.Name, v.Catelog, v.Url, v.IntranetUrl, v.Logo, v.Desc) utils.CheckErr(err) _, err = res.LastInsertId() utils.CheckErr(err) @@ -46,12 +46,12 @@ func UpdateTool(data types.UpdateToolDto) { // 除了更新工具本身之外,也要更新 img 表 sql_update_tool := ` UPDATE nav_table - SET name = ?, url = ?, logo = ?, catelog = ?, desc = ?, sort = ?, hide = ? + SET name = ?, url = ?, intranetUrl = ?, logo = ?, catelog = ?, desc = ?, sort = ?, hide = ? WHERE id = ?; ` stmt, err := database.DB.Prepare(sql_update_tool) utils.CheckErr(err) - res, err := stmt.Exec(data.Name, data.Url, data.Logo, data.Catelog, data.Desc, data.Sort, data.Hide, data.Id) + res, err := stmt.Exec(data.Name, data.Url, data.IntranetUrl, data.Logo, data.Catelog, data.Desc, data.Sort, data.Hide, data.Id) utils.CheckErr(err) _, err = res.RowsAffected() utils.CheckErr(err) @@ -76,8 +76,8 @@ func AddTool(data types.AddToolDto) (int64, error) { }() sql_add_tool := ` - INSERT INTO nav_table (name, url, logo, catelog, desc, sort, hide) - VALUES (?, ?, ?, ?, ?, ?, ?); + INSERT INTO nav_table (name, url, intranetUrl, logo, catelog, desc, sort, hide) + VALUES (?, ?, ?, ?, ?, ?, ?, ?); ` stmt, err := tx.Prepare(sql_add_tool) if err != nil { @@ -85,7 +85,7 @@ func AddTool(data types.AddToolDto) (int64, error) { } defer stmt.Close() - res, err := stmt.Exec(data.Name, data.Url, data.Logo, data.Catelog, data.Desc, data.Sort, data.Hide) + res, err := stmt.Exec(data.Name, data.Url, data.IntranetUrl, data.Logo, data.Catelog, data.Desc, data.Sort, data.Hide) if err != nil { return 0, err } @@ -111,7 +111,7 @@ func AddTool(data types.AddToolDto) (int64, error) { func GetAllTool() []types.Tool { sql_get_all := ` - SELECT id,name,url,logo,catelog,desc,sort,hide FROM nav_table order by sort; + SELECT id,name,url,intranetUrl,logo,catelog,desc,sort,hide FROM nav_table order by sort; ` results := make([]types.Tool, 0) rows, err := database.DB.Query(sql_get_all) @@ -120,7 +120,7 @@ func GetAllTool() []types.Tool { var tool types.Tool var hide interface{} var sort interface{} - err = rows.Scan(&tool.Id, &tool.Name, &tool.Url, &tool.Logo, &tool.Catelog, &tool.Desc, &sort, &hide) + err = rows.Scan(&tool.Id, &tool.Name, &tool.Url, &tool.IntranetUrl, &tool.Logo, &tool.Catelog, &tool.Desc, &sort, &hide) if hide == nil { tool.Hide = false } else { diff --git a/types/dto.go b/types/dto.go index 334894e..8b8a9ee 100644 --- a/types/dto.go +++ b/types/dto.go @@ -31,23 +31,25 @@ type AddCatelogDto struct { Hide bool `json:"hide"` } type UpdateToolDto struct { - Id int `json:"id"` - Name string `json:"name"` - Url string `json:"url"` - Logo string `json:"logo"` - Catelog string `json:"catelog"` - Desc string `json:"desc"` - Sort int `json:"sort"` - Hide bool `json:"hide"` + Id int `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + IntranetUrl string `json:"intranetUrl"` + Logo string `json:"logo"` + Catelog string `json:"catelog"` + Desc string `json:"desc"` + Sort int `json:"sort"` + Hide bool `json:"hide"` } type AddToolDto struct { - Name string `json:"name"` - Url string `json:"url"` - Logo string `json:"logo"` - Catelog string `json:"catelog"` - Desc string `json:"desc"` - Sort int `json:"sort"` - Hide bool `json:"hide"` + Name string `json:"name"` + Url string `json:"url"` + IntranetUrl string `json:"intranetUrl"` + Logo string `json:"logo"` + Catelog string `json:"catelog"` + Desc string `json:"desc"` + Sort int `json:"sort"` + Hide bool `json:"hide"` } type UpdateToolsSortDto struct { Id int `json:"id"` diff --git a/types/types.go b/types/types.go index 8500107..90f86db 100644 --- a/types/types.go +++ b/types/types.go @@ -33,14 +33,15 @@ type Img struct { } type Tool struct { - Id int `json:"id"` - Name string `json:"name"` - Url string `json:"url"` - Logo string `json:"logo"` - Catelog string `json:"catelog"` - Desc string `json:"desc"` - Sort int `json:"sort"` - Hide bool `json:"hide"` + Id int `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + IntranetUrl string `json:"intranetUrl"` + Logo string `json:"logo"` + Catelog string `json:"catelog"` + Desc string `json:"desc"` + Sort int `json:"sort"` + Hide bool `json:"hide"` } type Catelog struct { diff --git a/ui/src/components/CardV2/index.css b/ui/src/components/CardV2/index.css index c6a1970..ac69901 100644 --- a/ui/src/components/CardV2/index.css +++ b/ui/src/components/CardV2/index.css @@ -115,3 +115,37 @@ body.dark-mode .card-content.compact-mode .card-loading-spinner { border-width: 1.5px; } +/* 网络模式指示器样式 */ +.network-indicator { + position: absolute; + left: 8px; + top: 8px; + background: rgba(24, 144, 255, 0.9); + color: white; + font-size: 10px; + font-weight: bold; + padding: 2px 4px; + border-radius: 3px; + z-index: 10; + line-height: 1; + min-width: 12px; + text-align: center; +} + +.network-indicator.intranet { + background: rgba(82, 196, 26, 0.9); +} + +.network-indicator.internet { + background: rgba(24, 144, 255, 0.9); +} + +/* 暗色模式下的网络指示器 */ +body.dark-mode .network-indicator { + background: rgba(24, 144, 255, 0.8); +} + +body.dark-mode .network-indicator.intranet { + background: rgba(82, 196, 26, 0.8); +} + diff --git a/ui/src/components/CardV2/index.tsx b/ui/src/components/CardV2/index.tsx index e9650f7..fd4539e 100644 --- a/ui/src/components/CardV2/index.tsx +++ b/ui/src/components/CardV2/index.tsx @@ -1,108 +1,113 @@ -import { useMemo, useState, useEffect } from "react"; -import "./index.css"; -import { getLogoUrl } from "../../utils/check"; -import { getJumpTarget } from "../../utils/setting"; - -const Card = ({ title, url, des, logo, catelog, onClick, index, isSearching, noImageMode, compactMode }) => { - const [imageLoaded, setImageLoaded] = useState(false); - const [imageError, setImageError] = useState(false); - const [showLoading, setShowLoading] = useState(true); - - const imageSrc = useMemo(() => { - return url === "admin" ? logo : getLogoUrl(logo); - }, [logo, url]); - - // 当图片源变化时重置状态,并添加超时保护 - useEffect(() => { - setImageLoaded(false); - setImageError(false); - setShowLoading(true); - - // 10秒超时保护 - const timeout = setTimeout(() => { - setShowLoading(false); - console.warn('Image loading timeout:', imageSrc); - }, 10000); - - return () => clearTimeout(timeout); - }, [imageSrc]); - - const handleImageLoad = () => { - setImageLoaded(true); - setShowLoading(false); - }; - - const handleImageError = () => { - setImageError(true); - setShowLoading(false); - }; - - const el = useMemo(() => { - if (imageError) { - return
🖼️
; - } - - return ( - <> - {showLoading && !imageLoaded && ( -
- )} - {title} - - ); - }, [imageSrc, title, imageLoaded, imageError, showLoading]); - - // 处理空分类,显示为"未分类" - const displayCatelog = useMemo(() => { - return catelog === null || catelog === undefined || catelog === "" || (typeof catelog === 'string' && catelog.trim() === "") - ? "未分类" - : catelog; - }, [catelog]); - - const showNumIndex = index < 10 && isSearching; - return ( - { - onClick(); - }} - target={getJumpTarget() === "blank" ? "_blank" : "_self"} - rel="noreferrer" - className="card-box" - > - {showNumIndex && {index + 1}} -
- {!noImageMode && ( -
- {el} -
- )} -
-
- {title} - {!compactMode && {displayCatelog}} -
- {!compactMode &&
{des}
} -
-
-
- ); -}; - -export default Card; +import { useMemo, useState, useEffect } from "react"; +import "./index.css"; +import { getLogoUrl } from "../../utils/check"; +import { getJumpTarget } from "../../utils/setting"; + +const Card = ({ title, url, des, logo, catelog, onClick, index, isSearching, noImageMode, compactMode, networkMode, hasIntranetUrl }) => { + const [imageLoaded, setImageLoaded] = useState(false); + const [imageError, setImageError] = useState(false); + const [showLoading, setShowLoading] = useState(true); + + const imageSrc = useMemo(() => { + return url === "admin" ? logo : getLogoUrl(logo); + }, [logo, url]); + + // 当图片源变化时重置状态,并添加超时保护 + useEffect(() => { + setImageLoaded(false); + setImageError(false); + setShowLoading(true); + + // 10秒超时保护 + const timeout = setTimeout(() => { + setShowLoading(false); + console.warn('Image loading timeout:', imageSrc); + }, 10000); + + return () => clearTimeout(timeout); + }, [imageSrc]); + + const handleImageLoad = () => { + setImageLoaded(true); + setShowLoading(false); + }; + + const handleImageError = () => { + setImageError(true); + setShowLoading(false); + }; + + const el = useMemo(() => { + if (imageError) { + return
🖼️
; + } + + return ( + <> + {showLoading && !imageLoaded && ( +
+ )} + {title} + + ); + }, [imageSrc, title, imageLoaded, imageError, showLoading]); + + // 处理空分类,显示为"未分类" + const displayCatelog = useMemo(() => { + return catelog === null || catelog === undefined || catelog === "" || (typeof catelog === 'string' && catelog.trim() === "") + ? "未分类" + : catelog; + }, [catelog]); + + const showNumIndex = index < 10 && isSearching; + return ( + { + onClick(); + }} + target={getJumpTarget() === "blank" ? "_blank" : "_self"} + rel="noreferrer" + className="card-box" + > + {showNumIndex && {index + 1}} + {hasIntranetUrl && ( + + {networkMode === 'intranet' ? '内' : '外'} + + )} +
+ {!noImageMode && ( +
+ {el} +
+ )} +
+
+ {title} + {!compactMode && {displayCatelog}} +
+ {!compactMode &&
{des}
} +
+
+
+ ); +}; + +export default Card; diff --git a/ui/src/components/Content/index.css b/ui/src/components/Content/index.css index f2f8acb..f7ceaa4 100644 --- a/ui/src/components/Content/index.css +++ b/ui/src/components/Content/index.css @@ -26,6 +26,17 @@ width: 24px; height: 24px; } + .network-switch-box { + right: 10px; + bottom: 75px; + } + .network-switch-box.hide-github { + bottom: 40px; + } + .network-switch-box svg { + width: 24px; + height: 24px; + } .select-tag { font-size: 13px; padding: 2px 6px; @@ -168,6 +179,17 @@ width: 30px; height: 30px; } + .network-switch-box { + right: 14px; + bottom: 80px; + } + .network-switch-box.hide-github { + bottom: 46px; + } + .network-switch-box svg { + width: 30px; + height: 30px; + } .select-tag { font-size: 13px; padding: 2px 8px; @@ -310,6 +332,17 @@ width: 30px; height: 30px; } + .network-switch-box { + right: 10px; + bottom: 85px; + } + .network-switch-box.hide-github { + bottom: 50px; + } + .network-switch-box svg { + width: 30px; + height: 30px; + } .select-tag { font-size: 13px; padding: 2px 10px; @@ -461,6 +494,17 @@ width: 40px; height: 40px; } + .network-switch-box { + right: 10px; + bottom: 125px; + } + .network-switch-box.hide-github { + bottom: 70px; + } + .network-switch-box svg { + width: 40px; + height: 40px; + } .select-tag { font-size: 14px; padding: 2px 10px; diff --git a/ui/src/components/Content/index.tsx b/ui/src/components/Content/index.tsx index ce17a53..c92376f 100644 --- a/ui/src/components/Content/index.tsx +++ b/ui/src/components/Content/index.tsx @@ -1,236 +1,263 @@ -import "./index.css"; -import CardV2 from "../CardV2"; -import SearchBar from "../SearchBar"; -import { Loading } from "../Loading"; -import { Helmet } from "react-helmet"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { FetchList } from "../../utils/api"; -import TagSelector from "../TagSelector"; -import pinyin from "pinyin-match"; -import GithubLink from "../GithubLink"; -import DarkSwitch from "../DarkSwitch"; -import { isLogin } from "../../utils/check"; -import { generateSearchEngineCard } from "../../utils/serachEngine"; -import { toggleJumpTarget } from "../../utils/setting"; - -const mutiSearch = (s, t) => { - const source = (s as string).toLowerCase(); - const target = t.toLowerCase(); - const rawInclude = source.includes(target); - const pinYinInlcude = Boolean(pinyin.match(source, target)); - return rawInclude || pinYinInlcude; -}; - -const Content = (props: any) => { - const [data, setData] = useState({}); - const [loading, setLoading] = useState(true); - const [currTag, setCurrTag] = useState("全部工具"); - const [searchString, setSearchString] = useState(""); - const [val, setVal] = useState(""); - const [searchEngineCards, setSearchEngineCards] = useState([]); - - const filteredDataRef = useRef([]); - - const showGithub = useMemo(() => { - const hide = data?.setting?.hideGithub === true - return !hide; - }, [data]) - - const loadData = useCallback(async () => { - try { - setLoading(true); - const r = await FetchList(); - setData(r); - const tagInLocalStorage = window.localStorage.getItem("tag"); - if (tagInLocalStorage && tagInLocalStorage !== "") { - if (r?.catelogs && r?.catelogs.includes(tagInLocalStorage)) { - setCurrTag(tagInLocalStorage); - } - } - } catch (e) { - console.log(e); - } finally { - setLoading(false); - } - }, [setData, setLoading, setCurrTag]); - - useEffect(() => { - loadData(); - }, [loadData]); - - // 异步加载搜索引擎卡片 - useEffect(() => { - const loadSearchEngineCards = async () => { - try { - const cards = await generateSearchEngineCard(searchString); - setSearchEngineCards(cards); - } catch (error) { - console.error('加载搜索引擎卡片失败:', error); - setSearchEngineCards([]); - } - }; - - loadSearchEngineCards(); - }, [searchString]); - - const handleSetCurrTag = (tag: string) => { - setCurrTag(tag); - // 管理后台不记录了 - if (tag !== "管理后台") { - window.localStorage.setItem("tag", tag); - } - resetSearch(true); - }; - - const resetSearch = (notSetTag?: boolean) => { - setVal(""); - setSearchString(""); - const tagInLocalStorage = window.localStorage.getItem("tag"); - if (!notSetTag && tagInLocalStorage && tagInLocalStorage !== "" && tagInLocalStorage !== "管理后台") { - setCurrTag(tagInLocalStorage); - } - }; - - const handleSetSearch = (val: string) => { - if (val !== "" && val) { - setCurrTag("全部工具"); - setSearchString(val.trim()); - } else { - resetSearch(); - } - } - - const filteredData = useMemo(() => { - if (data.tools) { - const localResult = data.tools - .filter((item: any) => { - if (currTag === "全部工具") { - return true; - } - return item.catelog === currTag; - }) - .filter((item: any) => { - if (searchString === "") { - return true; - } - return ( - mutiSearch(item.name, searchString) || - mutiSearch(item.desc, searchString) || - mutiSearch(item.url, searchString) - ); - }); - return [...localResult, ...searchEngineCards] - } else { - return [...searchEngineCards]; - } - }, [data, currTag, searchString, searchEngineCards]); - - useEffect(() => { - filteredDataRef.current = filteredData - }, [filteredData]) - - useEffect(() => { - if (searchString.trim() === "") { - document.removeEventListener("keydown", onKeyEnter); - } else { - document.addEventListener("keydown", onKeyEnter); - } - return () => { - document.removeEventListener("keydown", onKeyEnter); - } - // eslint-disable-next-line - }, [searchString]) - - const renderCardsV2 = useCallback(() => { - return filteredData.map((item, index) => { - return ( - { - resetSearch(); - if (item.url === "toggleJumpTarget") { - toggleJumpTarget(); - loadData(); - } - }} - /> - ); - }); - // eslint-disable-next-line - }, [filteredData, searchString, data?.siteConfig?.noImageMode, data?.siteConfig?.compactMode]); - - const onKeyEnter = (ev: KeyboardEvent) => { - const cards = filteredDataRef.current; - // 使用 keyCode 防止与中文输入冲突 - if (ev.keyCode === 13) { - if (cards && cards.length) { - window.open(cards[0]?.url, "_blank"); - resetSearch(); - } - } - // 如果按了数字键 + ctrl/meta,打开对应的卡片 - if (ev.ctrlKey || ev.metaKey) { - const num = Number(ev.key); - if (isNaN(num)) return; - ev.preventDefault() - const index = Number(ev.key) - 1; - if (index >= 0 && index < cards.length) { - window.open(cards[index]?.url, "_blank"); - resetSearch(); - } - } - - }; - - return ( - <> - - - - {data?.setting?.title ?? "Van Nav"} - -
-
- { - setVal(t); - handleSetSearch(t); - }} - /> - -
-
-
-
- {loading ? : renderCardsV2()} -
-
- - {showGithub && } - - - ); -}; - -export default Content; +import "./index.css"; +import CardV2 from "../CardV2"; +import SearchBar from "../SearchBar"; +import { Loading } from "../Loading"; +import { Helmet } from "react-helmet"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { FetchList } from "../../utils/api"; +import TagSelector from "../TagSelector"; +import pinyin from "pinyin-match"; +import GithubLink from "../GithubLink"; +import DarkSwitch from "../DarkSwitch"; +import NetworkSwitch from "../NetworkSwitch"; +import { isLogin } from "../../utils/check"; +import { generateSearchEngineCard } from "../../utils/serachEngine"; +import { toggleJumpTarget } from "../../utils/setting"; + +const mutiSearch = (s, t) => { + const source = (s as string).toLowerCase(); + const target = t.toLowerCase(); + const rawInclude = source.includes(target); + const pinYinInlcude = Boolean(pinyin.match(source, target)); + return rawInclude || pinYinInlcude; +}; + +const Content = (props: any) => { + const [data, setData] = useState({}); + const [loading, setLoading] = useState(true); + const [currTag, setCurrTag] = useState("全部工具"); + const [searchString, setSearchString] = useState(""); + const [val, setVal] = useState(""); + const [searchEngineCards, setSearchEngineCards] = useState([]); + const [networkMode, setNetworkMode] = useState<'internet' | 'intranet'>('internet'); + + const filteredDataRef = useRef([]); + + const showGithub = useMemo(() => { + const hide = data?.setting?.hideGithub === true + return !hide; + }, [data]) + + const loadData = useCallback(async () => { + try { + setLoading(true); + const r = await FetchList(); + setData(r); + const tagInLocalStorage = window.localStorage.getItem("tag"); + if (tagInLocalStorage && tagInLocalStorage !== "") { + if (r?.catelogs && r?.catelogs.includes(tagInLocalStorage)) { + setCurrTag(tagInLocalStorage); + } + } + } catch (e) { + console.log(e); + } finally { + setLoading(false); + } + }, [setData, setLoading, setCurrTag]); + + useEffect(() => { + loadData(); + }, [loadData]); + + // 监听网络模式切换 + useEffect(() => { + const handleNetworkModeChange = (event: CustomEvent) => { + setNetworkMode(event.detail.mode); + }; + + window.addEventListener('networkModeChanged', handleNetworkModeChange as EventListener); + + // 初始化网络模式 + const savedMode = localStorage.getItem("networkMode"); + if (savedMode === 'intranet' || savedMode === 'internet') { + setNetworkMode(savedMode); + } + + return () => { + window.removeEventListener('networkModeChanged', handleNetworkModeChange as EventListener); + }; + }, []); + + // 异步加载搜索引擎卡片 + useEffect(() => { + const loadSearchEngineCards = async () => { + try { + const cards = await generateSearchEngineCard(searchString); + setSearchEngineCards(cards); + } catch (error) { + console.error('加载搜索引擎卡片失败:', error); + setSearchEngineCards([]); + } + }; + + loadSearchEngineCards(); + }, [searchString]); + + const handleSetCurrTag = (tag: string) => { + setCurrTag(tag); + // 管理后台不记录了 + if (tag !== "管理后台") { + window.localStorage.setItem("tag", tag); + } + resetSearch(true); + }; + + const resetSearch = (notSetTag?: boolean) => { + setVal(""); + setSearchString(""); + const tagInLocalStorage = window.localStorage.getItem("tag"); + if (!notSetTag && tagInLocalStorage && tagInLocalStorage !== "" && tagInLocalStorage !== "管理后台") { + setCurrTag(tagInLocalStorage); + } + }; + + const handleSetSearch = (val: string) => { + if (val !== "" && val) { + setCurrTag("全部工具"); + setSearchString(val.trim()); + } else { + resetSearch(); + } + } + + const filteredData = useMemo(() => { + if (data.tools) { + const localResult = data.tools + .filter((item: any) => { + if (currTag === "全部工具") { + return true; + } + return item.catelog === currTag; + }) + .filter((item: any) => { + if (searchString === "") { + return true; + } + return ( + mutiSearch(item.name, searchString) || + mutiSearch(item.desc, searchString) || + mutiSearch(item.url, searchString) + ); + }); + return [...localResult, ...searchEngineCards] + } else { + return [...searchEngineCards]; + } + }, [data, currTag, searchString, searchEngineCards]); + + useEffect(() => { + filteredDataRef.current = filteredData + }, [filteredData]) + + useEffect(() => { + if (searchString.trim() === "") { + document.removeEventListener("keydown", onKeyEnter); + } else { + document.addEventListener("keydown", onKeyEnter); + } + return () => { + document.removeEventListener("keydown", onKeyEnter); + } + // eslint-disable-next-line + }, [searchString]) + + const renderCardsV2 = useCallback(() => { + return filteredData.map((item, index) => { + // 根据网络模式选择URL + const currentUrl = networkMode === 'intranet' && item.intranetUrl ? item.intranetUrl : item.url; + + return ( + { + resetSearch(); + if (item.url === "toggleJumpTarget") { + toggleJumpTarget(); + loadData(); + } + }} + /> + ); + }); + // eslint-disable-next-line + }, [filteredData, searchString, data?.siteConfig?.noImageMode, data?.siteConfig?.compactMode, networkMode]); + + const onKeyEnter = (ev: KeyboardEvent) => { + const cards = filteredDataRef.current; + // 使用 keyCode 防止与中文输入冲突 + if (ev.keyCode === 13) { + if (cards && cards.length) { + window.open(cards[0]?.url, "_blank"); + resetSearch(); + } + } + // 如果按了数字键 + ctrl/meta,打开对应的卡片 + if (ev.ctrlKey || ev.metaKey) { + const num = Number(ev.key); + if (isNaN(num)) return; + ev.preventDefault() + const index = Number(ev.key) - 1; + if (index >= 0 && index < cards.length) { + window.open(cards[index]?.url, "_blank"); + resetSearch(); + } + } + + }; + + return ( + <> + + + + {data?.setting?.title ?? "Van Nav"} + +
+
+ { + setVal(t); + handleSetSearch(t); + }} + /> + +
+
+
+
+ {loading ? : renderCardsV2()} +
+
+ + {showGithub && } + + + + ); +}; + +export default Content; diff --git a/ui/src/components/NetworkSwitch/index.css b/ui/src/components/NetworkSwitch/index.css new file mode 100644 index 0000000..7f9042d --- /dev/null +++ b/ui/src/components/NetworkSwitch/index.css @@ -0,0 +1,9 @@ +.network-switch-box { + position: fixed; + cursor: pointer; +} +.network-switch-box svg { + fill: #ffffff; +} + + diff --git a/ui/src/components/NetworkSwitch/index.tsx b/ui/src/components/NetworkSwitch/index.tsx new file mode 100644 index 0000000..14ac8d5 --- /dev/null +++ b/ui/src/components/NetworkSwitch/index.tsx @@ -0,0 +1,77 @@ +import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import "./index.css"; + +const NetworkSwitch = ({ showGithub }: { showGithub: boolean }) => { + const [networkMode, setNetworkMode] = useState<'internet' | 'intranet'>('internet'); + const { current } = useRef({ hasInit: false }); + + useLayoutEffect(() => { + if (!current.hasInit) { + current.hasInit = true; + const savedMode = localStorage.getItem("networkMode"); + if (savedMode === 'intranet' || savedMode === 'internet') { + setNetworkMode(savedMode); + } else { + setNetworkMode('internet'); + } + } + }, []); + + useEffect(() => { + localStorage.setItem("networkMode", networkMode); + // 触发自定义事件,通知其他组件网络模式已切换 + window.dispatchEvent(new CustomEvent('networkModeChanged', { + detail: { mode: networkMode } + })); + }, [networkMode]); + + const handleSwitch = () => { + setNetworkMode(prev => prev === 'internet' ? 'intranet' : 'internet'); + }; + + const internetIcon = ( + + + + + + + ); + + const intranetIcon = ( + + + + + + + + ); + + return ( +
+ {networkMode === 'internet' ? internetIcon : intranetIcon} +
+ ); +}; + +export default NetworkSwitch; + diff --git a/ui/src/index.css b/ui/src/index.css index c40fb3a..aba8e0a 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -32,6 +32,9 @@ body.dark-mode .theme-switch-box svg { body.dark-mode .theme-switch-box svg:hover { fill: #f1f1f1; } +body.dark-mode .network-switch-box svg { + fill: white; +} body.dark-mode { background-color: #121212; color: rgba(255, 255, 255, 0.6); diff --git a/ui/src/pages/admin/tabs/Tools.tsx b/ui/src/pages/admin/tabs/Tools.tsx index 041c692..ae6995d 100644 --- a/ui/src/pages/admin/tabs/Tools.tsx +++ b/ui/src/pages/admin/tabs/Tools.tsx @@ -557,10 +557,23 @@ export const Tools: React.FC = (props) => { } ]} required - label="网址" + label="外网网址" labelCol={{ span: 4 }} > - + + + + @@ -640,8 +653,11 @@ export const Tools: React.FC = (props) => { - - + + + + + @@ -696,3 +712,4 @@ export const Tools: React.FC = (props) => { ); }; +