mirror of https://github.com/openspug/spug
update
parent
74e064002a
commit
41990ef08b
|
@ -16,6 +16,7 @@ module.exports = {
|
|||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
globals: {
|
||||
t: 'readonly',
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^5.11.5",
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"antd": "^5.12.4",
|
||||
"dayjs": "^1.11.10",
|
||||
"i18next": "^23.7.7",
|
||||
"react": "^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 {t} = useTranslation()
|
||||
window.t = t
|
||||
const router = createBrowserRouter(routes)
|
||||
|
||||
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
|
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 {initReactI18next} from 'react-i18next'
|
||||
import {session} from '@/libs'
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: localStorage.getItem('lang') || 'zh',
|
||||
lng: session.lang,
|
||||
resources: {
|
||||
en: {
|
||||
translation: {
|
||||
|
@ -12,6 +13,15 @@ i18n.use(initReactI18next).init({
|
|||
'批量执行': 'Batch',
|
||||
'执行任务': 'Task',
|
||||
'文件分发': '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 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 logo from "@/assets/spug-default.png";
|
||||
|
||||
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}) {
|
||||
localStorage.setItem('lang', key)
|
||||
|
@ -19,9 +31,9 @@ function Header() {
|
|||
key: 'en',
|
||||
}]
|
||||
|
||||
console.log('lang', i18n.language)
|
||||
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}}>
|
||||
<div className={css.item}>admin</div>
|
||||
<Dropdown menu={{items: locales, selectable: true, onClick: handleLangChange, selectedKeys: [i18n.language]}}>
|
||||
|
@ -29,6 +41,9 @@ function Header() {
|
|||
<AiOutlineTranslation size={18}/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<div className={css.item} onClick={handleThemeChange}>
|
||||
{theme === 'light' ? <IoMoon size={16}/> : <IoSunny size={18}/>}
|
||||
</div>
|
||||
</Flex>
|
||||
</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 {Layout, Breadcrumb, Flex, Menu, theme} from 'antd'
|
||||
import {Layout, Flex, Menu, theme} from 'antd'
|
||||
import Header from './Header.jsx'
|
||||
import {menus} from '@/routes'
|
||||
import logo1 from '@/assets/logo-spug-white.png'
|
||||
import logo2 from '@/assets/logo-white.png'
|
||||
import css from './index.module.scss'
|
||||
|
||||
|
||||
function LayoutIndex() {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const {token: {colorBgContainer, colorTextTertiary}} = theme.useToken()
|
||||
const {token: {colorTextTertiary}} = theme.useToken()
|
||||
const navigate = useNavigate()
|
||||
const matches = useMatches()
|
||||
const crumbs = matches.map(x => ({title: x.handle.crumb, href: x.pathname}))
|
||||
|
||||
function handleMenuClick({key}) {
|
||||
navigate(key)
|
||||
|
@ -21,29 +17,20 @@ function LayoutIndex() {
|
|||
const selectedKey = matches[matches.length - 1]?.pathname
|
||||
return (
|
||||
<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>
|
||||
<Header/>
|
||||
<Layout>
|
||||
<Header/>
|
||||
<Layout.Content style={{margin: '0 16px'}}>
|
||||
<Breadcrumb style={{margin: '16px 0'}} items={crumbs}/>
|
||||
<div style={{padding: 24, minHeight: 360, background: colorBgContainer}}>
|
||||
<Outlet/>
|
||||
</div>
|
||||
<Layout.Sider theme="light" collapsible className={css.sider}>
|
||||
<Menu theme="light" mode="inline" items={menus} selectedKeys={[selectedKey]}
|
||||
onClick={handleMenuClick}/>
|
||||
</Layout.Sider>
|
||||
<Layout.Content style={{margin: '16px 16px 0 16px'}}>
|
||||
<Outlet/>
|
||||
<Layout.Footer>
|
||||
<Flex justify="center" align="center" style={{color: colorTextTertiary}}>
|
||||
Copyright © 2023 OpenSpug All Rights Reserved.
|
||||
</Flex>
|
||||
</Layout.Footer>
|
||||
</Layout.Content>
|
||||
<Layout.Footer>
|
||||
<Flex justify="center" align="center" style={{color: colorTextTertiary}}>
|
||||
Copyright © 2023 OpenSpug All Rights Reserved.
|
||||
</Flex>
|
||||
</Layout.Footer>
|
||||
</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 session from './session'
|
||||
|
||||
const SContext = React.createContext({})
|
||||
export * from './utils.js'
|
||||
export {
|
||||
http,
|
||||
session,
|
||||
}
|
||||
SContext,
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import {isSubArray} from "@/libs/utils.js";
|
|||
class Session {
|
||||
constructor() {
|
||||
this._session = {};
|
||||
this.lang = localStorage.getItem('lang') || 'zh';
|
||||
this.theme = localStorage.getItem('theme') || 'light';
|
||||
const tmp = localStorage.getItem('session');
|
||||
if (tmp) {
|
||||
try {
|
||||
|
|
|
@ -1,30 +1,9 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import {createBrowserRouter, RouterProvider} from 'react-router-dom'
|
||||
import {ConfigProvider, App, theme} from 'antd'
|
||||
import routes from './routes.jsx'
|
||||
import './i18n.js'
|
||||
|
||||
|
||||
const router = createBrowserRouter(routes)
|
||||
import App from './App.jsx'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<ConfigProvider theme={{
|
||||
algorithm: [theme.defaultAlgorithm],
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
headerHeight: 48,
|
||||
footerPadding: 16
|
||||
},
|
||||
},
|
||||
}}>
|
||||
<App>
|
||||
<RouterProvider router={router}/>
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
|
|
@ -11,7 +11,6 @@ let routes = [
|
|||
path: '/',
|
||||
element: <Layout/>,
|
||||
errorElement: <ErrorPage/>,
|
||||
title: t('首页'),
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
|
@ -72,17 +71,5 @@ function routes2menu(routes, parentPath = '') {
|
|||
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 default routes
|
Loading…
Reference in New Issue