pull/661/head
vapao 2023-12-26 00:30:05 +08:00
parent 74e064002a
commit 41990ef08b
15 changed files with 146 additions and 95 deletions

View File

@ -16,6 +16,7 @@ module.exports = {
'warn',
{ allowConstantExport: true },
],
'react/prop-types': 'off',
},
globals: {
t: 'readonly',

View File

@ -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",

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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}} 条',
}
}
}

View File

@ -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>
)

View File

@ -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;
}
}
}

View File

@ -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>
)

View File

@ -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;
}

View File

@ -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,
}

View File

@ -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 {

View File

@ -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>
)

View File

@ -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