mirror of https://github.com/openspug/spug
update
parent
74e064002a
commit
41990ef08b
|
@ -16,6 +16,7 @@ module.exports = {
|
||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
|
'react/prop-types': 'off',
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
t: 'readonly',
|
t: 'readonly',
|
||||||
|
|
|
@ -10,7 +10,9 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"antd": "^5.11.5",
|
"@ant-design/icons": "^5.2.6",
|
||||||
|
"antd": "^5.12.4",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"i18next": "^23.7.7",
|
"i18next": "^23.7.7",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
|
@ -1,11 +1,47 @@
|
||||||
import {useTranslation } from 'react-i18next'
|
import {createBrowserRouter, RouterProvider} from 'react-router-dom'
|
||||||
|
import {ConfigProvider, App as AntdApp, theme} from 'antd'
|
||||||
|
import zhCN from 'antd/locale/zh_CN'
|
||||||
|
import enUS from 'antd/locale/en_US'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import routes from './routes.jsx'
|
||||||
|
import {session, SContext} from '@/libs'
|
||||||
|
import {useImmer} from 'use-immer'
|
||||||
|
import './i18n.js'
|
||||||
|
|
||||||
|
dayjs.locale(session.lang)
|
||||||
|
|
||||||
function App(props) {
|
const router = createBrowserRouter(routes)
|
||||||
const {t} = useTranslation()
|
|
||||||
window.t = t
|
|
||||||
|
|
||||||
return props.children
|
function App() {
|
||||||
|
const [S, updateS] = useImmer({theme: session.theme})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SContext.Provider value={{S, updateS}}>
|
||||||
|
<ConfigProvider
|
||||||
|
locale={session.lang === 'en' ? enUS : zhCN}
|
||||||
|
theme={{
|
||||||
|
cssVar: true,
|
||||||
|
hashed: false,
|
||||||
|
algorithm: S.theme === 'dark' ? theme.darkAlgorithm : null,
|
||||||
|
token: {
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 12,
|
||||||
|
paddingLG: 16,
|
||||||
|
controlHeight: 30,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Layout: {
|
||||||
|
headerHeight: 48,
|
||||||
|
footerPadding: 16
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
<AntdApp>
|
||||||
|
<RouterProvider router={router}/>
|
||||||
|
</AntdApp>
|
||||||
|
</ConfigProvider>
|
||||||
|
</SContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
Binary file not shown.
Before Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
@ -1,8 +1,9 @@
|
||||||
import i18n from 'i18next'
|
import i18n from 'i18next'
|
||||||
import {initReactI18next} from 'react-i18next'
|
import {initReactI18next} from 'react-i18next'
|
||||||
|
import {session} from '@/libs'
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
lng: localStorage.getItem('lang') || 'zh',
|
lng: session.lang,
|
||||||
resources: {
|
resources: {
|
||||||
en: {
|
en: {
|
||||||
translation: {
|
translation: {
|
||||||
|
@ -12,6 +13,15 @@ i18n.use(initReactI18next).init({
|
||||||
'批量执行': 'Batch',
|
'批量执行': 'Batch',
|
||||||
'执行任务': 'Task',
|
'执行任务': 'Task',
|
||||||
'文件分发': 'Transfer',
|
'文件分发': 'Transfer',
|
||||||
|
'重置': 'Reset',
|
||||||
|
'展示字段': 'Columns Display',
|
||||||
|
'年龄': 'Age',
|
||||||
|
'page': 'Total {{total}} items',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
translation: {
|
||||||
|
'page': '共 {{total}} 条',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,22 @@
|
||||||
import {Layout, Flex, Dropdown, theme} from 'antd'
|
import {useContext} from 'react'
|
||||||
|
import {Layout, Flex, Dropdown} from 'antd'
|
||||||
import {AiOutlineTranslation} from 'react-icons/ai'
|
import {AiOutlineTranslation} from 'react-icons/ai'
|
||||||
import css from './header.module.scss'
|
import {IoSunny, IoMoon} from 'react-icons/io5'
|
||||||
|
import {SContext} from '@/libs'
|
||||||
|
import css from './index.module.scss'
|
||||||
import i18n from '@/i18n.js'
|
import i18n from '@/i18n.js'
|
||||||
|
import logo from "@/assets/spug-default.png";
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const {token: {colorBgContainer}} = theme.useToken()
|
const {S: {theme}, updateS} = useContext(SContext)
|
||||||
|
|
||||||
|
function handleThemeChange() {
|
||||||
|
const newTheme = theme === 'light' ? 'dark' : 'light'
|
||||||
|
localStorage.setItem('theme', newTheme)
|
||||||
|
updateS(draft => {
|
||||||
|
draft.theme = newTheme
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handleLangChange({key}) {
|
function handleLangChange({key}) {
|
||||||
localStorage.setItem('lang', key)
|
localStorage.setItem('lang', key)
|
||||||
|
@ -19,9 +31,9 @@ function Header() {
|
||||||
key: 'en',
|
key: 'en',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
console.log('lang', i18n.language)
|
|
||||||
return (
|
return (
|
||||||
<Layout.Header className={css.header} style={{background: colorBgContainer}}>
|
<Layout.Header theme="light" className={css.header}>
|
||||||
|
<img src={logo} alt="logo" className={css.logo}/>
|
||||||
<Flex justify="flex-end" align="center" gap="small" style={{height: 48}}>
|
<Flex justify="flex-end" align="center" gap="small" style={{height: 48}}>
|
||||||
<div className={css.item}>admin</div>
|
<div className={css.item}>admin</div>
|
||||||
<Dropdown menu={{items: locales, selectable: true, onClick: handleLangChange, selectedKeys: [i18n.language]}}>
|
<Dropdown menu={{items: locales, selectable: true, onClick: handleLangChange, selectedKeys: [i18n.language]}}>
|
||||||
|
@ -29,6 +41,9 @@ function Header() {
|
||||||
<AiOutlineTranslation size={18}/>
|
<AiOutlineTranslation size={18}/>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<div className={css.item} onClick={handleThemeChange}>
|
||||||
|
{theme === 'light' ? <IoMoon size={16}/> : <IoSunny size={18}/>}
|
||||||
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
.header {
|
|
||||||
padding: 0 8px;
|
|
||||||
|
|
||||||
.item {
|
|
||||||
height: 48px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.1s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f1f1f1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +1,14 @@
|
||||||
import {useState} from 'react'
|
|
||||||
import {Outlet, useMatches, useNavigate} from 'react-router-dom'
|
import {Outlet, useMatches, useNavigate} from 'react-router-dom'
|
||||||
import {Layout, Breadcrumb, Flex, Menu, theme} from 'antd'
|
import {Layout, Flex, Menu, theme} from 'antd'
|
||||||
import Header from './Header.jsx'
|
import Header from './Header.jsx'
|
||||||
import {menus} from '@/routes'
|
import {menus} from '@/routes'
|
||||||
import logo1 from '@/assets/logo-spug-white.png'
|
import css from './index.module.scss'
|
||||||
import logo2 from '@/assets/logo-white.png'
|
|
||||||
|
|
||||||
|
|
||||||
function LayoutIndex() {
|
function LayoutIndex() {
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const {token: {colorTextTertiary}} = theme.useToken()
|
||||||
const {token: {colorBgContainer, colorTextTertiary}} = theme.useToken()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const matches = useMatches()
|
const matches = useMatches()
|
||||||
const crumbs = matches.map(x => ({title: x.handle.crumb, href: x.pathname}))
|
|
||||||
|
|
||||||
function handleMenuClick({key}) {
|
function handleMenuClick({key}) {
|
||||||
navigate(key)
|
navigate(key)
|
||||||
|
@ -21,29 +17,20 @@ function LayoutIndex() {
|
||||||
const selectedKey = matches[matches.length - 1]?.pathname
|
const selectedKey = matches[matches.length - 1]?.pathname
|
||||||
return (
|
return (
|
||||||
<Layout style={{minHeight: '100vh'}}>
|
<Layout style={{minHeight: '100vh'}}>
|
||||||
<Layout.Sider collapsible collapsed={collapsed} onCollapse={setCollapsed}>
|
|
||||||
<Flex justify="center" align="center" style={{height: 64}}>
|
|
||||||
{collapsed ? (
|
|
||||||
<img src={logo2} alt="logo" style={{width: 64}}/>
|
|
||||||
) : (
|
|
||||||
<img src={logo1} alt="logo" style={{width: 160, height: 32}}/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<Menu theme="dark" mode="inline" items={menus} selectedKeys={[selectedKey]} onClick={handleMenuClick}/>
|
|
||||||
</Layout.Sider>
|
|
||||||
<Layout>
|
|
||||||
<Header/>
|
<Header/>
|
||||||
<Layout.Content style={{margin: '0 16px'}}>
|
<Layout>
|
||||||
<Breadcrumb style={{margin: '16px 0'}} items={crumbs}/>
|
<Layout.Sider theme="light" collapsible className={css.sider}>
|
||||||
<div style={{padding: 24, minHeight: 360, background: colorBgContainer}}>
|
<Menu theme="light" mode="inline" items={menus} selectedKeys={[selectedKey]}
|
||||||
|
onClick={handleMenuClick}/>
|
||||||
|
</Layout.Sider>
|
||||||
|
<Layout.Content style={{margin: '16px 16px 0 16px'}}>
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
</div>
|
|
||||||
</Layout.Content>
|
|
||||||
<Layout.Footer>
|
<Layout.Footer>
|
||||||
<Flex justify="center" align="center" style={{color: colorTextTertiary}}>
|
<Flex justify="center" align="center" style={{color: colorTextTertiary}}>
|
||||||
Copyright © 2023 OpenSpug All Rights Reserved.
|
Copyright © 2023 OpenSpug All Rights Reserved.
|
||||||
</Flex>
|
</Flex>
|
||||||
</Layout.Footer>
|
</Layout.Footer>
|
||||||
|
</Layout.Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
.header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
padding: 0 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--ant-color-border);
|
||||||
|
background: var(--ant-color-bg-container);
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin-left: 16px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
height: 47px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.1s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--ant-color-fill-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider {
|
||||||
|
:global(.ant-menu) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-layout-sider-trigger) {
|
||||||
|
border-inline-end: 1px solid var(--ant-color-split);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
border-inline-end: none;
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
|
import React from 'react'
|
||||||
import http from './http'
|
import http from './http'
|
||||||
import session from './session'
|
import session from './session'
|
||||||
|
|
||||||
|
const SContext = React.createContext({})
|
||||||
|
export * from './utils.js'
|
||||||
export {
|
export {
|
||||||
http,
|
http,
|
||||||
session,
|
session,
|
||||||
|
SContext,
|
||||||
}
|
}
|
|
@ -3,6 +3,8 @@ import {isSubArray} from "@/libs/utils.js";
|
||||||
class Session {
|
class Session {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._session = {};
|
this._session = {};
|
||||||
|
this.lang = localStorage.getItem('lang') || 'zh';
|
||||||
|
this.theme = localStorage.getItem('theme') || 'light';
|
||||||
const tmp = localStorage.getItem('session');
|
const tmp = localStorage.getItem('session');
|
||||||
if (tmp) {
|
if (tmp) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,30 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import {createBrowserRouter, RouterProvider} from 'react-router-dom'
|
import App from './App.jsx'
|
||||||
import {ConfigProvider, App, theme} from 'antd'
|
|
||||||
import routes from './routes.jsx'
|
|
||||||
import './i18n.js'
|
|
||||||
|
|
||||||
|
|
||||||
const router = createBrowserRouter(routes)
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ConfigProvider theme={{
|
<App/>
|
||||||
algorithm: [theme.defaultAlgorithm],
|
|
||||||
token: {
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Layout: {
|
|
||||||
headerHeight: 48,
|
|
||||||
footerPadding: 16
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}>
|
|
||||||
<App>
|
|
||||||
<RouterProvider router={router}/>
|
|
||||||
</App>
|
|
||||||
</ConfigProvider>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,6 @@ let routes = [
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <Layout/>,
|
element: <Layout/>,
|
||||||
errorElement: <ErrorPage/>,
|
errorElement: <ErrorPage/>,
|
||||||
title: t('首页'),
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
|
@ -72,17 +71,5 @@ function routes2menu(routes, parentPath = '') {
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle(routes) {
|
|
||||||
for (const route of routes) {
|
|
||||||
if (route.children) {
|
|
||||||
route.children = handle(route.children)
|
|
||||||
}
|
|
||||||
route.handle = {crumb: route.title}
|
|
||||||
}
|
|
||||||
return routes
|
|
||||||
}
|
|
||||||
|
|
||||||
routes = handle(routes)
|
|
||||||
|
|
||||||
export const menus = routes2menu(routes[0].children)
|
export const menus = routes2menu(routes[0].children)
|
||||||
export default routes
|
export default routes
|
Loading…
Reference in New Issue