diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index c751c11c4f4d04f138a58ec257dbe52ba59f626d..3d5ba1142b651027dbe0995b4e7490a3b999d7c2 100644 --- a/src/assets/styles/index.scss +++ b/src/assets/styles/index.scss @@ -1,5 +1,5 @@ html { - background-color: #fff; + background-color: #fefefe; } body { @@ -15,53 +15,4 @@ code { monospace; } -.layout { - width: 100%; - height: 100%; - display: flex; - - &__sidebar { - height: 100vh; - flex: 1 1 25%; - border-right: 1px solid #ccc; - - > h1 { - text-align: center; - color: #303133; - } - - > h3 { - text-align: center; - color: #909399; - } - } - - &__links { - $link-height: 60px; - - border-top: 1px solid #ccc; - - > a { - display: block; - width: 100%; - height: $link-height; - line-height: $link-height; - text-align: center; - user-select: none; - font-size: 24px; - color: #606266; - border-bottom: 1px solid #ccc; - text-decoration: none; - - &.active { - background-color: #eee; - } - } - } - - &__content { - flex: 1 1 75%; - padding: 20px 30px; - box-sizing: border-box; - } -} +@import "./layout.scss"; diff --git a/src/assets/styles/layout.scss b/src/assets/styles/layout.scss new file mode 100644 index 0000000000000000000000000000000000000000..5f52266af3a5ad783b56213683c9fbb14e1372f4 --- /dev/null +++ b/src/assets/styles/layout.scss @@ -0,0 +1,28 @@ +.layout { + width: 100%; + height: 100%; + display: flex; + + &__sidebar { + position: relative; + height: 100vh; + flex: 1 1 25%; + } + + &__traffic { + position: absolute; + left: 0; + right: 0; + bottom: 18px; + + > div { + margin: 0 auto; + } + } + + &__content { + flex: 1 1 75%; + padding: 20px 30px; + box-sizing: border-box; + } +} diff --git a/src/components/list-item-link.tsx b/src/components/list-item-link.tsx new file mode 100644 index 0000000000000000000000000000000000000000..95fc5272a5e1bbd037fa86508c2a6f0411652803 --- /dev/null +++ b/src/components/list-item-link.tsx @@ -0,0 +1,31 @@ +import { ListItem, ListItemButton, ListItemText } from "@mui/material"; +import { useMatch, useResolvedPath, useNavigate } from "react-router-dom"; +import type { LinkProps } from "react-router-dom"; + +const ListItemLink = (props: LinkProps) => { + const { to, children } = props; + + const resolved = useResolvedPath(to); + const match = useMatch({ path: resolved.pathname, end: true }); + const navigate = useNavigate(); + + return ( + <ListItem sx={{ py: 0.5, maxWidth: 250, mx: "auto" }}> + <ListItemButton + sx={{ + borderRadius: 2, + textAlign: "center", + bgcolor: match ? "rgba(91,92,157,0.15)" : "transparent", + }} + onClick={() => navigate(to)} + > + <ListItemText + primary={children} + sx={{ color: match ? "primary.main" : "text.primary" }} + /> + </ListItemButton> + </ListItem> + ); +}; + +export default ListItemLink; diff --git a/src/components/traffic.tsx b/src/components/traffic.tsx new file mode 100644 index 0000000000000000000000000000000000000000..26b8f55cb41c574abe35f805f806f51d3fcb39fe --- /dev/null +++ b/src/components/traffic.tsx @@ -0,0 +1,69 @@ +import axios from "axios"; +import { useEffect, useState } from "react"; +import { ArrowDownward, ArrowUpward } from "@mui/icons-material"; +import parseTraffic from "../utils/parse-traffic"; +import { Typography } from "@mui/material"; +import { Box } from "@mui/system"; + +const Traffic = () => { + const [traffic, setTraffic] = useState({ up: 0, down: 0 }); + + useEffect(() => { + const onTraffic = () => { + axios({ + url: `http://127.0.0.1:9090/traffic`, + method: "GET", + onDownloadProgress: (progressEvent) => { + const data = progressEvent.currentTarget.response || ""; + const lastData = data.slice(data.trim().lastIndexOf("\n") + 1); + try { + if (lastData) setTraffic(JSON.parse(lastData)); + } catch {} + }, + }).catch(() => setTimeout(onTraffic, 500)); + }; + + onTraffic(); + }, []); + + const [up, upUnit] = parseTraffic(traffic.up); + const [down, downUnit] = parseTraffic(traffic.down); + + const valStyle: any = { + component: "span", + color: "primary", + textAlign: "center", + sx: { flex: "1 1 54px" }, + }; + const unitStyle: any = { + component: "span", + color: "grey.500", + fontSize: "12px", + textAlign: "right", + sx: { flex: "0 1 28px", userSelect: "none" }, + }; + + return ( + <Box width="110px"> + <Box mb={2} display="flex" alignItems="center" whiteSpace="nowrap"> + <ArrowUpward + fontSize="small" + color={+up > 0 ? "primary" : "disabled"} + /> + <Typography {...valStyle}>{up}</Typography> + <Typography {...unitStyle}>{upUnit}</Typography> + </Box> + + <Box display="flex" alignItems="center" whiteSpace="nowrap"> + <ArrowDownward + fontSize="small" + color={+down > 0 ? "primary" : "disabled"} + /> + <Typography {...valStyle}>{down}</Typography> + <Typography {...unitStyle}>{downUnit}</Typography> + </Box> + </Box> + ); +}; + +export default Traffic; diff --git a/src/main.tsx b/src/main.tsx index 40dca8cee76a81ec120030ec1145d752f910de8c..a9fa3296aa510328ce68606cd85f4f1dfef0ba6b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,39 +2,32 @@ import "./assets/styles/index.scss"; import React from "react"; import ReactDOM from "react-dom"; -import { BrowserRouter, NavLink, Route, Routes } from "react-router-dom"; -import HomePage from "./pages/home"; -import ProfilesPage from "./pages/profiles"; -import { version } from "../package.json"; +import { BrowserRouter } from "react-router-dom"; +import { createTheme, ThemeProvider } from "@mui/material"; +import Layout from "./pages/_layout"; -function Layout() { - return ( - <div className="layout"> - <div className="layout__sidebar"> - <h1>Clash Verge</h1> - <h3>{version}</h3> +const theme = createTheme({ + palette: { + mode: "light", + primary: { + main: "#5b5c9d", + }, + text: { + primary: "#637381", + secondary: "#909399", + }, + }, +}); - <div className="layout__links"> - <NavLink to="/">Home</NavLink> - <NavLink to="/profiles">Profiles</NavLink> - </div> - </div> - - <div className="layout__content"> - <Routes> - <Route path="/" element={<HomePage />} /> - <Route path="/profiles" element={<ProfilesPage />} /> - </Routes> - </div> - </div> - ); -} +// console.log(theme); ReactDOM.render( <React.StrictMode> - <BrowserRouter> - <Layout /> - </BrowserRouter> + <ThemeProvider theme={theme}> + <BrowserRouter> + <Layout /> + </BrowserRouter> + </ThemeProvider> </React.StrictMode>, document.getElementById("root") ); diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ee0907730916da494fc562d8892d13e98f6f5011 --- /dev/null +++ b/src/pages/_layout.tsx @@ -0,0 +1,74 @@ +import { Route, Routes } from "react-router-dom"; +import { List, Paper, Typography } from "@mui/material"; +import LogPage from "../pages/log"; +import HomePage from "../pages/home"; +import ProxyPage from "../pages/proxy"; +import SettingPage from "../pages/setting"; +import ProfilesPage from "../pages/profiles"; +import ConnectionsPage from "../pages/connections"; +import ListItemLink from "../components/list-item-link"; +import Traffic from "../components/traffic"; + +const Layout = () => { + const routers = [ + { + label: "代ç†", + link: "/proxy", + }, + { + label: "规则", + link: "/profiles", + }, + { + label: "连接", + link: "/connections", + }, + { + label: "日志", + link: "/log", + }, + { + label: "设置", + link: "/setting", + }, + ]; + + return ( + <Paper square elevation={0} className="layout"> + <div className="layout__sidebar"> + <Typography + variant="h3" + component="h1" + sx={{ my: 2, px: 2, textAlign: "center", userSelect: "none" }} + > + Clash Verge + </Typography> + + <List sx={{ userSelect: "none" }}> + {routers.map((router) => ( + <ListItemLink key={router.label} to={router.link}> + {router.label} + </ListItemLink> + ))} + </List> + + <div className="layout__traffic"> + <Traffic /> + </div> + </div> + + <div className="layout__content"> + <Routes> + <Route path="/" element={<HomePage />} /> + <Route path="/proxy" element={<ProxyPage />} /> + <Route path="/profiles" element={<ProfilesPage />} /> + <Route path="/log" element={<LogPage />} /> + <Route path="/connections" element={<ConnectionsPage />} /> + <Route path="/setting" element={<SettingPage />} /> + </Routes> + </div> + </Paper> + ); +}; + +export default Layout; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cb267e047115325bb380d71ec8ad56b13c23b35d --- /dev/null +++ b/src/pages/connections.tsx @@ -0,0 +1,5 @@ +const ConnectionsPage = () => { + return <h1>Connection</h1>; +}; + +export default ConnectionsPage; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index f7ff78f4483d3a37753f9d77447da518fd0ed564..5bda849e5bd4d575c422cfef3860afd70b294f6f 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,18 +1,10 @@ -import { useState } from "react"; -import { TextField } from "@material-ui/core"; +import { Typography } from "@mui/material"; const HomePage = () => { - const [port, setPort] = useState("7890"); - return ( - <div> - <TextField - label="Port" - fullWidth - value={port} - onChange={(e) => setPort(e.target.value)} - /> - </div> + <Typography variant="h1" textAlign="center" mt={10}> + Hello Clash! + </Typography> ); }; diff --git a/src/pages/log.tsx b/src/pages/log.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6b5c9e689969119de793fb1f69de28467bb8b131 --- /dev/null +++ b/src/pages/log.tsx @@ -0,0 +1,5 @@ +const LogPage = () => { + return <h1>Log</h1>; +}; + +export default LogPage; diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 4ed657ba482ad0b386caaa107d7762219c99eb40..067833eb60ccb905efee0be57b4f0ce1b32ab64f 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { invoke } from "@tauri-apps/api"; -import { Button, Grid, TextField } from "@material-ui/core"; +import { Button, Grid, TextField } from "@mui/material"; const ProfilesPage = () => { const [url, setUrl] = useState(""); diff --git a/src/pages/proxy.tsx b/src/pages/proxy.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2808614d425710f089180c4022f4c5d9f5585628 --- /dev/null +++ b/src/pages/proxy.tsx @@ -0,0 +1,5 @@ +const ProxyPage = () => { + return <h1>Proxy</h1>; +}; + +export default ProxyPage; diff --git a/src/pages/setting.tsx b/src/pages/setting.tsx new file mode 100644 index 0000000000000000000000000000000000000000..be8d857e4a0eae51063dc0eb235a6dbc16c13950 --- /dev/null +++ b/src/pages/setting.tsx @@ -0,0 +1,5 @@ +const SettingPage = () => { + return <h1>Setting</h1>; +}; + +export default SettingPage; diff --git a/src/utils/parse-traffic.ts b/src/utils/parse-traffic.ts new file mode 100644 index 0000000000000000000000000000000000000000..529cc3d09bd09c86486d9279763fb7ef5cac0613 --- /dev/null +++ b/src/utils/parse-traffic.ts @@ -0,0 +1,23 @@ +const parseTraffic = (num: number) => { + const gb = 1024 ** 3; + const mb = 1024 ** 2; + const kb = 1024; + let t = num; + let u = "B"; + + if (num < 1000) return [`${Math.round(t)}`, "B/s"]; + if (num <= mb) { + t = num / kb; + u = "KB"; + } else if (num <= gb) { + t = num / mb; + u = "MB"; + } else { + t = num / gb; + u = "GB"; + } + if (t >= 100) return [`${Math.round(t)}`, `${u}/s`]; + return [`${Math.round(t * 10) / 10}`, `${u}/s`]; +}; + +export default parseTraffic;