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 && (
-
- )}
-
- >
- );
- }, [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 && (
+
+ )}
+
+ >
+ );
+ }, [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) => {
);
};
+